Skip to content

Commit

Permalink
Synchronize provider configuration with kubernetes provider (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
gavinbunney authored May 24, 2021
1 parent 6011af6 commit 697873b
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 86 deletions.
30 changes: 29 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ then either place it at the root of your Terraform folder or in the Terraform pl

## Configuration

The provider supports the same configuration parameters as the [Kubernetes Terraform Provider](https://www.terraform.io/docs/providers/kubernetes/index.html)
The provider supports the same configuration parameters as the [Kubernetes Terraform Provider](https://www.terraform.io/docs/providers/kubernetes/index.html),
with the addition of `load_config_file` and `apply_retry_count`.

> Note: Unlike the Terraform Kubernetes Provider, this provider will load the `KUBECONFIG` file if the environment variable is set.
```hcl
provider "kubectl" {
Expand All @@ -68,6 +71,31 @@ provider "kubectl" {
}
```

### Argument Reference

The following arguments are supported:

* `apply_retry_count` - (Optional) Defines the number of attempts any create/update action will take. Default `1`.
* `load_config_file` - (Optional) Flag to enable/disable loading of the local kubeconf file. Default `true`. Can be sourced from `KUBE_LOAD_CONFIG_FILE`.
* `host` - (Optional) The hostname (in form of URI) of the Kubernetes API. Can be sourced from `KUBE_HOST`.
* `username` - (Optional) The username to use for HTTP basic authentication when accessing the Kubernetes API. Can be sourced from `KUBE_USER`.
* `password` - (Optional) The password to use for HTTP basic authentication when accessing the Kubernetes API. Can be sourced from `KUBE_PASSWORD`.
* `insecure` - (Optional) Whether the server should be accessed without verifying the TLS certificate. Can be sourced from `KUBE_INSECURE`. Defaults to `false`.
* `client_certificate` - (Optional) PEM-encoded client certificate for TLS authentication. Can be sourced from `KUBE_CLIENT_CERT_DATA`.
* `client_key` - (Optional) PEM-encoded client certificate key for TLS authentication. Can be sourced from `KUBE_CLIENT_KEY_DATA`.
* `cluster_ca_certificate` - (Optional) PEM-encoded root certificates bundle for TLS authentication. Can be sourced from `KUBE_CLUSTER_CA_CERT_DATA`.
* `config_path` - (Optional) A path to a kube config file. Can be sourced from `KUBE_CONFIG_PATH` or `KUBECONFIG`.
* `config_paths` - (Optional) A list of paths to the kube config files. Can be sourced from `KUBE_CONFIG_PATHS`.
* `config_context` - (Optional) Context to choose from the config file. Can be sourced from `KUBE_CTX`.
* `config_context_auth_info` - (Optional) Authentication info context of the kube config (name of the kubeconfig user, `--user` flag in `kubectl`). Can be sourced from `KUBE_CTX_AUTH_INFO`.
* `config_context_cluster` - (Optional) Cluster context of the kube config (name of the kubeconfig cluster, `--cluster` flag in `kubectl`). Can be sourced from `KUBE_CTX_CLUSTER`.
* `token` - (Optional) Token of your service account. Can be sourced from `KUBE_TOKEN`.
* `exec` - (Optional) Configuration block to use an [exec-based credential plugin] (https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins), e.g. call an external command to receive user credentials.
* `api_version` - (Required) API version to use when decoding the ExecCredentials resource, e.g. `client.authentication.k8s.io/v1beta1`.
* `command` - (Required) Command to execute.
* `args` - (Optional) List of arguments to pass when executing the plugin.
* `env` - (Optional) Map of environment variables to set when executing the plugin.

### Exec Plugin Support

As with the Kubernetes Terraform Provider, this provider also supports using a `exec` based plugin (for example when running on EKS).
Expand Down
208 changes: 123 additions & 85 deletions kubernetes/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/mitchellh/go-homedir"
"k8s.io/apimachinery/pkg/api/meta"
apimachineryschema "k8s.io/apimachinery/pkg/runtime/schema"
k8sresource "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/discovery"
diskcached "k8s.io/client-go/discovery/cached/disk"
Expand Down Expand Up @@ -78,13 +79,20 @@ func Provider() *schema.Provider {
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.MultiEnvDefaultFunc(
[]string{
"KUBE_CONFIG",
"KUBECONFIG",
"KUBE_CONFIG_PATH",
},
"~/.kube/config"),
Description: "Path to the kube config file, defaults to ~/.kube/config",
Expand Down Expand Up @@ -213,11 +221,13 @@ var kubectlApplyRetryCount uint64

func providerConfigure(d *schema.ResourceData, terraformVersion string) (interface{}, diag.Diagnostics) {

var cfg *restclient.Config
var err error
if d.Get("load_config_file").(bool) {
// Config file loading
cfg, err = tryLoadingConfigFile(d)
cfg, err := initializeConfiguration(d)
if err != nil {
return nil, diag.FromErr(err)
}

if cfg == nil {
cfg = &restclient.Config{}
}

kubectlApplyRetryCount = uint64(d.Get("apply_retry_count").(int))
Expand All @@ -226,59 +236,12 @@ func providerConfigure(d *schema.ResourceData, terraformVersion string) (interfa
kubectlApplyRetryCount = uint64(applyEnvValue)
}

if err != nil {
return nil, diag.FromErr(err)
}
if cfg == nil {
cfg = &restclient.Config{}
}

cfg.QPS = 100.0
cfg.Burst = 100

// Overriding with static configuration
cfg.UserAgent = fmt.Sprintf("HashiCorp/1.0 Terraform/%s", terraformVersion)

if v, ok := d.GetOk("host"); ok {
cfg.Host = v.(string)
}
if v, ok := d.GetOk("username"); ok {
cfg.Username = v.(string)
}
if v, ok := d.GetOk("password"); ok {
cfg.Password = v.(string)
}
if v, ok := d.GetOk("insecure"); ok {
cfg.Insecure = v.(bool)
}
if v, ok := d.GetOk("cluster_ca_certificate"); ok {
cfg.CAData = bytes.NewBufferString(v.(string)).Bytes()
}
if v, ok := d.GetOk("client_certificate"); ok {
cfg.CertData = bytes.NewBufferString(v.(string)).Bytes()
}
if v, ok := d.GetOk("client_key"); ok {
cfg.KeyData = bytes.NewBufferString(v.(string)).Bytes()
}
if v, ok := d.GetOk("token"); ok {
cfg.BearerToken = v.(string)
}

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, diag.FromErr(fmt.Errorf("failed to parse exec"))
}
cfg.ExecProvider = exec
}

k, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, diag.FromErr(fmt.Errorf("failed to configure: %s", err))
Expand All @@ -298,53 +261,128 @@ func providerConfigure(d *schema.ResourceData, terraformVersion string) (interfa
}, nil
}

func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) {
path, err := homedir.Expand(d.Get("config_path").(string))
if err != nil {
return nil, err
}
func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, error) {
overrides := &clientcmd.ConfigOverrides{}
loader := &clientcmd.ClientConfigLoadingRules{}

configPaths := []string{}

loader := &clientcmd.ClientConfigLoadingRules{
ExplicitPath: path,
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 != "" {
// NOTE we have to do this here because the schema
// does not yet allow you to set a default for a TypeList
configPaths = filepath.SplitList(v)
}

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)
if d.Get("load_config_file").(bool) && len(configPaths) > 0 {
expandedPaths := []string{}
for _, p := range configPaths {
path, err := homedir.Expand(p)
if err != nil {
return nil, err
}

log.Printf("[DEBUG] Using kubeconfig: %s", path)
expandedPaths = append(expandedPaths, path)
}

overrides.Context = clientcmdapi.Context{}
if authInfoOk {
overrides.Context.AuthInfo = authInfo.(string)
ctxSuffix += fmt.Sprintf("; auth_info: %s", overrides.Context.AuthInfo)
if len(expandedPaths) == 1 {
loader.ExplicitPath = expandedPaths[0]
} else {
loader.Precedence = expandedPaths
}

ctxSuffix := "; default context"

kubectx, 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 = kubectx.(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)
}
}

// Overriding with static configuration
if v, ok := d.GetOk("insecure"); ok {
overrides.ClusterInfo.InsecureSkipTLSVerify = v.(bool)
}
if v, ok := d.GetOk("cluster_ca_certificate"); ok {
overrides.ClusterInfo.CertificateAuthorityData = bytes.NewBufferString(v.(string)).Bytes()
}
if v, ok := d.GetOk("client_certificate"); ok {
overrides.AuthInfo.ClientCertificateData = bytes.NewBufferString(v.(string)).Bytes()
}
if v, ok := d.GetOk("host"); ok {
// Server has to be the complete address of the kubernetes cluster (scheme://hostname:port), not just the hostname,
// because `overrides` are processed too late to be taken into account by `defaultServerUrlFor()`.
// This basically replicates what defaultServerUrlFor() does with config but for overrides,
// see https://github.com/kubernetes/client-go/blob/v12.0.0/rest/url_utils.go#L85-L87
hasCA := len(overrides.ClusterInfo.CertificateAuthorityData) != 0
hasCert := len(overrides.AuthInfo.ClientCertificateData) != 0
defaultTLS := hasCA || hasCert || overrides.ClusterInfo.InsecureSkipTLSVerify
host, _, err := restclient.DefaultServerURL(v.(string), "", apimachineryschema.GroupVersion{}, defaultTLS)
if err != nil {
return nil, fmt.Errorf("Failed to parse host: %s", err)
}
if clusterOk {
overrides.Context.Cluster = cluster.(string)
ctxSuffix += fmt.Sprintf("; cluster: %s", overrides.Context.Cluster)

overrides.ClusterInfo.Server = host.String()
}
if v, ok := d.GetOk("username"); ok {
overrides.AuthInfo.Username = v.(string)
}
if v, ok := d.GetOk("password"); ok {
overrides.AuthInfo.Password = v.(string)
}
if v, ok := d.GetOk("client_key"); ok {
overrides.AuthInfo.ClientKeyData = bytes.NewBufferString(v.(string)).Bytes()
}
if v, ok := d.GetOk("token"); ok {
overrides.AuthInfo.Token = v.(string)
}

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")
}
log.Printf("[DEBUG] Using overidden context: %#v", overrides.Context)
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", path)
return nil, nil
}
return nil, fmt.Errorf("failed to load config (%s%s): %s", path, ctxSuffix, err)
log.Printf("[WARN] Invalid provider configuration was supplied. Provider operations likely to fail: %v", err)
return nil, nil
}

log.Printf("[INFO] Successfully loaded config file (%s%s)", path, ctxSuffix)
return cfg, nil
}

Expand Down

0 comments on commit 697873b

Please sign in to comment.