github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/backend/remote-state/kubernetes/backend.go (about)

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