diff --git a/internal/backend/remote-state/kubernetes/backend.go b/internal/backend/remote-state/kubernetes/backend.go index 85d3037c6734..8ba390dfb177 100644 --- a/internal/backend/remote-state/kubernetes/backend.go +++ b/internal/backend/remote-state/kubernetes/backend.go @@ -5,16 +5,13 @@ package kubernetes import ( "bytes" - "context" "fmt" "log" "os" "path/filepath" - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" - "github.com/hashicorp/terraform/version" "github.com/mitchellh/go-homedir" + "github.com/zclconf/go-cty/cty" k8sSchema "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" @@ -22,6 +19,12 @@ import ( restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendbase" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/hashicorp/terraform/version" ) // Modified from github.com/terraform-providers/terraform-provider-kubernetes @@ -44,158 +47,191 @@ var ( // New creates a new backend for kubernetes remote state. func New() backend.Backend { - s := &schema.Backend{ - Schema: map[string]*schema.Schema{ - "secret_suffix": { - Type: schema.TypeString, - Required: true, - Description: "Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`.", - }, - "labels": { - Type: schema.TypeMap, - Optional: true, - Description: "Map of additional labels to be applied to the secret.", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "namespace": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_NAMESPACE", "default"), - Description: "Namespace to store the secret in.", - }, - "in_cluster_config": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_IN_CLUSTER_CONFIG", false), - Description: "Used to authenticate to the cluster from inside a pod.", - }, - "load_config_file": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_LOAD_CONFIG_FILE", true), - Description: "Load local kubeconfig.", - }, - "host": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", ""), - Description: "The hostname (in form of URI) of Kubernetes master.", - }, - "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.", - }, - "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.", - }, - "insecure": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", false), - Description: "Whether server should be accessed without verifying the TLS certificate.", - }, - "client_certificate": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", ""), - Description: "PEM-encoded client certificate for TLS authentication.", - }, - "client_key": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", ""), - Description: "PEM-encoded client certificate key for TLS authentication.", - }, - "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.", - }, - "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.EnvDefaultFunc("KUBE_CONFIG_PATH", ""), - Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH environment variable.", - }, - "config_context": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", ""), - }, - "config_context_auth_info": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", ""), - Description: "", - }, - "config_context_cluster": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", ""), - Description: "", - }, - "token": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", ""), - Description: "Token to authentifcate a service account.", - }, - "proxy_url": { - Type: schema.TypeString, - Optional: true, - Description: "URL to the proxy to be used for all API requests", - DefaultFunc: schema.EnvDefaultFunc("KUBE_PROXY_URL", ""), - }, - "exec": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "api_version": { - Type: schema.TypeString, - Required: true, - }, - "command": { - Type: schema.TypeString, - Required: true, - }, - "env": { - Type: schema.TypeMap, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "args": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, + return &Backend{ + Base: backendbase.Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "secret_suffix": { + Type: cty.String, + Required: true, + Description: "Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`.", + }, + "labels": { + Type: cty.Map(cty.String), + Optional: true, + Description: "Map of additional labels to be applied to the secret.", + }, + "namespace": { + Type: cty.String, + Optional: true, + Description: "Namespace to store the secret in.", + }, + "in_cluster_config": { + Type: cty.Bool, + Optional: true, + Description: "Used to authenticate to the cluster from inside a pod.", + }, + "load_config_file": { + Type: cty.Bool, + Optional: true, + Description: "Load local kubeconfig.", + }, + "host": { + Type: cty.String, + Optional: true, + Description: "The hostname (in form of URI) of Kubernetes master.", + }, + "username": { + Type: cty.String, + Optional: true, + Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", + }, + "password": { + Type: cty.String, + Optional: true, + Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", + }, + "insecure": { + Type: cty.Bool, + Optional: true, + Description: "Whether server should be accessed without verifying the TLS certificate.", + }, + "client_certificate": { + Type: cty.String, + Optional: true, + Description: "PEM-encoded client certificate for TLS authentication.", + }, + "client_key": { + Type: cty.String, + Optional: true, + Description: "PEM-encoded client certificate key for TLS authentication.", + }, + "cluster_ca_certificate": { + Type: cty.String, + Optional: true, + Description: "PEM-encoded root certificates bundle for TLS authentication.", + }, + "config_paths": { + Type: cty.List(cty.String), + Optional: true, + Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.", + }, + "config_path": { + Type: cty.String, + Optional: true, + Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH environment variable.", + }, + "config_context": { + Type: cty.String, + Optional: true, + }, + "config_context_auth_info": { + Type: cty.String, + Optional: true, + Description: "", + }, + "config_context_cluster": { + Type: cty.String, + Optional: true, + Description: "", + }, + "token": { + Type: cty.String, + Optional: true, + Description: "Token to authentifcate a service account.", + }, + "proxy_url": { + Type: cty.String, + Optional: true, + Description: "URL to the proxy to be used for all API requests", + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "exec": { + Nesting: configschema.NestingSingle, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "api_version": { + Type: cty.String, + Required: true, + }, + "command": { + Type: cty.String, + Required: true, + }, + "env": { + Type: cty.Map(cty.String), + Optional: true, + }, + "args": { + Type: cty.List(cty.String), + Optional: true, + }, + }, }, }, }, - Description: "Use a credential plugin to authenticate.", + }, + SDKLikeDefaults: backendbase.SDKLikeDefaults{ + "namespace": { + EnvVars: []string{"KUBE_NAMESPACE"}, + Fallback: "default", + }, + "in_cluster_config": { + EnvVars: []string{"KUBE_IN_CLUSTER_CONFIG"}, + Fallback: "false", + }, + "load_config_file": { + EnvVars: []string{"KUBE_LOAD_CONFIG_FILE"}, + Fallback: "true", + }, + "host": { + EnvVars: []string{"KUBE_HOST"}, + }, + "username": { + EnvVars: []string{"KUBE_USER"}, + }, + "password": { + EnvVars: []string{"KUBE_PASSWORD"}, + }, + "insecure": { + EnvVars: []string{"KUBE_INSECURE"}, + Fallback: "false", + }, + "client_certificate": { + EnvVars: []string{"KUBE_CLIENT_CERT_DATA"}, + }, + "client_key": { + EnvVars: []string{"KUBE_CLIENT_KEY_DATA"}, + }, + "cluster_ca_certificate": { + EnvVars: []string{"KUBE_CLUSTER_CA_CERT_DATA"}, + }, + "config_path": { + EnvVars: []string{"KUBE_CONFIG_PATH"}, + }, + "config_context": { + EnvVars: []string{"KUBE_CTX"}, + }, + "config_context_auth_info": { + EnvVars: []string{"KUBE_CTX_AUTH_INFO"}, + }, + "config_context_cluster": { + EnvVars: []string{"KUBE_CTX_CLUSTER"}, + }, + "token": { + EnvVars: []string{"KUBE_TOKEN"}, + }, + "proxy_url": { + EnvVars: []string{"KUBE_PROXY_URL"}, + }, }, }, } - - result := &Backend{Backend: s} - result.Backend.ConfigureFunc = result.configure - return result } type Backend struct { - *schema.Backend + backendbase.Base // The fields below are set from configure kubernetesSecretClient dynamic.ResourceInterface @@ -234,68 +270,69 @@ func (b Backend) KubernetesLeaseClient() (coordinationv1.LeaseInterface, error) return b.kubernetesLeaseClient, nil } -func (b *Backend) configure(ctx context.Context) error { +func (b *Backend) Configure(configVal cty.Value) tfdiags.Diagnostics { if b.config != nil { return nil } - // Grab the resource data - data := schema.FromContextBackendConfig(ctx) + data := backendbase.NewSDKLikeData(configVal) cfg, err := getInitialConfig(data) if err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } // Overriding with static configuration cfg.UserAgent = fmt.Sprintf("HashiCorp/1.0 Terraform/%s", version.String()) - if v, ok := data.GetOk("host"); ok { - cfg.Host = v.(string) - } - if v, ok := data.GetOk("username"); ok { - cfg.Username = v.(string) + if v := data.String("host"); v != "" { + cfg.Host = v } - if v, ok := data.GetOk("password"); ok { - cfg.Password = v.(string) + if v := data.String("username"); v != "" { + cfg.Username = v } - if v, ok := data.GetOk("insecure"); ok { - cfg.Insecure = v.(bool) + if v := data.String("password"); v != "" { + cfg.Password = v } - if v, ok := data.GetOk("cluster_ca_certificate"); ok { - cfg.CAData = bytes.NewBufferString(v.(string)).Bytes() + cfg.Insecure = data.Bool("insecure") + if v := data.String("cluster_ca_certificate"); v != "" { + cfg.CAData = bytes.NewBufferString(v).Bytes() } - if v, ok := data.GetOk("client_certificate"); ok { - cfg.CertData = bytes.NewBufferString(v.(string)).Bytes() + if v := data.String("client_certificate"); v != "" { + cfg.CertData = bytes.NewBufferString(v).Bytes() } - if v, ok := data.GetOk("client_key"); ok { - cfg.KeyData = bytes.NewBufferString(v.(string)).Bytes() + if v := data.String("client_key"); v != "" { + cfg.KeyData = bytes.NewBufferString(v).Bytes() } - if v, ok := data.GetOk("token"); ok { - cfg.BearerToken = v.(string) + if v := data.String("token"); v != "" { + cfg.BearerToken = v } - if v, ok := data.GetOk("labels"); ok { + if v := data.GetAttr("labels", cty.Map(cty.String)); !v.IsNull() { labels := map[string]string{} - for k, vv := range v.(map[string]interface{}) { - labels[k] = vv.(string) + for it := v.ElementIterator(); it.Next(); { + kV, vV := it.Element() + if vV.IsNull() { + vV = cty.StringVal("") + } + labels[kV.AsString()] = vV.AsString() } b.labels = labels } - ns := data.Get("namespace").(string) + ns := data.String("namespace") b.namespace = ns - b.nameSuffix = data.Get("secret_suffix").(string) + b.nameSuffix = data.String("secret_suffix") b.config = cfg return nil } -func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) { +func getInitialConfig(data backendbase.SDKLikeData) (*restclient.Config, error) { var cfg *restclient.Config var err error - inCluster := data.Get("in_cluster_config").(bool) + inCluster := data.Bool("in_cluster_config") if inCluster { cfg, err = restclient.InClusterConfig() if err != nil { @@ -314,16 +351,14 @@ func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) { return cfg, err } -func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) { +func tryLoadingConfigFile(d backendbase.SDKLikeData) (*restclient.Config, error) { loader := &clientcmd.ClientConfigLoadingRules{} configPaths := []string{} - if v, ok := d.Get("config_path").(string); ok && v != "" { + if v := d.String("config_path"); 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 := d.GetAttr("config_paths", cty.List(cty.String)); !v.IsNull() { + configPaths = append(configPaths, decodeListOfString(v)...) } else if v := os.Getenv("KUBE_CONFIG_PATHS"); v != "" { configPaths = filepath.SplitList(v) } @@ -348,46 +383,56 @@ func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) { 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 { + configCtx := d.String("config_context") + authInfo := d.String("config_context_auth_info") + cluster := d.String("config_context_cluster") + if configCtx != "" || authInfo != "" || cluster != "" { ctxSuffix = "; overriden context" - if ctxOk { - overrides.CurrentContext = ctx.(string) + if configCtx != "" { + overrides.CurrentContext = configCtx 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) + if authInfo != "" { + overrides.Context.AuthInfo = authInfo ctxSuffix += fmt.Sprintf("; auth_info: %s", overrides.Context.AuthInfo) } - if clusterOk { - overrides.Context.Cluster = cluster.(string) + if cluster != "" { + overrides.Context.Cluster = cluster ctxSuffix += fmt.Sprintf("; cluster: %s", overrides.Context.Cluster) } log.Printf("[DEBUG] Using overidden context: %#v", overrides.Context) } - 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)}) + // exec is a nested block with nesting mode NestingSingle, so GetAttr + // will return a value of an object type that will be null if the block + // isn't present at all. + if v := d.GetAttr("exec", cty.DynamicPseudoType); !v.IsNull() { + spec := backendbase.NewSDKLikeData(v) + exec := &clientcmdapi.ExecConfig{ + APIVersion: spec.String("api_version"), + Command: spec.String("command"), + Args: decodeListOfString(spec.GetAttr("args", cty.List(cty.String))), + } + if envMap := spec.GetAttr("env", cty.Map(cty.String)); !envMap.IsNull() { + for it := envMap.ElementIterator(); it.Next(); { + kV, vV := it.Element() + if vV.IsNull() { + vV = cty.StringVal("") + } + exec.Env = append(exec.Env, clientcmdapi.ExecEnvVar{ + Name: kV.AsString(), + Value: vV.AsString(), + }) } - } else { - return nil, fmt.Errorf("Failed to parse exec") } overrides.AuthInfo.Exec = exec } - if v, ok := d.GetOk("proxy_url"); ok { - overrides.ClusterDefaults.ProxyURL = v.(string) + if v := d.String("proxy_url"); v != "" { + overrides.ClusterDefaults.ProxyURL = v } cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides) @@ -404,15 +449,18 @@ func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) { return cfg, nil } -func expandStringSlice(s []interface{}) []string { - result := make([]string, len(s), len(s)) - for k, v := range s { - // Handle the Terraform parser bug which turns empty strings in lists to nil. - if v == nil { - result[k] = "" +func decodeListOfString(v cty.Value) []string { + if v.IsNull() { + return nil + } + ret := make([]string, 0, v.LengthInt()) + for it := v.ElementIterator(); it.Next(); { + _, vV := it.Element() + if vV.IsNull() { + ret = append(ret, "") } else { - result[k] = v.(string) + ret = append(ret, vV.AsString()) } } - return result + return ret } diff --git a/internal/backend/remote-state/kubernetes/go.mod b/internal/backend/remote-state/kubernetes/go.mod index f75f81932b2b..bb51e40246f2 100644 --- a/internal/backend/remote-state/kubernetes/go.mod +++ b/internal/backend/remote-state/kubernetes/go.mod @@ -4,8 +4,8 @@ go 1.22.5 require ( github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 - github.com/hashicorp/terraform/internal/legacy v0.0.0-00010101000000-000000000000 github.com/mitchellh/go-homedir v1.1.0 + github.com/zclconf/go-cty v1.15.0 k8s.io/api v0.25.5 k8s.io/apimachinery v0.25.5 k8s.io/client-go v0.25.5 @@ -49,10 +49,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailru/easyjson v0.7.6 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -60,7 +57,6 @@ require ( github.com/spf13/afero v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.4 // indirect - github.com/zclconf/go-cty v1.15.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.23.0 // indirect @@ -99,6 +95,4 @@ replace github.com/hashicorp/terraform/internal/backend/remote-state/pg => ../pg replace github.com/hashicorp/terraform/internal/backend/remote-state/s3 => ../s3 -replace github.com/hashicorp/terraform/internal/legacy => ../../../legacy - replace github.com/hashicorp/terraform => ../../../.. diff --git a/internal/backend/remote-state/kubernetes/go.sum b/internal/backend/remote-state/kubernetes/go.sum index 47c5b4b80e27..80d0a20d0881 100644 --- a/internal/backend/remote-state/kubernetes/go.sum +++ b/internal/backend/remote-state/kubernetes/go.sum @@ -261,18 +261,12 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/internal/backend/remote-state/pg/backend.go b/internal/backend/remote-state/pg/backend.go index 54a069b6489e..7ba0fd3f8816 100644 --- a/internal/backend/remote-state/pg/backend.go +++ b/internal/backend/remote-state/pg/backend.go @@ -4,15 +4,16 @@ package pg import ( - "context" "database/sql" "fmt" - "os" - "strconv" - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" "github.com/lib/pq" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendbase" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" ) const ( @@ -20,94 +21,93 @@ const ( statesIndexName = "states_by_name" ) -func defaultBoolFunc(k string, dv bool) schema.SchemaDefaultFunc { - return func() (interface{}, error) { - if v := os.Getenv(k); v != "" { - return strconv.ParseBool(v) - } - - return dv, nil - } -} - // New creates a new backend for Postgres remote state. func New() backend.Backend { - s := &schema.Backend{ - Schema: map[string]*schema.Schema{ - "conn_str": { - Type: schema.TypeString, - Optional: true, - Description: "Postgres connection string; a `postgres://` URL", - DefaultFunc: schema.EnvDefaultFunc("PG_CONN_STR", nil), + return &Backend{ + Base: backendbase.Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "conn_str": { + Type: cty.String, + Optional: true, + Description: "Postgres connection string; a `postgres://` URL", + }, + "schema_name": { + Type: cty.String, + Optional: true, + Description: "Name of the automatically managed Postgres schema to store state", + }, + "skip_schema_creation": { + Type: cty.Bool, + Optional: true, + Description: "If set to `true`, Terraform won't try to create the Postgres schema", + }, + "skip_table_creation": { + Type: cty.Bool, + Optional: true, + Description: "If set to `true`, Terraform won't try to create the Postgres table", + }, + "skip_index_creation": { + Type: cty.Bool, + Optional: true, + Description: "If set to `true`, Terraform won't try to create the Postgres index", + }, + }, }, - - "schema_name": { - Type: schema.TypeString, - Optional: true, - Description: "Name of the automatically managed Postgres schema to store state", - DefaultFunc: schema.EnvDefaultFunc("PG_SCHEMA_NAME", "terraform_remote_state"), - }, - - "skip_schema_creation": { - Type: schema.TypeBool, - Optional: true, - Description: "If set to `true`, Terraform won't try to create the Postgres schema", - DefaultFunc: defaultBoolFunc("PG_SKIP_SCHEMA_CREATION", false), - }, - - "skip_table_creation": { - Type: schema.TypeBool, - Optional: true, - Description: "If set to `true`, Terraform won't try to create the Postgres table", - DefaultFunc: defaultBoolFunc("PG_SKIP_TABLE_CREATION", false), - }, - - "skip_index_creation": { - Type: schema.TypeBool, - Optional: true, - Description: "If set to `true`, Terraform won't try to create the Postgres index", - DefaultFunc: defaultBoolFunc("PG_SKIP_INDEX_CREATION", false), + SDKLikeDefaults: backendbase.SDKLikeDefaults{ + "conn_str": { + EnvVars: []string{"PG_CONN_STR"}, + }, + "schema_name": { + EnvVars: []string{"PG_SCHEMA_NAME"}, + Fallback: "terraform_remote_state", + }, + "skip_schema_creation": { + EnvVars: []string{"PG_SKIP_SCHEMA_CREATION"}, + Fallback: "false", + }, + "skip_table_creation": { + EnvVars: []string{"PG_SKIP_TABLE_CREATION"}, + Fallback: "false", + }, + "skip_index_creation": { + EnvVars: []string{"PG_SKIP_INDEX_CREATION"}, + Fallback: "false", + }, }, }, } - - result := &Backend{Backend: s} - result.Backend.ConfigureFunc = result.configure - return result } type Backend struct { - *schema.Backend + backendbase.Base // The fields below are set from configure db *sql.DB - configData *schema.ResourceData connStr string schemaName string } -func (b *Backend) configure(ctx context.Context) error { - // Grab the resource data - b.configData = schema.FromContextBackendConfig(ctx) - data := b.configData +func (b *Backend) Configure(configVal cty.Value) tfdiags.Diagnostics { + data := backendbase.NewSDKLikeData(configVal) - b.connStr = data.Get("conn_str").(string) - b.schemaName = pq.QuoteIdentifier(data.Get("schema_name").(string)) + b.connStr = data.String("conn_str") + b.schemaName = pq.QuoteIdentifier(data.String("schema_name")) db, err := sql.Open("postgres", b.connStr) if err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } // Prepare database schema, tables, & indexes. var query string - if !data.Get("skip_schema_creation").(bool) { + if !data.Bool("skip_schema_creation") { // list all schemas to see if it exists var count int query = `select count(1) from information_schema.schemata where schema_name = $1` - if err := db.QueryRow(query, data.Get("schema_name").(string)).Scan(&count); err != nil { - return err + if err := db.QueryRow(query, data.String("schema_name")).Scan(&count); err != nil { + return backendbase.ErrorAsDiagnostics(err) } // skip schema creation if schema already exists @@ -117,14 +117,14 @@ func (b *Backend) configure(ctx context.Context) error { // tries to create the schema query = `CREATE SCHEMA IF NOT EXISTS %s` if _, err := db.Exec(fmt.Sprintf(query, b.schemaName)); err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } } } - if !data.Get("skip_table_creation").(bool) { + if !data.Bool("skip_table_creation") { if _, err := db.Exec("CREATE SEQUENCE IF NOT EXISTS public.global_states_id_seq AS bigint"); err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } query = `CREATE TABLE IF NOT EXISTS %s.%s ( @@ -133,14 +133,14 @@ func (b *Backend) configure(ctx context.Context) error { data text )` if _, err := db.Exec(fmt.Sprintf(query, b.schemaName, statesTableName)); err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } } - if !data.Get("skip_index_creation").(bool) { + if !data.Bool("skip_index_creation") { query = `CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s.%s (name)` if _, err := db.Exec(fmt.Sprintf(query, statesIndexName, b.schemaName, statesTableName)); err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } } diff --git a/internal/backend/remote-state/pg/backend_test.go b/internal/backend/remote-state/pg/backend_test.go index be7cb4f8aef7..f9da1a927cc4 100644 --- a/internal/backend/remote-state/pg/backend_test.go +++ b/internal/backend/remote-state/pg/backend_test.go @@ -95,7 +95,7 @@ func TestBackendConfig(t *testing.T) { "conn_str": connStr, "schema_name": fmt.Sprintf("terraform_%s", t.Name()), }, - ExpectConnectionError: `role "baduser" does not exist`, + ExpectConnectionError: `password authentication failed for user "baduser"`, }, { Name: "host-in-env-vars", @@ -130,7 +130,7 @@ func TestBackendConfig(t *testing.T) { Config: map[string]interface{}{ "schema_name": fmt.Sprintf("terraform_%s", t.Name()), }, - ExpectConfigurationError: `error getting default for "skip_schema_creation"`, + ExpectConfigurationError: `invalid value for "skip_schema_creation"`, }, } diff --git a/internal/backend/remote-state/pg/go.mod b/internal/backend/remote-state/pg/go.mod index 511a57acf608..851d01cb89b0 100644 --- a/internal/backend/remote-state/pg/go.mod +++ b/internal/backend/remote-state/pg/go.mod @@ -6,8 +6,8 @@ require ( github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/hcl/v2 v2.21.0 github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 - github.com/hashicorp/terraform/internal/legacy v0.0.0-00010101000000-000000000000 github.com/lib/pq v1.10.3 + github.com/zclconf/go-cty v1.15.0 ) require ( @@ -20,12 +20,8 @@ require ( github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/spf13/afero v1.9.3 // indirect - github.com/zclconf/go-cty v1.15.0 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.20.0 // indirect @@ -49,6 +45,4 @@ replace github.com/hashicorp/terraform/internal/backend/remote-state/pg => ../pg replace github.com/hashicorp/terraform/internal/backend/remote-state/s3 => ../s3 -replace github.com/hashicorp/terraform/internal/legacy => ../../../legacy - replace github.com/hashicorp/terraform => ../../../.. diff --git a/internal/backend/remote-state/pg/go.sum b/internal/backend/remote-state/pg/go.sum index 877eeed590cd..2ef6e0f52dd1 100644 --- a/internal/backend/remote-state/pg/go.sum +++ b/internal/backend/remote-state/pg/go.sum @@ -196,18 +196,12 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/internal/stacks/stackruntime/apply_test.go b/internal/stacks/stackruntime/apply_test.go index 1a1a6da3e25a..65bbee972680 100644 --- a/internal/stacks/stackruntime/apply_test.go +++ b/internal/stacks/stackruntime/apply_test.go @@ -3147,7 +3147,6 @@ func TestApply_RemovedBlocks(t *testing.T) { ) // TODO: Add tests for and implement the following cases: - // - Removed and component blocks that target the same instance. // - Add a test for a removed block targeting state that has already been // removed. // - Add a test for a removed block that forgets instead of destroys. diff --git a/internal/stacks/stackruntime/internal/stackeval/removed_instance.go b/internal/stacks/stackruntime/internal/stackeval/removed_instance.go index 97b8804f03f1..7aaf8d37318b 100644 --- a/internal/stacks/stackruntime/internal/stackeval/removed_instance.go +++ b/internal/stacks/stackruntime/internal/stackeval/removed_instance.go @@ -78,6 +78,24 @@ func (r *RemovedInstance) ModuleTreePlan(ctx context.Context) (*plans.Plan, tfdi return doOnceWithDiags(ctx, &r.moduleTreePlan, r.main, func(ctx context.Context) (*plans.Plan, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics + component := r.main.Stack(ctx, r.Addr().Stack, PlanPhase).Component(ctx, r.Addr().Item.Component) + if component != nil { + insts, unknown := component.Instances(ctx, PlanPhase) + if !unknown { + if _, exists := insts[r.key]; exists { + // The instance we're planning to remove is also targeted + // by a component block. We won't remove it, and we'll + // report a diagnostic to that effect. + return nil, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot remove component instance", + Detail: fmt.Sprintf("The component instance %s is targeted by a component block and cannot be removed. The relevant component is defined at %s.", r.Addr(), component.Declaration(ctx).DeclRange.ToHCL()), + Subject: r.DeclRange(ctx), + }) + } + } + } + known, unknown, moreDiags := EvalProviderValues(ctx, r.main, r.call.Config(ctx).config.ProviderConfigs, PlanPhase, r) if moreDiags.HasErrors() { // We won't actually add the diagnostics here, they should be diff --git a/internal/stacks/stackruntime/plan_test.go b/internal/stacks/stackruntime/plan_test.go index 0a2cb8280537..287b60c5a21d 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -4889,6 +4889,114 @@ func TestPlan_RemovedBlocks(t *testing.T) { }, }, }, + "duplicate component": { + source: filepath.Join("with-single-input", "removed-component-instance"), + initialState: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")). + AddInputVariable("id", cty.StringVal("a")). + AddInputVariable("input", cty.StringVal("a"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "a", + "value": "a", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("a", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })). + Build(), + inputs: map[string]cty.Value{ + "input": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "removed": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + }, + wantPlanChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: false, // No! The removed block is a duplicate of the component! + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"a\"]"), + PlanComplete: true, + PlanApplyable: false, // no changes + Action: plans.Update, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "a", + "value": "a", + }), + Dependencies: make([]addrs.ConfigResource, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Value: cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed"}, + Value: cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + }, + }, + wantPlanDiags: []expectedDiagnostic{ + { + severity: tfdiags.Error, + summary: "Cannot remove component instance", + detail: "The component instance component.self[\"a\"] is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/removed-component-instance/removed-component-instance.tfstack.hcl:18,1-17.", + }, + }, + }, } for name, tc := range tcs {