diff --git a/internal/framework/provider/provider.go b/internal/framework/provider/provider.go index 7c6c6bb18e..82418244a3 100644 --- a/internal/framework/provider/provider.go +++ b/internal/framework/provider/provider.go @@ -59,6 +59,8 @@ type KubernetesProviderModel struct { ProxyURL types.String `tfsdk:"proxy_url"` + ConfigDataBase64 types.String `tfsdk:"config_data_base64"` + IgnoreAnnotations types.List `tfsdk:"ignore_annotations"` IgnoreLabels types.List `tfsdk:"ignore_labels"` @@ -143,6 +145,10 @@ func (p *KubernetesProvider) Schema(ctx context.Context, req provider.SchemaRequ Description: "URL to the proxy to be used for all API requests", Optional: true, }, + "config_data_base64": schema.StringAttribute{ + Description: "Kubeconfig content in base64 format", + Optional: true, + }, "ignore_annotations": schema.ListAttribute{ ElementType: types.StringType, Description: "List of Kubernetes metadata annotations to ignore across all resources handled by this provider for situations where external systems are managing certain resource annotations. Each item is a regular expression.", diff --git a/kubernetes/provider.go b/kubernetes/provider.go index f0018379df..b23bb4147d 100644 --- a/kubernetes/provider.go +++ b/kubernetes/provider.go @@ -13,6 +13,8 @@ import ( "path/filepath" "strconv" + "github.com/hashicorp/terraform-provider-kubernetes/util" + "github.com/hashicorp/go-cty/cty" gversion "github.com/hashicorp/go-version" "github.com/mitchellh/go-homedir" @@ -21,7 +23,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" @@ -130,6 +131,13 @@ func Provider() *schema.Provider { Description: "URL to the proxy to be used for all API requests", DefaultFunc: schema.EnvDefaultFunc("KUBE_PROXY_URL", ""), }, + "config_data_base64": { + Type: schema.TypeString, + Optional: true, + Description: "Kubeconfig content in base64 format", + DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG_DATA_BASE64", nil), + ConflictsWith: []string{"config_path", "config_paths"}, + }, "exec": { Type: schema.TypeList, Optional: true, @@ -500,9 +508,9 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData, terraformVer func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, diag.Diagnostics) { diags := make(diag.Diagnostics, 0) overrides := &clientcmd.ConfigOverrides{} - loader := &clientcmd.ClientConfigLoadingRules{} + fileLoader := &clientcmd.ClientConfigLoadingRules{} - configPaths := []string{} + var configPaths []string if v, ok := d.Get("config_path").(string); ok && v != "" { configPaths = []string{v} @@ -517,7 +525,7 @@ func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, diag.D } if len(configPaths) > 0 { - expandedPaths := []string{} + var expandedPaths []string for _, p := range configPaths { path, err := homedir.Expand(p) if err != nil { @@ -529,9 +537,9 @@ func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, diag.D } if len(expandedPaths) == 1 { - loader.ExplicitPath = expandedPaths[0] + fileLoader.ExplicitPath = expandedPaths[0] } else { - loader.Precedence = expandedPaths + fileLoader.Precedence = expandedPaths } ctxSuffix := "; default context" @@ -631,7 +639,14 @@ func initializeConfiguration(d *schema.ResourceData) (*restclient.Config, diag.D overrides.ClusterDefaults.ProxyURL = v.(string) } + var configDataBase64 string + if v, ok := d.GetOk("config_data_base64"); ok { + configDataBase64 = v.(string) + } + + loader := util.NewConfigLoader(fileLoader, configDataBase64) cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides) + cfg, err := cc.ClientConfig() if err != nil { nd := diag.Diagnostic{ diff --git a/kubernetes/provider_test.go b/kubernetes/provider_test.go index 1158b51f34..6a0e18ddb3 100644 --- a/kubernetes/provider_test.go +++ b/kubernetes/provider_test.go @@ -5,6 +5,7 @@ package kubernetes import ( "context" + "encoding/base64" "errors" "fmt" "net/url" @@ -102,6 +103,24 @@ func TestProvider_configure_paths(t *testing.T) { } } +func TestProvider_configure_config_data_base64(t *testing.T) { + ctx := context.TODO() + resetEnv := unsetEnv(t) + defer resetEnv() + data, err := os.ReadFile("test-fixtures/kube-config-sa-token.yaml") + if err != nil { + t.Fatal("Cannot read kubeconfig") + } + data64 := base64.StdEncoding.EncodeToString(data) + os.Setenv("KUBE_CONFIG_DATA_BASE64", data64) + rc := terraform.NewResourceConfigRaw(map[string]interface{}{}) + p := Provider() + diags := p.Configure(ctx, rc) + if diags.HasError() { + t.Fatal(diags) + } +} + func unsetEnv(t *testing.T) func() { e := getEnv() @@ -120,6 +139,7 @@ func unsetEnv(t *testing.T) func() { "KUBE_INSECURE": e.Insecure, "KUBE_TLS_SERVER_NAME": e.TLSServerName, "KUBE_TOKEN": e.Token, + "KUBE_CONFIG_DATA_BASE64": e.ConfigDataBase64, } for k := range envVars { @@ -158,11 +178,15 @@ func getEnv() *currentEnv { if v := os.Getenv("KUBE_CONFIG_PATH"); v != "" { e.ConfigPaths = filepath.SplitList(v) } + if v := os.Getenv("KUBE_CONFIG_DATA_BASE64"); v != "" { + e.ConfigDataBase64 = v + } return e } func testAccPreCheck(t *testing.T) { ctx := context.TODO() + hasEnvCfg := os.Getenv("KUBE_CONFIG_DATA_BASE64") != "" hasFileCfg := (os.Getenv("KUBE_CTX_AUTH_INFO") != "" && os.Getenv("KUBE_CTX_CLUSTER") != "") || os.Getenv("KUBE_CTX") != "" || os.Getenv("KUBE_CONFIG_PATH") != "" @@ -172,7 +196,7 @@ func testAccPreCheck(t *testing.T) { os.Getenv("KUBE_CLUSTER_CA_CERT_DATA") != "") && (hasUserCredentials || hasClientCert || os.Getenv("KUBE_TOKEN") != "") - if !hasFileCfg && !hasStaticCfg && !hasUserCredentials { + if !hasEnvCfg && !hasFileCfg && !hasStaticCfg && !hasUserCredentials { t.Fatalf("File config (KUBE_CTX_AUTH_INFO and KUBE_CTX_CLUSTER) or static configuration"+ "(%s) or (%s) must be set for acceptance tests", strings.Join([]string{ @@ -482,6 +506,7 @@ func clusterVersionGreaterThanOrEqual(vs string) bool { type currentEnv struct { ConfigPath string + ConfigDataBase64 string ConfigPaths []string Ctx string CtxAuthInfo string diff --git a/kubernetes/test-fixtures/kube-config-sa-token.yaml b/kubernetes/test-fixtures/kube-config-sa-token.yaml new file mode 100644 index 0000000000..19a3a78206 --- /dev/null +++ b/kubernetes/test-fixtures/kube-config-sa-token.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Config +preferences: {} +clusters: + - cluster: + certificate-authority-data: ZHVtbXk= + server: https://127.0.0.1 + name: dummy + +current-context: dummy + +contexts: + - context: + cluster: dummy + user: dummy + name: dummy + +users: + - name: dummy + user: + token: ZHVtbXk= \ No newline at end of file diff --git a/manifest/provider/configure.go b/manifest/provider/configure.go index cd57f8a209..7a9e554d88 100644 --- a/manifest/provider/configure.go +++ b/manifest/provider/configure.go @@ -8,6 +8,7 @@ import ( "encoding/pem" "errors" "fmt" + "github.com/hashicorp/terraform-provider-kubernetes/util" "net/url" "os" "path/filepath" @@ -68,7 +69,7 @@ func (s *RawProviderServer) ConfigureProvider(ctx context.Context, req *tfprotov } overrides := &clientcmd.ConfigOverrides{} - loader := &clientcmd.ClientConfigLoadingRules{} + fileLoader := &clientcmd.ClientConfigLoadingRules{} // Handle 'config_path' attribute // @@ -101,8 +102,30 @@ func (s *RawProviderServer) ConfigureProvider(ctx context.Context, req *tfprotov Detail: fmt.Sprintf("'config_path' refers to an invalid path: %q: %v", configPathAbs, err), }) } - loader.ExplicitPath = configPathAbs + fileLoader.ExplicitPath = configPathAbs } + // Handle 'config_data_base64' attribute + // + var configDataBase64 string + + if !providerConfig["config_data_base64"].IsNull() && providerConfig["config_data_base64"].IsKnown() { + + err = providerConfig["config_data_base64"].As(&configDataBase64) + if err != nil { + // invalid attribute - this shouldn't happen, bail out now + response.Diagnostics = append(response.Diagnostics, &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Provider configuration: failed to extract 'config_data_base64' value", + Detail: err.Error(), + }) + return response, nil + } + } + // check environment - this overrides any value found in provider configuration + if configBase64Env, ok := os.LookupEnv("KUBE_CONFIG_DATA_BASE64"); ok && configBase64Env != "" { + configDataBase64 = configBase64Env + } + // Handle 'config_paths' attribute // var precedence []string @@ -143,7 +166,7 @@ func (s *RawProviderServer) ConfigureProvider(ctx context.Context, req *tfprotov } precedence[i] = absPath } - loader.Precedence = precedence + fileLoader.Precedence = precedence } // Handle 'client_certificate' attribute @@ -589,7 +612,7 @@ func (s *RawProviderServer) ConfigureProvider(ctx context.Context, req *tfprotov overrides.AuthInfo.Exec = &execCfg } } - + loader := util.NewConfigLoader(fileLoader, configDataBase64) cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides) clientConfig, err := cc.ClientConfig() if err != nil { diff --git a/manifest/provider/provider_config.go b/manifest/provider/provider_config.go index 2adfb1cc63..a109f245eb 100644 --- a/manifest/provider/provider_config.go +++ b/manifest/provider/provider_config.go @@ -178,6 +178,17 @@ func GetProviderConfigSchema() *tfprotov5.Schema { DescriptionKind: 0, Deprecated: false, }, + { + Name: "config_data_base64", + Type: tftypes.String, + Description: "Kubeconfig content in base64 format", + Required: false, + Optional: true, + Computed: false, + Sensitive: false, + DescriptionKind: 0, + Deprecated: false, + }, { Name: "ignore_annotations", Type: tftypes.List{ElementType: tftypes.String}, diff --git a/util/loader.go b/util/loader.go new file mode 100644 index 0000000000..768c3083f4 --- /dev/null +++ b/util/loader.go @@ -0,0 +1,38 @@ +package util + +import ( + "encoding/base64" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +func NewConfigLoader(loader *clientcmd.ClientConfigLoadingRules, configBase64Data string) *ConfigLoader { + return &ConfigLoader{ + ClientConfigLoadingRules: loader, + configBase64Data: configBase64Data, + } +} + +type ConfigLoader struct { + *clientcmd.ClientConfigLoadingRules + configBase64Data string +} + +func (cl ConfigLoader) Load() (*clientcmdapi.Config, error) { + if cl.configBase64Data == "" { + return cl.ClientConfigLoadingRules.Load() + } + data, err := base64.StdEncoding.DecodeString(cl.configBase64Data) + if err != nil { + return nil, err + } + cc, err := clientcmd.NewClientConfigFromBytes(data) + if err != nil { + return nil, err + } + cfg, err := cc.RawConfig() + if err != nil { + return nil, err + } + return &cfg, nil +}