github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/local/backend_refresh_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package local
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/terramate-io/tf/addrs"
    13  	"github.com/terramate-io/tf/backend"
    14  	"github.com/terramate-io/tf/command/arguments"
    15  	"github.com/terramate-io/tf/command/clistate"
    16  	"github.com/terramate-io/tf/command/views"
    17  	"github.com/terramate-io/tf/configs/configschema"
    18  	"github.com/terramate-io/tf/depsfile"
    19  	"github.com/terramate-io/tf/initwd"
    20  	"github.com/terramate-io/tf/providers"
    21  	"github.com/terramate-io/tf/states"
    22  	"github.com/terramate-io/tf/terminal"
    23  	"github.com/terramate-io/tf/terraform"
    24  
    25  	"github.com/zclconf/go-cty/cty"
    26  )
    27  
    28  func TestLocal_refresh(t *testing.T) {
    29  	b := TestLocal(t)
    30  
    31  	p := TestLocalProvider(t, b, "test", refreshFixtureSchema())
    32  	testStateFile(t, b.StatePath, testRefreshState())
    33  
    34  	p.ReadResourceFn = nil
    35  	p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{
    36  		"id": cty.StringVal("yes"),
    37  	})}
    38  
    39  	op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh")
    40  	defer configCleanup()
    41  	defer done(t)
    42  
    43  	run, err := b.Operation(context.Background(), op)
    44  	if err != nil {
    45  		t.Fatalf("bad: %s", err)
    46  	}
    47  	<-run.Done()
    48  
    49  	if !p.ReadResourceCalled {
    50  		t.Fatal("ReadResource should be called")
    51  	}
    52  
    53  	checkState(t, b.StateOutPath, `
    54  test_instance.foo:
    55    ID = yes
    56    provider = provider["registry.terraform.io/hashicorp/test"]
    57  	`)
    58  
    59  	// the backend should be unlocked after a run
    60  	assertBackendStateUnlocked(t, b)
    61  }
    62  
    63  func TestLocal_refreshInput(t *testing.T) {
    64  	b := TestLocal(t)
    65  
    66  	schema := providers.ProviderSchema{
    67  		Provider: providers.Schema{
    68  			Block: &configschema.Block{
    69  				Attributes: map[string]*configschema.Attribute{
    70  					"value": {Type: cty.String, Optional: true},
    71  				},
    72  			},
    73  		},
    74  		ResourceTypes: map[string]providers.Schema{
    75  			"test_instance": {
    76  				Block: &configschema.Block{
    77  					Attributes: map[string]*configschema.Attribute{
    78  						"id":  {Type: cty.String, Computed: true},
    79  						"foo": {Type: cty.String, Optional: true},
    80  						"ami": {Type: cty.String, Optional: true},
    81  					},
    82  				},
    83  			},
    84  		},
    85  	}
    86  
    87  	p := TestLocalProvider(t, b, "test", schema)
    88  	testStateFile(t, b.StatePath, testRefreshState())
    89  
    90  	p.ReadResourceFn = nil
    91  	p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{
    92  		"id": cty.StringVal("yes"),
    93  	})}
    94  	p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
    95  		val := req.Config.GetAttr("value")
    96  		if val.IsNull() || val.AsString() != "bar" {
    97  			resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("incorrect value %#v", val))
    98  		}
    99  
   100  		return
   101  	}
   102  
   103  	// Enable input asking since it is normally disabled by default
   104  	b.OpInput = true
   105  	b.ContextOpts.UIInput = &terraform.MockUIInput{InputReturnString: "bar"}
   106  
   107  	op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh-var-unset")
   108  	defer configCleanup()
   109  	defer done(t)
   110  	op.UIIn = b.ContextOpts.UIInput
   111  
   112  	run, err := b.Operation(context.Background(), op)
   113  	if err != nil {
   114  		t.Fatalf("bad: %s", err)
   115  	}
   116  	<-run.Done()
   117  
   118  	if !p.ReadResourceCalled {
   119  		t.Fatal("ReadResource should be called")
   120  	}
   121  
   122  	checkState(t, b.StateOutPath, `
   123  test_instance.foo:
   124    ID = yes
   125    provider = provider["registry.terraform.io/hashicorp/test"]
   126  	`)
   127  }
   128  
   129  func TestLocal_refreshValidate(t *testing.T) {
   130  	b := TestLocal(t)
   131  	p := TestLocalProvider(t, b, "test", refreshFixtureSchema())
   132  	testStateFile(t, b.StatePath, testRefreshState())
   133  	p.ReadResourceFn = nil
   134  	p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{
   135  		"id": cty.StringVal("yes"),
   136  	})}
   137  
   138  	// Enable validation
   139  	b.OpValidation = true
   140  
   141  	op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh")
   142  	defer configCleanup()
   143  	defer done(t)
   144  
   145  	run, err := b.Operation(context.Background(), op)
   146  	if err != nil {
   147  		t.Fatalf("bad: %s", err)
   148  	}
   149  	<-run.Done()
   150  
   151  	checkState(t, b.StateOutPath, `
   152  test_instance.foo:
   153    ID = yes
   154    provider = provider["registry.terraform.io/hashicorp/test"]
   155  	`)
   156  }
   157  
   158  func TestLocal_refreshValidateProviderConfigured(t *testing.T) {
   159  	b := TestLocal(t)
   160  
   161  	schema := providers.ProviderSchema{
   162  		Provider: providers.Schema{
   163  			Block: &configschema.Block{
   164  				Attributes: map[string]*configschema.Attribute{
   165  					"value": {Type: cty.String, Optional: true},
   166  				},
   167  			},
   168  		},
   169  		ResourceTypes: map[string]providers.Schema{
   170  			"test_instance": {
   171  				Block: &configschema.Block{
   172  					Attributes: map[string]*configschema.Attribute{
   173  						"id":  {Type: cty.String, Computed: true},
   174  						"ami": {Type: cty.String, Optional: true},
   175  					},
   176  				},
   177  			},
   178  		},
   179  	}
   180  
   181  	p := TestLocalProvider(t, b, "test", schema)
   182  	testStateFile(t, b.StatePath, testRefreshState())
   183  	p.ReadResourceFn = nil
   184  	p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{
   185  		"id": cty.StringVal("yes"),
   186  	})}
   187  
   188  	// Enable validation
   189  	b.OpValidation = true
   190  
   191  	op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh-provider-config")
   192  	defer configCleanup()
   193  	defer done(t)
   194  
   195  	run, err := b.Operation(context.Background(), op)
   196  	if err != nil {
   197  		t.Fatalf("bad: %s", err)
   198  	}
   199  	<-run.Done()
   200  
   201  	if !p.ValidateProviderConfigCalled {
   202  		t.Fatal("Validate provider config should be called")
   203  	}
   204  
   205  	checkState(t, b.StateOutPath, `
   206  test_instance.foo:
   207    ID = yes
   208    provider = provider["registry.terraform.io/hashicorp/test"]
   209  	`)
   210  }
   211  
   212  // This test validates the state lacking behavior when the inner call to
   213  // Context() fails
   214  func TestLocal_refresh_context_error(t *testing.T) {
   215  	b := TestLocal(t)
   216  	testStateFile(t, b.StatePath, testRefreshState())
   217  	op, configCleanup, done := testOperationRefresh(t, "./testdata/apply")
   218  	defer configCleanup()
   219  	defer done(t)
   220  
   221  	// we coerce a failure in Context() by omitting the provider schema
   222  
   223  	run, err := b.Operation(context.Background(), op)
   224  	if err != nil {
   225  		t.Fatalf("bad: %s", err)
   226  	}
   227  	<-run.Done()
   228  	if run.Result == backend.OperationSuccess {
   229  		t.Fatal("operation succeeded; want failure")
   230  	}
   231  	assertBackendStateUnlocked(t, b)
   232  }
   233  
   234  func TestLocal_refreshEmptyState(t *testing.T) {
   235  	b := TestLocal(t)
   236  
   237  	p := TestLocalProvider(t, b, "test", refreshFixtureSchema())
   238  	testStateFile(t, b.StatePath, states.NewState())
   239  
   240  	p.ReadResourceFn = nil
   241  	p.ReadResourceResponse = &providers.ReadResourceResponse{NewState: cty.ObjectVal(map[string]cty.Value{
   242  		"id": cty.StringVal("yes"),
   243  	})}
   244  
   245  	op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh")
   246  	defer configCleanup()
   247  
   248  	run, err := b.Operation(context.Background(), op)
   249  	if err != nil {
   250  		t.Fatalf("bad: %s", err)
   251  	}
   252  	<-run.Done()
   253  
   254  	output := done(t)
   255  
   256  	if stderr := output.Stderr(); stderr != "" {
   257  		t.Fatalf("expected only warning diags, got errors: %s", stderr)
   258  	}
   259  	if got, want := output.Stdout(), "Warning: Empty or non-existent state"; !strings.Contains(got, want) {
   260  		t.Errorf("wrong diags\n got: %s\nwant: %s", got, want)
   261  	}
   262  
   263  	// the backend should be unlocked after a run
   264  	assertBackendStateUnlocked(t, b)
   265  }
   266  
   267  func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
   268  	t.Helper()
   269  
   270  	_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
   271  
   272  	streams, done := terminal.StreamsForTesting(t)
   273  	view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
   274  
   275  	// Many of our tests use an overridden "test" provider that's just in-memory
   276  	// inside the test process, not a separate plugin on disk.
   277  	depLocks := depsfile.NewLocks()
   278  	depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test"))
   279  
   280  	return &backend.Operation{
   281  		Type:            backend.OperationTypeRefresh,
   282  		ConfigDir:       configDir,
   283  		ConfigLoader:    configLoader,
   284  		StateLocker:     clistate.NewNoopLocker(),
   285  		View:            view,
   286  		DependencyLocks: depLocks,
   287  	}, configCleanup, done
   288  }
   289  
   290  // testRefreshState is just a common state that we use for testing refresh.
   291  func testRefreshState() *states.State {
   292  	state := states.NewState()
   293  	root := state.EnsureModule(addrs.RootModuleInstance)
   294  	root.SetResourceInstanceCurrent(
   295  		mustResourceInstanceAddr("test_instance.foo").Resource,
   296  		&states.ResourceInstanceObjectSrc{
   297  			Status:    states.ObjectReady,
   298  			AttrsJSON: []byte(`{"id":"bar"}`),
   299  		},
   300  		mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
   301  	)
   302  	return state
   303  }
   304  
   305  // refreshFixtureSchema returns a schema suitable for processing the
   306  // configuration in testdata/refresh . This schema should be
   307  // assigned to a mock provider named "test".
   308  func refreshFixtureSchema() providers.ProviderSchema {
   309  	return providers.ProviderSchema{
   310  		ResourceTypes: map[string]providers.Schema{
   311  			"test_instance": {
   312  				Block: &configschema.Block{
   313  					Attributes: map[string]*configschema.Attribute{
   314  						"ami": {Type: cty.String, Optional: true},
   315  						"id":  {Type: cty.String, Computed: true},
   316  					},
   317  				},
   318  			},
   319  		},
   320  	}
   321  }