terraform/internal/backend/local/backend_refresh_test.go

309 lines
8.6 KiB
Go
Raw Normal View History

package local
import (
"context"
"fmt"
"strings"
"testing"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/clistate"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/configs/configschema"
backend/local: Check dependency lock consistency before any operations In historical versions of Terraform the responsibility to check this was inside the terraform.NewContext function, along with various other assorted concerns that made that function particularly complicated. More recently, we reduced the responsibility of the "terraform" package only to instantiating particular named plugins, assuming that its caller is responsible for selecting appropriate versions of any providers that _are_ external. However, until this commit we were just assuming that "terraform init" had correctly selected appropriate plugins and recorded them in the lock file, and so nothing was dealing with the problem of ensuring that there haven't been any changes to the lock file or config since the most recent "terraform init" which would cause us to need to re-evaluate those decisions. Part of the game here is to slightly extend the role of the dependency locks object to also carry information about a subset of provider addresses whose lock entries we're intentionally disregarding as part of the various little edge-case features we have for overridding providers: dev_overrides, "unmanaged providers", and the testing overrides in our own unit tests. This is an in-memory-only annotation, never included in the serialized plan files on disk. I had originally intended to create a new package to encapsulate all of this plugin-selection logic, including both the version constraint checking here and also the handling of the provider factory functions, but as an interim step I've just made version constraint consistency checks the responsibility of the backend/local package, which means that we'll always catch problems as part of preparing for local operations, while not imposing these additional checks on commands that _don't_ run local operations, such as "terraform apply" when in remote operations mode.
2021-09-30 02:31:43 +02:00
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
2021-02-17 19:01:30 +01:00
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/zclconf/go-cty/cty"
)
func TestLocal_refresh(t *testing.T) {
2021-09-14 15:13:13 +02:00
b := TestLocal(t)
2018-03-28 16:54:08 +02:00
p := TestLocalProvider(t, b, "test", refreshFixtureSchema())
testStateFile(t, b.StatePath, testRefreshState())
2018-10-04 00:50:04 +02:00
p.ReadResourceFn = nil
2021-01-12 22:13:10 +01:00
p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{
2018-10-04 00:50:04 +02:00
"id": cty.StringVal("yes"),
})}
2021-02-17 19:01:30 +01:00
op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh")
defer configCleanup()
2021-02-17 19:01:30 +01:00
defer done(t)
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if !p.ReadResourceCalled {
t.Fatal("ReadResource should be called")
}
checkState(t, b.StateOutPath, `
test_instance.foo:
ID = yes
provider = provider["registry.terraform.io/hashicorp/test"]
`)
// the backend should be unlocked after a run
assertBackendStateUnlocked(t, b)
}
func TestLocal_refreshInput(t *testing.T) {
2021-09-14 15:13:13 +02:00
b := TestLocal(t)
schema := &terraform.ProviderSchema{
Provider: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {Type: cty.String, Optional: true},
},
},
ResourceTypes: map[string]*configschema.Block{
"test_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
"foo": {Type: cty.String, Optional: true},
"ami": {Type: cty.String, Optional: true},
},
},
},
}
p := TestLocalProvider(t, b, "test", schema)
testStateFile(t, b.StatePath, testRefreshState())
2018-10-04 00:50:04 +02:00
p.ReadResourceFn = nil
2021-01-12 22:13:10 +01:00
p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{
2018-10-04 00:50:04 +02:00
"id": cty.StringVal("yes"),
})}
p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
2020-10-08 19:52:04 +02:00
val := req.Config.GetAttr("value")
if val.IsNull() || val.AsString() != "bar" {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("incorrect value %#v", val))
}
2020-10-08 19:52:04 +02:00
return
}
// Enable input asking since it is normally disabled by default
b.OpInput = true
b.ContextOpts.UIInput = &terraform.MockUIInput{InputReturnString: "bar"}
2021-02-17 19:01:30 +01:00
op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh-var-unset")
defer configCleanup()
2021-02-17 19:01:30 +01:00
defer done(t)
op.UIIn = b.ContextOpts.UIInput
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if !p.ReadResourceCalled {
t.Fatal("ReadResource should be called")
}
checkState(t, b.StateOutPath, `
test_instance.foo:
ID = yes
provider = provider["registry.terraform.io/hashicorp/test"]
`)
}
func TestLocal_refreshValidate(t *testing.T) {
2021-09-14 15:13:13 +02:00
b := TestLocal(t)
p := TestLocalProvider(t, b, "test", refreshFixtureSchema())
testStateFile(t, b.StatePath, testRefreshState())
2018-10-04 00:50:04 +02:00
p.ReadResourceFn = nil
2021-01-12 22:13:10 +01:00
p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{
2018-10-04 00:50:04 +02:00
"id": cty.StringVal("yes"),
})}
// Enable validation
b.OpValidation = true
2021-02-17 19:01:30 +01:00
op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh")
defer configCleanup()
2021-02-17 19:01:30 +01:00
defer done(t)
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
checkState(t, b.StateOutPath, `
test_instance.foo:
ID = yes
provider = provider["registry.terraform.io/hashicorp/test"]
`)
}
func TestLocal_refreshValidateProviderConfigured(t *testing.T) {
2021-09-14 15:13:13 +02:00
b := TestLocal(t)
schema := &terraform.ProviderSchema{
Provider: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {Type: cty.String, Optional: true},
},
},
ResourceTypes: map[string]*configschema.Block{
"test_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
"ami": {Type: cty.String, Optional: true},
},
},
},
}
p := TestLocalProvider(t, b, "test", schema)
testStateFile(t, b.StatePath, testRefreshState())
p.ReadResourceFn = nil
2021-01-12 22:13:10 +01:00
p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("yes"),
})}
// Enable validation
b.OpValidation = true
2021-02-17 19:01:30 +01:00
op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh-provider-config")
defer configCleanup()
2021-02-17 19:01:30 +01:00
defer done(t)
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if !p.ValidateProviderConfigCalled {
t.Fatal("Validate provider config should be called")
}
checkState(t, b.StateOutPath, `
test_instance.foo:
ID = yes
provider = provider["registry.terraform.io/hashicorp/test"]
`)
}
// This test validates the state lacking behavior when the inner call to
// Context() fails
func TestLocal_refresh_context_error(t *testing.T) {
2021-09-14 15:13:13 +02:00
b := TestLocal(t)
testStateFile(t, b.StatePath, testRefreshState())
2021-02-17 19:01:30 +01:00
op, configCleanup, done := testOperationRefresh(t, "./testdata/apply")
defer configCleanup()
2021-02-17 19:01:30 +01:00
defer done(t)
// we coerce a failure in Context() by omitting the provider schema
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result == backend.OperationSuccess {
t.Fatal("operation succeeded; want failure")
}
assertBackendStateUnlocked(t, b)
}
func TestLocal_refreshEmptyState(t *testing.T) {
2021-09-14 15:13:13 +02:00
b := TestLocal(t)
p := TestLocalProvider(t, b, "test", refreshFixtureSchema())
testStateFile(t, b.StatePath, states.NewState())
p.ReadResourceFn = nil
p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("yes"),
})}
2021-02-17 19:01:30 +01:00
op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh")
defer configCleanup()
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
output := done(t)
if stderr := output.Stderr(); stderr != "" {
t.Fatalf("expected only warning diags, got errors: %s", stderr)
}
if got, want := output.Stdout(), "Warning: Empty or non-existent state"; !strings.Contains(got, want) {
t.Errorf("wrong diags\n got: %s\nwant: %s", got, want)
}
// the backend should be unlocked after a run
assertBackendStateUnlocked(t, b)
}
2021-02-17 19:01:30 +01:00
func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
2021-02-17 19:01:30 +01:00
streams, done := terminal.StreamsForTesting(t)
view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
backend/local: Check dependency lock consistency before any operations In historical versions of Terraform the responsibility to check this was inside the terraform.NewContext function, along with various other assorted concerns that made that function particularly complicated. More recently, we reduced the responsibility of the "terraform" package only to instantiating particular named plugins, assuming that its caller is responsible for selecting appropriate versions of any providers that _are_ external. However, until this commit we were just assuming that "terraform init" had correctly selected appropriate plugins and recorded them in the lock file, and so nothing was dealing with the problem of ensuring that there haven't been any changes to the lock file or config since the most recent "terraform init" which would cause us to need to re-evaluate those decisions. Part of the game here is to slightly extend the role of the dependency locks object to also carry information about a subset of provider addresses whose lock entries we're intentionally disregarding as part of the various little edge-case features we have for overridding providers: dev_overrides, "unmanaged providers", and the testing overrides in our own unit tests. This is an in-memory-only annotation, never included in the serialized plan files on disk. I had originally intended to create a new package to encapsulate all of this plugin-selection logic, including both the version constraint checking here and also the handling of the provider factory functions, but as an interim step I've just made version constraint consistency checks the responsibility of the backend/local package, which means that we'll always catch problems as part of preparing for local operations, while not imposing these additional checks on commands that _don't_ run local operations, such as "terraform apply" when in remote operations mode.
2021-09-30 02:31:43 +02:00
// Many of our tests use an overridden "test" provider that's just in-memory
// inside the test process, not a separate plugin on disk.
depLocks := depsfile.NewLocks()
depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test"))
return &backend.Operation{
backend/local: Check dependency lock consistency before any operations In historical versions of Terraform the responsibility to check this was inside the terraform.NewContext function, along with various other assorted concerns that made that function particularly complicated. More recently, we reduced the responsibility of the "terraform" package only to instantiating particular named plugins, assuming that its caller is responsible for selecting appropriate versions of any providers that _are_ external. However, until this commit we were just assuming that "terraform init" had correctly selected appropriate plugins and recorded them in the lock file, and so nothing was dealing with the problem of ensuring that there haven't been any changes to the lock file or config since the most recent "terraform init" which would cause us to need to re-evaluate those decisions. Part of the game here is to slightly extend the role of the dependency locks object to also carry information about a subset of provider addresses whose lock entries we're intentionally disregarding as part of the various little edge-case features we have for overridding providers: dev_overrides, "unmanaged providers", and the testing overrides in our own unit tests. This is an in-memory-only annotation, never included in the serialized plan files on disk. I had originally intended to create a new package to encapsulate all of this plugin-selection logic, including both the version constraint checking here and also the handling of the provider factory functions, but as an interim step I've just made version constraint consistency checks the responsibility of the backend/local package, which means that we'll always catch problems as part of preparing for local operations, while not imposing these additional checks on commands that _don't_ run local operations, such as "terraform apply" when in remote operations mode.
2021-09-30 02:31:43 +02:00
Type: backend.OperationTypeRefresh,
ConfigDir: configDir,
ConfigLoader: configLoader,
StateLocker: clistate.NewNoopLocker(),
View: view,
DependencyLocks: depLocks,
2021-02-17 19:01:30 +01:00
}, configCleanup, done
}
// testRefreshState is just a common state that we use for testing refresh.
func testRefreshState() *states.State {
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
)
return state
}
// refreshFixtureSchema returns a schema suitable for processing the
// configuration in testdata/refresh . This schema should be
// assigned to a mock provider named "test".
func refreshFixtureSchema() *terraform.ProviderSchema {
return &terraform.ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_instance": {
Attributes: map[string]*configschema.Attribute{
"ami": {Type: cty.String, Optional: true},
2018-10-04 00:50:04 +02:00
"id": {Type: cty.String, Computed: true},
},
},
},
}
}