// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package kubernetes import ( "bytes" "context" "fmt" "log" "os" "path/filepath" "github.com/mitchellh/go-homedir" "github.com/opentofu/opentofu/internal/backend" "github.com/opentofu/opentofu/internal/httpclient" "github.com/opentofu/opentofu/internal/legacy/helper/schema" "github.com/opentofu/opentofu/version" k8sSchema "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" coordinationv1 "k8s.io/client-go/kubernetes/typed/coordination/v1" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) var ( secretResource = k8sSchema.GroupVersionResource{ Group: "", Version: "v1", Resource: "secrets", } ) // New creates a new backend for kubernetes remote state. func New() backend.Backend { s := &schema.Backend{ Schema: map[string]*schema.Schema{ "secret_suffix": { Type: schema.TypeString, Required: true, Description: "Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`.", }, "labels": { Type: schema.TypeMap, Optional: true, Description: "Map of additional labels to be applied to the secret.", Elem: &schema.Schema{Type: schema.TypeString}, }, "namespace": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_NAMESPACE", "default"), Description: "Namespace to store the secret in.", }, "in_cluster_config": { Type: schema.TypeBool, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_IN_CLUSTER_CONFIG", false), Description: "Used to authenticate to the cluster from inside a pod.", }, "load_config_file": { Type: schema.TypeBool, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_LOAD_CONFIG_FILE", true), Description: "Load local kubeconfig.", }, "host": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", ""), Description: "The hostname (in form of URI) of Kubernetes master.", }, "username": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_USER", ""), Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", }, "password": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_PASSWORD", ""), Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", }, "insecure": { Type: schema.TypeBool, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", false), Description: "Whether server should be accessed without verifying the TLS certificate.", }, "client_certificate": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", ""), Description: "PEM-encoded client certificate for TLS authentication.", }, "client_key": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", ""), Description: "PEM-encoded client certificate key for TLS authentication.", }, "cluster_ca_certificate": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", ""), Description: "PEM-encoded root certificates bundle for TLS authentication.", }, "config_paths": { Type: schema.TypeList, Elem: &schema.Schema{Type: schema.TypeString}, Optional: true, Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.", }, "config_path": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG_PATH", ""), Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH environment variable.", }, "config_context": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", ""), }, "config_context_auth_info": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", ""), Description: "", }, "config_context_cluster": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", ""), Description: "", }, "token": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", ""), Description: "Token to authentifcate a service account.", }, "exec": { Type: schema.TypeList, Optional: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "api_version": { Type: schema.TypeString, Required: true, }, "command": { Type: schema.TypeString, Required: true, }, "env": { Type: schema.TypeMap, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, "args": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, }, }, Description: "Use a credential plugin to authenticate.", }, }, } result := &Backend{Backend: s} result.Backend.ConfigureFunc = result.configure return result } type Backend struct { *schema.Backend // The fields below are set from configure kubernetesSecretClient dynamic.ResourceInterface kubernetesLeaseClient coordinationv1.LeaseInterface config *restclient.Config namespace string labels map[string]string nameSuffix string } func (b Backend) getKubernetesSecretClient() (dynamic.ResourceInterface, error) { if b.kubernetesSecretClient != nil { return b.kubernetesSecretClient, nil } client, err := dynamic.NewForConfig(b.config) if err != nil { return nil, fmt.Errorf("Failed to configure: %w", err) } b.kubernetesSecretClient = client.Resource(secretResource).Namespace(b.namespace) return b.kubernetesSecretClient, nil } func (b Backend) getKubernetesLeaseClient() (coordinationv1.LeaseInterface, error) { if b.kubernetesLeaseClient != nil { return b.kubernetesLeaseClient, nil } client, err := kubernetes.NewForConfig(b.config) if err != nil { return nil, err } b.kubernetesLeaseClient = client.CoordinationV1().Leases(b.namespace) return b.kubernetesLeaseClient, nil } func (b *Backend) configure(ctx context.Context) error { if b.config != nil { return nil } // Grab the resource data data := schema.FromContextBackendConfig(ctx) cfg, err := getInitialConfig(data) if err != nil { return err } // Overriding with static configuration cfg.UserAgent = httpclient.OpenTofuUserAgent(version.Version) if v, ok := data.GetOk("host"); ok { cfg.Host = v.(string) } if v, ok := data.GetOk("username"); ok { cfg.Username = v.(string) } if v, ok := data.GetOk("password"); ok { cfg.Password = v.(string) } if v, ok := data.GetOk("insecure"); ok { cfg.Insecure = v.(bool) } if v, ok := data.GetOk("cluster_ca_certificate"); ok { cfg.CAData = bytes.NewBufferString(v.(string)).Bytes() } if v, ok := data.GetOk("client_certificate"); ok { cfg.CertData = bytes.NewBufferString(v.(string)).Bytes() } if v, ok := data.GetOk("client_key"); ok { cfg.KeyData = bytes.NewBufferString(v.(string)).Bytes() } if v, ok := data.GetOk("token"); ok { cfg.BearerToken = v.(string) } if v, ok := data.GetOk("labels"); ok { labels := map[string]string{} for k, vv := range v.(map[string]interface{}) { labels[k] = vv.(string) } b.labels = labels } ns := data.Get("namespace").(string) b.namespace = ns b.nameSuffix = data.Get("secret_suffix").(string) b.config = cfg return nil } func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) { var cfg *restclient.Config var err error inCluster := data.Get("in_cluster_config").(bool) if inCluster { cfg, err = restclient.InClusterConfig() if err != nil { return nil, err } } else { cfg, err = tryLoadingConfigFile(data) if err != nil { return nil, err } } if cfg == nil { cfg = &restclient.Config{} } return cfg, err } func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) { loader := &clientcmd.ClientConfigLoadingRules{} configPaths := []string{} if v, ok := d.Get("config_path").(string); ok && v != "" { configPaths = []string{v} } else if v, ok := d.Get("config_paths").([]interface{}); ok && len(v) > 0 { for _, p := range v { configPaths = append(configPaths, p.(string)) } } else if v := os.Getenv("KUBE_CONFIG_PATHS"); v != "" { configPaths = filepath.SplitList(v) } expandedPaths := []string{} for _, p := range configPaths { path, err := homedir.Expand(p) if err != nil { log.Printf("[DEBUG] Could not expand path: %s", err) return nil, err } log.Printf("[DEBUG] Using kubeconfig: %s", path) expandedPaths = append(expandedPaths, path) } if len(expandedPaths) == 1 { loader.ExplicitPath = expandedPaths[0] } else { loader.Precedence = expandedPaths } overrides := &clientcmd.ConfigOverrides{} ctxSuffix := "; default context" ctx, ctxOk := d.GetOk("config_context") authInfo, authInfoOk := d.GetOk("config_context_auth_info") cluster, clusterOk := d.GetOk("config_context_cluster") if ctxOk || authInfoOk || clusterOk { ctxSuffix = "; overriden context" if ctxOk { overrides.CurrentContext = ctx.(string) ctxSuffix += fmt.Sprintf("; config ctx: %s", overrides.CurrentContext) log.Printf("[DEBUG] Using custom current context: %q", overrides.CurrentContext) } overrides.Context = clientcmdapi.Context{} if authInfoOk { overrides.Context.AuthInfo = authInfo.(string) ctxSuffix += fmt.Sprintf("; auth_info: %s", overrides.Context.AuthInfo) } if clusterOk { overrides.Context.Cluster = cluster.(string) ctxSuffix += fmt.Sprintf("; cluster: %s", overrides.Context.Cluster) } log.Printf("[DEBUG] Using overidden context: %#v", overrides.Context) } if v, ok := d.GetOk("exec"); ok { exec := &clientcmdapi.ExecConfig{} if spec, ok := v.([]interface{})[0].(map[string]interface{}); ok { exec.APIVersion = spec["api_version"].(string) exec.Command = spec["command"].(string) exec.Args = expandStringSlice(spec["args"].([]interface{})) for kk, vv := range spec["env"].(map[string]interface{}) { exec.Env = append(exec.Env, clientcmdapi.ExecEnvVar{Name: kk, Value: vv.(string)}) } } else { return nil, fmt.Errorf("Failed to parse exec") } overrides.AuthInfo.Exec = exec } cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides) cfg, err := cc.ClientConfig() if err != nil { if pathErr, ok := err.(*os.PathError); ok && os.IsNotExist(pathErr.Err) { log.Printf("[INFO] Unable to load config file as it doesn't exist at %q", pathErr.Path) return nil, nil } return nil, fmt.Errorf("Failed to initialize kubernetes configuration: %w", err) } log.Printf("[INFO] Successfully initialized config") return cfg, nil } func expandStringSlice(s []interface{}) []string { result := make([]string, len(s), len(s)) for k, v := range s { // Handle the Terraform parser bug which turns empty strings in lists to nil. if v == nil { result[k] = "" } else { result[k] = v.(string) } } return result }