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:
parent
079aabb70e
commit
02e62c9851
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -223,8 +223,25 @@ func (m *Meta) selectWorkspace(b backend.Backend) error {
|
|||
return fmt.Errorf("Failed to get existing workspaces: %s", err)
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// Get the currently selected 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.
|
||||
`
|
||||
|
||||
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 = `
|
||||
Successfully unset the backend %q. Terraform will now operate locally.
|
||||
`
|
||||
|
|
Loading…
Reference in New Issue