From 1d91853ea5554409559e39e48e3c0fd4a6eeb86d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Lapeyre?= Date: Tue, 14 Sep 2021 00:43:08 +0200 Subject: [PATCH] Disambiguate backend configurations during migration A user reported at https://github.com/hashicorp/terraform-provider-consul/issues/275 that the message when migrating backend was not detailed enough to be sure what was the previously configured backend, and what is the new one. This can make migrating from one backend to another dangerous as there is no way to check the configuration is correct when actually migrating to a new backend. The message when using the consul backend (though the issue still stands with other backends as well) was: Do you want to migrate all workspaces to "consul"? Both the existing "consul" backend and the newly configured "consul" backend support workspaces. When migrating between backends, Terraform will copy all workspaces (with the same names). THIS WILL OVERWRITE any conflicting states in the destination. Terraform initialization doesn't currently migrate only select workspaces. If you want to migrate a select number of workspaces, you must manually pull and push those states. If you answer "yes", Terraform will migrate all states. If you answer "no", Terraform will abort. I changed the Backend interface to add a String() method that returns an HCL representation of the backend and used it to return more information when migrating. The new message is: Do you want to copy existing state to the new backend? Pre-existing state was found while migrating the previous "local" backend to the newly configured "consul" backend. No existing state was found in the newly configured "consul" backend. The configuration for the previous "local" backend was: backend "local" { path = "" workspace_dir = "" } The configuration for the new "consul" backend is: backend "consul" { access_token = "" address = "" ca_file = "" cert_file = "" datacenter = "" gzip = false http_auth = "" key_file = "" lock = true path = "full/path" scheme = "" } Do you want to copy this state to the new "consul" backend? Enter "yes" to copy and "no" to start with an empty state. For the local and remote backends the implementation is straightforward, for all the others the representation is generated from the backend's Schema and its configuration. --- internal/backend/backend.go | 4 ++ internal/backend/local/backend.go | 12 ++++++ internal/backend/local/backend_local_test.go | 4 ++ .../remote-state/artifactory/backend.go | 1 + .../backend/remote-state/azure/backend.go | 1 + .../backend/remote-state/consul/backend.go | 1 + internal/backend/remote-state/cos/backend.go | 1 + .../backend/remote-state/etcdv2/backend.go | 1 + .../backend/remote-state/etcdv3/backend.go | 1 + internal/backend/remote-state/gcs/backend.go | 1 + internal/backend/remote-state/http/backend.go | 1 + .../backend/remote-state/inmem/backend.go | 1 + .../remote-state/kubernetes/backend.go | 3 +- .../backend/remote-state/manta/backend.go | 1 + internal/backend/remote-state/oss/backend.go | 1 + internal/backend/remote-state/pg/backend.go | 1 + internal/backend/remote-state/s3/backend.go | 1 + .../backend/remote-state/swift/backend.go | 1 + internal/backend/remote/backend.go | 21 ++++++++++ .../terraform/data_source_state_test.go | 4 ++ internal/command/meta_backend_migrate.go | 31 ++++++++++++--- internal/legacy/helper/schema/backend.go | 38 +++++++++++++++++++ internal/legacy/helper/schema/backend_test.go | 8 ++++ 23 files changed, 133 insertions(+), 6 deletions(-) diff --git a/internal/backend/backend.go b/internal/backend/backend.go index caac42cc6731..3bbb1b8e79b0 100644 --- a/internal/backend/backend.go +++ b/internal/backend/backend.go @@ -113,6 +113,10 @@ type Backend interface { // States returns a list of the names of all of the workspaces that exist // in this backend. Workspaces() ([]string, error) + + // String returns a valid string representation of the backend as it would + // appear in the Terraform configuration + String() string } // Enhanced implements additional behavior on top of a normal backend. diff --git a/internal/backend/local/backend.go b/internal/backend/local/backend.go index f5d07b20f039..bcfd164bbc72 100644 --- a/internal/backend/local/backend.go +++ b/internal/backend/local/backend.go @@ -9,8 +9,10 @@ import ( "os" "path/filepath" "sort" + "strings" "sync" + "github.com/hashicorp/hcl/v2/hclwrite" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs/configschema" @@ -325,6 +327,16 @@ func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend. return runningOp, nil } +func (b *Local) String() string { + f := hclwrite.NewFile() + + body := f.Body().AppendNewBlock("backend", []string{"local"}).Body() + body.SetAttributeValue("path", cty.StringVal(b.StatePath)) + body.SetAttributeValue("workspace_dir", cty.StringVal(b.StateWorkspaceDir)) + + return strings.TrimSpace(string(f.Bytes())) +} + // opWait waits for the operation to complete, and a stop signal or a // cancelation signal. func (b *Local) opWait( diff --git a/internal/backend/local/backend_local_test.go b/internal/backend/local/backend_local_test.go index fae2b1ae0953..625d51251a78 100644 --- a/internal/backend/local/backend_local_test.go +++ b/internal/backend/local/backend_local_test.go @@ -193,6 +193,10 @@ func (b backendWithStateStorageThatFailsRefresh) Workspaces() ([]string, error) return []string{"default"}, nil } +func (b backendWithStateStorageThatFailsRefresh) String() string { + return "" +} + type stateStorageThatFailsRefresh struct { locked bool } diff --git a/internal/backend/remote-state/artifactory/backend.go b/internal/backend/remote-state/artifactory/backend.go index bf2bfcf7e5ce..525e2c4b3377 100644 --- a/internal/backend/remote-state/artifactory/backend.go +++ b/internal/backend/remote-state/artifactory/backend.go @@ -13,6 +13,7 @@ import ( func New() backend.Backend { s := &schema.Backend{ + Type: "artifactory", Schema: map[string]*schema.Schema{ "username": &schema.Schema{ Type: schema.TypeString, diff --git a/internal/backend/remote-state/azure/backend.go b/internal/backend/remote-state/azure/backend.go index 889b1f7f825b..eed9163d2c4e 100644 --- a/internal/backend/remote-state/azure/backend.go +++ b/internal/backend/remote-state/azure/backend.go @@ -11,6 +11,7 @@ import ( // New creates a new backend for Azure remote state. func New() backend.Backend { s := &schema.Backend{ + Type: "azure", Schema: map[string]*schema.Schema{ "storage_account_name": { Type: schema.TypeString, diff --git a/internal/backend/remote-state/consul/backend.go b/internal/backend/remote-state/consul/backend.go index 884696981350..a9fa0beb3a99 100644 --- a/internal/backend/remote-state/consul/backend.go +++ b/internal/backend/remote-state/consul/backend.go @@ -14,6 +14,7 @@ import ( // New creates a new backend for Consul remote state. func New() backend.Backend { s := &schema.Backend{ + Type: "consul", Schema: map[string]*schema.Schema{ "path": &schema.Schema{ Type: schema.TypeString, diff --git a/internal/backend/remote-state/cos/backend.go b/internal/backend/remote-state/cos/backend.go index 8d0d0145417e..959e3a2bb777 100644 --- a/internal/backend/remote-state/cos/backend.go +++ b/internal/backend/remote-state/cos/backend.go @@ -42,6 +42,7 @@ type Backend struct { // New creates a new backend for TencentCloud cos remote state. func New() backend.Backend { s := &schema.Backend{ + Type: "cos", Schema: map[string]*schema.Schema{ "secret_id": { Type: schema.TypeString, diff --git a/internal/backend/remote-state/etcdv2/backend.go b/internal/backend/remote-state/etcdv2/backend.go index e6d3cf8ce98e..cf361ac69f21 100644 --- a/internal/backend/remote-state/etcdv2/backend.go +++ b/internal/backend/remote-state/etcdv2/backend.go @@ -15,6 +15,7 @@ import ( func New() backend.Backend { s := &schema.Backend{ + Type: "etcd", Schema: map[string]*schema.Schema{ "path": &schema.Schema{ Type: schema.TypeString, diff --git a/internal/backend/remote-state/etcdv3/backend.go b/internal/backend/remote-state/etcdv3/backend.go index 7285bda968f9..227660e6ea33 100644 --- a/internal/backend/remote-state/etcdv3/backend.go +++ b/internal/backend/remote-state/etcdv3/backend.go @@ -25,6 +25,7 @@ const ( func New() backend.Backend { s := &schema.Backend{ + Type: "etcdv3", Schema: map[string]*schema.Schema{ endpointsKey: &schema.Schema{ Type: schema.TypeList, diff --git a/internal/backend/remote-state/gcs/backend.go b/internal/backend/remote-state/gcs/backend.go index af2a667eb184..9236764e45c5 100644 --- a/internal/backend/remote-state/gcs/backend.go +++ b/internal/backend/remote-state/gcs/backend.go @@ -35,6 +35,7 @@ type Backend struct { func New() backend.Backend { b := &Backend{} b.Backend = &schema.Backend{ + Type: "gcs", ConfigureFunc: b.configure, Schema: map[string]*schema.Schema{ "bucket": { diff --git a/internal/backend/remote-state/http/backend.go b/internal/backend/remote-state/http/backend.go index a3f98c05ac66..3709669c5343 100644 --- a/internal/backend/remote-state/http/backend.go +++ b/internal/backend/remote-state/http/backend.go @@ -18,6 +18,7 @@ import ( func New() backend.Backend { s := &schema.Backend{ + Type: "http", Schema: map[string]*schema.Schema{ "address": &schema.Schema{ Type: schema.TypeString, diff --git a/internal/backend/remote-state/inmem/backend.go b/internal/backend/remote-state/inmem/backend.go index 7f8f56ef2034..3cd72317d633 100644 --- a/internal/backend/remote-state/inmem/backend.go +++ b/internal/backend/remote-state/inmem/backend.go @@ -44,6 +44,7 @@ func Reset() { func New() backend.Backend { // Set the schema s := &schema.Backend{ + Type: "inmem", Schema: map[string]*schema.Schema{ "lock_id": &schema.Schema{ Type: schema.TypeString, diff --git a/internal/backend/remote-state/kubernetes/backend.go b/internal/backend/remote-state/kubernetes/backend.go index 907cda9e246e..041d5f6ca6c6 100644 --- a/internal/backend/remote-state/kubernetes/backend.go +++ b/internal/backend/remote-state/kubernetes/backend.go @@ -26,7 +26,7 @@ import ( const ( noConfigError = ` -[Kubernetes backend] Neither service_account nor load_config_file were set to true, +[Kubernetes backend] Neither service_account nor load_config_file were set to true, this could cause issues connecting to your Kubernetes cluster. ` ) @@ -42,6 +42,7 @@ var ( // New creates a new backend for kubernetes remote state. func New() backend.Backend { s := &schema.Backend{ + Type: "kubernetes", Schema: map[string]*schema.Schema{ "secret_suffix": { Type: schema.TypeString, diff --git a/internal/backend/remote-state/manta/backend.go b/internal/backend/remote-state/manta/backend.go index 3a7a21bc5a6c..e03f117e8ec4 100644 --- a/internal/backend/remote-state/manta/backend.go +++ b/internal/backend/remote-state/manta/backend.go @@ -19,6 +19,7 @@ import ( func New() backend.Backend { s := &schema.Backend{ + Type: "manta", Schema: map[string]*schema.Schema{ "account": { Type: schema.TypeString, diff --git a/internal/backend/remote-state/oss/backend.go b/internal/backend/remote-state/oss/backend.go index de08af37d68f..44f6f44b38a1 100644 --- a/internal/backend/remote-state/oss/backend.go +++ b/internal/backend/remote-state/oss/backend.go @@ -34,6 +34,7 @@ import ( // New creates a new backend for OSS remote state. func New() backend.Backend { s := &schema.Backend{ + Type: "oss", Schema: map[string]*schema.Schema{ "access_key": &schema.Schema{ Type: schema.TypeString, diff --git a/internal/backend/remote-state/pg/backend.go b/internal/backend/remote-state/pg/backend.go index cdcfb3a6e462..ce10dafb57bb 100644 --- a/internal/backend/remote-state/pg/backend.go +++ b/internal/backend/remote-state/pg/backend.go @@ -18,6 +18,7 @@ const ( // New creates a new backend for Postgres remote state. func New() backend.Backend { s := &schema.Backend{ + Type: "pg", Schema: map[string]*schema.Schema{ "conn_str": { Type: schema.TypeString, diff --git a/internal/backend/remote-state/s3/backend.go b/internal/backend/remote-state/s3/backend.go index 98aa1c561ef3..ba2775888659 100644 --- a/internal/backend/remote-state/s3/backend.go +++ b/internal/backend/remote-state/s3/backend.go @@ -20,6 +20,7 @@ import ( // New creates a new backend for S3 remote state. func New() backend.Backend { s := &schema.Backend{ + Type: "s3", Schema: map[string]*schema.Schema{ "bucket": { Type: schema.TypeString, diff --git a/internal/backend/remote-state/swift/backend.go b/internal/backend/remote-state/swift/backend.go index 6084131338c9..a2b2aa448a67 100644 --- a/internal/backend/remote-state/swift/backend.go +++ b/internal/backend/remote-state/swift/backend.go @@ -25,6 +25,7 @@ type Config struct { // New creates a new backend for Swift remote state. func New() backend.Backend { s := &schema.Backend{ + Type: "swift", Schema: map[string]*schema.Schema{ "auth_url": { Type: schema.TypeString, diff --git a/internal/backend/remote/backend.go b/internal/backend/remote/backend.go index b4aa115a798b..8a71f34f5f4f 100644 --- a/internal/backend/remote/backend.go +++ b/internal/backend/remote/backend.go @@ -14,6 +14,8 @@ import ( tfe "github.com/hashicorp/go-tfe" version "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/hclwrite" svchost "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform/internal/backend" @@ -805,6 +807,25 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend return runningOp, nil } +func (b *Remote) String() string { + f := hclwrite.NewFile() + + body := f.Body().AppendNewBlock("backend", []string{"remote"}).Body() + body.SetAttributeValue("hostname", cty.StringVal(b.hostname)) + body.SetAttributeValue("organization", cty.StringVal(b.organization)) + body.SetAttributeRaw("token", hclwrite.Tokens{&hclwrite.Token{ + Type: hclsyntax.TokenIdent, + Bytes: []byte("(sensitive)"), + }}) + body.AppendNewline() + + body = body.AppendNewBlock("workspaces", []string{}).Body() + body.SetAttributeValue("name", cty.StringVal(b.workspace)) + body.SetAttributeValue("prefix", cty.StringVal(b.prefix)) + + return strings.TrimSpace(string(f.Bytes())) +} + func (b *Remote) cancel(cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { if r.Actions.IsCancelable { // Only ask if the remote operation should be canceled diff --git a/internal/builtin/providers/terraform/data_source_state_test.go b/internal/builtin/providers/terraform/data_source_state_test.go index 1a9f514ecbbc..1e61aa72646b 100644 --- a/internal/builtin/providers/terraform/data_source_state_test.go +++ b/internal/builtin/providers/terraform/data_source_state_test.go @@ -369,3 +369,7 @@ func (b backendFailsConfigure) DeleteWorkspace(name string) error { func (b backendFailsConfigure) Workspaces() ([]string, error) { return nil, fmt.Errorf("Workspaces not implemented") } + +func (b backendFailsConfigure) String() string { + return "" +} diff --git a/internal/command/meta_backend_migrate.go b/internal/command/meta_backend_migrate.go index 0021b5850a53..783780502e75 100644 --- a/internal/command/meta_backend_migrate.go +++ b/internal/command/meta_backend_migrate.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" @@ -436,7 +437,8 @@ func (m *Meta) backendMigrateEmptyConfirm(one, two statemgr.Full, opts *backendM Query: "Do you want to copy existing state to the new backend?", Description: fmt.Sprintf( strings.TrimSpace(inputBackendMigrateEmpty), - opts.OneType, opts.TwoType), + opts.OneType, opts.TwoType, + logging.Indent(opts.One.String()), logging.Indent(opts.Two.String())), } return m.confirm(inputOpts) @@ -477,7 +479,8 @@ func (m *Meta) backendMigrateNonEmptyConfirm( Query: "Do you want to copy existing state to the new backend?", Description: fmt.Sprintf( strings.TrimSpace(inputBackendMigrateNonEmpty), - opts.OneType, opts.TwoType, onePath, twoPath), + opts.OneType, opts.TwoType, onePath, twoPath, + logging.Indent(opts.One.String()), logging.Indent(opts.Two.String())), } // Confirm with the user that the copy should occur @@ -520,7 +523,7 @@ This will attempt to copy (with permission) all workspaces again. ` const errBackendStateCopy = ` -Error copying state from the previous %q backend to the newly configured +Error copying state from the previous %q backend to the newly configured %q backend: %s @@ -531,7 +534,17 @@ the error above and try again. const inputBackendMigrateEmpty = ` Pre-existing state was found while migrating the previous %q backend to the newly configured %q backend. No existing state was found in the newly -configured %[2]q backend. Do you want to copy this state to the new %[2]q +configured %[2]q backend. + +The configuration for the previous %[1]q backend was: + +%[3]s + +The configuration for the new %[2]q backend is: + +%[4]s + +Do you want to copy this state to the new %[2]q backend? Enter "yes" to copy and "no" to start with an empty state. ` @@ -544,6 +557,14 @@ removed after responding to this query. Previous (type %[1]q): %[3]s New (type %[2]q): %[4]s +The configuration for the previous %[1]q backend was: + +%[5]s + +The configuration for the new %[2]q backend is: + +%[6]s + Do you want to overwrite the state in the new backend with the previous state? Enter "yes" to copy and "no" to start with the existing state in the newly configured %[2]q backend. @@ -574,7 +595,7 @@ If you answer "yes", Terraform will migrate all states. If you answer const inputBackendNewWorkspaceName = ` Please provide a new workspace name (e.g. dev, test) that will be used -to migrate the existing default workspace. +to migrate the existing default workspace. ` const inputBackendSelectWorkspace = ` diff --git a/internal/legacy/helper/schema/backend.go b/internal/legacy/helper/schema/backend.go index 7bd9426abea0..83f4fc932133 100644 --- a/internal/legacy/helper/schema/backend.go +++ b/internal/legacy/helper/schema/backend.go @@ -3,7 +3,11 @@ package schema import ( "context" "fmt" + "sort" + "strings" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/hclwrite" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" @@ -32,6 +36,8 @@ type Backend struct { // config will still be stored. ConfigureFunc func(context.Context) error + Type string + config *ResourceData } @@ -198,3 +204,35 @@ func (b *Backend) shimConfig(obj cty.Value) *terraform.ResourceConfig { func (b *Backend) Config() *ResourceData { return b.config } + +func (b *Backend) String() string { + // At the moment all backends only have attributes so we can skip blocks + f := hclwrite.NewFile() + + var names []string + for name := range b.Schema { + names = append(names, name) + } + sort.Strings(names) + + body := f.Body().AppendNewBlock("backend", []string{b.Type}).Body() + for _, name := range names { + sch := b.Schema[name] + switch sch.Type { + case TypeBool: + body.SetAttributeValue(name, cty.BoolVal(b.config.Get(name).(bool))) + case TypeInt: + body.SetAttributeValue(name, cty.NumberIntVal(int64(b.config.Get(name).(int)))) + case TypeString: + body.SetAttributeValue(name, cty.StringVal(b.config.Get(name).(string))) + default: + body.SetAttributeRaw(name, hclwrite.Tokens{&hclwrite.Token{ + Type: hclsyntax.TokenIdent, + Bytes: []byte("(unsupported type)"), + }}) + + } + } + + return strings.TrimSpace(string(f.Bytes())) +} diff --git a/internal/legacy/helper/schema/backend_test.go b/internal/legacy/helper/schema/backend_test.go index 8b0336fe0e63..62c5599e0b35 100644 --- a/internal/legacy/helper/schema/backend_test.go +++ b/internal/legacy/helper/schema/backend_test.go @@ -155,10 +155,12 @@ func TestBackendConfigure(t *testing.T) { B *Backend Config map[string]cty.Value Err bool + String string }{ { "Basic config", &Backend{ + Type: "test", Schema: map[string]*Schema{ "foo": &Schema{ Type: TypeInt, @@ -179,6 +181,9 @@ func TestBackendConfigure(t *testing.T) { "foo": cty.NumberIntVal(42), }, false, + `backend "test" { + foo = 42 +}`, }, } @@ -188,6 +193,9 @@ func TestBackendConfigure(t *testing.T) { if diags.HasErrors() != tc.Err { t.Errorf("wrong number of diagnostics") } + if tc.B.String() != tc.String { + t.Error(tc.B.String()) + } }) } }