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),
AutoApply: tfe.Bool(false),
}
workspace := createWorkspace(t, org, wOpts)
workspace := createWorkspace(t, org.Name, wOpts)
cleanup := func() {
defer orgCleanup()
}
@ -73,7 +73,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) {
TerraformVersion: tfe.String(terraformVersion),
AutoApply: tfe.Bool(true),
}
workspace := createWorkspace(t, org, wOpts)
workspace := createWorkspace(t, org.Name, wOpts)
cleanup := func() {
defer orgCleanup()
}
@ -117,7 +117,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) {
TerraformVersion: tfe.String(terraformVersion),
AutoApply: tfe.Bool(false),
}
workspace := createWorkspace(t, org, wOpts)
workspace := createWorkspace(t, org.Name, wOpts)
cleanup := func() {
defer orgCleanup()
}
@ -162,7 +162,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) {
TerraformVersion: tfe.String(terraformVersion),
AutoApply: tfe.Bool(true),
}
workspace := createWorkspace(t, org, wOpts)
workspace := createWorkspace(t, org.Name, wOpts)
cleanup := func() {
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()
w, err := tfeClient.Workspaces.Create(ctx, org.Name, wOpts)
w, err := tfeClient.Workspaces.Create(ctx, orgName, wOpts)
if err != nil {
t.Fatal(err)
}
@ -66,6 +66,15 @@ func createWorkspace(t *testing.T, org *tfe.Organization, wOpts tfe.WorkspaceCre
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 {
v, err := uuid.GenerateUUID()
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 {
return fmt.Sprintf(`
terraform {

View File

@ -5,7 +5,6 @@ package main
import (
"context"
"fmt"
"io/ioutil"
"os"
"testing"
@ -168,7 +167,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) {
}
for name, tc := range cases {
fmt.Println("Test: ", name)
t.Log("Test: ", name)
organization, cleanup := createOrganization(t)
defer cleanup()
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())
for _, tfCmd := range op.commands {
t.Log("Running commands: ", tfCmd.command)
tfCmd.command = append(tfCmd.command, "-ignore-remote-version")
cmd := tf.Cmd(tfCmd.command...)
cmd.Stdin = 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 {
fmt.Println("Test: ", name)
t.Log("Test: ", name)
organization, cleanup := createOrganization(t)
t.Log(organization.Name)
defer cleanup()

View File

@ -4,56 +4,912 @@
package main
import (
"context"
"io/ioutil"
"os"
"testing"
expect "github.com/Netflix/go-expect"
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform/internal/e2e"
)
// REMOTE BACKEND
/*
- RB name -> TFC name
-- 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 {
func Test_migrate_remote_backend_name_to_tfc_name(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 name strategy": {},
"single workspace with backend name strategy, to cloud with tags strategy": {},
"backend name strategy, to cloud with name 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) {
t.Skip("TODO: see comments")
_ = map[string]struct {
ctx := context.Background()
cases := map[string]struct {
operations []operationSets
validations func(t *testing.T, orgName string)
}{
"single workspace with backend prefix strategy, to cloud with name strategy": {},
"multiple workspaces with backend prefix strategy, to cloud with name strategy": {},
"single workspace 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) {
t.Skip("TODO: see comments")
_ = map[string]struct {
ctx := context.Background()
cases := map[string]struct {
operations []operationSets
validations func(t *testing.T, orgName string)
}{
"single workspace with backend prefix strategy, to cloud with tags strategy": {},
"multiple workspaces with backend prefix strategy, to cloud with tags strategy": {},
"single workspace 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

@ -58,15 +58,15 @@ func Test_cloud_run_variables(t *testing.T) {
commands: []tfCommand{
{
command: []string{"init"},
expectedOutput: `Successfully configured the backend "cloud"!`,
expectedCmdOutput: `Successfully configured the backend "cloud"!`,
},
{
command: []string{"plan", "-var", "foo=bar"},
expectedOutput: ` + test_cli = "bar"`,
expectedCmdOutput: ` + test_cli = "bar"`,
},
{
command: []string{"plan", "-var", "foo=bar"},
expectedOutput: ` + test_env = "qux"`,
expectedCmdOutput: ` + test_env = "qux"`,
},
},
},
@ -110,25 +110,30 @@ func Test_cloud_run_variables(t *testing.T) {
t.Fatal(err)
}
if tfCmd.expectedOutput != "" {
_, err := exp.ExpectString(tfCmd.expectedOutput)
if tfCmd.expectedCmdOutput != "" {
_, err := exp.ExpectString(tfCmd.expectedCmdOutput)
if err != nil {
t.Fatal(err)
}
}
if len(tfCmd.userInput) > 0 {
for _, input := range tfCmd.userInput {
lenInput := len(tfCmd.userInput)
lenInputOutput := len(tfCmd.postInputOutput)
if lenInput > 0 {
for i := 0; i < lenInput; i++ {
input := tfCmd.userInput[i]
exp.SendLine(input)
}
}
if tfCmd.postInputOutput != "" {
_, err := exp.ExpectString(tfCmd.postInputOutput)
// 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 {

View File

@ -44,7 +44,7 @@ type backendMigrateOpts struct {
//
// This will attempt to lock both states for the migration.
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
// from multi-state to single-state for example, we need to handle that.
var sourceSingleState, destinationSingleState, sourceTFC, destinationTFC bool
@ -154,7 +154,7 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
// Multi-state to multi-state.
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
if !migrate {
@ -209,9 +209,9 @@ func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error {
// Multi-state to single state.
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 {
return err
}
@ -228,7 +228,7 @@ func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error {
opts.DestinationType),
Description: fmt.Sprintf(
strings.TrimSpace(inputBackendMigrateMultiToSingle),
opts.SourceType, opts.DestinationType, currentEnv),
opts.SourceType, opts.DestinationType, currentWorkspace),
})
if err != nil {
return fmt.Errorf(
@ -241,7 +241,7 @@ func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error {
}
// Copy the default state
opts.sourceWorkspace = currentEnv
opts.sourceWorkspace = currentWorkspace
// now switch back to the default env so we can acccess the new backend
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.
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)
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
// destination is always Terraform Cloud (TFC).
sourceSingle := sourceSingleState || (len(sourceWorkspaces) == 1 && sourceWorkspaces[0] == backend.DefaultStateName)
sourceSingle := sourceSingleState || (len(sourceWorkspaces) == 1)
if sourceSingle {
if cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy {
// 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.
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
// This will handle both situations where the new cloud backend
// configuration is using a workspace.name strategy or workspace.tags
@ -559,13 +567,14 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error {
return err
}
currentEnv, err := m.Workspace()
currentWorkspace, err := m.Workspace()
if err != nil {
return err
}
opts.sourceWorkspace = currentEnv
opts.sourceWorkspace = currentWorkspace
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)
}
@ -574,9 +583,13 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error {
// workspace over to new one, prompt for workspace name pattern (*),
// and start migrating, and create tags for each workspace.
if multiSource && destinationTagsStrategy {
log.Printf("[INFO] backendMigrateTFC: multi-to-multi migration from source workspaces %q", 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
}
@ -619,6 +632,7 @@ func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspa
opts.force = true
// 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 {
return fmt.Errorf(strings.TrimSpace(
errMigrateMulti), name, opts.SourceType, opts.DestinationType, err)