github.com/kevinklinger/open_terraform@v1.3.6/noninternal/backend/local/backend_apply_test.go (about)

     1  package local
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"sync"
    10  	"testing"
    11  
    12  	"github.com/zclconf/go-cty/cty"
    13  
    14  	"github.com/kevinklinger/open_terraform/noninternal/addrs"
    15  	"github.com/kevinklinger/open_terraform/noninternal/backend"
    16  	"github.com/kevinklinger/open_terraform/noninternal/command/arguments"
    17  	"github.com/kevinklinger/open_terraform/noninternal/command/clistate"
    18  	"github.com/kevinklinger/open_terraform/noninternal/command/views"
    19  	"github.com/kevinklinger/open_terraform/noninternal/configs/configschema"
    20  	"github.com/kevinklinger/open_terraform/noninternal/depsfile"
    21  	"github.com/kevinklinger/open_terraform/noninternal/initwd"
    22  	"github.com/kevinklinger/open_terraform/noninternal/plans"
    23  	"github.com/kevinklinger/open_terraform/noninternal/providers"
    24  	"github.com/kevinklinger/open_terraform/noninternal/states"
    25  	"github.com/kevinklinger/open_terraform/noninternal/states/statemgr"
    26  	"github.com/kevinklinger/open_terraform/noninternal/terminal"
    27  	"github.com/kevinklinger/open_terraform/noninternal/terraform"
    28  	"github.com/kevinklinger/open_terraform/noninternal/tfdiags"
    29  )
    30  
    31  func TestLocal_applyBasic(t *testing.T) {
    32  	b := TestLocal(t)
    33  
    34  	p := TestLocalProvider(t, b, "test", applyFixtureSchema())
    35  	p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{
    36  		"id":  cty.StringVal("yes"),
    37  		"ami": cty.StringVal("bar"),
    38  	})}
    39  
    40  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
    41  	defer configCleanup()
    42  
    43  	run, err := b.Operation(context.Background(), op)
    44  	if err != nil {
    45  		t.Fatalf("bad: %s", err)
    46  	}
    47  	<-run.Done()
    48  	if run.Result != backend.OperationSuccess {
    49  		t.Fatal("operation failed")
    50  	}
    51  
    52  	if p.ReadResourceCalled {
    53  		t.Fatal("ReadResource should not be called")
    54  	}
    55  
    56  	if !p.PlanResourceChangeCalled {
    57  		t.Fatal("diff should be called")
    58  	}
    59  
    60  	if !p.ApplyResourceChangeCalled {
    61  		t.Fatal("apply should be called")
    62  	}
    63  
    64  	checkState(t, b.StateOutPath, `
    65  test_instance.foo:
    66    ID = yes
    67    provider = provider["registry.terraform.io/hashicorp/test"]
    68    ami = bar
    69  `)
    70  
    71  	if errOutput := done(t).Stderr(); errOutput != "" {
    72  		t.Fatalf("unexpected error output:\n%s", errOutput)
    73  	}
    74  }
    75  
    76  func TestLocal_applyEmptyDir(t *testing.T) {
    77  	b := TestLocal(t)
    78  
    79  	p := TestLocalProvider(t, b, "test", &terraform.ProviderSchema{})
    80  	p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{"id": cty.StringVal("yes")})}
    81  
    82  	op, configCleanup, done := testOperationApply(t, "./testdata/empty")
    83  	defer configCleanup()
    84  
    85  	run, err := b.Operation(context.Background(), op)
    86  	if err != nil {
    87  		t.Fatalf("bad: %s", err)
    88  	}
    89  	<-run.Done()
    90  	if run.Result == backend.OperationSuccess {
    91  		t.Fatal("operation succeeded; want error")
    92  	}
    93  
    94  	if p.ApplyResourceChangeCalled {
    95  		t.Fatal("apply should not be called")
    96  	}
    97  
    98  	if _, err := os.Stat(b.StateOutPath); err == nil {
    99  		t.Fatal("should not exist")
   100  	}
   101  
   102  	// the backend should be unlocked after a run
   103  	assertBackendStateUnlocked(t, b)
   104  
   105  	if got, want := done(t).Stderr(), "Error: No configuration files"; !strings.Contains(got, want) {
   106  		t.Fatalf("unexpected error output:\n%s\nwant: %s", got, want)
   107  	}
   108  }
   109  
   110  func TestLocal_applyEmptyDirDestroy(t *testing.T) {
   111  	b := TestLocal(t)
   112  
   113  	p := TestLocalProvider(t, b, "test", &terraform.ProviderSchema{})
   114  	p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{}
   115  
   116  	op, configCleanup, done := testOperationApply(t, "./testdata/empty")
   117  	defer configCleanup()
   118  	op.PlanMode = plans.DestroyMode
   119  
   120  	run, err := b.Operation(context.Background(), op)
   121  	if err != nil {
   122  		t.Fatalf("bad: %s", err)
   123  	}
   124  	<-run.Done()
   125  	if run.Result != backend.OperationSuccess {
   126  		t.Fatalf("apply operation failed")
   127  	}
   128  
   129  	if p.ApplyResourceChangeCalled {
   130  		t.Fatal("apply should not be called")
   131  	}
   132  
   133  	checkState(t, b.StateOutPath, `<no state>`)
   134  
   135  	if errOutput := done(t).Stderr(); errOutput != "" {
   136  		t.Fatalf("unexpected error output:\n%s", errOutput)
   137  	}
   138  }
   139  
   140  func TestLocal_applyError(t *testing.T) {
   141  	b := TestLocal(t)
   142  
   143  	schema := &terraform.ProviderSchema{
   144  		ResourceTypes: map[string]*configschema.Block{
   145  			"test_instance": {
   146  				Attributes: map[string]*configschema.Attribute{
   147  					"ami": {Type: cty.String, Optional: true},
   148  					"id":  {Type: cty.String, Computed: true},
   149  				},
   150  			},
   151  		},
   152  	}
   153  	p := TestLocalProvider(t, b, "test", schema)
   154  
   155  	var lock sync.Mutex
   156  	errored := false
   157  	p.ApplyResourceChangeFn = func(
   158  		r providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
   159  
   160  		lock.Lock()
   161  		defer lock.Unlock()
   162  		var diags tfdiags.Diagnostics
   163  
   164  		ami := r.Config.GetAttr("ami").AsString()
   165  		if !errored && ami == "error" {
   166  			errored = true
   167  			diags = diags.Append(errors.New("ami error"))
   168  			return providers.ApplyResourceChangeResponse{
   169  				Diagnostics: diags,
   170  			}
   171  		}
   172  		return providers.ApplyResourceChangeResponse{
   173  			Diagnostics: diags,
   174  			NewState: cty.ObjectVal(map[string]cty.Value{
   175  				"id":  cty.StringVal("foo"),
   176  				"ami": cty.StringVal("bar"),
   177  			}),
   178  		}
   179  	}
   180  
   181  	op, configCleanup, done := testOperationApply(t, "./testdata/apply-error")
   182  	defer configCleanup()
   183  
   184  	run, err := b.Operation(context.Background(), op)
   185  	if err != nil {
   186  		t.Fatalf("bad: %s", err)
   187  	}
   188  	<-run.Done()
   189  	if run.Result == backend.OperationSuccess {
   190  		t.Fatal("operation succeeded; want failure")
   191  	}
   192  
   193  	checkState(t, b.StateOutPath, `
   194  test_instance.foo:
   195    ID = foo
   196    provider = provider["registry.terraform.io/hashicorp/test"]
   197    ami = bar
   198  	`)
   199  
   200  	// the backend should be unlocked after a run
   201  	assertBackendStateUnlocked(t, b)
   202  
   203  	if got, want := done(t).Stderr(), "Error: ami error"; !strings.Contains(got, want) {
   204  		t.Fatalf("unexpected error output:\n%s\nwant: %s", got, want)
   205  	}
   206  }
   207  
   208  func TestLocal_applyBackendFail(t *testing.T) {
   209  	b := TestLocal(t)
   210  
   211  	p := TestLocalProvider(t, b, "test", applyFixtureSchema())
   212  
   213  	p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{
   214  		NewState: cty.ObjectVal(map[string]cty.Value{
   215  			"id":  cty.StringVal("yes"),
   216  			"ami": cty.StringVal("bar"),
   217  		}),
   218  		Diagnostics: tfdiags.Diagnostics.Append(nil, errors.New("error before backend failure")),
   219  	}
   220  
   221  	wd, err := os.Getwd()
   222  	if err != nil {
   223  		t.Fatalf("failed to get current working directory")
   224  	}
   225  	err = os.Chdir(filepath.Dir(b.StatePath))
   226  	if err != nil {
   227  		t.Fatalf("failed to set temporary working directory")
   228  	}
   229  	defer os.Chdir(wd)
   230  
   231  	op, configCleanup, done := testOperationApply(t, wd+"/testdata/apply")
   232  	defer configCleanup()
   233  
   234  	b.Backend = &backendWithFailingState{}
   235  
   236  	run, err := b.Operation(context.Background(), op)
   237  	if err != nil {
   238  		t.Fatalf("bad: %s", err)
   239  	}
   240  	<-run.Done()
   241  
   242  	output := done(t)
   243  
   244  	if run.Result == backend.OperationSuccess {
   245  		t.Fatalf("apply succeeded; want error")
   246  	}
   247  
   248  	diagErr := output.Stderr()
   249  
   250  	if !strings.Contains(diagErr, "Error saving state: fake failure") {
   251  		t.Fatalf("missing \"fake failure\" message in diags:\n%s", diagErr)
   252  	}
   253  
   254  	if !strings.Contains(diagErr, "error before backend failure") {
   255  		t.Fatalf("missing 'error before backend failure' diagnostic from apply")
   256  	}
   257  
   258  	// The fallback behavior should've created a file errored.tfstate in the
   259  	// current working directory.
   260  	checkState(t, "errored.tfstate", `
   261  test_instance.foo: (tainted)
   262    ID = yes
   263    provider = provider["registry.terraform.io/hashicorp/test"]
   264    ami = bar
   265  	`)
   266  
   267  	// the backend should be unlocked after a run
   268  	assertBackendStateUnlocked(t, b)
   269  }
   270  
   271  func TestLocal_applyRefreshFalse(t *testing.T) {
   272  	b := TestLocal(t)
   273  
   274  	p := TestLocalProvider(t, b, "test", planFixtureSchema())
   275  	testStateFile(t, b.StatePath, testPlanState())
   276  
   277  	op, configCleanup, done := testOperationApply(t, "./testdata/plan")
   278  	defer configCleanup()
   279  
   280  	run, err := b.Operation(context.Background(), op)
   281  	if err != nil {
   282  		t.Fatalf("bad: %s", err)
   283  	}
   284  	<-run.Done()
   285  	if run.Result != backend.OperationSuccess {
   286  		t.Fatalf("plan operation failed")
   287  	}
   288  
   289  	if p.ReadResourceCalled {
   290  		t.Fatal("ReadResource should not be called")
   291  	}
   292  
   293  	if errOutput := done(t).Stderr(); errOutput != "" {
   294  		t.Fatalf("unexpected error output:\n%s", errOutput)
   295  	}
   296  }
   297  
   298  type backendWithFailingState struct {
   299  	Local
   300  }
   301  
   302  func (b *backendWithFailingState) StateMgr(name string) (statemgr.Full, error) {
   303  	return &failingState{
   304  		statemgr.NewFilesystem("failing-state.tfstate"),
   305  	}, nil
   306  }
   307  
   308  type failingState struct {
   309  	*statemgr.Filesystem
   310  }
   311  
   312  func (s failingState) WriteState(state *states.State) error {
   313  	return errors.New("fake failure")
   314  }
   315  
   316  func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
   317  	t.Helper()
   318  
   319  	_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
   320  
   321  	streams, done := terminal.StreamsForTesting(t)
   322  	view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
   323  
   324  	// Many of our tests use an overridden "test" provider that's just in-memory
   325  	// inside the test process, not a separate plugin on disk.
   326  	depLocks := depsfile.NewLocks()
   327  	depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test"))
   328  
   329  	return &backend.Operation{
   330  		Type:            backend.OperationTypeApply,
   331  		ConfigDir:       configDir,
   332  		ConfigLoader:    configLoader,
   333  		StateLocker:     clistate.NewNoopLocker(),
   334  		View:            view,
   335  		DependencyLocks: depLocks,
   336  	}, configCleanup, done
   337  }
   338  
   339  // applyFixtureSchema returns a schema suitable for processing the
   340  // configuration in testdata/apply . This schema should be
   341  // assigned to a mock provider named "test".
   342  func applyFixtureSchema() *terraform.ProviderSchema {
   343  	return &terraform.ProviderSchema{
   344  		ResourceTypes: map[string]*configschema.Block{
   345  			"test_instance": {
   346  				Attributes: map[string]*configschema.Attribute{
   347  					"ami": {Type: cty.String, Optional: true},
   348  					"id":  {Type: cty.String, Computed: true},
   349  				},
   350  			},
   351  		},
   352  	}
   353  }
   354  
   355  func TestApply_applyCanceledAutoApprove(t *testing.T) {
   356  	b := TestLocal(t)
   357  
   358  	TestLocalProvider(t, b, "test", applyFixtureSchema())
   359  
   360  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   361  	op.AutoApprove = true
   362  	defer configCleanup()
   363  	defer func() {
   364  		output := done(t)
   365  		if !strings.Contains(output.Stderr(), "execution halted") {
   366  			t.Fatal("expected 'execution halted', got:\n", output.All())
   367  		}
   368  	}()
   369  
   370  	ctx, cancel := context.WithCancel(context.Background())
   371  	testHookStopPlanApply = cancel
   372  	defer func() {
   373  		testHookStopPlanApply = nil
   374  	}()
   375  
   376  	run, err := b.Operation(ctx, op)
   377  	if err != nil {
   378  		t.Fatalf("error starting operation: %v", err)
   379  	}
   380  
   381  	<-run.Done()
   382  	if run.Result == backend.OperationSuccess {
   383  		t.Fatal("expected apply operation to fail")
   384  	}
   385  
   386  }