Backend State Migration: Add remote backend test

This commit is contained in:
Omar Ismail 2021-10-09 08:47:12 -04:00 committed by Chris Arcand
parent 3fedd6898c
commit dc76bbee73
6 changed files with 989 additions and 67 deletions

View File

@ -29,7 +29,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) {
TerraformVersion: tfe.String(terraformVersion), TerraformVersion: tfe.String(terraformVersion),
AutoApply: tfe.Bool(false), AutoApply: tfe.Bool(false),
} }
workspace := createWorkspace(t, org, wOpts) workspace := createWorkspace(t, org.Name, wOpts)
cleanup := func() { cleanup := func() {
defer orgCleanup() defer orgCleanup()
} }
@ -73,7 +73,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) {
TerraformVersion: tfe.String(terraformVersion), TerraformVersion: tfe.String(terraformVersion),
AutoApply: tfe.Bool(true), AutoApply: tfe.Bool(true),
} }
workspace := createWorkspace(t, org, wOpts) workspace := createWorkspace(t, org.Name, wOpts)
cleanup := func() { cleanup := func() {
defer orgCleanup() defer orgCleanup()
} }
@ -117,7 +117,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) {
TerraformVersion: tfe.String(terraformVersion), TerraformVersion: tfe.String(terraformVersion),
AutoApply: tfe.Bool(false), AutoApply: tfe.Bool(false),
} }
workspace := createWorkspace(t, org, wOpts) workspace := createWorkspace(t, org.Name, wOpts)
cleanup := func() { cleanup := func() {
defer orgCleanup() defer orgCleanup()
} }
@ -162,7 +162,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) {
TerraformVersion: tfe.String(terraformVersion), TerraformVersion: tfe.String(terraformVersion),
AutoApply: tfe.Bool(true), AutoApply: tfe.Bool(true),
} }
workspace := createWorkspace(t, org, wOpts) workspace := createWorkspace(t, org.Name, wOpts)
cleanup := func() { cleanup := func() {
defer orgCleanup() defer orgCleanup()
} }

View File

@ -56,9 +56,9 @@ func createOrganization(t *testing.T) (*tfe.Organization, func()) {
} }
} }
func createWorkspace(t *testing.T, org *tfe.Organization, wOpts tfe.WorkspaceCreateOptions) *tfe.Workspace { func createWorkspace(t *testing.T, orgName string, wOpts tfe.WorkspaceCreateOptions) *tfe.Workspace {
ctx := context.Background() ctx := context.Background()
w, err := tfeClient.Workspaces.Create(ctx, org.Name, wOpts) w, err := tfeClient.Workspaces.Create(ctx, orgName, wOpts)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -66,6 +66,15 @@ func createWorkspace(t *testing.T, org *tfe.Organization, wOpts tfe.WorkspaceCre
return w return w
} }
func getWorkspace(workspaces []*tfe.Workspace, workspace string) (*tfe.Workspace, bool) {
for _, ws := range workspaces {
if ws.Name == workspace {
return ws, false
}
}
return nil, true
}
func randomString(t *testing.T) string { func randomString(t *testing.T) string {
v, err := uuid.GenerateUUID() v, err := uuid.GenerateUUID()
if err != nil { if err != nil {
@ -87,6 +96,44 @@ output "val" {
`) `)
} }
func terraformConfigRemoteBackendName(org, name string) string {
return fmt.Sprintf(`
terraform {
backend "remote" {
hostname = "%s"
organization = "%s"
workspaces {
name = "%s"
}
}
}
output "val" {
value = "${terraform.workspace}"
}
`, tfeHostname, org, name)
}
func terraformConfigRemoteBackendPrefix(org, prefix string) string {
return fmt.Sprintf(`
terraform {
backend "remote" {
hostname = "%s"
organization = "%s"
workspaces {
prefix = "%s"
}
}
}
output "val" {
value = "${terraform.workspace}"
}
`, tfeHostname, org, prefix)
}
func terraformConfigCloudBackendTags(org, tag string) string { func terraformConfigCloudBackendTags(org, tag string) string {
return fmt.Sprintf(` return fmt.Sprintf(`
terraform { terraform {

View File

@ -5,7 +5,6 @@ package main
import ( import (
"context" "context"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
@ -168,7 +167,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) {
} }
for name, tc := range cases { for name, tc := range cases {
fmt.Println("Test: ", name) t.Log("Test: ", name)
organization, cleanup := createOrganization(t) organization, cleanup := createOrganization(t)
defer cleanup() defer cleanup()
exp, err := expect.NewConsole(expect.WithStdout(os.Stdout), expect.WithDefaultTimeout(expectConsoleTimeout)) exp, err := expect.NewConsole(expect.WithStdout(os.Stdout), expect.WithDefaultTimeout(expectConsoleTimeout))
@ -192,6 +191,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) {
op.prep(t, organization.Name, tf.WorkDir()) op.prep(t, organization.Name, tf.WorkDir())
for _, tfCmd := range op.commands { for _, tfCmd := range op.commands {
t.Log("Running commands: ", tfCmd.command) t.Log("Running commands: ", tfCmd.command)
tfCmd.command = append(tfCmd.command, "-ignore-remote-version")
cmd := tf.Cmd(tfCmd.command...) cmd := tf.Cmd(tfCmd.command...)
cmd.Stdin = exp.Tty() cmd.Stdin = exp.Tty()
cmd.Stdout = exp.Tty() cmd.Stdout = exp.Tty()
@ -357,7 +357,7 @@ func Test_migrate_multi_to_tfc_cloud_tags_strategy(t *testing.T) {
} }
for name, tc := range cases { for name, tc := range cases {
fmt.Println("Test: ", name) t.Log("Test: ", name)
organization, cleanup := createOrganization(t) organization, cleanup := createOrganization(t)
t.Log(organization.Name) t.Log(organization.Name)
defer cleanup() defer cleanup()

View File

@ -4,56 +4,912 @@
package main package main
import ( import (
"context"
"io/ioutil"
"os"
"testing" "testing"
expect "github.com/Netflix/go-expect"
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform/internal/e2e"
) )
// REMOTE BACKEND func Test_migrate_remote_backend_name_to_tfc_name(t *testing.T) {
/* ctx := context.Background()
- RB name -> TFC name cases := map[string]struct {
-- straight copy if only if different name, or same WS name in diff org
-- other
-- ensure that the local workspace, after migration, is the new name (in the tfc config block)
- RB name -> TFC tags
-- just add tag, if in same org
-- If new org, if WS exists, just add tag
-- If new org, if WS not exists, create and add tag
- RB prefix -> TFC name
-- create if not exists
-- migrate the current worksapce state to ws name
- RB prefix -> TFC tags
-- update previous workspaces (prefix + local) with cloud config tag
-- Rename the local workspaces to match the TFC workspaces (prefix + former local, ie app-prod). inform user
*/
func Test_migrate_remote_backend_name_to_tfc(t *testing.T) {
t.Skip("TODO: see comments")
_ = map[string]struct {
operations []operationSets operations []operationSets
validations func(t *testing.T, orgName string) validations func(t *testing.T, orgName string)
}{ }{
"single workspace with backend name strategy, to cloud with name strategy": {}, "backend name strategy, to cloud with name strategy": {
"single workspace with backend name strategy, to cloud with tags strategy": {}, operations: []operationSets{
{
prep: func(t *testing.T, orgName, dir string) {
remoteWorkspace := "remote-workspace"
tfBlock := terraformConfigRemoteBackendName(orgName, remoteWorkspace)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init"},
expectedCmdOutput: `Successfully configured the backend "remote"!`,
},
{
command: []string{"apply"},
expectedCmdOutput: `Do you want to perform these actions in workspace "remote-workspace"?`,
userInput: []string{"yes"},
postInputOutput: []string{`Apply complete!`},
},
},
},
{
prep: func(t *testing.T, orgName, dir string) {
wsName := "cloud-workspace"
tfBlock := terraformConfigCloudBackendName(orgName, wsName)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
expectedCmdOutput: `Do you want to copy existing state to the new backend?`,
userInput: []string{"yes"},
postInputOutput: []string{`Successfully configured the backend "cloud"!`},
},
{
command: []string{"workspace", "show"},
expectedCmdOutput: `cloud-workspace`,
},
},
},
},
validations: func(t *testing.T, orgName string) {
expectedName := "cloud-workspace"
ws, err := tfeClient.Workspaces.Read(ctx, orgName, expectedName)
if err != nil {
t.Fatal(err)
}
if ws == nil {
t.Fatalf("Expected workspace %s to be present, but is not.", expectedName)
}
},
},
"backend name strategy, to cloud name strategy, using the same name": {
operations: []operationSets{
{
prep: func(t *testing.T, orgName, dir string) {
remoteWorkspace := "remote-workspace"
tfBlock := terraformConfigRemoteBackendName(orgName, remoteWorkspace)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init"},
expectedCmdOutput: `Successfully configured the backend "remote"!`,
},
{
command: []string{"apply"},
expectedCmdOutput: `Do you want to perform these actions in workspace "remote-workspace"?`,
userInput: []string{"yes"},
postInputOutput: []string{`Apply complete!`},
},
},
},
{
prep: func(t *testing.T, orgName, dir string) {
wsName := "remote-workspace"
tfBlock := terraformConfigCloudBackendName(orgName, wsName)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
expectedCmdOutput: `Terraform has been successfully initialized!`,
},
{
command: []string{"workspace", "show"},
expectedCmdOutput: `remote-workspace`,
},
},
},
},
validations: func(t *testing.T, orgName string) {
expectedName := "remote-workspace"
ws, err := tfeClient.Workspaces.Read(ctx, orgName, expectedName)
if err != nil {
t.Fatal(err)
}
if ws == nil {
t.Fatalf("Expected workspace %s to be present, but is not.", expectedName)
}
},
},
}
for name, tc := range cases {
t.Log("Test: ", name)
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()
organization, cleanup := createOrganization(t)
defer cleanup()
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 {
t.Fatal(err)
}
}
}
if tc.validations != nil {
tc.validations(t, organization.Name)
}
}
}
func Test_migrate_remote_backend_name_to_tfc_name_different_org(t *testing.T) {
ctx := context.Background()
cases := map[string]struct {
operations []operationSets
validations func(t *testing.T, orgName string)
}{
"backend name strategy, to cloud name strategy, using the same name, different organization": {
operations: []operationSets{
{
prep: func(t *testing.T, orgName, dir string) {
remoteWorkspace := "remote-workspace"
tfBlock := terraformConfigRemoteBackendName(orgName, remoteWorkspace)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init"},
expectedCmdOutput: `Successfully configured the backend "remote"!`,
},
{
command: []string{"apply"},
expectedCmdOutput: `Do you want to perform these actions in workspace "remote-workspace"?`,
userInput: []string{"yes"},
postInputOutput: []string{`Apply complete!`},
},
},
},
{
prep: func(t *testing.T, orgName, dir string) {
wsName := "remote-workspace"
tfBlock := terraformConfigCloudBackendName(orgName, wsName)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
expectedCmdOutput: `Do you want to copy existing state to the new backend?`,
userInput: []string{"yes"},
postInputOutput: []string{`Successfully configured the backend "cloud"!`},
},
{
command: []string{"workspace", "show"},
expectedCmdOutput: `remote-workspace`,
},
},
},
},
validations: func(t *testing.T, orgName string) {
expectedName := "remote-workspace"
ws, err := tfeClient.Workspaces.Read(ctx, orgName, expectedName)
if err != nil {
t.Fatal(err)
}
if ws == nil {
t.Fatalf("Expected workspace %s to be present, but is not.", expectedName)
}
},
},
}
for name, tc := range cases {
t.Log("Test: ", name)
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()
orgOne, cleanupOne := createOrganization(t)
orgTwo, cleanupTwo := createOrganization(t)
defer cleanupOne()
defer cleanupTwo()
orgs := []string{orgOne.Name, orgTwo.Name}
var orgName string
for index, op := range tc.operations {
orgName = orgs[index]
op.prep(t, orgName, 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 {
t.Fatal(err)
}
}
}
if tc.validations != nil {
tc.validations(t, orgName)
}
}
}
func Test_migrate_remote_backend_name_to_tfc_tags(t *testing.T) {
ctx := context.Background()
cases := map[string]struct {
operations []operationSets
validations func(t *testing.T, orgName string)
}{
"single workspace with backend name strategy, to cloud with tags strategy": {
operations: []operationSets{
{
prep: func(t *testing.T, orgName, dir string) {
remoteWorkspace := "remote-workspace"
tfBlock := terraformConfigRemoteBackendName(orgName, remoteWorkspace)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init"},
expectedCmdOutput: `Successfully configured the backend "remote"!`,
},
{
command: []string{"apply"},
expectedCmdOutput: `Do you want to perform these actions in workspace "remote-workspace"?`,
userInput: []string{"yes"},
postInputOutput: []string{`Apply complete!`},
},
{
command: []string{"workspace", "show"},
expectedCmdOutput: `default`,
},
},
},
{
prep: func(t *testing.T, orgName, dir string) {
tag := "app"
tfBlock := terraformConfigCloudBackendTags(orgName, tag)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
expectedCmdOutput: `The "cloud" backend configuration only allows named workspaces!`,
userInput: []string{"cloud-workspace", "yes"},
postInputOutput: []string{
`Do you want to copy existing state to the new backend?`,
`Successfully configured the backend "cloud"!`},
},
{
command: []string{"workspace", "show"},
expectedCmdOutput: `cloud-workspace`,
},
},
},
},
validations: func(t *testing.T, orgName string) {
wsList, err := tfeClient.Workspaces.List(ctx, orgName, tfe.WorkspaceListOptions{
Tags: tfe.String("app"),
})
if err != nil {
t.Fatal(err)
}
if len(wsList.Items) != 1 {
t.Fatalf("Expected number of workspaces to be 1, but got %d", len(wsList.Items))
}
ws := wsList.Items[0]
if ws.Name != "cloud-workspace" {
t.Fatalf("Expected workspace to be `cloud-workspace`, but is %s", ws.Name)
}
},
},
}
for name, tc := range cases {
t.Log("Test: ", name)
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()
organization, cleanup := createOrganization(t)
defer cleanup()
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 {
t.Fatal(err)
}
}
}
if tc.validations != nil {
tc.validations(t, organization.Name)
}
} }
} }
func Test_migrate_remote_backend_prefix_to_tfc_name(t *testing.T) { func Test_migrate_remote_backend_prefix_to_tfc_name(t *testing.T) {
t.Skip("TODO: see comments") ctx := context.Background()
_ = map[string]struct { cases := map[string]struct {
operations []operationSets operations []operationSets
validations func(t *testing.T, orgName string) validations func(t *testing.T, orgName string)
}{ }{
"single workspace with backend prefix strategy, to cloud with name strategy": {}, "single workspace with backend prefix strategy, to cloud with name strategy": {
"multiple workspaces with backend prefix strategy, to cloud with name strategy": {}, operations: []operationSets{
{
prep: func(t *testing.T, orgName, dir string) {
_ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{Name: tfe.String("app-one")})
prefix := "app-"
tfBlock := terraformConfigRemoteBackendPrefix(orgName, prefix)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init"},
expectedCmdOutput: `Terraform has been successfully initialized!`,
},
{
command: []string{"apply"},
expectedCmdOutput: `Do you want to perform these actions in workspace "app-one"?`,
userInput: []string{"yes"},
postInputOutput: []string{`Apply complete!`},
},
},
},
{
prep: func(t *testing.T, orgName, dir string) {
wsName := "cloud-workspace"
tfBlock := terraformConfigCloudBackendName(orgName, wsName)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
expectedCmdOutput: `Do you want to copy existing state to the new backend?`,
userInput: []string{"yes"},
postInputOutput: []string{
`Successfully configured the backend "cloud"!`},
},
{
command: []string{"workspace", "show"},
expectedCmdOutput: `cloud-workspace`,
},
},
},
},
validations: func(t *testing.T, orgName string) {
expectedName := "cloud-workspace"
ws, err := tfeClient.Workspaces.Read(ctx, orgName, expectedName)
if err != nil {
t.Fatal(err)
}
if ws == nil {
t.Fatalf("Expected workspace %s to be present, but is not.", expectedName)
}
},
},
"multiple workspaces with backend prefix strategy, to cloud with name strategy": {
operations: []operationSets{
{
prep: func(t *testing.T, orgName, dir string) {
_ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{Name: tfe.String("app-one")})
_ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{Name: tfe.String("app-two")})
prefix := "app-"
tfBlock := terraformConfigRemoteBackendPrefix(orgName, prefix)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init"},
expectedCmdOutput: `The currently selected workspace (default) does not exist.`,
userInput: []string{"1"},
postInputOutput: []string{`Terraform has been successfully initialized!`},
},
{
command: []string{"apply"},
expectedCmdOutput: `Do you want to perform these actions in workspace "app-one"?`,
userInput: []string{"yes"},
postInputOutput: []string{`Apply complete!`},
},
{
command: []string{"workspace", "list"},
expectedCmdOutput: "* one", // app name retrieved via prefix
},
{
command: []string{"workspace", "select", "two"},
expectedCmdOutput: `Switched to workspace "two".`, // app name retrieved via prefix
},
},
},
{
prep: func(t *testing.T, orgName, dir string) {
wsName := "cloud-workspace"
tfBlock := terraformConfigCloudBackendName(orgName, wsName)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
expectedCmdOutput: `Do you want to copy only your current workspace?`,
userInput: []string{"yes"},
postInputOutput: []string{
`Successfully configured the backend "cloud"!`},
},
{
command: []string{"workspace", "show"},
expectedCmdOutput: `cloud-workspace`,
},
},
},
},
validations: func(t *testing.T, orgName string) {
expectedName := "cloud-workspace"
ws, err := tfeClient.Workspaces.Read(ctx, orgName, expectedName)
if err != nil {
t.Fatal(err)
}
if ws == nil {
t.Fatalf("Expected workspace %s to be present, but is not.", expectedName)
}
wsList, err := tfeClient.Workspaces.List(ctx, orgName, tfe.WorkspaceListOptions{})
if err != nil {
t.Fatal(err)
}
if len(wsList.Items) != 3 {
t.Fatalf("expected number of workspaces in this org to be 3, but got %d", len(wsList.Items))
}
ws, empty := getWorkspace(wsList.Items, "cloud-workspace")
if empty {
t.Fatalf("expected workspaces to include 'cloud-workspace' but didn't.")
}
ws, empty = getWorkspace(wsList.Items, "app-one")
if empty {
t.Fatalf("expected workspaces to include 'app-one' but didn't.")
}
ws, empty = getWorkspace(wsList.Items, "app-two")
if empty {
t.Fatalf("expected workspaces to include 'app-two' but didn't.")
}
},
},
}
for name, tc := range cases {
t.Log("Test: ", name)
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()
organization, cleanup := createOrganization(t)
defer cleanup()
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 {
t.Fatal(err)
}
}
}
if tc.validations != nil {
tc.validations(t, organization.Name)
}
} }
} }
func Test_migrate_remote_backend_prefix_to_tfc_tags(t *testing.T) { func Test_migrate_remote_backend_prefix_to_tfc_tags(t *testing.T) {
t.Skip("TODO: see comments") ctx := context.Background()
_ = map[string]struct { cases := map[string]struct {
operations []operationSets operations []operationSets
validations func(t *testing.T, orgName string) validations func(t *testing.T, orgName string)
}{ }{
"single workspace with backend prefix strategy, to cloud with tags strategy": {}, "single workspace with backend prefix strategy, to cloud with tags strategy": {
"multiple workspaces with backend prefix strategy, to cloud with tags strategy": {}, operations: []operationSets{
{
prep: func(t *testing.T, orgName, dir string) {
_ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{Name: tfe.String("app-one")})
prefix := "app-"
tfBlock := terraformConfigRemoteBackendPrefix(orgName, prefix)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init"},
expectedCmdOutput: `Terraform has been successfully initialized!`,
},
{
command: []string{"apply"},
expectedCmdOutput: `Do you want to perform these actions in workspace "app-one"?`,
userInput: []string{"yes"},
postInputOutput: []string{`Apply complete!`},
},
},
},
{
prep: func(t *testing.T, orgName, dir string) {
tag := "app"
tfBlock := terraformConfigCloudBackendTags(orgName, tag)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
expectedCmdOutput: `The "cloud" backend configuration only allows named workspaces!`,
userInput: []string{"cloud-workspace", "yes"},
postInputOutput: []string{
`Do you want to copy existing state to the new backend?`,
`Successfully configured the backend "cloud"!`},
},
{
command: []string{"workspace", "list"},
expectedCmdOutput: `cloud-workspace`,
},
},
},
},
validations: func(t *testing.T, orgName string) {
expectedName := "cloud-workspace"
ws, err := tfeClient.Workspaces.Read(ctx, orgName, expectedName)
if err != nil {
t.Fatal(err)
}
if ws == nil {
t.Fatalf("Expected workspace %s to be present, but is not.", expectedName)
}
},
},
"multiple workspaces with backend prefix strategy, to cloud with tags strategy": {
operations: []operationSets{
{
prep: func(t *testing.T, orgName, dir string) {
_ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{Name: tfe.String("app-one")})
_ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{Name: tfe.String("app-two")})
prefix := "app-"
tfBlock := terraformConfigRemoteBackendPrefix(orgName, prefix)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init"},
expectedCmdOutput: `The currently selected workspace (default) does not exist.`,
userInput: []string{"1"},
postInputOutput: []string{`Terraform has been successfully initialized!`},
},
{
command: []string{"apply"},
expectedCmdOutput: `Do you want to perform these actions in workspace "app-one"?`,
userInput: []string{"yes"},
postInputOutput: []string{`Apply complete!`},
},
{
command: []string{"workspace", "select", "two"},
},
{
command: []string{"apply"},
expectedCmdOutput: `Do you want to perform these actions in workspace "app-two"?`,
userInput: []string{"yes"},
postInputOutput: []string{`Apply complete!`},
},
},
},
{
prep: func(t *testing.T, orgName, dir string) {
tag := "app"
tfBlock := terraformConfigCloudBackendTags(orgName, tag)
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state", "-ignore-remote-version"},
expectedCmdOutput: `Would you like to rename your workspaces?`,
userInput: []string{"1", "*"},
postInputOutput: []string{`What pattern would you like to add to all your workspaces?`,
`Successfully configured the backend "cloud"!`},
},
{
command: []string{"workspace", "show"},
expectedCmdOutput: "two", // this comes from the original workspace name from the previous backend.
},
{
command: []string{"workspace", "select", "one"},
expectedCmdOutput: `Switched to workspace "one".`, // this comes from the original workspace name from the previous backend.
},
},
},
},
validations: func(t *testing.T, orgName string) {
wsList, err := tfeClient.Workspaces.List(ctx, orgName, tfe.WorkspaceListOptions{
Tags: tfe.String("app"),
})
if err != nil {
t.Fatal(err)
}
if len(wsList.Items) != 2 {
t.Logf("Expected the number of workspaces to be 2, but got %d", len(wsList.Items))
}
ws, empty := getWorkspace(wsList.Items, "one")
if empty {
t.Fatalf("expected workspaces to include 'one' but didn't.")
}
if len(ws.TagNames) == 0 {
t.Fatalf("expected workspaces 'one' to have tags.")
}
ws, empty = getWorkspace(wsList.Items, "two")
if empty {
t.Fatalf("expected workspaces to include 'two' but didn't.")
}
if len(ws.TagNames) == 0 {
t.Fatalf("expected workspaces 'two' to have tags.")
}
},
},
}
for name, tc := range cases {
t.Log("Test: ", name)
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()
organization, cleanup := createOrganization(t)
defer cleanup()
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 {
t.Fatal(err)
}
}
}
if tc.validations != nil {
tc.validations(t, organization.Name)
}
} }
} }

View File

@ -57,16 +57,16 @@ func Test_cloud_run_variables(t *testing.T) {
}, },
commands: []tfCommand{ commands: []tfCommand{
{ {
command: []string{"init"}, command: []string{"init"},
expectedOutput: `Successfully configured the backend "cloud"!`, expectedCmdOutput: `Successfully configured the backend "cloud"!`,
}, },
{ {
command: []string{"plan", "-var", "foo=bar"}, command: []string{"plan", "-var", "foo=bar"},
expectedOutput: ` + test_cli = "bar"`, expectedCmdOutput: ` + test_cli = "bar"`,
}, },
{ {
command: []string{"plan", "-var", "foo=bar"}, command: []string{"plan", "-var", "foo=bar"},
expectedOutput: ` + test_env = "qux"`, expectedCmdOutput: ` + test_env = "qux"`,
}, },
}, },
}, },
@ -110,23 +110,28 @@ func Test_cloud_run_variables(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if tfCmd.expectedOutput != "" { if tfCmd.expectedCmdOutput != "" {
_, err := exp.ExpectString(tfCmd.expectedOutput) _, err := exp.ExpectString(tfCmd.expectedCmdOutput)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
if len(tfCmd.userInput) > 0 { lenInput := len(tfCmd.userInput)
for _, input := range tfCmd.userInput { lenInputOutput := len(tfCmd.postInputOutput)
if lenInput > 0 {
for i := 0; i < lenInput; i++ {
input := tfCmd.userInput[i]
exp.SendLine(input) exp.SendLine(input)
} // use the index to find the corresponding
} // output that matches the input.
if lenInputOutput-1 >= i {
if tfCmd.postInputOutput != "" { output := tfCmd.postInputOutput[i]
_, err := exp.ExpectString(tfCmd.postInputOutput) _, err := exp.ExpectString(output)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
}
}
} }
} }

View File

@ -44,7 +44,7 @@ type backendMigrateOpts struct {
// //
// This will attempt to lock both states for the migration. // This will attempt to lock both states for the migration.
func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
log.Printf("[TRACE] backendMigrateState: need to migrate from %q to %q backend config", opts.SourceType, opts.DestinationType) log.Printf("[INFO] backendMigrateState: need to migrate from %q to %q backend config", opts.SourceType, opts.DestinationType)
// We need to check what the named state status is. If we're converting // We need to check what the named state status is. If we're converting
// from multi-state to single-state for example, we need to handle that. // from multi-state to single-state for example, we need to handle that.
var sourceSingleState, destinationSingleState, sourceTFC, destinationTFC bool var sourceSingleState, destinationSingleState, sourceTFC, destinationTFC bool
@ -154,7 +154,7 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
// Multi-state to multi-state. // Multi-state to multi-state.
func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error { func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error {
log.Print("[TRACE] backendMigrateState: migrating all named workspaces") log.Print("[INFO] backendMigrateState: migrating all named workspaces")
migrate := opts.force migrate := opts.force
if !migrate { if !migrate {
@ -209,9 +209,9 @@ func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error {
// Multi-state to single state. // Multi-state to single state.
func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error { func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error {
log.Printf("[TRACE] backendMigrateState: destination backend type %q does not support named workspaces", opts.DestinationType) log.Printf("[INFO] backendMigrateState: destination backend type %q does not support named workspaces", opts.DestinationType)
currentEnv, err := m.Workspace() currentWorkspace, err := m.Workspace()
if err != nil { if err != nil {
return err return err
} }
@ -228,7 +228,7 @@ func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error {
opts.DestinationType), opts.DestinationType),
Description: fmt.Sprintf( Description: fmt.Sprintf(
strings.TrimSpace(inputBackendMigrateMultiToSingle), strings.TrimSpace(inputBackendMigrateMultiToSingle),
opts.SourceType, opts.DestinationType, currentEnv), opts.SourceType, opts.DestinationType, currentWorkspace),
}) })
if err != nil { if err != nil {
return fmt.Errorf( return fmt.Errorf(
@ -241,7 +241,7 @@ func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error {
} }
// Copy the default state // Copy the default state
opts.sourceWorkspace = currentEnv opts.sourceWorkspace = currentWorkspace
// now switch back to the default env so we can acccess the new backend // now switch back to the default env so we can acccess the new backend
m.SetWorkspace(backend.DefaultStateName) m.SetWorkspace(backend.DefaultStateName)
@ -251,7 +251,7 @@ func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error {
// Single state to single state, assumed default state name. // Single state to single state, assumed default state name.
func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
log.Printf("[TRACE] backendMigrateState: migrating %q workspace to %q workspace", opts.sourceWorkspace, opts.destinationWorkspace) log.Printf("[INFO] backendMigrateState: single-to-single migrating %q workspace to %q workspace", opts.sourceWorkspace, opts.destinationWorkspace)
sourceState, err := opts.Source.StateMgr(opts.sourceWorkspace) sourceState, err := opts.Source.StateMgr(opts.sourceWorkspace)
if err != nil { if err != nil {
@ -534,7 +534,7 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error {
// Everything below, by the above two conditionals, now assumes that the // Everything below, by the above two conditionals, now assumes that the
// destination is always Terraform Cloud (TFC). // destination is always Terraform Cloud (TFC).
sourceSingle := sourceSingleState || (len(sourceWorkspaces) == 1 && sourceWorkspaces[0] == backend.DefaultStateName) sourceSingle := sourceSingleState || (len(sourceWorkspaces) == 1)
if sourceSingle { if sourceSingle {
if cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy { if cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy {
// If we know the name via WorkspaceNameStrategy, then set the // If we know the name via WorkspaceNameStrategy, then set the
@ -543,6 +543,14 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error {
// in TFC if it does not exist. // in TFC if it does not exist.
opts.destinationWorkspace = cloudBackendDestination.WorkspaceMapping.Name opts.destinationWorkspace = cloudBackendDestination.WorkspaceMapping.Name
} }
currentWorkspace, err := m.Workspace()
if err != nil {
return err
}
opts.sourceWorkspace = currentWorkspace
log.Printf("[INFO] backendMigrateTFC: single-to-single migration from source %s to destination %q", opts.sourceWorkspace, opts.destinationWorkspace)
// Run normal single-to-single state migration // Run normal single-to-single state migration
// This will handle both situations where the new cloud backend // This will handle both situations where the new cloud backend
// configuration is using a workspace.name strategy or workspace.tags // configuration is using a workspace.name strategy or workspace.tags
@ -559,13 +567,14 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error {
return err return err
} }
currentEnv, err := m.Workspace() currentWorkspace, err := m.Workspace()
if err != nil { if err != nil {
return err return err
} }
opts.sourceWorkspace = currentEnv opts.sourceWorkspace = currentWorkspace
opts.destinationWorkspace = cloudBackendDestination.WorkspaceMapping.Name opts.destinationWorkspace = cloudBackendDestination.WorkspaceMapping.Name
log.Printf("[INFO] backendMigrateTFC: multi-to-single migration from source %s to destination %q", opts.sourceWorkspace, opts.destinationWorkspace)
return m.backendMigrateState_s_s(opts) return m.backendMigrateState_s_s(opts)
} }
@ -574,9 +583,13 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error {
// workspace over to new one, prompt for workspace name pattern (*), // workspace over to new one, prompt for workspace name pattern (*),
// and start migrating, and create tags for each workspace. // and start migrating, and create tags for each workspace.
if multiSource && destinationTagsStrategy { if multiSource && destinationTagsStrategy {
log.Printf("[INFO] backendMigrateTFC: multi-to-multi migration from source workspaces %q", sourceWorkspaces)
return m.backendMigrateState_S_TFC(opts, sourceWorkspaces) return m.backendMigrateState_S_TFC(opts, sourceWorkspaces)
} }
// TODO(omar): after the check for sourceSingle is done, everything following
// it has to be multi. So rework the code to not need to check for multi, adn
// return m.backendMigrateState_S_TFC here.
return nil return nil
} }
@ -619,6 +632,7 @@ func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspa
opts.force = true opts.force = true
// Perform the migration // Perform the migration
log.Printf("[INFO] backendMigrateTFC: multi-to-multi migration, source workspace %q to destination workspace %q", opts.sourceWorkspace, opts.destinationWorkspace)
if err := m.backendMigrateState_s_s(opts); err != nil { if err := m.backendMigrateState_s_s(opts); err != nil {
return fmt.Errorf(strings.TrimSpace( return fmt.Errorf(strings.TrimSpace(
errMigrateMulti), name, opts.SourceType, opts.DestinationType, err) errMigrateMulti), name, opts.SourceType, opts.DestinationType, err)