Commit 55fc5909 authored by Omar Ismail's avatar Omar Ismail Committed by Chris Arcand
Browse files

Teraform Cloud Backend State Migration

* determining source or destination to cloud
* handling single to single state migrations to cloud,
using a name strategy or a tags strategy
* Add end-to-end tests for state migration.
parent 7cc53fe1
main MatthewTestBranch add-cont-valid-callout add-internals-to-sidebar add-jsonstate-to-cloudbackendstate add-jsonstate-to-cloudbackendstate2 add-learn-callout-moved-blocks add-new-intro-docs add-note-about-spaces add-tutorial-custom-conditions add-version-notes-1.2 add-warnings-backends alisdair/disable-preconditions-postconditions alisdair/fix-configload-snapshot-panic alisdair/metadata-functions-command alisdair/resource-instance-object-dependencies b-1.1-module-source-git b-check-output-multi-expand b-check-resource-multi-expand b-flatten-panic b-move-implied-cross-package b-type-conversion-funcs-null b-yamldecode-emptydoc-null backport/29156_do_not_log_sensitive_values/nearly-safe-bullfrog backport/4th-alternate-mirror-directory-fix/closely-key-civet backport/add-cont-valid-callout/luckily-golden-wildcat backport/add-format-function-guidance/ultimately-lucky-halibut backport/add-internals-to-sidebar/willingly-bright-sunbird backport/add-learn-callout-moved-blocks/multiply-awaited-cub backport/add-new-intro-docs/strangely-lucky-bluegill backport/add-powershell-warning/certainly-amazing-lemur backport/add-version-notes-1.2/amazingly-premium-woodcock backport/add-warnings-backends/physically-many-mantis backport/alisdair/fix-30641/firmly-musical-lemming backport/alisdair/fix-show-plan-against-non-default-state/blatantly-balanced-porpoise backport/alisdair/pre-convert-optional-defaults/virtually-able-mouse backport/alisdair/redact-sensitive-values-from-function-errors/definitely-assured-mantis backport/b-check-resource-multi-expand/extremely-key-sheep backport/backport/cstella84/patch-add-hyperlink-for-referenced-argument/manually-patient-locust backport/barrettclark/update-go-slug/severely-destined-crow backport/bugfix_typos/hardly-sharp-mosquito backport/clarify-backend-state-storage/ultimately-causal-ladybird backport/cstella84/patch-add-hyperlink-for-referenced-argument backport/doc-provisioner-scp/definitely-capital-bug backport/doc-refactoring-nav-link/honestly-sweet-sawfly backport/doc-s3-fix/utterly-close-rabbit backport/docs-fix-typo/usefully-blessed-monkey backport/docs-for-each-list-toset/basically-still-zebra backport/docs/unknwon-value/completely-musical-lionfish backport/f-build-go1.19.3/largely-peaceful-grouper backport/f-non-existing-module-instance-crash/neatly-perfect-kiwi backport/file-provisioner-powershell-warning/noticeably-adequate-mullet backport/fix-apt-page/abnormally-relative-quagga backport/fix-backends-link/strangely-emerging-crane backport/fix-backends-link/vertically-noble-hornet backport/fix-broken-link/certainly-measured-chipmunk backport/fix-broken-links-1-10/firmly-equal-rabbit backport/fix-cdktf-link/highly-pumped-snapper backport/fix-glossary-table-contents/uniquely-pro-garfish backport/fix-grammar/precisely-polite-tortoise backport/fix-internals-overview/globally-allowed-kid backport/fix-internals-overview/noticeably-up-rodent backport/fix-intro-page-images/constantly-capable-shiner backport/fix-last-intro-nits/infinitely-workable-redfish backport/fix-links-devdot/strictly-notable-sparrow backport/fix-links-release/seemingly-living-dinosaur backport/fix-remote-backend-references/primarily-tops-mite backport/fix-workspace-name-docs/ideally-uncommon-pheasant backport/jbardin/cancel-auto-approve/extremely-brave-bear backport/jbardin/k8s-mod-update/likely-probable-falcon backport/jbardin/static-validate-nested-types/possibly-crack-mouse backport/kevin/rewrite-internal-redirects/quietly-helped-pelican backport/main/cleanly-mature-scorpion backport/mg_no_code_prov_followup/marginally-relevant-eagle backport/mktg-tf-76ef54dc3c574e032725e0341be8e1d2/constantly-smart-kingfish backport/mktg-tf-76ef54dc3c574e032725e0341be8e1d2/distinctly-sharp-ferret backport/mktg-tf-76ef54dc3c574e032725e0341be8e1d2/friendly-evident-grouse backport/module-invocation-warning/fully-fitting-buzzard backport/nvanthao/update-docs-implicit-provider/locally-neutral-lemur backport/optional-type-attributes-note/inherently-dear-goat backport/patch-1/gladly-mature-oarfish backport/patch-1/manually-fine-mantis backport/patch-1/nationally-working-kite backport/patch-1/noticeably-comic-manatee backport/patch-1/rarely-informed-gopher backport/patch-1/sensibly-saving-swine backport/patch-1/usually-clear-shad backport/patch-1/vaguely-deciding-beagle backport/patch-1/virtually-more-rhino backport/patch-1/wholly-verified-racer backport/patch-1/willingly-usable-husky backport/patch-1/yearly-rich-skunk backport/patch-2/badly-game-spider backport/patch-2/finally-amazed-catfish backport/patch-2/openly-clean-tick backport/patch-2/weekly-selected-tiger backport/remove-future-statement-import/briefly-viable-glowworm backport/remove-provisioners/readily-correct-ferret backport/remove-provisioners/widely-singular-hound backport/replace-flag-clarifications/definitely-saved-elf backport/replace-flag-updates/nominally-assured-weevil backport/startsswith-to-startswith/highly-gorgeous-katydid backport/system-parameter/infinitely-open-bluebird backport/tweak-multi-to-multi-migration-tfc/suddenly-real-duckling backport/update-cloud-block-pages/verbally-key-kangaroo backport/update-console-docs/closely-genuine-javelin backport/update-for-each-example/early-crucial-piranha backport/update-plan-page/lightly-outgoing-halibut backport/update-run-task-result/factually-star-sunfish backport/update_docs_for_30072/gradually-trusting-wahoo backport/workspaces-confusion-fixes/secondly-huge-titmouse barrettclark/fix-state-outputs-read-permissions brandonc/changelog_nested_sensitive brandonc/changelog_sensitive_diff_fixes brandonc/cloud_upgrade_013 brandonc/nested_attr_sensitive brandonc/output_cloud_reads brandonc/providers-estimate brandonc/run_variables_types brandonc/scheme_override_cloud build-pr-checks build-workflow-dev/cgo-enabled build-workflow-dev/liamcervante/equivalence-test-action bump-gcp-storage-dependency bump-gcp-storage-dependency-2 cloud-e2e-fix cloud-integration-changelog-entry dependabot/go_modules/github.com/bmatcuk/doublestar-1.3.4 dependabot/go_modules/github.com/mattn/go-shellwords-1.0.12 dev-portal-updates-docs dividers-devdot-fixes doc-provisioner-scp doc-refactoring-nav-link doc-unicode-hcl doc-yamlencode-stable docs-for-each-list-toset docs-readme-updates-versioned-docs ds.submodule-nav-main f-addrs-static-checkable f-build-go1.19.3 f-cli-hide-fast-refresh f-cmd-web f-diagnostics-cli-reorg f-dynamic-provider-assignment f-e2etest-deps-forbidden f-expand-root-outputs f-fileexists-errmsg f-functions-in-providers f-implied-move-module-call f-init-provider-source-feedback f-jsonstate-2 f-moduletest-2 f-new-build-pipeline f-ng-workflow f-output-value-types f-partial-plan-on-error f-partial-plan-on-error-ui f-persistent-checks-old f-svcauth-environment f-testing-with-conditions f/azurerm-backend-msal fix-apt-page fix-broken-link fix-broken-links-1-10 fix-cdktf-link fix-dividers-for-devdot fix-future-facing-language fix-future-lang-2 fix-internals-overview fix-intro-page-images fix-last-intro-nits fix-links-devdot fix-links-release fix-postconditions-example fix-preconditions fix-provisioners-content fix-readme-again gcs-backend-add-kms gcs-backend-add-private-connect-support gcs-refactor-credential-handling gs/add-pre-plan-run-tasks jbardin/1.3-destroy-perf jbardin/backport-31576 jbardin/call-plan-destroy jbardin/data-source-destroy-edges jbardin/lookup-objects jbardin/output-perf jbardin/plan-orphan-deleted jbardin/remove-deprecated-backends jbardin/resolved-provided-by jbardin/terraform-data jbardin/terraform-null jbardin/trigger-replacement jbardin/variable-eval kevin/local-preview-post-split kevin/preview kevin/remove-guides-docs kevin/vercel-config kmoe/http-backend-debug-log kmoe/init-checksum-miss-error kmoe/misc-help-text kmoe/unused-resource-attributes lafentres/autolabel-dependabot-prs lafentres/refactor-show-command laura-update-docs-readme laura-update-pre-post-conditions liamcervante/cicd-go-vet liamcervante/structured-run-output link_workflow_tutorials migrate-go-tfe-1_0 nf/nov21-migrate-away-from-cloud preapply-runtasks-cli-output preapply-runtasks-clioutput release-notes-env-credentials replace-flag-updates rt-backport-changelog rt-changelog-entry-1.1 run-tasks-backport sebasslash/add-cloud-e2e-test-workflow sebasslash/add-tf-hostname-env-var sebasslash/add-tf-org-env-var sebasslash/env-cloud-e2e-tests sebasslash/err-approval-input-false sebasslash/resolve-flaky-env-var-test sebasslash/tf-workspace-cloud-config stable-website tags-reconfigure-msg tchupp/override-local-vars test-branch-protection-workflow tfc-integration-docs uk1288/backport-cloud-integration-panic-fix uk1288/fix-for-cloud-integration-panic uk1288/update-changelog-md uk1288/update-changelog-md-v1-1 update-TF-WORKSPACE-variable update-cidrnetmask-docs update-depends-on-docs update-for-each-example update-packaging-action-name update_gen_meta uturunku1-patch-1 uturunku1-patch-2 v1.1 v1.4.0-alpha20221109 v1.3.5 v1.3.4 v1.3.3 v1.3.2 v1.3.1 v1.3.0 v1.3.0-rc1 v1.3.0-dev v1.3.0-beta1 v1.3.0-alpha20220817 v1.3.0-alpha20220803 v1.3.0-alpha20220706 v1.3.0-alpha20220622 v1.3.0-alpha20220608 v1.2.9 v1.2.8 v1.2.7 v1.2.6 v1.2.5 v1.2.4 v1.2.3 v1.2.2 v1.2.1 v1.2.0 v1.2.0-rc2 v1.2.0-rc1 v1.2.0-beta1 v1.2.0-alpha20220413 v1.2.0-alpha-20220328 v1.1.9 v1.1.8 v1.1.7 v1.1.6 v1.1.5 v1.1.4 v1.1.3 v1.1.2 v1.1.1 v1.1.0 v1.1.0-rc1 v1.1.0-beta2 v1.1.0-beta1 v1.1.0-alpha20211029
No related merge requests found
Showing with 567 additions and 83 deletions
+567 -83
...@@ -4,6 +4,7 @@ require ( ...@@ -4,6 +4,7 @@ require (
cloud.google.com/go/storage v1.10.0 cloud.google.com/go/storage v1.10.0
github.com/Azure/azure-sdk-for-go v52.5.0+incompatible github.com/Azure/azure-sdk-for-go v52.5.0+incompatible
github.com/Azure/go-autorest/autorest v0.11.18 github.com/Azure/go-autorest/autorest v0.11.18
github.com/Netflix/go-expect v0.0.0-20211003183012-e1a7c020ce25
github.com/agext/levenshtein v1.2.3 github.com/agext/levenshtein v1.2.3
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190329064014-6e358769c32a github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190329064014-6e358769c32a
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70 github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70
...@@ -154,6 +155,7 @@ require ( ...@@ -154,6 +155,7 @@ require (
github.com/jstemmer/go-junit-report v0.9.1 // indirect github.com/jstemmer/go-junit-report v0.9.1 // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/klauspost/compress v1.11.2 // indirect github.com/klauspost/compress v1.11.2 // indirect
github.com/kr/pty v1.1.1 // indirect
github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect
github.com/mattn/go-colorable v0.1.6 // indirect github.com/mattn/go-colorable v0.1.6 // indirect
github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect
......
...@@ -89,6 +89,8 @@ github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN ...@@ -89,6 +89,8 @@ github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN
github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU= github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU=
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/Netflix/go-expect v0.0.0-20211003183012-e1a7c020ce25 h1:hWfsqBaNZUHztXA78g7Y2Jj3rDQaTCZhhFwz43i2VlA=
github.com/Netflix/go-expect v0.0.0-20211003183012-e1a7c020ce25/go.mod h1:68ORG0HSEWDuH5Eh73AFbYWZ1zT4Y+b0vhOa+vZRUdI=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=
...@@ -453,6 +455,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB ...@@ -453,6 +455,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
......
...@@ -16,12 +16,6 @@ import ( ...@@ -16,12 +16,6 @@ import (
"github.com/hashicorp/terraform/internal/e2e" "github.com/hashicorp/terraform/internal/e2e"
) )
type tfCommand struct {
command []string
expectedOutput string
expectedErr string
}
func Test_terraform_apply_autoApprove(t *testing.T) { func Test_terraform_apply_autoApprove(t *testing.T) {
ctx := context.Background() ctx := context.Background()
cases := map[string]struct { cases := map[string]struct {
...@@ -218,7 +212,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) { ...@@ -218,7 +212,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) {
} }
orgName := resourceData["organization"] orgName := resourceData["organization"]
wsName := resourceData["workspace"] wsName := resourceData["workspace"]
tfBlock := createTerraformBlock(orgName, wsName) tfBlock := terraformConfigCloudBackendName(orgName, wsName)
writeMainTF(t, tfBlock, tmpDir) writeMainTF(t, tfBlock, tmpDir)
tf := e2e.NewBinary(terraformBin, tmpDir) tf := e2e.NewBinary(terraformBin, tmpDir)
defer tf.Close() defer tf.Close()
...@@ -245,28 +239,6 @@ func Test_terraform_apply_autoApprove(t *testing.T) { ...@@ -245,28 +239,6 @@ func Test_terraform_apply_autoApprove(t *testing.T) {
} }
} }
func createTerraformBlock(org, ws string) string {
return fmt.Sprintf(
`terraform {
cloud {
hostname = "%s"
organization = "%s"
workspaces {
name = "%s"
}
}
}
resource "random_pet" "server" {
keepers = {
uuid = uuid()
}
length = 3
}`, tfeHostname, org, ws)
}
func writeMainTF(t *testing.T, block string, dir string) { func writeMainTF(t *testing.T, block string, dir string) {
f, err := os.Create(fmt.Sprintf("%s/main.tf", dir)) f, err := os.Create(fmt.Sprintf("%s/main.tf", dir))
if err != nil { if err != nil {
......
...@@ -7,11 +7,30 @@ import ( ...@@ -7,11 +7,30 @@ import (
"context" "context"
"fmt" "fmt"
"testing" "testing"
"time"
tfe "github.com/hashicorp/go-tfe" tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/go-uuid" "github.com/hashicorp/go-uuid"
) )
const (
expectConsoleTimeout = 15 * time.Second
)
type tfCommand struct {
command []string
expectedOutput string
expectedErr string
expectError bool
userInput []string
postInputOutput string
}
type operationSets struct {
commands []tfCommand
prep func(t *testing.T, orgName, dir string)
}
func createOrganization(t *testing.T) (*tfe.Organization, func()) { func createOrganization(t *testing.T) (*tfe.Organization, func()) {
ctx := context.Background() ctx := context.Background()
org, err := tfeClient.Organizations.Create(ctx, tfe.OrganizationCreateOptions{ org, err := tfeClient.Organizations.Create(ctx, tfe.OrganizationCreateOptions{
...@@ -48,3 +67,62 @@ func randomString(t *testing.T) string { ...@@ -48,3 +67,62 @@ func randomString(t *testing.T) string {
} }
return v return v
} }
func terraformConfigLocalBackend() string {
return fmt.Sprintf(`
terraform {
backend "local" {
}
}
output "val" {
value = "${terraform.workspace}"
}
`)
}
func terraformConfigCloudBackendTags(org, tag string) string {
return fmt.Sprintf(`
terraform {
cloud {
hostname = "%s"
organization = "%s"
workspaces {
tags = ["%s"]
}
}
}
resource "random_pet" "server" {
keepers = {
uuid = uuid()
}
length = 3
}
`, tfeHostname, org, tag)
}
func terraformConfigCloudBackendName(org, name string) string {
return fmt.Sprintf(`
terraform {
cloud {
hostname = "%s"
organization = "%s"
workspaces {
name = "%s"
}
}
}
resource "random_pet" "server" {
keepers = {
uuid = uuid()
}
length = 3
}
`, tfeHostname, org, name)
}
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
...@@ -48,7 +47,6 @@ func setup() func() { ...@@ -48,7 +47,6 @@ func setup() func() {
setTfeClient() setTfeClient()
teardown := setupBinary() teardown := setupBinary()
setVersion() setVersion()
ensureVersionExists()
return func() { return func() {
teardown() teardown()
...@@ -143,42 +141,6 @@ func setVersion() { ...@@ -143,42 +141,6 @@ func setVersion() {
terraformVersion = fmt.Sprintf("%s-%s", version, hash) terraformVersion = fmt.Sprintf("%s-%s", version, hash)
} }
func ensureVersionExists() {
opts := tfe.AdminTerraformVersionsListOptions{
ListOptions: tfe.ListOptions{
PageNumber: 1,
PageSize: 100,
},
}
hasVersion := false
findTfVersion:
for {
tfVersionList, err := tfeClient.Admin.TerraformVersions.List(context.Background(), opts)
if err != nil {
log.Fatalf("Could not retrieve list of terraform versions: %v", err)
}
for _, item := range tfVersionList.Items {
if item.Version == terraformVersion {
hasVersion = true
break findTfVersion
}
}
// Exit the loop when we've seen all pages.
if tfVersionList.CurrentPage >= tfVersionList.TotalPages {
break
}
// Update the page number to get the next page.
opts.PageNumber = tfVersionList.NextPage
}
if !hasVersion {
log.Fatalf("Terraform Version %s does not exist in the list. Please add it.", terraformVersion)
}
}
func writeCredRC(file string) { func writeCredRC(file string) {
creds := credentialBlock() creds := credentialBlock()
f, err := os.Create(file) f, err := os.Create(file)
......
package main
import (
"testing"
)
/*
"multi" == multi-backend, multiple workspaces
-- when cloud config == name ->
---- prompt -> do you want to ONLY migrate the current workspace
-- when cloud config == tags
-- If Default present, prompt to rename default.
-- Then -> Prompt with *
*/
func Test_migrate_multi_to_tfc(t *testing.T) {
t.Skip("todo: see comments")
}
package main
import (
"testing"
)
// 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")
}
func Test_migrate_remote_backend_prefix_to_tfc(t *testing.T) {
t.Skip("todo: see comments")
}
//go:build e2e
// +build e2e
package main
import (
"context"
"fmt"
"io/ioutil"
"os"
"testing"
expect "github.com/Netflix/go-expect"
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform/internal/e2e"
)
func Test_migrate_single_to_tfc(t *testing.T) {
ctx := context.Background()
cases := map[string]struct {
operations []operationSets
validations func(t *testing.T, orgName string)
}{
"migrate using cloud workspace name strategy": {
operations: []operationSets{
{
prep: func(t *testing.T, orgName, dir string) {
tfBlock := terraformConfigLocalBackend()
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init"},
expectedOutput: `Successfully configured the backend "local"!`,
},
{
command: []string{"apply"},
userInput: []string{"yes"},
expectedOutput: `Do you want to perform these actions?`,
postInputOutput: `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{"init", "-migrate-state"},
expectedOutput: `Do you want to copy existing state to the new backend?`,
userInput: []string{"yes"},
postInputOutput: `Successfully configured the backend "cloud"!`,
},
{
command: []string{"workspace", "list"},
expectedOutput: `new-workspace`,
},
},
},
},
validations: func(t *testing.T, orgName string) {
wsList, err := tfeClient.Workspaces.List(ctx, orgName, tfe.WorkspaceListOptions{})
if err != nil {
t.Fatal(err)
}
ws := wsList.Items[0]
if ws.Name != "new-workspace" {
t.Fatalf("Expected workspace to be `new-workspace`, but is %s", ws.Name)
}
},
},
"migrate using cloud workspace tags strategy": {
operations: []operationSets{
{
prep: func(t *testing.T, orgName, dir string) {
tfBlock := terraformConfigLocalBackend()
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init"},
expectedOutput: `Successfully configured the backend "local"!`,
},
{
command: []string{"apply"},
userInput: []string{"yes"},
expectedOutput: `Do you want to perform these actions?`,
postInputOutput: `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"},
expectedOutput: `The "cloud" backend configuration only allows named workspaces!`,
userInput: []string{"new-workspace", "yes"},
postInputOutput: `Successfully configured the backend "cloud"!`,
},
{
command: []string{"workspace", "list"},
expectedOutput: `new-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)
}
ws := wsList.Items[0]
if ws.Name != "new-workspace" {
t.Fatalf("Expected workspace to be `new-workspace`, but is %s", ws.Name)
}
},
},
}
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.expectedOutput != "" {
_, err := exp.ExpectString(tfCmd.expectedOutput)
if err != nil {
t.Fatal(err)
}
}
if len(tfCmd.userInput) > 0 {
for _, input := range tfCmd.userInput {
exp.SendLine(input)
}
}
if tfCmd.postInputOutput != "" {
_, err := exp.ExpectString(tfCmd.postInputOutput)
if err != nil {
t.Fatal(err)
}
}
err = cmd.Wait()
if err != nil {
t.Fatal(err)
}
}
}
if tc.validations != nil {
tc.validations(t, organization.Name)
}
}
}
//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_migrate_tfc_to_other(t *testing.T) {
cases := map[string]struct {
operations []operationSets
}{
"migrate from cloud to local backend": {
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{"init"},
expectedOutput: `Successfully configured the backend "cloud"!`,
},
},
},
{
prep: func(t *testing.T, orgName, dir string) {
tfBlock := terraformConfigLocalBackend()
writeMainTF(t, tfBlock, dir)
},
commands: []tfCommand{
{
command: []string{"init", "-migrate-state"},
expectedOutput: `Migrating state from Terraform Cloud to another backend is not yet implemented.`,
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.expectedOutput != "" {
_, err := exp.ExpectString(tfCmd.expectedOutput)
if err != nil {
t.Fatal(err)
}
}
if len(tfCmd.userInput) > 0 {
for _, input := range tfCmd.userInput {
exp.SendLine(input)
}
}
if tfCmd.postInputOutput != "" {
_, err := exp.ExpectString(tfCmd.postInputOutput)
if err != nil {
t.Fatal(err)
}
}
err = cmd.Wait()
if err != nil && !tfCmd.expectError {
t.Fatal(err)
}
}
}
}
}
package main
import (
"testing"
)
/*
If org to org, treat it like a new backend. Then go through the multi/single logic
If same org, but name/tag changes
config name -> config name
-- straight copy
config name -> config tags
-- jsut add tag to workspace.
config tags -> config name
-- straight copy
*/
func Test_migrate_tfc_to_tfc(t *testing.T) {
t.Skip("todo: see comments")
}
...@@ -12,6 +12,7 @@ import ( ...@@ -12,6 +12,7 @@ import (
"strings" "strings"
"github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/cloud"
"github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/clistate"
"github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/command/views"
...@@ -46,25 +47,18 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { ...@@ -46,25 +47,18 @@ 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("[TRACE] 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 bool var sourceSingleState, destinationSingleState, sourceTFC, destinationTFC bool
sourceWorkspaces, err := opts.Source.Workspaces()
if err == backend.ErrWorkspacesNotSupported {
sourceSingleState = true
err = nil
}
if err != nil {
return fmt.Errorf(strings.TrimSpace(
errMigrateLoadStates), opts.SourceType, err)
}
destinationWorkspaces, err := opts.Destination.Workspaces() _, sourceTFC = opts.Source.(*cloud.Cloud)
if err == backend.ErrWorkspacesNotSupported { _, destinationTFC = opts.Destination.(*cloud.Cloud)
destinationSingleState = true
err = nil sourceWorkspaces, sourceSingleState, err := retrieveWorkspaces(opts.Source, opts.SourceType)
if err != nil {
return err
} }
destinationWorkspaces, destinationSingleState, err := retrieveWorkspaces(opts.Destination, opts.SourceType)
if err != nil { if err != nil {
return fmt.Errorf(strings.TrimSpace( return err
errMigrateLoadStates), opts.DestinationType, err)
} }
// Set up defaults // Set up defaults
...@@ -103,6 +97,9 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { ...@@ -103,6 +97,9 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
// Determine migration behavior based on whether the source/destination // Determine migration behavior based on whether the source/destination
// supports multi-state. // supports multi-state.
switch { switch {
case sourceTFC || destinationTFC:
return m.backendMigrateTFC(opts)
// Single-state to single-state. This is the easiest case: we just // Single-state to single-state. This is the easiest case: we just
// copy the default state directly. // copy the default state directly.
case sourceSingleState && destinationSingleState: case sourceSingleState && destinationSingleState:
...@@ -497,6 +494,91 @@ func (m *Meta) backendMigrateNonEmptyConfirm( ...@@ -497,6 +494,91 @@ func (m *Meta) backendMigrateNonEmptyConfirm(
return m.confirm(inputOpts) return m.confirm(inputOpts)
} }
func retrieveWorkspaces(back backend.Backend, sourceType string) ([]string, bool, error) {
var singleState bool
var err error
workspaces, err := back.Workspaces()
if err == backend.ErrWorkspacesNotSupported {
singleState = true
err = nil
}
if err != nil {
return nil, singleState, fmt.Errorf(strings.TrimSpace(
errMigrateLoadStates), sourceType, err)
}
return workspaces, singleState, err
}
func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error {
_, sourceTFC := opts.Source.(*cloud.Cloud)
cloudBackendDestination, destinationTFC := opts.Destination.(*cloud.Cloud)
sourceWorkspaces, sourceSingleState, err := retrieveWorkspaces(opts.Source, opts.SourceType)
if err != nil {
return err
}
//to be used below, not yet implamented
// destinationWorkspaces, destinationSingleState
_, _, err = retrieveWorkspaces(opts.Destination, opts.SourceType)
if err != nil {
return err
}
// from TFC to non-TFC backend
if sourceTFC && !destinationTFC {
// From Terraform Cloud to another backend. This is not yet implemented, and
// we recommend people to use the TFC API.
return fmt.Errorf(strings.TrimSpace(errTFCMigrateNotYetImplemented))
}
// from TFC to TFC
if sourceTFC && destinationTFC {
// TODO: see internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go for notes
panic("not yet implemented")
}
// 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)
if sourceSingle {
if cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy {
// If we know the name via WorkspaceNameStrategy, then set the
// destinationWorkspace to the new Name and skip the user prompt. Here the
// destinationWorkspace is not set to `default` thereby we will create it
// in TFC if it does not exist.
opts.destinationWorkspace = cloudBackendDestination.WorkspaceMapping.Name
}
// 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
// strategy.
return m.backendMigrateState_s_s(opts)
}
destinationTagsStrategy := cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceTagsStrategy
destinationNameStrategy := cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy
multiSource := !sourceSingleState && len(sourceWorkspaces) > 1
if multiSource && destinationNameStrategy {
// we have to take the current workspace from the source and migrate that
// over to destination. Since there is multiple sources, and we are using a
// name strategy, we will only migrate the current workspace.
panic("not yet implemented")
}
// Multiple sources, and using tags strategy. So migrate every source
// workspace over to new one, prompt for workspace name pattern (*),
// and start migrating, and create tags for each workspace.
if multiSource && destinationTagsStrategy {
// TODO: see internal/cloud/e2e/migrate_state_multi_to_tfc_test.go for notes
panic("not yet implemented")
}
return nil
}
const errMigrateLoadStates = ` const errMigrateLoadStates = `
Error inspecting states in the %q backend: Error inspecting states in the %q backend:
%s %s
...@@ -541,6 +623,12 @@ The state in the previous backend remains intact and unmodified. Please resolve ...@@ -541,6 +623,12 @@ The state in the previous backend remains intact and unmodified. Please resolve
the error above and try again. the error above and try again.
` `
const errTFCMigrateNotYetImplemented = `
Migrating state from Terraform Cloud to another backend is not yet implemented.
Please use the API to do this: https://www.terraform.io/docs/cloud/api/state-versions.html
`
const inputBackendMigrateEmpty = ` const inputBackendMigrateEmpty = `
Pre-existing state was found while migrating the previous %q backend to the Pre-existing state was found while migrating the previous %q backend to the
newly configured %q backend. No existing state was found in the newly newly configured %q backend. No existing state was found in the newly
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment