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()) + } }) } }