github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/kubernetes/backend.go (about)

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