diff --git a/internal/cloud/e2e/backend_apply_before_init_test.go b/internal/cloud/e2e/backend_apply_before_init_test.go new file mode 100644 index 000000000..86f2f515c --- /dev/null +++ b/internal/cloud/e2e/backend_apply_before_init_test.go @@ -0,0 +1,141 @@ +//go:build e2e +// +build e2e + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + + expect "github.com/Netflix/go-expect" + "github.com/hashicorp/terraform/internal/e2e" +) + +func Test_backend_apply_before_init(t *testing.T) { + cases := map[string]struct { + operations []operationSets + }{ + "terraform apply with cloud block - blank state": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "new-workspace" + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"apply"}, + expectedCmdOutput: `Terraform Cloud has been configured but needs to be initialized`, + expectError: true, + }, + }, + }, + }, + }, + "terraform apply with cloud block - local state": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + tfBlock := terraformConfigLocalBackend() + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Successfully configured the backend "local"!`, + }, + { + command: []string{"apply"}, + expectedCmdOutput: `Do you want to perform these actions?`, + userInput: []string{"yes"}, + postInputOutput: []string{`Apply complete!`}, + }, + }, + }, + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "new-workspace" + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"apply"}, + expectedCmdOutput: `Terraform Cloud has been configured but needs to be initialized`, + expectError: true, + }, + }, + }, + }, + }, + } + + for name, tc := range cases { + fmt.Println("Test: ", name) + organization, cleanup := createOrganization(t) + defer cleanup() + exp, err := expect.NewConsole(expect.WithStdout(os.Stdout), expect.WithDefaultTimeout(expectConsoleTimeout)) + if err != nil { + t.Fatal(err) + } + defer exp.Close() + + tmpDir, err := ioutil.TempDir("", "terraform-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + tf := e2e.NewBinary(terraformBin, tmpDir) + tf.AddEnv("TF_LOG=info") + tf.AddEnv(cliConfigFileEnv) + defer tf.Close() + + for _, op := range tc.operations { + op.prep(t, organization.Name, tf.WorkDir()) + for _, tfCmd := range op.commands { + cmd := tf.Cmd(tfCmd.command...) + cmd.Stdin = exp.Tty() + cmd.Stdout = exp.Tty() + cmd.Stderr = exp.Tty() + + err = cmd.Start() + if err != nil { + t.Fatal(err) + } + + if tfCmd.expectedCmdOutput != "" { + _, err := exp.ExpectString(tfCmd.expectedCmdOutput) + if err != nil { + t.Fatal(err) + } + } + + lenInput := len(tfCmd.userInput) + lenInputOutput := len(tfCmd.postInputOutput) + if lenInput > 0 { + for i := 0; i < lenInput; i++ { + input := tfCmd.userInput[i] + exp.SendLine(input) + // use the index to find the corresponding + // output that matches the input. + if lenInputOutput-1 >= i { + output := tfCmd.postInputOutput[i] + _, err := exp.ExpectString(output) + if err != nil { + t.Fatal(err) + } + } + } + } + err = cmd.Wait() + if err != nil && !tfCmd.expectError { + t.Fatal(err) + } + } + } + } +} diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 5a26c71b0..a521289b5 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -593,13 +593,14 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di return m.backend_c_r_S(c, cHash, sMgr, true) // Configuring Terraform Cloud for the first time. + // NOTE: There may be an implicit local backend with state that is not visible to this block. case c != nil && c.Type == "cloud" && s.Backend.Empty(): log.Printf("[TRACE] Meta.Backend: moving from default local state only to Terraform Cloud") if !opts.Init { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Terraform Cloud has been configured but needs to be initialized.", - "Run \"terraform init\" to initialize Terraform Cloud.", + strings.TrimSpace(errBackendInitCloud), )) return nil, diags } @@ -640,17 +641,30 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } log.Printf("[TRACE] Meta.Backend: backend configuration has changed (from type %q to type %q)", s.Backend.Type, c.Type) - initReason := fmt.Sprintf("Backend configuration changed for %q", c.Type) - if s.Backend.Type != c.Type { + initReason := "" + switch { + case c.Type == "cloud": + initReason = fmt.Sprintf("Backend configuration changed from %q to Terraform Cloud", s.Backend.Type) + case s.Backend.Type != c.Type: initReason = fmt.Sprintf("Backend configuration changed from %q to %q", s.Backend.Type, c.Type) + default: + initReason = fmt.Sprintf("Backend configuration changed for %q", c.Type) } if !opts.Init { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Backend initialization required, please run \"terraform init\"", - fmt.Sprintf(strings.TrimSpace(errBackendInit), initReason), - )) + if c.Type == "cloud" { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Terraform Cloud has been configured but needs to be initialized.", + strings.TrimSpace(errBackendInitCloud), + )) + } else { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Backend initialization required, please run \"terraform init\"", + fmt.Sprintf(strings.TrimSpace(errBackendInit), initReason), + )) + } return nil, diags } @@ -1315,6 +1329,24 @@ hasn't changed and try again. At this point, no changes to your existing configuration or state have been made. ` +const errBackendInitCloud = ` +Changes to the Terraform Cloud configuration block require reinitialization. +This allows Terraform to set up the new configuration, copy existing state, +etc. Learn more about Terraform Settings: +https://www.terraform.io/docs/language/settings/index.html + +Please run "terraform init" with either the "-reconfigure" or "-migrate-state" +flags. The "-reconfigure" option disregards any existing configuration, +preventing migration of any existing state. The "-migrate-state" option +will attempt to copy existing state to Terraform Cloud. Learn more about +using "terraform init": +https://www.terraform.io/docs/cli/commands/init.html#backend-initialization + +If the change reason above is incorrect, please verify your configuration +hasn't changed and try again. At this point, no changes to your existing +configuration or state have been made. +` + const errBackendWriteSaved = ` Error saving the backend configuration: %s