From 3cc1c1e4e762c531ca44c2a0fad16b273f55c03a Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 6 Mar 2024 15:31:53 -0800 Subject: [PATCH 1/6] backend/pg: Stop using legacy helper/schema The legacy SDK is a very heavy dependency that this backend was only using tiny parts of. We'll now instead use our new "backendbase" package, which aims to provide a smaller set of helper functions that cover the main use-cases that the existing backends were relying on the SDK to meet, but with considerably less code and fewer layers of abstraction. --- internal/backend/remote-state/pg/backend.go | 142 ++++++++++---------- internal/backend/remote-state/pg/go.mod | 6 +- internal/backend/remote-state/pg/go.sum | 6 - 3 files changed, 72 insertions(+), 82 deletions(-) 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/go.mod b/internal/backend/remote-state/pg/go.mod index 511a57acf608..e555c298305e 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 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= From fce24d588f8fbd54cad5da88545c67934f6223a0 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 6 Mar 2024 15:09:49 -0800 Subject: [PATCH 2/6] backend/kubernetes: Remove legacy helper/schema dependency As with most of our remote state backends, this one was depending on just a tiny slice of the (enormous and now-poorly-understood) legacy SDK. In an effort to eliminate the legacy SDK snapshot from this codebase, this replaces it with functionality from our new "backendbase" package, which aims to provide just a narrow set of utilities to minimize the churn caused by removing the legacy SDK and thus reduce the risk of this change. This is currently using the "SDK-like" utilities, which emulate some of the questionable-but-convenient assumptions the legacy SDK makes, such as the idea that empty string and null are equivalent. Hopefully in future we can wean this backend even further off of these older assumptions, but the priority for now is to eliminate the legacy SDK without significantly disturbing the shape of the existing working code. --- .../remote-state/kubernetes/backend.go | 470 ++++++++++-------- .../backend/remote-state/kubernetes/go.mod | 6 +- .../backend/remote-state/kubernetes/go.sum | 6 - 3 files changed, 260 insertions(+), 222 deletions(-) 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..c7a0cc2bf266 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 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= From a168379937ed6c5317a761115205caa0d16060ff Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 15 Apr 2024 16:58:28 -0400 Subject: [PATCH 3/6] update test fixtures There were two failing ACC tests with this update. One from from a newer version of pg, which causes a different auth error message. The other is is directly related to this PR, which was checking for a specific schema error, which is now `invalid value for "skip_schema_creation"` when a non-bool argument is present. --- internal/backend/remote-state/pg/backend_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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"`, }, } From 17e79d6d4cb2ae1c62dae1d4bee403c7f37ef503 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 9 Sep 2024 16:15:59 -0400 Subject: [PATCH 4/6] remove unused replace directive --- internal/backend/remote-state/pg/go.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/backend/remote-state/pg/go.mod b/internal/backend/remote-state/pg/go.mod index e555c298305e..851d01cb89b0 100644 --- a/internal/backend/remote-state/pg/go.mod +++ b/internal/backend/remote-state/pg/go.mod @@ -45,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 => ../../../.. From 5001b937fe262bf96fd4118846e4ba8de49cb730 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 9 Sep 2024 16:23:08 -0400 Subject: [PATCH 5/6] remove unused replace directive --- internal/backend/remote-state/kubernetes/go.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/backend/remote-state/kubernetes/go.mod b/internal/backend/remote-state/kubernetes/go.mod index c7a0cc2bf266..bb51e40246f2 100644 --- a/internal/backend/remote-state/kubernetes/go.mod +++ b/internal/backend/remote-state/kubernetes/go.mod @@ -95,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 => ../../../.. From b22db510d42cad586833ff9f7e0ec0a32b379e9e Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Tue, 10 Sep 2024 08:50:09 +0200 Subject: [PATCH 6/6] stacks: error when removing a component still in config (#35692) --- internal/stacks/stackruntime/apply_test.go | 1 - .../internal/stackeval/removed_instance.go | 18 +++ internal/stacks/stackruntime/plan_test.go | 108 ++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/internal/stacks/stackruntime/apply_test.go b/internal/stacks/stackruntime/apply_test.go index 5000ddb6dc88..a8f8307564b8 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. // - Validate what happens when a removed block foreach evaluates to // unknown. // - Add a test for a removed block targeting state that has already been diff --git a/internal/stacks/stackruntime/internal/stackeval/removed_instance.go b/internal/stacks/stackruntime/internal/stackeval/removed_instance.go index 471da76eef3a..de77901d3d95 100644 --- a/internal/stacks/stackruntime/internal/stackeval/removed_instance.go +++ b/internal/stacks/stackruntime/internal/stackeval/removed_instance.go @@ -76,6 +76,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 66ead05a0edd..c4490bade10b 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -4437,6 +4437,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 {