Cloud: Init without erroring when no workspaces match the `tags` (#29835)

Previously, `terraform init` was throwing an error if you configured the cloud
block with `tags` and there weren't any tagged workspaces yet. Confusing and
alienating, since that that's a fairly normal situation! Basically TFC was
handling an empty list of workspaces worse than other backends, because it
doesn't support an unnamed default workspace.

This commit catches that condition during `Meta.selectBackend()` and asks the
user to pick a name for their first tagged workspace. If they cancel out, we
still error, but if we know what name they want, we can handle it the same way
as a nonexistent workspace specified in `name` -- just pass it to
`Meta.SetWorkspace()`, and let the workspace get implicitly created when
`InitCommand.Run()` eventually calls `StateMgr()`.
This commit is contained in:
Nick Fagerlund 2021-11-01 10:20:15 -07:00 committed by GitHub
parent 079aabb70e
commit 02e62c9851
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 135 additions and 1 deletions

View File

@ -0,0 +1,108 @@
//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_init_with_empty_tags(t *testing.T) {
skipWithoutRemoteTerraformVersion(t)
cases := map[string]struct {
operations []operationSets
}{
"terraform init with cloud block - no tagged workspaces exist yet": {
operations: []operationSets{
{
prep: func(t *testing.T, orgName, dir string) {
wsTag := "emptytag"
tfBlock := terraformConfigCloudBackendTags(orgName, wsTag)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init"},
expectedCmdOutput: `There are no workspaces with the configured tags`,
userInput: []string{"emptytag-prod"},
postInputOutput: []string{`Terraform Cloud has been successfully initialized!`},
},
},
},
},
},
}
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

@ -223,8 +223,25 @@ func (m *Meta) selectWorkspace(b backend.Backend) error {
return fmt.Errorf("Failed to get existing workspaces: %s", err) return fmt.Errorf("Failed to get existing workspaces: %s", err)
} }
if len(workspaces) == 0 { if len(workspaces) == 0 {
if c, ok := b.(*cloud.Cloud); ok && m.input {
// len is always 1 if using Name; 0 means we're using Tags and there
// aren't any matching workspaces. Which might be normal and fine, so
// let's just ask:
name, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{
Id: "create-workspace",
Query: "\n[reset][bold][yellow]No workspaces found.[reset]",
Description: fmt.Sprintf(inputCloudInitCreateWorkspace, strings.Join(c.WorkspaceMapping.Tags, ", ")),
})
name = strings.TrimSpace(name)
if err != nil || name == "" {
return fmt.Errorf("Couldn't create initial workspace: no name provided")
}
log.Printf("[TRACE] Meta.selectWorkspace: selecting the new TFC workspace requested by the user (%s)", name)
return m.SetWorkspace(name)
} else {
return fmt.Errorf(strings.TrimSpace(errBackendNoExistingWorkspaces)) return fmt.Errorf(strings.TrimSpace(errBackendNoExistingWorkspaces))
} }
}
// Get the currently selected workspace. // Get the currently selected workspace.
workspace, err := m.Workspace() workspace, err := m.Workspace()
@ -1376,6 +1393,15 @@ Terraform has detected that the configuration specified for the backend
has changed. Terraform will now check for existing state in the backends. has changed. Terraform will now check for existing state in the backends.
` `
const inputCloudInitCreateWorkspace = `
There are no workspaces with the configured tags (%s)
in your Terraform Cloud organization. To finish initializing, Terraform needs at
least one workspace available.
Terraform can create a properly tagged workspace for you now. Please enter a
name to create a new Terraform Cloud workspace:
`
const successBackendUnset = ` const successBackendUnset = `
Successfully unset the backend %q. Terraform will now operate locally. Successfully unset the backend %q. Terraform will now operate locally.
` `