Found another path to backend init error

There are actually a few different ways to get to this message.

1. Blank state — no previous terraform applied. Start with a cloud block.
1. Implicit local — start with no backend specified. This actually goes
   through the same code execution path as the first scenario.
1. Explicit local — start with a backend local block that has been
   applied, then change from the local backend to a cloud block. This
   will recognize the state, and is a different path through the code in
   the meta backend.

This commit handles the last case. The messaging has also been tweaked.

End to end test included as well.
This commit is contained in:
Barrett Clark 2021-10-21 06:19:33 -05:00 committed by Chris Arcand
parent d29532cfeb
commit ab304d831f
2 changed files with 181 additions and 8 deletions

View File

@ -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)
}
}
}
}
}

View File

@ -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 {
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