diff --git a/GNUmakefile b/GNUmakefile index 1a05fce5bb..1f70d3f7b7 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -8,6 +8,12 @@ PKG_NAME := kubernetes OS_ARCH := $(shell go env GOOS)_$(shell go env GOARCH) TF_PROV_DOCS := $(PWD)/kubernetes/test-infra/tfproviderdocs +ifdef KUBE_CONFIG_PATHS +KUBECONFIG1 := $(shell echo $(KUBE_CONFIG_PATHS) | cut -d\: -f1) +else +KUBECONFIG1 := $(shell echo $(KUBECONFIG) | cut -d\: -f1) +endif + ifneq ($(PWD),$(PROVIDER_DIR)) $(error "Makefile must be run from the provider directory") endif @@ -74,6 +80,15 @@ test: fmtcheck go test ./tools testacc: fmtcheck vet + @rm -rf kubernetes/testdata || true + @mkdir kubernetes/testdata + @cp $(KUBECONFIG1) kubernetes/testdata/kubeconfig || (echo "Please set KUBE_CONFIG_PATHS or KUBECONFIG environment variable"; exit 1) + @rm -rf $(EXT_PROV_DIR)/.terraform $(EXT_PROV_DIR)/.terraform.lock.hcl || true + @mkdir $(EXT_PROV_DIR)/.terraform + @mkdir -p /tmp/.terraform.d/localhost/test/kubernetes/9.9.9/$(OS_ARCH) || true + @ls $(EXT_PROV_BIN) || go build -o $(EXT_PROV_BIN) + @cd $(EXT_PROV_DIR) && TF_CLI_CONFIG_FILE=$(EXT_PROV_DIR)/.terraformrc TF_PLUGIN_CACHE_DIR=$(EXT_PROV_DIR)/.terraform terraform init -upgrade + TF_CLI_CONFIG_FILE=$(EXT_PROV_DIR)/.terraformrc TF_PLUGIN_CACHE_DIR=$(EXT_PROV_DIR)/.terraform TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 120m TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 3h test-compile: diff --git a/kubernetes/provider.go b/kubernetes/provider.go index 8e0c7a3e72..3f2a0007a7 100644 --- a/kubernetes/provider.go +++ b/kubernetes/provider.go @@ -30,6 +30,7 @@ import ( apimachineryschema "k8s.io/apimachinery/pkg/runtime/schema" _ "k8s.io/client-go/plugin/pkg/client/auth" restclient "k8s.io/client-go/rest" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" aggregator "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" ) @@ -37,85 +38,147 @@ import ( const defaultFieldManagerName = "Terraform" func Provider() *schema.Provider { + conditionsMessage := "Specifying more than one authentication method can lead to unpredictable behavior." + + " This option will be removed in a future release. Please update your configuration." p := &schema.Provider{ Schema: map[string]*schema.Schema{ "host": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", ""), - Description: "The hostname (in form of URI) of Kubernetes master.", + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", nil), + Description: "The hostname (in form of URI) of Kubernetes master.", + ConflictsWith: []string{"config_path", "config_paths"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: conditionsMessage, + // TODO: enable this when AtLeastOneOf works with optional attributes. + // https://github.com/hashicorp/terraform-plugin-sdk/issues/705 + // AtLeastOneOf: []string{"token", "exec", "username", "password", "client_certificate", "client_key"}, }, "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.", + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("KUBE_USER", nil), + Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", + ConflictsWith: []string{"config_path", "config_paths", "exec", "token", "client_certificate", "client_key"}, + RequiredWith: []string{"password", "host"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: conditionsMessage, }, "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.", + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("KUBE_PASSWORD", nil), + Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", + ConflictsWith: []string{"config_path", "config_paths", "exec", "token", "client_certificate", "client_key"}, + RequiredWith: []string{"username", "host"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: conditionsMessage, }, "insecure": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", false), - Description: "Whether server should be accessed without verifying the TLS certificate.", + Type: schema.TypeBool, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", nil), + Description: "Whether server should be accessed without verifying the TLS certificate.", + ConflictsWith: []string{"cluster_ca_certificate", "client_key", "client_certificate", "exec"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: conditionsMessage, }, "client_certificate": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", ""), - Description: "PEM-encoded client certificate for TLS authentication.", + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", nil), + Description: "PEM-encoded client certificate for TLS authentication.", + ConflictsWith: []string{"config_path", "config_paths", "username", "password", "insecure"}, + RequiredWith: []string{"client_key", "cluster_ca_certificate", "host"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: conditionsMessage, }, "client_key": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", ""), - Description: "PEM-encoded client certificate key for TLS authentication.", + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", nil), + Description: "PEM-encoded client certificate key for TLS authentication.", + ConflictsWith: []string{"config_path", "config_paths", "username", "password", "exec", "insecure"}, + RequiredWith: []string{"client_certificate", "cluster_ca_certificate", "host"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: conditionsMessage, }, "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.", + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", nil), + Description: "PEM-encoded root certificates bundle for TLS authentication.", + ConflictsWith: []string{"config_path", "config_paths", "insecure"}, + RequiredWith: []string{"host"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: conditionsMessage, + // TODO: enable this when AtLeastOneOf works with optional attributes. + // https://github.com/hashicorp/terraform-plugin-sdk/issues/705 + // AtLeastOneOf: []string{"token", "exec", "client_certificate", "client_key"}, }, "config_paths": { Type: schema.TypeList, Elem: &schema.Schema{Type: schema.TypeString}, + DefaultFunc: configPathsEnv, Optional: true, Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.", + // config_paths conflicts with every attribute except for "insecure", since all of these options will be read from the kubeconfig. + ConflictsWith: []string{"config_path", "exec", "token", "host", "client_certificate", "client_key", "cluster_ca_certificate", "username", "password"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: conditionsMessage, }, "config_path": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG_PATH", nil), - Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH.", - ConflictsWith: []string{"config_paths"}, - }, - "config_context": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", ""), + DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG_PATH", nil), + Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH.", + // config_path conflicts with every attribute except for "insecure", since all of these options will be read from the kubeconfig. + ConflictsWith: []string{"config_paths", "exec", "token", "host", "client_certificate", "client_key", "cluster_ca_certificate", "username", "password"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: conditionsMessage, + }, + "config_context": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", nil), + Description: "Context to choose from the kube config file. ", + ConflictsWith: []string{"exec", "token", "client_certificate", "client_key", "username", "password"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: "This functionality will be removed in a later release. Please update your configuration.", + // TODO: enable this when AtLeastOneOf works with optional attributes. + // AtLeastOneOf: []string{"config_path", "config_paths"}, }, "config_context_auth_info": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", ""), - Description: "", + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", nil), + Description: "Authentication info context of the kube config (name of the kubeconfig user, --user flag in kubectl).", + ConflictsWith: []string{"exec", "token", "client_certificate", "client_key", "username", "password"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: "This functionality will be removed in a later release. Please update your configuration.", + // TODO: enable this when AtLeastOneOf works with optional attributes. + // AtLeastOneOf: []string{"config_path", "config_paths"}, }, "config_context_cluster": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", ""), - Description: "", + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", nil), + Description: "Cluster context of the kube config (name of the kubeconfig cluster, --cluster flag in kubectl).", + ConflictsWith: []string{"exec", "token", "client_certificate", "client_key", "username", "password"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: "Specifying more than one authentication method can lead to unpredictable behavior. This option will be removed in a future release. Please update your configuration.", + // TODO: enable this when AtLeastOneOf works with optional attributes. + // AtLeastOneOf: []string{"config_path", "config_paths"}, }, "token": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", ""), - Description: "Token to authenticate an service account", + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", nil), + Description: "Bearer token for authenticating the Kubernetes API.", + ConflictsWith: []string{"config_path", "config_paths", "exec", "client_certificate", "client_key", "username", "password"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: "Specifying more than one authentication method can lead to unpredictable behavior. This option will be removed in a future release. Please update your configuration.", + RequiredWith: []string{"host"}, }, "proxy_url": { Type: schema.TypeString, @@ -160,7 +223,11 @@ func Provider() *schema.Provider { }, }, }, - Description: "", + Description: "Configuration block to use an exec-based credential plugin, e.g. call an external command to receive user credentials.", + ConflictsWith: []string{"config_path", "config_paths", "token", "client_certificate", "client_key", "username", "password", "insecure"}, + RequiredWith: []string{"host", "cluster_ca_certificate"}, + ConditionsMode: schema.SchemaConditionsModeWarning, + ConditionsMessage: "Specifying more than one authentication method can lead to unpredictable behavior. This option will be removed in a future release. Please update your configuration.", }, "experiments": { Type: schema.TypeList, @@ -355,6 +422,22 @@ func Provider() *schema.Provider { return p } +// configPathsEnv fetches the value of the environment variable KUBE_CONFIG_PATHS, if defined. +func configPathsEnv() (interface{}, error) { + value, exists := os.LookupEnv("KUBE_CONFIG_PATHS") + if exists { + log.Print("[DEBUG] using environment variable KUBE_CONFIG_PATHS to define config_paths") + log.Printf("[DEBUG] value of KUBE_CONFIG_PATHS: %v", value) + pathList := filepath.SplitList(value) + configPaths := new([]interface{}) + for _, p := range pathList { + *configPaths = append(*configPaths, p) + } + return *configPaths, nil + } + return nil, nil +} + type KubeClientsets interface { MainClientset() (*kubernetes.Clientset, error) AggregatorClientset() (*aggregator.Clientset, error) @@ -380,6 +463,10 @@ func (k kubeClientsets) MainClientset() (*kubernetes.Clientset, error) { return k.mainClientset, nil } + if err := checkConfigurationValid(k.configData); err != nil { + return nil, err + } + if k.config != nil { kc, err := kubernetes.NewForConfig(k.config) if err != nil { @@ -404,6 +491,51 @@ func (k kubeClientsets) AggregatorClientset() (*aggregator.Clientset, error) { return k.aggregatorClientset, nil } +var apiTokenMountPath = "/var/run/secrets/kubernetes.io/serviceaccount" + +func inCluster() bool { + host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT") + if host == "" || port == "" { + return false + } + + if _, err := os.Stat(apiTokenMountPath); err != nil { + return false + } + return true +} + +var authDocumentationURL = "https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs#authentication" + +func checkConfigurationValid(d *schema.ResourceData) error { + if inCluster() { + log.Printf("[DEBUG] Terraform appears to be running inside the Kubernetes cluster") + return nil + } + + if os.Getenv("KUBE_CONFIG_PATHS") != "" { + return nil + } + + atLeastOneOf := []string{ + "host", + "config_path", + "config_paths", + "client_certificate", + "token", + "exec", + } + for _, a := range atLeastOneOf { + if _, ok := d.GetOk(a); ok { + return nil + } + } + + return fmt.Errorf(`provider not configured: you must configure a path to your kubeconfig +or explicitly supply credentials via the provider block or environment variables. + +See our documentation at: %s`, authDocumentationURL) + func (k kubeClientsets) DynamicClient() (dynamic.Interface, error) { if k.dynamicClient != nil { return k.dynamicClient, nil @@ -479,7 +611,9 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData, terraformVer func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, error) { overrides := &clientcmd.ConfigOverrides{} - loader := &clientcmd.ClientConfigLoadingRules{} + loader := &clientcmd.ClientConfigLoadingRules{ + WarnIfAllMissing: true, + } configPaths := []string{} @@ -489,10 +623,6 @@ func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, error) 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) } if len(configPaths) > 0 { @@ -519,7 +649,7 @@ func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, error) authInfo, authInfoOk := d.GetOk("config_context_auth_info") cluster, clusterOk := d.GetOk("config_context_cluster") if ctxOk || authInfoOk || clusterOk { - ctxSuffix = "; overriden context" + ctxSuffix = "; overridden context" if ctxOk { overrides.CurrentContext = kubectx.(string) ctxSuffix += fmt.Sprintf("; config ctx: %s", overrides.CurrentContext) @@ -540,16 +670,16 @@ func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, error) } // Overriding with static configuration - if v, ok := d.GetOk("insecure"); ok { + if v, ok := d.GetOk("insecure"); ok && v != "" { overrides.ClusterInfo.InsecureSkipTLSVerify = v.(bool) } - if v, ok := d.GetOk("cluster_ca_certificate"); ok { + if v, ok := d.GetOk("cluster_ca_certificate"); ok && v != "" { overrides.ClusterInfo.CertificateAuthorityData = bytes.NewBufferString(v.(string)).Bytes() } - if v, ok := d.GetOk("client_certificate"); ok { + if v, ok := d.GetOk("client_certificate"); ok && v != "" { overrides.AuthInfo.ClientCertificateData = bytes.NewBufferString(v.(string)).Bytes() } - if v, ok := d.GetOk("host"); ok { + if v, ok := d.GetOk("host"); ok && v != "" { // 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, @@ -564,16 +694,16 @@ func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, error) overrides.ClusterInfo.Server = host.String() } - if v, ok := d.GetOk("username"); ok { + if v, ok := d.GetOk("username"); ok && v != "" { overrides.AuthInfo.Username = v.(string) } - if v, ok := d.GetOk("password"); ok { + if v, ok := d.GetOk("password"); ok && v != "" { overrides.AuthInfo.Password = v.(string) } - if v, ok := d.GetOk("client_key"); ok { + if v, ok := d.GetOk("client_key"); ok && v != "" { overrides.AuthInfo.ClientKeyData = bytes.NewBufferString(v.(string)).Bytes() } - if v, ok := d.GetOk("token"); ok { + if v, ok := d.GetOk("token"); ok && v != "" { overrides.AuthInfo.Token = v.(string) } diff --git a/kubernetes/provider_test.go b/kubernetes/provider_test.go index e2f935eb91..681cc53f4d 100644 --- a/kubernetes/provider_test.go +++ b/kubernetes/provider_test.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "testing" @@ -161,6 +162,9 @@ func getEnv() *currentEnv { return e } +// testAccPreCheck verifies and sets required provider testing configuration +// This PreCheck function should be present in every acceptance test. It allows +// test configurations to omit a provider configuration func testAccPreCheck(t *testing.T) { ctx := context.TODO() hasFileCfg := (os.Getenv("KUBE_CTX_AUTH_INFO") != "" && os.Getenv("KUBE_CTX_CLUSTER") != "") || @@ -197,6 +201,37 @@ func testAccPreCheck(t *testing.T) { return } +// testAccPreCheckInternal configures the provider for internal tests. +// This is the equivalent of running `terraform init`, but with a bare +// minimum configuration, to create a fully separate environment where +// all configuration options (including environment variables) can be +// tested separately from the user's environment. It is used exclusively +// in functions labelled testAccKubernetesProviderConfig_*. +func testAccPreCheckInternal(t *testing.T) { + ctx := context.TODO() + unsetEnv(t) + diags := testAccProvider.Configure(ctx, terraform.NewResourceConfigRaw(nil)) + if diags.HasError() { + t.Fatal(diags[0].Summary) + } + return +} + +// testAccPreCheckInternal_setEnv is used for internal testing where +// specific environment variables are needed to configure the provider. +func testAccPreCheckInternal_setEnv(t *testing.T, envVars map[string]string) { + ctx := context.TODO() + unsetEnv(t) + for k, v := range envVars { + os.Setenv(k, v) + } + diags := testAccProvider.Configure(ctx, terraform.NewResourceConfigRaw(nil)) + if diags.HasError() { + t.Fatal(diags[0].Summary) + } + return +} + func getClusterVersion() (*gversion.Version, error) { meta := testAccProvider.Meta() @@ -451,3 +486,401 @@ type currentEnv struct { Insecure string Token string } + +func requiredProviders() string { + return fmt.Sprintf(`terraform { + required_providers { + kubernetes-local = { + source = "localhost/test/kubernetes" + version = "9.9.9" + } + kubernetes-released = { + source = "hashicorp/kubernetes" + version = "~> 1.13.2" + } + } +} +`) +} + +// testAccProviderFactoriesInternal is a factory used for provider configuration testing. +// This should only be used for TestAccKubernetesProviderConfig_ tests which need to +// reference the provider instance itself. Other testing should use testAccProviderFactories. +var testAccProviderFactoriesInternal = map[string]func() (*schema.Provider, error){ + "kubernetes": func() (*schema.Provider, error) { + return Provider(), nil + }, +} + +func TestAccKubernetesProviderConfig_config_path(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckInternal(t) }, + ProviderFactories: testAccProviderFactoriesInternal, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesProviderConfig( + providerConfig_config_path("./testdata/kubeconfig"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_config_path("./testdata/kubeconfig") + + providerConfig_config_context("test-context"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_config_path("./missing/file"), + ), + ExpectError: regexp.MustCompile("could not open kubeconfig"), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_config_path("./testdata/kubeconfig") + + providerConfig_token("test-token"), + ), + ExpectError: regexp.MustCompile(`"config_path": conflicts with token`), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_config_path("./testdata/kubeconfig") + + providerConfig_host("test-host"), + ), + ExpectError: regexp.MustCompile(`"config_path": conflicts with host`), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_config_path("./testdata/kubeconfig") + + providerConfig_cluster_ca_certificate("test-ca-cert"), + ), + ExpectError: regexp.MustCompile(`"config_path": conflicts with cluster_ca_certificate`), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_config_path("./testdata/kubeconfig") + + providerConfig_client_cert("test-client-cert"), + ), + ExpectError: regexp.MustCompile(`"config_path": conflicts with client_certificate`), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_config_path("./testdata/kubeconfig") + + providerConfig_client_key("test-client-key"), + ), + ExpectError: regexp.MustCompile(`"config_path": conflicts with client_key`), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccKubernetesProviderConfig_config_paths(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckInternal(t) }, + ProviderFactories: testAccProviderFactoriesInternal, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesProviderConfig( + providerConfig_config_paths(`["./testdata/kubeconfig", "./testdata/kubeconfig"]`), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_config_paths(`["./testdata/kubeconfig"]`) + + providerConfig_config_context("test-context"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_config_paths(`["./missing/file", "./testdata/kubeconfig"]`), + ), + ExpectError: regexp.MustCompile("could not open kubeconfig"), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_config_path("./internal/testdata/kubeconfig") + + providerConfig_config_paths(`["./testdata/kubeconfig", "./testdata/kubeconfig"]`), + ), + ExpectError: regexp.MustCompile(`"config_path": conflicts with config_paths`), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccKubernetesProviderConfig_config_paths_env(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckInternal_setEnv(t, map[string]string{ + "KUBE_CONFIG_PATHS": strings.Join([]string{ + "./testdata/kubeconfig", + "./testdata/kubeconfig", + }, string(os.PathListSeparator)), + }) + }, + ProviderFactories: testAccProviderFactoriesInternal, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesProviderConfig("# empty"), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig("# empty"), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccKubernetesProviderConfig_config_paths_env_wantError(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckInternal_setEnv(t, map[string]string{ + "KUBE_CONFIG_PATHS": strings.Join([]string{ + "./testdata/kubeconfig", + "./testdata/kubeconfig", + }, string(os.PathListSeparator)), + "KUBE_CONFIG_PATH": "./testdata/kubeconfig", + }) + }, + ProviderFactories: testAccProviderFactoriesInternal, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesProviderConfig("# empty"), + ExpectError: regexp.MustCompile(`"config_path": conflicts with config_paths`), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccKubernetesProviderConfig_host_env_wantError(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckInternal_setEnv(t, map[string]string{ + "KUBE_HOST": "test-host", + "KUBE_CONFIG_PATHS": strings.Join([]string{ + "./testdata/kubeconfig", + "./testdata/kubeconfig", + }, string(os.PathListSeparator)), + }) + }, + ProviderFactories: testAccProviderFactoriesInternal, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesProviderConfig("# empty"), + ExpectError: regexp.MustCompile(`"host": conflicts with config_paths`), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccKubernetesProviderConfig_host(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckInternal(t) }, + ProviderFactories: testAccProviderFactoriesInternal, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesProviderConfig( + providerConfig_host("https://test-host") + + providerConfig_token("test-token"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_host("http://test-host") + + providerConfig_token("test-token"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_host("https://127.0.0.1") + + providerConfig_token("test-token"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_host("test-host") + + providerConfig_token("test-token"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + ExpectError: regexp.MustCompile(`Error: expected "host" to have a host, got test-host`), + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_exec("test-exec"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + // Error: "exec": all of `host,exec` must be specified + ExpectError: regexp.MustCompile("exec,host"), + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_exec("test-exec") + + providerConfig_cluster_ca_certificate("test-ca-cert"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + // Error: "exec": all of `cluster_ca_certificate,exec,host` must be specified + ExpectError: regexp.MustCompile("cluster_ca_certificate,exec,host"), + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_exec("test-exec") + + providerConfig_host("https://test-host") + + providerConfig_cluster_ca_certificate("test-ca-cert"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_token("test-token"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + // Error: "host": all of `host,token` must be specified + ExpectError: regexp.MustCompile("host,token"), + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_cluster_ca_certificate("test-cert"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + // Error: "cluster_ca_certificate": all of `cluster_ca_certificate,host` must be specified + ExpectError: regexp.MustCompile("cluster_ca_certificate,host"), + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_host("https://test-host") + + providerConfig_cluster_ca_certificate("test-cert"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_host("https://test-host") + + providerConfig_cluster_ca_certificate("test-cert") + + providerConfig_token("test-token"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + { + Config: testAccKubernetesProviderConfig( + providerConfig_host("https://test-host") + + providerConfig_cluster_ca_certificate("test-ca-cert") + + providerConfig_client_cert("test-client-cert"), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + // Error: "client_certificate": all of `client_certificate,client_key,cluster_ca_certificate,host` must be specified + ExpectError: regexp.MustCompile("client_certificate,client_key,cluster_ca_certificate,host"), + }, + }, + }) +} + +// testAccKubernetesProviderConfig is used together with the providerConfig_* functions +// to assemble a Kubernetes provider configuration with interchangeable options. +func testAccKubernetesProviderConfig(providerConfig string) string { + return fmt.Sprintf(`provider "kubernetes" { + %s +} + +# Needed for provider initialization. +resource kubernetes_namespace "test" { + metadata { + name = "tf-k8s-acc-test" + } +} +`, providerConfig) +} + +func providerConfig_config_path(path string) string { + return fmt.Sprintf(` config_path = "%s" +`, path) +} + +func providerConfig_config_context(context string) string { + return fmt.Sprintf(` config_context = "%s" +`, context) +} + +func providerConfig_config_paths(paths string) string { + return fmt.Sprintf(` config_paths = %s +`, paths) +} + +func providerConfig_token(token string) string { + return fmt.Sprintf(` token = "%s" +`, token) +} + +func providerConfig_cluster_ca_certificate(ca_cert string) string { + return fmt.Sprintf(` cluster_ca_certificate = "%s" +`, ca_cert) +} + +func providerConfig_client_cert(client_cert string) string { + return fmt.Sprintf(` client_certificate = "%s" +`, client_cert) +} + +func providerConfig_client_key(client_key string) string { + return fmt.Sprintf(` client_key = "%s" +`, client_key) +} + +func providerConfig_host(host string) string { + return fmt.Sprintf(` host = "%s" +`, host) +} + +func providerConfig_exec(clusterName string) string { + return fmt.Sprintf(` exec { + api_version = "client.authentication.k8s.io/v1alpha1" + args = ["eks", "get-token", "--cluster-name", "%s"] + command = "aws" + } +`, clusterName) +}