diff --git a/internal/cloud/configchangemode_string.go b/internal/cloud/configchangemode_string.go new file mode 100644 index 000000000..b60692be1 --- /dev/null +++ b/internal/cloud/configchangemode_string.go @@ -0,0 +1,37 @@ +// Code generated by "stringer -type ConfigChangeMode"; DO NOT EDIT. + +package cloud + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ConfigMigrationIn-8600] + _ = x[ConfigMigrationOut-8598] + _ = x[ConfigChangeInPlace-8635] + _ = x[ConfigChangeIrrelevant-129335] +} + +const ( + _ConfigChangeMode_name_0 = "ConfigMigrationOut" + _ConfigChangeMode_name_1 = "ConfigMigrationIn" + _ConfigChangeMode_name_2 = "ConfigChangeInPlace" + _ConfigChangeMode_name_3 = "ConfigChangeIrrelevant" +) + +func (i ConfigChangeMode) String() string { + switch { + case i == 8598: + return _ConfigChangeMode_name_0 + case i == 8600: + return _ConfigChangeMode_name_1 + case i == 8635: + return _ConfigChangeMode_name_2 + case i == 129335: + return _ConfigChangeMode_name_3 + default: + return "ConfigChangeMode(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/internal/cloud/migration.go b/internal/cloud/migration.go new file mode 100644 index 000000000..069d1b28e --- /dev/null +++ b/internal/cloud/migration.go @@ -0,0 +1,106 @@ +package cloud + +import ( + "github.com/hashicorp/terraform/internal/configs" + legacy "github.com/hashicorp/terraform/internal/legacy/terraform" +) + +// Most of the logic for migrating into and out of "cloud mode" actually lives +// in the "command" package as part of the general backend init mechanisms, +// but we have some cloud-specific helper functionality here. + +// ConfigChangeMode is a rough way to think about different situations that +// our backend change and state migration codepaths need to distinguish in +// the context of Cloud integration mode. +type ConfigChangeMode rune + +//go:generate go run golang.org/x/tools/cmd/stringer -type ConfigChangeMode + +const ( + // ConfigMigrationIn represents when the configuration calls for using + // Cloud mode but the working directory state disagrees. + ConfigMigrationIn ConfigChangeMode = '↘' + + // ConfigMigrationOut represents when the working directory state calls + // for using Cloud mode but the working directory state disagrees. + ConfigMigrationOut ConfigChangeMode = '↖' + + // ConfigChangeInPlace represents when both the working directory state + // and the config call for using Cloud mode, and so there might be + // (but won't necessarily be) cloud settings changing, but we don't + // need to do any actual migration. + ConfigChangeInPlace ConfigChangeMode = '↻' + + // ConfigChangeIrrelevant represents when the config and working directory + // state disagree but neither calls for using Cloud mode, and so the + // Cloud integration is not involved in dealing with this. + ConfigChangeIrrelevant ConfigChangeMode = '🤷' +) + +// DetectConfigChangeType encapsulates the fiddly logic for deciding what kind +// of Cloud configuration change we seem to be making, based on the existing +// working directory state (if any) and the current configuration. +// +// This is a pretty specialized sort of thing focused on finicky details of +// the way we currently model working directory settings and config, so its +// signature probably won't survive any non-trivial refactoring of how +// the CLI layer thinks about backends/state storage. +func DetectConfigChangeType(wdState *legacy.BackendState, config *configs.Backend, haveLocalStates bool) ConfigChangeMode { + // Although externally the cloud integration isn't really a "backend", + // internally we treat it a bit like one just to preserve all of our + // existing interfaces that assume backends. "cloud" is the placeholder + // name we use for it, even though that isn't a backend that's actually + // available for selection in the usual way. + wdIsCloud := wdState != nil && wdState.Type == "cloud" + configIsCloud := config != nil && config.Type == "cloud" + + // "uninit" here means that the working directory is totally uninitialized, + // even taking into account the possibility of implied local state that + // therefore doesn't typically require explicit "terraform init". + wdIsUninit := wdState == nil && !haveLocalStates + + switch { + case configIsCloud: + switch { + case wdIsCloud || wdIsUninit: + // If config has cloud and the working directory is completely + // uninitialized then we assume we're doing the initial activation + // of this working directory for an already-migrated-to-cloud + // remote state. + return ConfigChangeInPlace + default: + // Otherwise, we seem to be migrating into cloud mode from a backend. + return ConfigMigrationIn + } + default: + switch { + case wdIsCloud: + // If working directory is already cloud but config isn't, we're + // migrating away from cloud to a backend. + return ConfigMigrationOut + default: + // Otherwise, this situation seems to be something unrelated to + // cloud mode and so outside of our scope here. + return ConfigChangeIrrelevant + } + } + +} + +func (m ConfigChangeMode) InvolvesCloud() bool { + switch m { + case ConfigMigrationIn, ConfigMigrationOut, ConfigChangeInPlace: + return true + default: + return false + } +} + +func (m ConfigChangeMode) IsCloudMigration() bool { + switch m { + case ConfigMigrationIn, ConfigMigrationOut: + return true + default: + return false + } +} diff --git a/internal/cloud/migration_test.go b/internal/cloud/migration_test.go new file mode 100644 index 000000000..f1ae0f48e --- /dev/null +++ b/internal/cloud/migration_test.go @@ -0,0 +1,138 @@ +package cloud + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/configs" + legacy "github.com/hashicorp/terraform/internal/legacy/terraform" +) + +func TestDetectConfigChangeType(t *testing.T) { + tests := map[string]struct { + stateType string + configType string + localStates bool + want ConfigChangeMode + wantInvolvesCloud bool + wantIsCloudMigration bool + }{ + "init cloud": { + ``, `cloud`, false, + ConfigChangeInPlace, + true, false, + }, + "reinit cloud": { + `cloud`, `cloud`, false, + ConfigChangeInPlace, + true, false, + }, + "migrate default local to cloud with existing local state": { + ``, `cloud`, true, + ConfigMigrationIn, + true, true, + }, + "migrate local to cloud": { + `local`, `cloud`, false, + ConfigMigrationIn, + true, true, + }, + "migrate remote to cloud": { + `local`, `cloud`, false, + ConfigMigrationIn, + true, true, + }, + "migrate cloud to local": { + `cloud`, `local`, false, + ConfigMigrationOut, + true, true, + }, + "migrate cloud to remote": { + `cloud`, `remote`, false, + ConfigMigrationOut, + true, true, + }, + "migrate cloud to default local": { + `cloud`, ``, false, + ConfigMigrationOut, + true, true, + }, + + // Various other cases can potentially be valid (decided by the + // Terraform CLI layer) but are irrelevant for Cloud mode purposes. + "init default local": { + ``, ``, false, + ConfigChangeIrrelevant, + false, false, + }, + "init default local with existing local state": { + ``, ``, true, + ConfigChangeIrrelevant, + false, false, + }, + "init remote backend": { + ``, `remote`, false, + ConfigChangeIrrelevant, + false, false, + }, + "init remote backend with existing local state": { + ``, `remote`, true, + ConfigChangeIrrelevant, + false, false, + }, + "reinit remote backend": { + `remote`, `remote`, false, + ConfigChangeIrrelevant, + false, false, + }, + "migrate local to remote backend": { + `local`, `remote`, false, + ConfigChangeIrrelevant, + false, false, + }, + "migrate remote to default local": { + `remote`, ``, false, + ConfigChangeIrrelevant, + false, false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var state *legacy.BackendState + var config *configs.Backend + if test.stateType != "" { + state = &legacy.BackendState{ + Type: test.stateType, + // everything else is irrelevant for our purposes here + } + } + if test.configType != "" { + config = &configs.Backend{ + Type: test.configType, + // everything else is irrelevant for our purposes here + } + } + got := DetectConfigChangeType(state, config, test.localStates) + + if got != test.want { + t.Errorf( + "wrong result\nstate type: %s\nconfig type: %s\nlocal states: %t\n\ngot: %s\nwant: %s", + test.stateType, test.configType, test.localStates, + got, test.want, + ) + } + if got, want := got.InvolvesCloud(), test.wantInvolvesCloud; got != want { + t.Errorf( + "wrong InvolvesCloud result\ngot: %t\nwant: %t", + got, want, + ) + } + if got, want := got.IsCloudMigration(), test.wantIsCloudMigration; got != want { + t.Errorf( + "wrong IsCloudMigration result\ngot: %t\nwant: %t", + got, want, + ) + } + }) + } +}