github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/kubernetes/backend.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package kubernetes
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"fmt"
    12  	"log"
    13  	"os"
    14  	"path/filepath"
    15  
    16  	"github.com/mitchellh/go-homedir"
    17  	"github.com/opentofu/opentofu/internal/backend"
    18  	"github.com/opentofu/opentofu/internal/encryption"
    19  	"github.com/opentofu/opentofu/internal/httpclient"
    20  	"github.com/opentofu/opentofu/internal/legacy/helper/schema"
    21  	"github.com/opentofu/opentofu/version"
    22  	k8sSchema "k8s.io/apimachinery/pkg/runtime/schema"
    23  	"k8s.io/client-go/dynamic"
    24  	"k8s.io/client-go/kubernetes"
    25  	coordinationv1 "k8s.io/client-go/kubernetes/typed/coordination/v1"
    26  	restclient "k8s.io/client-go/rest"
    27  	"k8s.io/client-go/tools/clientcmd"
    28  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    29  )
    30  
    31  var (
    32  	secretResource = k8sSchema.GroupVersionResource{
    33  		Group:    "",
    34  		Version:  "v1",
    35  		Resource: "secrets",
    36  	}
    37  )
    38  
    39  // New creates a new backend for kubernetes remote state.
    40  func New(enc encryption.StateEncryption) backend.Backend {
    41  	s := &schema.Backend{
    42  		Schema: map[string]*schema.Schema{
    43  			"secret_suffix": {
    44  				Type:        schema.TypeString,
    45  				Required:    true,
    46  				Description: "Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`.",
    47  			},
    48  			"labels": {
    49  				Type:        schema.TypeMap,
    50  				Optional:    true,
    51  				Description: "Map of additional labels to be applied to the secret.",
    52  				Elem:        &schema.Schema{Type: schema.TypeString},
    53  			},
    54  			"namespace": {
    55  				Type:        schema.TypeString,
    56  				Optional:    true,
    57  				DefaultFunc: schema.EnvDefaultFunc("KUBE_NAMESPACE", "default"),
    58  				Description: "Namespace to store the secret in.",
    59  			},
    60  			"in_cluster_config": {
    61  				Type:        schema.TypeBool,
    62  				Optional:    true,
    63  				DefaultFunc: schema.EnvDefaultFunc("KUBE_IN_CLUSTER_CONFIG", false),
    64  				Description: "Used to authenticate to the cluster from inside a pod.",
    65  			},
    66  			"load_config_file": {
    67  				Type:        schema.TypeBool,
    68  				Optional:    true,
    69  				DefaultFunc: schema.EnvDefaultFunc("KUBE_LOAD_CONFIG_FILE", true),
    70  				Description: "Load local kubeconfig.",
    71  			},
    72  			"host": {
    73  				Type:        schema.TypeString,
    74  				Optional:    true,
    75  				DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", ""),
    76  				Description: "The hostname (in form of URI) of Kubernetes master.",
    77  			},
    78  			"username": {
    79  				Type:        schema.TypeString,
    80  				Optional:    true,
    81  				DefaultFunc: schema.EnvDefaultFunc("KUBE_USER", ""),
    82  				Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
    83  			},
    84  			"password": {
    85  				Type:        schema.TypeString,
    86  				Optional:    true,
    87  				DefaultFunc: schema.EnvDefaultFunc("KUBE_PASSWORD", ""),
    88  				Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
    89  			},
    90  			"insecure": {
    91  				Type:        schema.TypeBool,
    92  				Optional:    true,
    93  				DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", false),
    94  				Description: "Whether server should be accessed without verifying the TLS certificate.",
    95  			},
    96  			"client_certificate": {
    97  				Type:        schema.TypeString,
    98  				Optional:    true,
    99  				DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", ""),
   100  				Description: "PEM-encoded client certificate for TLS authentication.",
   101  			},
   102  			"client_key": {
   103  				Type:        schema.TypeString,
   104  				Optional:    true,
   105  				DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", ""),
   106  				Description: "PEM-encoded client certificate key for TLS authentication.",
   107  			},
   108  			"cluster_ca_certificate": {
   109  				Type:        schema.TypeString,
   110  				Optional:    true,
   111  				DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", ""),
   112  				Description: "PEM-encoded root certificates bundle for TLS authentication.",
   113  			},
   114  			"config_paths": {
   115  				Type:        schema.TypeList,
   116  				Elem:        &schema.Schema{Type: schema.TypeString},
   117  				Optional:    true,
   118  				Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.",
   119  			},
   120  			"config_path": {
   121  				Type:        schema.TypeString,
   122  				Optional:    true,
   123  				DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG_PATH", ""),
   124  				Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH environment variable.",
   125  			},
   126  			"config_context": {
   127  				Type:        schema.TypeString,
   128  				Optional:    true,
   129  				DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", ""),
   130  			},
   131  			"config_context_auth_info": {
   132  				Type:        schema.TypeString,
   133  				Optional:    true,
   134  				DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", ""),
   135  				Description: "",
   136  			},
   137  			"config_context_cluster": {
   138  				Type:        schema.TypeString,
   139  				Optional:    true,
   140  				DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", ""),
   141  				Description: "",
   142  			},
   143  			"token": {
   144  				Type:        schema.TypeString,
   145  				Optional:    true,
   146  				DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", ""),
   147  				Description: "Token to authentifcate a service account.",
   148  			},
   149  			"exec": {
   150  				Type:     schema.TypeList,
   151  				Optional: true,
   152  				MaxItems: 1,
   153  				Elem: &schema.Resource{
   154  					Schema: map[string]*schema.Schema{
   155  						"api_version": {
   156  							Type:     schema.TypeString,
   157  							Required: true,
   158  						},
   159  						"command": {
   160  							Type:     schema.TypeString,
   161  							Required: true,
   162  						},
   163  						"env": {
   164  							Type:     schema.TypeMap,
   165  							Optional: true,
   166  							Elem:     &schema.Schema{Type: schema.TypeString},
   167  						},
   168  						"args": {
   169  							Type:     schema.TypeList,
   170  							Optional: true,
   171  							Elem:     &schema.Schema{Type: schema.TypeString},
   172  						},
   173  					},
   174  				},
   175  				Description: "Use a credential plugin to authenticate.",
   176  			},
   177  		},
   178  	}
   179  
   180  	result := &Backend{Backend: s, encryption: enc}
   181  	result.Backend.ConfigureFunc = result.configure
   182  	return result
   183  }
   184  
   185  type Backend struct {
   186  	*schema.Backend
   187  	encryption encryption.StateEncryption
   188  
   189  	// The fields below are set from configure
   190  	kubernetesSecretClient dynamic.ResourceInterface
   191  	kubernetesLeaseClient  coordinationv1.LeaseInterface
   192  	config                 *restclient.Config
   193  	namespace              string
   194  	labels                 map[string]string
   195  	nameSuffix             string
   196  }
   197  
   198  func (b Backend) getKubernetesSecretClient() (dynamic.ResourceInterface, error) {
   199  	if b.kubernetesSecretClient != nil {
   200  		return b.kubernetesSecretClient, nil
   201  	}
   202  
   203  	client, err := dynamic.NewForConfig(b.config)
   204  	if err != nil {
   205  		return nil, fmt.Errorf("Failed to configure: %w", err)
   206  	}
   207  
   208  	b.kubernetesSecretClient = client.Resource(secretResource).Namespace(b.namespace)
   209  	return b.kubernetesSecretClient, nil
   210  }
   211  
   212  func (b Backend) getKubernetesLeaseClient() (coordinationv1.LeaseInterface, error) {
   213  	if b.kubernetesLeaseClient != nil {
   214  		return b.kubernetesLeaseClient, nil
   215  	}
   216  
   217  	client, err := kubernetes.NewForConfig(b.config)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  
   222  	b.kubernetesLeaseClient = client.CoordinationV1().Leases(b.namespace)
   223  	return b.kubernetesLeaseClient, nil
   224  }
   225  
   226  func (b *Backend) configure(ctx context.Context) error {
   227  	if b.config != nil {
   228  		return nil
   229  	}
   230  
   231  	// Grab the resource data
   232  	data := schema.FromContextBackendConfig(ctx)
   233  
   234  	cfg, err := getInitialConfig(data)
   235  	if err != nil {
   236  		return err
   237  	}
   238  
   239  	// Overriding with static configuration
   240  	cfg.UserAgent = httpclient.OpenTofuUserAgent(version.Version)
   241  
   242  	if v, ok := data.GetOk("host"); ok {
   243  		cfg.Host = v.(string)
   244  	}
   245  	if v, ok := data.GetOk("username"); ok {
   246  		cfg.Username = v.(string)
   247  	}
   248  	if v, ok := data.GetOk("password"); ok {
   249  		cfg.Password = v.(string)
   250  	}
   251  	if v, ok := data.GetOk("insecure"); ok {
   252  		cfg.Insecure = v.(bool)
   253  	}
   254  	if v, ok := data.GetOk("cluster_ca_certificate"); ok {
   255  		cfg.CAData = bytes.NewBufferString(v.(string)).Bytes()
   256  	}
   257  	if v, ok := data.GetOk("client_certificate"); ok {
   258  		cfg.CertData = bytes.NewBufferString(v.(string)).Bytes()
   259  	}
   260  	if v, ok := data.GetOk("client_key"); ok {
   261  		cfg.KeyData = bytes.NewBufferString(v.(string)).Bytes()
   262  	}
   263  	if v, ok := data.GetOk("token"); ok {
   264  		cfg.BearerToken = v.(string)
   265  	}
   266  
   267  	if v, ok := data.GetOk("labels"); ok {
   268  		labels := map[string]string{}
   269  		for k, vv := range v.(map[string]interface{}) {
   270  			labels[k] = vv.(string)
   271  		}
   272  		b.labels = labels
   273  	}
   274  
   275  	ns := data.Get("namespace").(string)
   276  	b.namespace = ns
   277  	b.nameSuffix = data.Get("secret_suffix").(string)
   278  	b.config = cfg
   279  
   280  	return nil
   281  }
   282  
   283  func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) {
   284  	var cfg *restclient.Config
   285  	var err error
   286  
   287  	inCluster := data.Get("in_cluster_config").(bool)
   288  	if inCluster {
   289  		cfg, err = restclient.InClusterConfig()
   290  		if err != nil {
   291  			return nil, err
   292  		}
   293  	} else {
   294  		cfg, err = tryLoadingConfigFile(data)
   295  		if err != nil {
   296  			return nil, err
   297  		}
   298  	}
   299  
   300  	if cfg == nil {
   301  		cfg = &restclient.Config{}
   302  	}
   303  	return cfg, err
   304  }
   305  
   306  func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) {
   307  	loader := &clientcmd.ClientConfigLoadingRules{}
   308  
   309  	configPaths := []string{}
   310  	if v, ok := d.Get("config_path").(string); ok && v != "" {
   311  		configPaths = []string{v}
   312  	} else if v, ok := d.Get("config_paths").([]interface{}); ok && len(v) > 0 {
   313  		for _, p := range v {
   314  			configPaths = append(configPaths, p.(string))
   315  		}
   316  	} else if v := os.Getenv("KUBE_CONFIG_PATHS"); v != "" {
   317  		configPaths = filepath.SplitList(v)
   318  	}
   319  
   320  	expandedPaths := []string{}
   321  	for _, p := range configPaths {
   322  		path, err := homedir.Expand(p)
   323  		if err != nil {
   324  			log.Printf("[DEBUG] Could not expand path: %s", err)
   325  			return nil, err
   326  		}
   327  		log.Printf("[DEBUG] Using kubeconfig: %s", path)
   328  		expandedPaths = append(expandedPaths, path)
   329  	}
   330  
   331  	if len(expandedPaths) == 1 {
   332  		loader.ExplicitPath = expandedPaths[0]
   333  	} else {
   334  		loader.Precedence = expandedPaths
   335  	}
   336  
   337  	overrides := &clientcmd.ConfigOverrides{}
   338  	ctxSuffix := "; default context"
   339  
   340  	ctx, ctxOk := d.GetOk("config_context")
   341  	authInfo, authInfoOk := d.GetOk("config_context_auth_info")
   342  	cluster, clusterOk := d.GetOk("config_context_cluster")
   343  	if ctxOk || authInfoOk || clusterOk {
   344  		ctxSuffix = "; overriden context"
   345  		if ctxOk {
   346  			overrides.CurrentContext = ctx.(string)
   347  			ctxSuffix += fmt.Sprintf("; config ctx: %s", overrides.CurrentContext)
   348  			log.Printf("[DEBUG] Using custom current context: %q", overrides.CurrentContext)
   349  		}
   350  
   351  		overrides.Context = clientcmdapi.Context{}
   352  		if authInfoOk {
   353  			overrides.Context.AuthInfo = authInfo.(string)
   354  			ctxSuffix += fmt.Sprintf("; auth_info: %s", overrides.Context.AuthInfo)
   355  		}
   356  		if clusterOk {
   357  			overrides.Context.Cluster = cluster.(string)
   358  			ctxSuffix += fmt.Sprintf("; cluster: %s", overrides.Context.Cluster)
   359  		}
   360  		log.Printf("[DEBUG] Using overidden context: %#v", overrides.Context)
   361  	}
   362  
   363  	if v, ok := d.GetOk("exec"); ok {
   364  		exec := &clientcmdapi.ExecConfig{}
   365  		if spec, ok := v.([]interface{})[0].(map[string]interface{}); ok {
   366  			exec.APIVersion = spec["api_version"].(string)
   367  			exec.Command = spec["command"].(string)
   368  			exec.Args = expandStringSlice(spec["args"].([]interface{}))
   369  			for kk, vv := range spec["env"].(map[string]interface{}) {
   370  				exec.Env = append(exec.Env, clientcmdapi.ExecEnvVar{Name: kk, Value: vv.(string)})
   371  			}
   372  		} else {
   373  			return nil, fmt.Errorf("Failed to parse exec")
   374  		}
   375  		overrides.AuthInfo.Exec = exec
   376  	}
   377  
   378  	cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides)
   379  	cfg, err := cc.ClientConfig()
   380  	if err != nil {
   381  		if pathErr, ok := err.(*os.PathError); ok && os.IsNotExist(pathErr.Err) {
   382  			log.Printf("[INFO] Unable to load config file as it doesn't exist at %q", pathErr.Path)
   383  			return nil, nil
   384  		}
   385  		return nil, fmt.Errorf("Failed to initialize kubernetes configuration: %w", err)
   386  	}
   387  
   388  	log.Printf("[INFO] Successfully initialized config")
   389  	return cfg, nil
   390  }
   391  
   392  func expandStringSlice(s []interface{}) []string {
   393  	result := make([]string, len(s), len(s))
   394  	for k, v := range s {
   395  		// Handle the OpenTofu parser bug which turns empty strings in lists to nil.
   396  		if v == nil {
   397  			result[k] = ""
   398  		} else {
   399  			result[k] = v.(string)
   400  		}
   401  	}
   402  	return result
   403  }