mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-24 16:10:46 -06:00
5ab6167bbf
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
441 lines
12 KiB
Go
441 lines
12 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package local
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/backend"
|
|
"github.com/opentofu/opentofu/internal/command/arguments"
|
|
"github.com/opentofu/opentofu/internal/command/clistate"
|
|
"github.com/opentofu/opentofu/internal/command/views"
|
|
"github.com/opentofu/opentofu/internal/configs/configschema"
|
|
"github.com/opentofu/opentofu/internal/depsfile"
|
|
"github.com/opentofu/opentofu/internal/encryption"
|
|
"github.com/opentofu/opentofu/internal/initwd"
|
|
"github.com/opentofu/opentofu/internal/plans"
|
|
"github.com/opentofu/opentofu/internal/providers"
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/states/statemgr"
|
|
"github.com/opentofu/opentofu/internal/terminal"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
func TestLocal_applyBasic(t *testing.T) {
|
|
b := TestLocal(t)
|
|
|
|
p := TestLocalProvider(t, b, "test", applyFixtureSchema())
|
|
p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("yes"),
|
|
"ami": cty.StringVal("bar"),
|
|
})}
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
defer configCleanup()
|
|
|
|
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 failed")
|
|
}
|
|
|
|
if p.ReadResourceCalled {
|
|
t.Fatal("ReadResource should not be called")
|
|
}
|
|
|
|
if !p.PlanResourceChangeCalled {
|
|
t.Fatal("diff should be called")
|
|
}
|
|
|
|
if !p.ApplyResourceChangeCalled {
|
|
t.Fatal("apply should be called")
|
|
}
|
|
|
|
checkState(t, b.StateOutPath, `
|
|
test_instance.foo:
|
|
ID = yes
|
|
provider = provider["registry.opentofu.org/hashicorp/test"]
|
|
ami = bar
|
|
`)
|
|
|
|
if errOutput := done(t).Stderr(); errOutput != "" {
|
|
t.Fatalf("unexpected error output:\n%s", errOutput)
|
|
}
|
|
}
|
|
func TestLocal_applyCheck(t *testing.T) {
|
|
b := TestLocal(t)
|
|
|
|
p := TestLocalProvider(t, b, "test", applyFixtureSchema())
|
|
p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("yes"),
|
|
"ami": cty.StringVal("bar"),
|
|
})}
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-check")
|
|
defer configCleanup()
|
|
|
|
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 failed")
|
|
}
|
|
|
|
if p.ReadResourceCalled {
|
|
t.Fatal("ReadResource should not be called")
|
|
}
|
|
|
|
if !p.PlanResourceChangeCalled {
|
|
t.Fatal("diff should be called")
|
|
}
|
|
|
|
if !p.ApplyResourceChangeCalled {
|
|
t.Fatal("apply should be called")
|
|
}
|
|
|
|
d := done(t)
|
|
if errOutput := d.Stderr(); errOutput != "" {
|
|
t.Fatalf("unexpected error output:\n%s", errOutput)
|
|
}
|
|
|
|
if stdOutput := d.Stdout(); strings.Contains(stdOutput, "Check block assertion known after apply") {
|
|
// As we are running an auto approved plan the warning that was
|
|
// generated during the plan should have been hidden.
|
|
t.Fatalf("std output contained unexpected check output:\n%s", stdOutput)
|
|
}
|
|
}
|
|
|
|
func TestLocal_applyEmptyDir(t *testing.T) {
|
|
b := TestLocal(t)
|
|
|
|
p := TestLocalProvider(t, b, "test", providers.ProviderSchema{})
|
|
p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{"id": cty.StringVal("yes")})}
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/empty")
|
|
defer configCleanup()
|
|
|
|
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 error")
|
|
}
|
|
|
|
if p.ApplyResourceChangeCalled {
|
|
t.Fatal("apply should not be called")
|
|
}
|
|
|
|
if _, err := os.Stat(b.StateOutPath); err == nil {
|
|
t.Fatal("should not exist")
|
|
}
|
|
|
|
// the backend should be unlocked after a run
|
|
assertBackendStateUnlocked(t, b)
|
|
|
|
if got, want := done(t).Stderr(), "Error: No configuration files"; !strings.Contains(got, want) {
|
|
t.Fatalf("unexpected error output:\n%s\nwant: %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLocal_applyEmptyDirDestroy(t *testing.T) {
|
|
b := TestLocal(t)
|
|
|
|
p := TestLocalProvider(t, b, "test", providers.ProviderSchema{})
|
|
p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{}
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/empty")
|
|
defer configCleanup()
|
|
op.PlanMode = plans.DestroyMode
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("bad: %s", err)
|
|
}
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("apply operation failed")
|
|
}
|
|
|
|
if p.ApplyResourceChangeCalled {
|
|
t.Fatal("apply should not be called")
|
|
}
|
|
|
|
checkState(t, b.StateOutPath, `<no state>`)
|
|
|
|
if errOutput := done(t).Stderr(); errOutput != "" {
|
|
t.Fatalf("unexpected error output:\n%s", errOutput)
|
|
}
|
|
}
|
|
|
|
func TestLocal_applyError(t *testing.T) {
|
|
b := TestLocal(t)
|
|
|
|
schema := providers.ProviderSchema{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"ami": {Type: cty.String, Optional: true},
|
|
"id": {Type: cty.String, Computed: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
p := TestLocalProvider(t, b, "test", schema)
|
|
|
|
var lock sync.Mutex
|
|
errored := false
|
|
p.ApplyResourceChangeFn = func(
|
|
r providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
|
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
var diags tfdiags.Diagnostics
|
|
|
|
ami := r.Config.GetAttr("ami").AsString()
|
|
if !errored && ami == "error" {
|
|
errored = true
|
|
diags = diags.Append(errors.New("ami error"))
|
|
return providers.ApplyResourceChangeResponse{
|
|
Diagnostics: diags,
|
|
}
|
|
}
|
|
return providers.ApplyResourceChangeResponse{
|
|
Diagnostics: diags,
|
|
NewState: cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("foo"),
|
|
"ami": cty.StringVal("bar"),
|
|
}),
|
|
}
|
|
}
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-error")
|
|
defer configCleanup()
|
|
|
|
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")
|
|
}
|
|
|
|
checkState(t, b.StateOutPath, `
|
|
test_instance.foo:
|
|
ID = foo
|
|
provider = provider["registry.opentofu.org/hashicorp/test"]
|
|
ami = bar
|
|
`)
|
|
|
|
// the backend should be unlocked after a run
|
|
assertBackendStateUnlocked(t, b)
|
|
|
|
if got, want := done(t).Stderr(), "Error: ami error"; !strings.Contains(got, want) {
|
|
t.Fatalf("unexpected error output:\n%s\nwant: %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLocal_applyBackendFail(t *testing.T) {
|
|
b := TestLocal(t)
|
|
|
|
p := TestLocalProvider(t, b, "test", applyFixtureSchema())
|
|
|
|
p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{
|
|
NewState: cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("yes"),
|
|
"ami": cty.StringVal("bar"),
|
|
}),
|
|
Diagnostics: tfdiags.Diagnostics.Append(nil, errors.New("error before backend failure")),
|
|
}
|
|
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("failed to get current working directory")
|
|
}
|
|
err = os.Chdir(filepath.Dir(b.StatePath))
|
|
if err != nil {
|
|
t.Fatalf("failed to set temporary working directory")
|
|
}
|
|
defer os.Chdir(wd)
|
|
|
|
op, configCleanup, done := testOperationApply(t, wd+"/testdata/apply")
|
|
defer configCleanup()
|
|
|
|
b.Backend = &backendWithFailingState{}
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("bad: %s", err)
|
|
}
|
|
<-run.Done()
|
|
|
|
output := done(t)
|
|
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatalf("apply succeeded; want error")
|
|
}
|
|
|
|
diagErr := output.Stderr()
|
|
|
|
if !strings.Contains(diagErr, "Error saving state: fake failure") {
|
|
t.Fatalf("missing \"fake failure\" message in diags:\n%s", diagErr)
|
|
}
|
|
|
|
if !strings.Contains(diagErr, "error before backend failure") {
|
|
t.Fatalf("missing 'error before backend failure' diagnostic from apply")
|
|
}
|
|
|
|
// The fallback behavior should've created a file errored.tfstate in the
|
|
// current working directory.
|
|
checkState(t, "errored.tfstate", `
|
|
test_instance.foo: (tainted)
|
|
ID = yes
|
|
provider = provider["registry.opentofu.org/hashicorp/test"]
|
|
ami = bar
|
|
`)
|
|
|
|
// the backend should be unlocked after a run
|
|
assertBackendStateUnlocked(t, b)
|
|
}
|
|
|
|
func TestLocal_applyRefreshFalse(t *testing.T) {
|
|
b := TestLocal(t)
|
|
|
|
p := TestLocalProvider(t, b, "test", planFixtureSchema())
|
|
testStateFile(t, b.StatePath, testPlanState())
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/plan")
|
|
defer configCleanup()
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("bad: %s", err)
|
|
}
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("plan operation failed")
|
|
}
|
|
|
|
if p.ReadResourceCalled {
|
|
t.Fatal("ReadResource should not be called")
|
|
}
|
|
|
|
if errOutput := done(t).Stderr(); errOutput != "" {
|
|
t.Fatalf("unexpected error output:\n%s", errOutput)
|
|
}
|
|
}
|
|
|
|
type backendWithFailingState struct {
|
|
Local
|
|
}
|
|
|
|
func (b *backendWithFailingState) StateMgr(name string) (statemgr.Full, error) {
|
|
return &failingState{
|
|
statemgr.NewFilesystem("failing-state.tfstate", encryption.StateEncryptionDisabled()),
|
|
}, nil
|
|
}
|
|
|
|
type failingState struct {
|
|
*statemgr.Filesystem
|
|
}
|
|
|
|
func (s failingState) WriteState(state *states.State) error {
|
|
return errors.New("fake failure")
|
|
}
|
|
|
|
func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
|
|
t.Helper()
|
|
|
|
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
|
|
|
|
// 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.opentofu.org/hashicorp/test"))
|
|
|
|
return &backend.Operation{
|
|
Type: backend.OperationTypeApply,
|
|
Encryption: encryption.Disabled(),
|
|
ConfigDir: configDir,
|
|
ConfigLoader: configLoader,
|
|
StateLocker: clistate.NewNoopLocker(),
|
|
View: view,
|
|
DependencyLocks: depLocks,
|
|
}, configCleanup, done
|
|
}
|
|
|
|
// applyFixtureSchema returns a schema suitable for processing the
|
|
// configuration in testdata/apply . This schema should be
|
|
// assigned to a mock provider named "test".
|
|
func applyFixtureSchema() providers.ProviderSchema {
|
|
return providers.ProviderSchema{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"ami": {Type: cty.String, Optional: true},
|
|
"id": {Type: cty.String, Computed: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestApply_applyCanceledAutoApprove(t *testing.T) {
|
|
b := TestLocal(t)
|
|
|
|
TestLocalProvider(t, b, "test", applyFixtureSchema())
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
op.AutoApprove = true
|
|
defer configCleanup()
|
|
defer func() {
|
|
output := done(t)
|
|
if !strings.Contains(output.Stderr(), "execution halted") {
|
|
t.Fatal("expected 'execution halted', got:\n", output.All())
|
|
}
|
|
}()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
testHookStopPlanApply = cancel
|
|
defer func() {
|
|
testHookStopPlanApply = nil
|
|
}()
|
|
|
|
run, err := b.Operation(ctx, op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to fail")
|
|
}
|
|
|
|
}
|