Skip to content

Commit 6834e0e

Browse files
committed
Add ConflictsWith to provider config
In order to create a more explicit configuration, and more predictable behavior, this PR adds ConflictsWith to specific provider authentication attributes, effectively disallowing the use of conflicting methods of authentication, such as config_path and token. The provider now generates an error to tell the user that these options are mutually exclusive.
1 parent 34236b6 commit 6834e0e

File tree

3 files changed

+609
-74
lines changed

3 files changed

+609
-74
lines changed

GNUmakefile

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ TF_PROV_DOCS := $(PWD)/kubernetes/test-infra/tfproviderdocs
1010
EXT_PROV_DIR := $(PWD)/kubernetes/test-infra/external-providers
1111
EXT_PROV_BIN := /tmp/.terraform.d/localhost/test/kubernetes/9.9.9/$(OS_ARCH)/terraform-provider-kubernetes_9.9.9_$(OS_ARCH)
1212

13+
ifdef KUBE_CONFIG_PATHS
14+
KUBECONFIG1 := $(shell echo $(KUBE_CONFIG_PATHS) | cut -d\: -f1)
15+
else
16+
KUBECONFIG1 := $(shell echo $(KUBECONFIG) | cut -d\: -f1)
17+
endif
18+
1319
ifneq ($(PWD),$(PROVIDER_DIR))
1420
$(error "Makefile must be run from the provider directory")
1521
endif
@@ -57,11 +63,14 @@ test: fmtcheck
5763
xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4
5864

5965
testacc: fmtcheck vet
60-
rm -rf $(EXT_PROV_DIR)/.terraform $(EXT_PROV_DIR)/.terraform.lock.hcl || true
61-
mkdir $(EXT_PROV_DIR)/.terraform
62-
mkdir -p /tmp/.terraform.d/localhost/test/kubernetes/9.9.9/$(OS_ARCH) || true
63-
ls $(EXT_PROV_BIN) || go build -o $(EXT_PROV_BIN)
64-
cd $(EXT_PROV_DIR) && TF_CLI_CONFIG_FILE=$(EXT_PROV_DIR)/.terraformrc TF_PLUGIN_CACHE_DIR=$(EXT_PROV_DIR)/.terraform terraform init -upgrade
66+
@rm -rf kubernetes/testdata || true
67+
@mkdir kubernetes/testdata
68+
@cp $(KUBECONFIG1) kubernetes/testdata/kubeconfig || (echo "Please set KUBE_CONFIG_PATHS or KUBECONFIG environment variable"; exit 1)
69+
@rm -rf $(EXT_PROV_DIR)/.terraform $(EXT_PROV_DIR)/.terraform.lock.hcl || true
70+
@mkdir $(EXT_PROV_DIR)/.terraform
71+
@mkdir -p /tmp/.terraform.d/localhost/test/kubernetes/9.9.9/$(OS_ARCH) || true
72+
@ls $(EXT_PROV_BIN) || go build -o $(EXT_PROV_BIN)
73+
@cd $(EXT_PROV_DIR) && TF_CLI_CONFIG_FILE=$(EXT_PROV_DIR)/.terraformrc TF_PLUGIN_CACHE_DIR=$(EXT_PROV_DIR)/.terraform terraform init -upgrade
6574
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
6675

6776
test-compile:

kubernetes/provider.go

Lines changed: 169 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,20 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7-
"log"
8-
"net/http"
9-
"os"
10-
"path/filepath"
11-
127
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
13-
148
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging"
159
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1611
"github.com/mitchellh/go-homedir"
1712
apimachineryschema "k8s.io/apimachinery/pkg/runtime/schema"
1813
"k8s.io/client-go/discovery"
1914
"k8s.io/client-go/kubernetes"
2015
_ "k8s.io/client-go/plugin/pkg/client/auth"
2116
restclient "k8s.io/client-go/rest"
17+
"log"
18+
"net/http"
19+
"os"
20+
"path/filepath"
2221

2322
"k8s.io/client-go/tools/clientcmd"
2423
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
@@ -29,82 +28,117 @@ func Provider() *schema.Provider {
2928
p := &schema.Provider{
3029
Schema: map[string]*schema.Schema{
3130
"host": {
32-
Type: schema.TypeString,
33-
Optional: true,
34-
DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", ""),
35-
Description: "The hostname (in form of URI) of Kubernetes master.",
31+
Type: schema.TypeString,
32+
Optional: true,
33+
DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", nil),
34+
Description: "The hostname (in form of URI) of Kubernetes master.",
35+
ConflictsWith: []string{"config_path", "config_paths"},
36+
ValidateDiagFunc: validation.ToDiagFunc(validation.IsURLWithHTTPorHTTPS),
37+
// TODO: enable this when AtLeastOneOf works with optional attributes.
38+
// https://github.com/hashicorp/terraform-plugin-sdk/issues/705
39+
// AtLeastOneOf: []string{"token", "exec", "username", "password", "client_certificate", "client_key"},
3640
},
3741
"username": {
38-
Type: schema.TypeString,
39-
Optional: true,
40-
DefaultFunc: schema.EnvDefaultFunc("KUBE_USER", ""),
41-
Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
42+
Type: schema.TypeString,
43+
Optional: true,
44+
DefaultFunc: schema.EnvDefaultFunc("KUBE_USER", nil),
45+
Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
46+
ConflictsWith: []string{"config_path", "config_paths", "exec", "token", "client_certificate", "client_key"},
47+
RequiredWith: []string{"password", "host"},
4248
},
4349
"password": {
44-
Type: schema.TypeString,
45-
Optional: true,
46-
DefaultFunc: schema.EnvDefaultFunc("KUBE_PASSWORD", ""),
47-
Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
50+
Type: schema.TypeString,
51+
Optional: true,
52+
DefaultFunc: schema.EnvDefaultFunc("KUBE_PASSWORD", nil),
53+
Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
54+
ConflictsWith: []string{"config_path", "config_paths", "exec", "token", "client_certificate", "client_key"},
55+
RequiredWith: []string{"username", "host"},
4856
},
4957
"insecure": {
50-
Type: schema.TypeBool,
51-
Optional: true,
52-
DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", false),
53-
Description: "Whether server should be accessed without verifying the TLS certificate.",
58+
Type: schema.TypeBool,
59+
Optional: true,
60+
DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", nil),
61+
Description: "Whether server should be accessed without verifying the TLS certificate.",
62+
ConflictsWith: []string{"cluster_ca_certificate", "client_key", "client_certificate", "exec"},
5463
},
5564
"client_certificate": {
56-
Type: schema.TypeString,
57-
Optional: true,
58-
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", ""),
59-
Description: "PEM-encoded client certificate for TLS authentication.",
65+
Type: schema.TypeString,
66+
Optional: true,
67+
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", nil),
68+
Description: "PEM-encoded client certificate for TLS authentication.",
69+
ConflictsWith: []string{"config_path", "config_paths", "username", "password", "insecure"},
70+
RequiredWith: []string{"client_key", "cluster_ca_certificate", "host"},
6071
},
6172
"client_key": {
62-
Type: schema.TypeString,
63-
Optional: true,
64-
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", ""),
65-
Description: "PEM-encoded client certificate key for TLS authentication.",
73+
Type: schema.TypeString,
74+
Optional: true,
75+
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", nil),
76+
Description: "PEM-encoded client certificate key for TLS authentication.",
77+
ConflictsWith: []string{"config_path", "config_paths", "username", "password", "exec", "insecure"},
78+
RequiredWith: []string{"client_certificate", "cluster_ca_certificate", "host"},
6679
},
6780
"cluster_ca_certificate": {
68-
Type: schema.TypeString,
69-
Optional: true,
70-
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", ""),
71-
Description: "PEM-encoded root certificates bundle for TLS authentication.",
81+
Type: schema.TypeString,
82+
Optional: true,
83+
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", nil),
84+
Description: "PEM-encoded root certificates bundle for TLS authentication.",
85+
ConflictsWith: []string{"config_path", "config_paths", "insecure"},
86+
RequiredWith: []string{"host"},
87+
// TODO: enable this when AtLeastOneOf works with optional attributes.
88+
// https://github.com/hashicorp/terraform-plugin-sdk/issues/705
89+
// AtLeastOneOf: []string{"token", "exec", "client_certificate", "client_key"},
7290
},
7391
"config_paths": {
7492
Type: schema.TypeList,
7593
Elem: &schema.Schema{Type: schema.TypeString},
94+
DefaultFunc: configPathsEnv,
7695
Optional: true,
7796
Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.",
97+
// config_paths conflicts with every attribute except for "insecure", since all of these options will be read from the kubeconfig.
98+
ConflictsWith: []string{"config_path", "exec", "token", "host", "client_certificate", "client_key", "cluster_ca_certificate", "username", "password"},
7899
},
79100
"config_path": {
80-
Type: schema.TypeString,
81-
Optional: true,
82-
DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG_PATH", nil),
83-
Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH.",
84-
ConflictsWith: []string{"config_paths"},
85-
},
86-
"config_context": {
87101
Type: schema.TypeString,
88102
Optional: true,
89-
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", ""),
103+
DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG_PATH", nil),
104+
Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH.",
105+
// config_path conflicts with every attribute except for "insecure", since all of these options will be read from the kubeconfig.
106+
ConflictsWith: []string{"config_paths", "exec", "token", "host", "client_certificate", "client_key", "cluster_ca_certificate", "username", "password"},
107+
},
108+
"config_context": {
109+
Type: schema.TypeString,
110+
Optional: true,
111+
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", nil),
112+
Description: "Context to choose from the kube config file. ",
113+
ConflictsWith: []string{"exec", "token", "client_certificate", "client_key", "username", "password"},
114+
// TODO: enable this when AtLeastOneOf works with optional attributes.
115+
// AtLeastOneOf: []string{"config_path", "config_paths"},
90116
},
91117
"config_context_auth_info": {
92-
Type: schema.TypeString,
93-
Optional: true,
94-
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", ""),
95-
Description: "",
118+
Type: schema.TypeString,
119+
Optional: true,
120+
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", nil),
121+
Description: "Authentication info context of the kube config (name of the kubeconfig user, --user flag in kubectl).",
122+
ConflictsWith: []string{"exec", "token", "client_certificate", "client_key", "username", "password"},
123+
// TODO: enable this when AtLeastOneOf works with optional attributes.
124+
// AtLeastOneOf: []string{"config_path", "config_paths"},
96125
},
97126
"config_context_cluster": {
98-
Type: schema.TypeString,
99-
Optional: true,
100-
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", ""),
101-
Description: "",
127+
Type: schema.TypeString,
128+
Optional: true,
129+
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", nil),
130+
Description: "Cluster context of the kube config (name of the kubeconfig cluster, --cluster flag in kubectl).",
131+
ConflictsWith: []string{"exec", "token", "client_certificate", "client_key", "username", "password"},
132+
// TODO: enable this when AtLeastOneOf works with optional attributes.
133+
// AtLeastOneOf: []string{"config_path", "config_paths"},
102134
},
103135
"token": {
104-
Type: schema.TypeString,
105-
Optional: true,
106-
DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", ""),
107-
Description: "Token to authenticate an service account",
136+
Type: schema.TypeString,
137+
Optional: true,
138+
DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", nil),
139+
Description: "Bearer token for authenticating the Kubernetes API.",
140+
ConflictsWith: []string{"config_path", "config_paths", "exec", "client_certificate", "client_key", "username", "password"},
141+
RequiredWith: []string{"host"},
108142
},
109143
"exec": {
110144
Type: schema.TypeList,
@@ -132,7 +166,9 @@ func Provider() *schema.Provider {
132166
},
133167
},
134168
},
135-
Description: "",
169+
Description: "Configuration block to use an exec-based credential plugin, e.g. call an external command to receive user credentials.",
170+
ConflictsWith: []string{"config_path", "config_paths", "token", "client_certificate", "client_key", "username", "password", "insecure"},
171+
RequiredWith: []string{"host", "cluster_ca_certificate"},
136172
},
137173
},
138174

@@ -194,6 +230,22 @@ func Provider() *schema.Provider {
194230
return p
195231
}
196232

233+
// configPathsEnv fetches the value of the environment variable KUBE_CONFIG_PATHS, if defined.
234+
func configPathsEnv() (interface{}, error) {
235+
value, exists := os.LookupEnv("KUBE_CONFIG_PATHS")
236+
if exists {
237+
log.Print("[DEBUG] using environment variable KUBE_CONFIG_PATHS to define config_paths")
238+
log.Printf("[DEBUG] value of KUBE_CONFIG_PATHS: %v", value)
239+
pathList := filepath.SplitList(value)
240+
configPaths := new([]interface{})
241+
for _, p := range pathList {
242+
*configPaths = append(*configPaths, p)
243+
}
244+
return *configPaths, nil
245+
}
246+
return nil, nil
247+
}
248+
197249
type KubeClientsets interface {
198250
MainClientset() (*kubernetes.Clientset, error)
199251
AggregatorClientset() (*aggregator.Clientset, error)
@@ -212,6 +264,10 @@ func (k kubeClientsets) MainClientset() (*kubernetes.Clientset, error) {
212264
return k.mainClientset, nil
213265
}
214266

267+
if err := checkConfigurationValid(k.configData); err != nil {
268+
return nil, err
269+
}
270+
215271
if k.config != nil {
216272
kc, err := kubernetes.NewForConfig(k.config)
217273
if err != nil {
@@ -236,6 +292,52 @@ func (k kubeClientsets) AggregatorClientset() (*aggregator.Clientset, error) {
236292
return k.aggregatorClientset, nil
237293
}
238294

295+
var apiTokenMountPath = "/var/run/secrets/kubernetes.io/serviceaccount"
296+
297+
func inCluster() bool {
298+
host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
299+
if host == "" || port == "" {
300+
return false
301+
}
302+
303+
if _, err := os.Stat(apiTokenMountPath); err != nil {
304+
return false
305+
}
306+
return true
307+
}
308+
309+
var authDocumentationURL = "https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs#authentication"
310+
311+
func checkConfigurationValid(d *schema.ResourceData) error {
312+
if inCluster() {
313+
log.Printf("[DEBUG] Terraform appears to be running inside the Kubernetes cluster")
314+
return nil
315+
}
316+
317+
if os.Getenv("KUBE_CONFIG_PATHS") != "" {
318+
return nil
319+
}
320+
321+
atLeastOneOf := []string{
322+
"host",
323+
"config_path",
324+
"config_paths",
325+
"client_certificate",
326+
"token",
327+
"exec",
328+
}
329+
for _, a := range atLeastOneOf {
330+
if _, ok := d.GetOk(a); ok {
331+
return nil
332+
}
333+
}
334+
335+
return fmt.Errorf(`provider not configured: you must configure a path to your kubeconfig
336+
or explicitly supply credentials via the provider block or environment variables.
337+
338+
See our documentation at: %s`, authDocumentationURL)
339+
}
340+
239341
func providerConfigure(ctx context.Context, d *schema.ResourceData, terraformVersion string) (interface{}, diag.Diagnostics) {
240342
// Config initialization
241343
cfg, err := initializeConfiguration(d)
@@ -270,7 +372,9 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData, terraformVer
270372

271373
func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, error) {
272374
overrides := &clientcmd.ConfigOverrides{}
273-
loader := &clientcmd.ClientConfigLoadingRules{}
375+
loader := &clientcmd.ClientConfigLoadingRules{
376+
WarnIfAllMissing: true,
377+
}
274378

275379
configPaths := []string{}
276380

@@ -280,10 +384,6 @@ func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, error)
280384
for _, p := range v {
281385
configPaths = append(configPaths, p.(string))
282386
}
283-
} else if v := os.Getenv("KUBE_CONFIG_PATHS"); v != "" {
284-
// NOTE we have to do this here because the schema
285-
// does not yet allow you to set a default for a TypeList
286-
configPaths = filepath.SplitList(v)
287387
}
288388

289389
if len(configPaths) > 0 {
@@ -310,7 +410,7 @@ func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, error)
310410
authInfo, authInfoOk := d.GetOk("config_context_auth_info")
311411
cluster, clusterOk := d.GetOk("config_context_cluster")
312412
if ctxOk || authInfoOk || clusterOk {
313-
ctxSuffix = "; overriden context"
413+
ctxSuffix = "; overridden context"
314414
if ctxOk {
315415
overrides.CurrentContext = kubectx.(string)
316416
ctxSuffix += fmt.Sprintf("; config ctx: %s", overrides.CurrentContext)
@@ -331,16 +431,16 @@ func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, error)
331431
}
332432

333433
// Overriding with static configuration
334-
if v, ok := d.GetOk("insecure"); ok {
434+
if v, ok := d.GetOk("insecure"); ok && v != "" {
335435
overrides.ClusterInfo.InsecureSkipTLSVerify = v.(bool)
336436
}
337-
if v, ok := d.GetOk("cluster_ca_certificate"); ok {
437+
if v, ok := d.GetOk("cluster_ca_certificate"); ok && v != "" {
338438
overrides.ClusterInfo.CertificateAuthorityData = bytes.NewBufferString(v.(string)).Bytes()
339439
}
340-
if v, ok := d.GetOk("client_certificate"); ok {
440+
if v, ok := d.GetOk("client_certificate"); ok && v != "" {
341441
overrides.AuthInfo.ClientCertificateData = bytes.NewBufferString(v.(string)).Bytes()
342442
}
343-
if v, ok := d.GetOk("host"); ok {
443+
if v, ok := d.GetOk("host"); ok && v != "" {
344444
// Server has to be the complete address of the kubernetes cluster (scheme://hostname:port), not just the hostname,
345445
// because `overrides` are processed too late to be taken into account by `defaultServerUrlFor()`.
346446
// This basically replicates what defaultServerUrlFor() does with config but for overrides,
@@ -355,16 +455,16 @@ func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, error)
355455

356456
overrides.ClusterInfo.Server = host.String()
357457
}
358-
if v, ok := d.GetOk("username"); ok {
458+
if v, ok := d.GetOk("username"); ok && v != "" {
359459
overrides.AuthInfo.Username = v.(string)
360460
}
361-
if v, ok := d.GetOk("password"); ok {
461+
if v, ok := d.GetOk("password"); ok && v != "" {
362462
overrides.AuthInfo.Password = v.(string)
363463
}
364-
if v, ok := d.GetOk("client_key"); ok {
464+
if v, ok := d.GetOk("client_key"); ok && v != "" {
365465
overrides.AuthInfo.ClientKeyData = bytes.NewBufferString(v.(string)).Bytes()
366466
}
367-
if v, ok := d.GetOk("token"); ok {
467+
if v, ok := d.GetOk("token"); ok && v != "" {
368468
overrides.AuthInfo.Token = v.(string)
369469
}
370470

0 commit comments

Comments
 (0)