// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package helm import ( "context" "fmt" "os" "path/filepath" "sync" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/mitchellh/go-homedir" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" "k8s.io/client-go/discovery/cached/memory" "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) // Struct holding k8s client config, burst limit for api requests, and mutex for sync type KubeConfig struct { ClientConfig clientcmd.ClientConfig Burst int sync.Mutex } // Converting KubeConfig to a REST config, which will be used to create k8s clients func (k *KubeConfig) ToRESTConfig() (*rest.Config, error) { config, err := k.ToRawKubeConfigLoader().ClientConfig() return config, err } // Converting KubeConfig to a discovery client, which will be used to find api resources func (k *KubeConfig) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { config, err := k.ToRESTConfig() if err != nil { return nil, err } config.Burst = k.Burst return memory.NewMemCacheClient(discovery.NewDiscoveryClientForConfigOrDie(config)), nil } // Converting KubeConfig to a REST mapper, which will be used to map REST resources to their API obj func (k *KubeConfig) ToRESTMapper() (meta.RESTMapper, error) { discoveryClient, err := k.ToDiscoveryClient() if err != nil { return nil, err } // Using the appropriate types for the arguments mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) warningHandler := func(warning string) { fmt.Printf("Warning: %s\n", warning) } // Pass the warning handler to the NewShortcutExpander function expander := restmapper.NewShortcutExpander(mapper, discoveryClient, warningHandler) return expander, nil } // Function returning raw k8s client config func (k *KubeConfig) ToRawKubeConfigLoader() clientcmd.ClientConfig { return k.ClientConfig } // Generates a k8s client config, based on providers settings and namespace, which this config will be used to interact with the k8s cluster func (m *Meta) NewKubeConfig(ctx context.Context, namespace string) (*KubeConfig, error) { overrides := &clientcmd.ConfigOverrides{} loader := &clientcmd.ClientConfigLoadingRules{} configPaths := []string{} if m == nil || m.Data == nil || m.Data.Kubernetes.IsNull() || m.Data.Kubernetes.IsUnknown() { return nil, fmt.Errorf("configuration error: missing required structural data") } tflog.Debug(ctx, "Raw Kubernetes Data before conversion", map[string]interface{}{ "KubernetesData": m.Data.Kubernetes, }) // Needing to get the Kubernetes configuration as an obj var kubernetesConfig KubernetesConfigModel diags := m.Data.Kubernetes.As(ctx, &kubernetesConfig, basetypes.ObjectAsOptions{}) if diags.HasError() { for _, d := range diags { tflog.Error(ctx, "Kubernetes config conversion error", map[string]interface{}{ "summary": d.Summary(), "detail": d.Detail(), }) } return nil, fmt.Errorf("configuration error: unable to extract Kubernetes config") } // Check ConfigPath if !kubernetesConfig.ConfigPath.IsNull() { if v := kubernetesConfig.ConfigPath.ValueString(); v != "" { configPaths = []string{v} } } if !kubernetesConfig.ConfigPaths.IsNull() { additionalPaths := expandStringSlice(kubernetesConfig.ConfigPaths.Elements()) configPaths = append(configPaths, additionalPaths...) } if v := os.Getenv("KUBE_CONFIG_PATHS"); v != "" { configPaths = filepath.SplitList(v) } tflog.Debug(ctx, "Initial configPaths", map[string]interface{}{"configPaths": configPaths}) if len(configPaths) > 0 { expandedPaths := []string{} for _, p := range configPaths { path, err := homedir.Expand(p) if err != nil { tflog.Error(ctx, "Error expanding home directory", map[string]interface{}{ "path": p, "error": err, }) return nil, err } expandedPaths = append(expandedPaths, path) } if len(expandedPaths) == 1 { loader.ExplicitPath = expandedPaths[0] } else { loader.Precedence = expandedPaths } // Check ConfigContext if !kubernetesConfig.ConfigContext.IsNull() { overrides.CurrentContext = kubernetesConfig.ConfigContext.ValueString() } if !kubernetesConfig.ConfigContextAuthInfo.IsNull() { overrides.Context.AuthInfo = kubernetesConfig.ConfigContextAuthInfo.ValueString() } if !kubernetesConfig.ConfigContextCluster.IsNull() { overrides.Context.Cluster = kubernetesConfig.ConfigContextCluster.ValueString() } } // Check and assign remaining fields if !kubernetesConfig.Insecure.IsNull() { overrides.ClusterInfo.InsecureSkipTLSVerify = kubernetesConfig.Insecure.ValueBool() } if !kubernetesConfig.TLSServerName.IsNull() { overrides.ClusterInfo.TLSServerName = kubernetesConfig.TLSServerName.ValueString() } if !kubernetesConfig.ClusterCACertificate.IsNull() { overrides.ClusterInfo.CertificateAuthorityData = []byte(kubernetesConfig.ClusterCACertificate.ValueString()) } if !kubernetesConfig.ClientCertificate.IsNull() { overrides.AuthInfo.ClientCertificateData = []byte(kubernetesConfig.ClientCertificate.ValueString()) } if !kubernetesConfig.Host.IsNull() && kubernetesConfig.Host.ValueString() != "" { hasCA := len(overrides.ClusterInfo.CertificateAuthorityData) != 0 hasCert := len(overrides.AuthInfo.ClientCertificateData) != 0 defaultTLS := hasCA || hasCert || overrides.ClusterInfo.InsecureSkipTLSVerify host, _, err := rest.DefaultServerURL(kubernetesConfig.Host.ValueString(), "", schema.GroupVersion{}, defaultTLS) if err != nil { return nil, err } overrides.ClusterInfo.Server = host.String() } if !kubernetesConfig.Username.IsNull() { overrides.AuthInfo.Username = kubernetesConfig.Username.ValueString() } if !kubernetesConfig.Password.IsNull() { overrides.AuthInfo.Password = kubernetesConfig.Password.ValueString() } if !kubernetesConfig.ClientKey.IsNull() { overrides.AuthInfo.ClientKeyData = []byte(kubernetesConfig.ClientKey.ValueString()) } if !kubernetesConfig.Token.IsNull() { overrides.AuthInfo.Token = kubernetesConfig.Token.ValueString() } if !kubernetesConfig.ProxyURL.IsNull() { overrides.ClusterDefaults.ProxyURL = kubernetesConfig.ProxyURL.ValueString() } if kubernetesConfig.Exec != nil { execConfig := kubernetesConfig.Exec if !execConfig.APIVersion.IsNull() && !execConfig.Command.IsNull() { args := []string{} if !execConfig.Args.IsNull() && !execConfig.Args.IsUnknown() { args = expandStringSlice(execConfig.Args.Elements()) } env := []clientcmdapi.ExecEnvVar{} if !execConfig.Env.IsNull() && !execConfig.Env.IsUnknown() { for k, v := range execConfig.Env.Elements() { env = append(env, clientcmdapi.ExecEnvVar{ Name: k, Value: v.(types.String).ValueString(), }) } } overrides.AuthInfo.Exec = &clientcmdapi.ExecConfig{ APIVersion: execConfig.APIVersion.ValueString(), Command: execConfig.Command.ValueString(), Args: args, Env: env, InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode, } } } overrides.Context.Namespace = "default" if namespace != "" { overrides.Context.Namespace = namespace } burstLimit := int(m.Data.BurstLimit.ValueInt64()) client := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides) if client == nil { return nil, fmt.Errorf("failed to initialize kubernetes config") } tflog.Info(ctx, "Successfully initialized kubernetes config") return &KubeConfig{ClientConfig: client, Burst: burstLimit}, nil } func expandStringSlice(input []attr.Value) []string { result := make([]string, len(input)) for i, v := range input { result[i] = v.(types.String).ValueString() } return result }