diff --git a/internal/command/command_test.go b/internal/command/command_test.go index 4a56ecc73b23..b9c9bfc21587 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -19,7 +19,6 @@ import ( "testing" svchost "github.com/hashicorp/terraform-svchost" - "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform/internal/addrs" backendInit "github.com/hashicorp/terraform/internal/backend/init" diff --git a/internal/command/meta.go b/internal/command/meta.go index 6bfc2d8c71ec..459427a0df43 100644 --- a/internal/command/meta.go +++ b/internal/command/meta.go @@ -732,3 +732,35 @@ func (m *Meta) applyStateArguments(args *arguments.State) { m.stateOutPath = args.StateOutPath m.backupPath = args.BackupPath } + +// checkRequiredVersion loads the config and check if the +// core version requirements are satisfied. +func (m *Meta) checkRequiredVersion() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + loader, err := m.initConfigLoader() + if err != nil { + diags = diags.Append(err) + return diags + } + + pwd, err := os.Getwd() + if err != nil { + diags = diags.Append(fmt.Errorf("Error getting pwd: %s", err)) + return diags + } + + config, configDiags := loader.LoadConfig(pwd) + if configDiags.HasErrors() { + diags = diags.Append(configDiags) + return diags + } + + versionDiags := terraform.CheckCoreVersionRequirements(config) + if versionDiags.HasErrors() { + diags = diags.Append(versionDiags) + return diags + } + + return nil +} diff --git a/internal/command/meta_test.go b/internal/command/meta_test.go index e833f7d9c42f..5971840f014b 100644 --- a/internal/command/meta_test.go +++ b/internal/command/meta_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -13,6 +14,7 @@ import ( "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/backend/local" "github.com/hashicorp/terraform/internal/terraform" + "github.com/mitchellh/cli" ) func TestMetaColorize(t *testing.T) { @@ -386,3 +388,32 @@ func TestMeta_process(t *testing.T) { }) } } + +func TestCommand_checkRequiredVersion(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("command-check-required-version"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + ui := cli.NewMockUi() + meta := Meta{ + Ui: ui, + } + + diags := meta.checkRequiredVersion() + if diags == nil { + t.Fatalf("diagnostics should contain unmet version constraint, but is nil") + } + + meta.showDiagnostics(diags) + + // Required version diags are correct + errStr := ui.ErrorWriter.String() + if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) { + t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) + } + if strings.Contains(errStr, `required_version = ">= 0.13.0"`) { + t.Fatalf("output should not point to met version constraint, but is:\n\n%s", errStr) + } +} diff --git a/internal/command/state_mv.go b/internal/command/state_mv.go index 99bc82aa53a0..949f6c4b459d 100644 --- a/internal/command/state_mv.go +++ b/internal/command/state_mv.go @@ -43,6 +43,11 @@ func (c *StateMvCommand) Run(args []string) int { return cli.RunResultHelp } + if diags := c.Meta.checkRequiredVersion(); diags != nil { + c.showDiagnostics(diags) + return 1 + } + // If backup or backup-out options are set // and the state option is not set, make sure // the backend is local diff --git a/internal/command/state_mv_test.go b/internal/command/state_mv_test.go index b183a8668779..7a5d889cf07a 100644 --- a/internal/command/state_mv_test.go +++ b/internal/command/state_mv_test.go @@ -1713,6 +1713,84 @@ func TestStateMvInvalidSourceAddress(t *testing.T) { } } +func TestStateMv_checkRequiredVersion(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("command-check-required-version"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "baz", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")}, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }) + statePath := testStateFile(t, state) + + p := testProvider() + ui := new(cli.MockUi) + view, _ := testView(t) + c := &StateMvCommand{ + StateMeta{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + View: view, + }, + }, + } + + args := []string{ + "-state", statePath, + "test_instance.foo", + "test_instance.bar", + } + + if code := c.Run(args); code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + } + + // State is unchanged + testStateOutput(t, statePath, testStateMvOutputOriginal) + + // Required version diags are correct + errStr := ui.ErrorWriter.String() + if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) { + t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) + } + if strings.Contains(errStr, `required_version = ">= 0.13.0"`) { + t.Fatalf("output should not point to met version constraint, but is:\n\n%s", errStr) + } +} + const testStateMvOutputOriginal = ` test_instance.baz: ID = foo diff --git a/internal/command/state_pull.go b/internal/command/state_pull.go index 8ce16ff57bc5..8872cec65cfa 100644 --- a/internal/command/state_pull.go +++ b/internal/command/state_pull.go @@ -23,6 +23,11 @@ func (c *StatePullCommand) Run(args []string) int { return 1 } + if diags := c.Meta.checkRequiredVersion(); diags != nil { + c.showDiagnostics(diags) + return 1 + } + // Load the backend b, backendDiags := c.Backend(nil) if backendDiags.HasErrors() { diff --git a/internal/command/state_pull_test.go b/internal/command/state_pull_test.go index ec0d233dda71..fb54629721af 100644 --- a/internal/command/state_pull_test.go +++ b/internal/command/state_pull_test.go @@ -4,6 +4,7 @@ import ( "bytes" "io/ioutil" "os" + "strings" "testing" "github.com/mitchellh/cli" @@ -64,3 +65,34 @@ func TestStatePull_noState(t *testing.T) { t.Fatalf("bad: %s", actual) } } + +func TestStatePull_checkRequiredVersion(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("command-check-required-version"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + p := testProvider() + ui := cli.NewMockUi() + c := &StatePullCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + } + + // Required version diags are correct + errStr := ui.ErrorWriter.String() + if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) { + t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) + } + if strings.Contains(errStr, `required_version = ">= 0.13.0"`) { + t.Fatalf("output should not point to met version constraint, but is:\n\n%s", errStr) + } +} diff --git a/internal/command/state_push.go b/internal/command/state_push.go index eb0ea1679654..0b863740c59f 100644 --- a/internal/command/state_push.go +++ b/internal/command/state_push.go @@ -38,6 +38,11 @@ func (c *StatePushCommand) Run(args []string) int { return cli.RunResultHelp } + if diags := c.Meta.checkRequiredVersion(); diags != nil { + c.showDiagnostics(diags) + return 1 + } + // Determine our reader for the input state. This is the filepath // or stdin if "-" is given. var r io.Reader = os.Stdin diff --git a/internal/command/state_push_test.go b/internal/command/state_push_test.go index db708c8a424b..251d952d0b60 100644 --- a/internal/command/state_push_test.go +++ b/internal/command/state_push_test.go @@ -291,3 +291,36 @@ func TestStatePush_forceRemoteState(t *testing.T) { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } } + +func TestStatePush_checkRequiredVersion(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("command-check-required-version"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + p := testProvider() + ui := cli.NewMockUi() + view, _ := testView(t) + c := &StatePushCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + View: view, + }, + } + + args := []string{"replace.tfstate"} + if code := c.Run(args); code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + } + + // Required version diags are correct + errStr := ui.ErrorWriter.String() + if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) { + t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) + } + if strings.Contains(errStr, `required_version = ">= 0.13.0"`) { + t.Fatalf("output should not point to met version constraint, but is:\n\n%s", errStr) + } +} diff --git a/internal/command/state_replace_provider.go b/internal/command/state_replace_provider.go index c6c47e669759..ec5347a7697a 100644 --- a/internal/command/state_replace_provider.go +++ b/internal/command/state_replace_provider.go @@ -42,6 +42,11 @@ func (c *StateReplaceProviderCommand) Run(args []string) int { return cli.RunResultHelp } + if diags := c.Meta.checkRequiredVersion(); diags != nil { + c.showDiagnostics(diags) + return 1 + } + var diags tfdiags.Diagnostics // Parse from/to arguments into providers diff --git a/internal/command/state_replace_provider_test.go b/internal/command/state_replace_provider_test.go index 7ac5f42c40fa..dd6b0a852da2 100644 --- a/internal/command/state_replace_provider_test.go +++ b/internal/command/state_replace_provider_test.go @@ -2,6 +2,7 @@ package command import ( "bytes" + "os" "path/filepath" "strings" "testing" @@ -294,6 +295,100 @@ func TestStateReplaceProvider_docs(t *testing.T) { } } +func TestStateReplaceProvider_checkRequiredVersion(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("command-check-required-version"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "alpha", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"alpha","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("aws"), + Module: addrs.RootModule, + }, + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "beta", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"beta","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("aws"), + Module: addrs.RootModule, + }, + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "azurerm_virtual_machine", + Name: "gamma", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"gamma","baz":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewLegacyProvider("azurerm"), + Module: addrs.RootModule, + }, + ) + }) + + statePath := testStateFile(t, state) + + ui := new(cli.MockUi) + view, _ := testView(t) + c := &StateReplaceProviderCommand{ + StateMeta{ + Meta: Meta{ + Ui: ui, + View: view, + }, + }, + } + + inputBuf := &bytes.Buffer{} + ui.InputReader = inputBuf + inputBuf.WriteString("yes\n") + + args := []string{ + "-state", statePath, + "hashicorp/aws", + "acmecorp/aws", + } + if code := c.Run(args); code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + } + + // State is unchanged + testStateOutput(t, statePath, testStateReplaceProviderOutputOriginal) + + // Required version diags are correct + errStr := ui.ErrorWriter.String() + if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) { + t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) + } + if strings.Contains(errStr, `required_version = ">= 0.13.0"`) { + t.Fatalf("output should not point to met version constraint, but is:\n\n%s", errStr) + } +} + const testStateReplaceProviderOutputOriginal = ` aws_instance.alpha: ID = alpha diff --git a/internal/command/state_rm.go b/internal/command/state_rm.go index 03547f658f04..f126c5f5a561 100644 --- a/internal/command/state_rm.go +++ b/internal/command/state_rm.go @@ -37,6 +37,11 @@ func (c *StateRmCommand) Run(args []string) int { return cli.RunResultHelp } + if diags := c.Meta.checkRequiredVersion(); diags != nil { + c.showDiagnostics(diags) + return 1 + } + // Get the state stateMgr, err := c.State() if err != nil { diff --git a/internal/command/state_rm_test.go b/internal/command/state_rm_test.go index a3e954c436ca..3ec0e41cc969 100644 --- a/internal/command/state_rm_test.go +++ b/internal/command/state_rm_test.go @@ -486,6 +486,81 @@ func TestStateRm_backendState(t *testing.T) { testStateOutput(t, backupPath, testStateRmOutputOriginal) } +func TestStateRm_checkRequiredVersion(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("command-check-required-version"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }) + statePath := testStateFile(t, state) + + p := testProvider() + ui := new(cli.MockUi) + view, _ := testView(t) + c := &StateRmCommand{ + StateMeta{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + View: view, + }, + }, + } + + args := []string{ + "-state", statePath, + "test_instance.foo", + } + if code := c.Run(args); code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + } + + // State is unchanged + testStateOutput(t, statePath, testStateRmOutputOriginal) + + // Required version diags are correct + errStr := ui.ErrorWriter.String() + if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) { + t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) + } + if strings.Contains(errStr, `required_version = ">= 0.13.0"`) { + t.Fatalf("output should not point to met version constraint, but is:\n\n%s", errStr) + } +} + const testStateRmOutputOriginal = ` test_instance.bar: ID = foo diff --git a/internal/command/taint.go b/internal/command/taint.go index f2fbbb1efba9..0c5a499f2e32 100644 --- a/internal/command/taint.go +++ b/internal/command/taint.go @@ -2,7 +2,6 @@ package command import ( "fmt" - "os" "strings" "github.com/hashicorp/terraform/internal/addrs" @@ -10,7 +9,6 @@ import ( "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -58,30 +56,7 @@ func (c *TaintCommand) Run(args []string) int { return 1 } - // Load the config and check the core version requirements are satisfied - loader, err := c.initConfigLoader() - if err != nil { - diags = diags.Append(err) - c.showDiagnostics(diags) - return 1 - } - - pwd, err := os.Getwd() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) - return 1 - } - - config, configDiags := loader.LoadConfig(pwd) - diags = diags.Append(configDiags) - if diags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } - - versionDiags := terraform.CheckCoreVersionRequirements(config) - diags = diags.Append(versionDiags) - if diags.HasErrors() { + if diags := c.Meta.checkRequiredVersion(); diags != nil { c.showDiagnostics(diags) return 1 } diff --git a/internal/command/taint_test.go b/internal/command/taint_test.go index 57c626e97b3e..ee41ad382a77 100644 --- a/internal/command/taint_test.go +++ b/internal/command/taint_test.go @@ -494,7 +494,7 @@ func TestTaint_module(t *testing.T) { func TestTaint_checkRequiredVersion(t *testing.T) { // Create a temporary working directory that is empty td := tempDir(t) - testCopyDir(t, testFixturePath("taint-check-required-version"), td) + testCopyDir(t, testFixturePath("command-check-required-version"), td) defer os.RemoveAll(td) defer testChdir(t, td)() diff --git a/internal/command/testdata/taint-check-required-version/main.tf b/internal/command/testdata/command-check-required-version/main.tf similarity index 100% rename from internal/command/testdata/taint-check-required-version/main.tf rename to internal/command/testdata/command-check-required-version/main.tf