From e5172fea9540846ab32b8e9448b1390ac89655b9 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 12 Nov 2021 15:17:27 -0800 Subject: [PATCH] cloud: DetectConfigChangeType helper This aims to encapsulate the somewhat-weird logic we currently use to distinguish between the various "terraform init" situations involving Terraform Cloud mode, in the hope of making codepaths that branch based on this slightly easier to read. This isn't yet used, but uses of it will follow in subsequent commits. --- internal/cloud/configchangemode_string.go | 37 ++++++ internal/cloud/migration.go | 106 +++++++++++++++++ internal/cloud/migration_test.go | 138 ++++++++++++++++++++++ 3 files changed, 281 insertions(+) create mode 100644 internal/cloud/configchangemode_string.go create mode 100644 internal/cloud/migration.go create mode 100644 internal/cloud/migration_test.go 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, + ) + } + }) + } +}