github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/state/remote/state_test.go (about)

     1  package remote
     2  
     3  import (
     4  	"log"
     5  	"sync"
     6  	"testing"
     7  
     8  	"github.com/google/go-cmp/cmp"
     9  	"github.com/zclconf/go-cty/cty"
    10  
    11  	"github.com/hashicorp/terraform/states"
    12  	"github.com/hashicorp/terraform/states/statefile"
    13  	"github.com/hashicorp/terraform/states/statemgr"
    14  	"github.com/hashicorp/terraform/version"
    15  )
    16  
    17  func TestState_impl(t *testing.T) {
    18  	var _ statemgr.Reader = new(State)
    19  	var _ statemgr.Writer = new(State)
    20  	var _ statemgr.Persister = new(State)
    21  	var _ statemgr.Refresher = new(State)
    22  	var _ statemgr.Locker = new(State)
    23  }
    24  
    25  func TestStateRace(t *testing.T) {
    26  	s := &State{
    27  		Client: nilClient{},
    28  	}
    29  
    30  	current := states.NewState()
    31  
    32  	var wg sync.WaitGroup
    33  
    34  	for i := 0; i < 100; i++ {
    35  		wg.Add(1)
    36  		go func() {
    37  			defer wg.Done()
    38  			s.WriteState(current)
    39  			s.PersistState()
    40  			s.RefreshState()
    41  		}()
    42  	}
    43  	wg.Wait()
    44  }
    45  
    46  // testCase encapsulates a test state test
    47  type testCase struct {
    48  	name string
    49  	// A function to mutate state and return a cleanup function
    50  	mutationFunc func(*State) (*states.State, func())
    51  	// The expected request to have taken place
    52  	expectedRequest mockClientRequest
    53  	// Mark this case as not having a request
    54  	noRequest bool
    55  }
    56  
    57  // isRequested ensures a test that is specified as not having
    58  // a request doesn't have one by checking if a method exists
    59  // on the expectedRequest.
    60  func (tc testCase) isRequested(t *testing.T) bool {
    61  	hasMethod := tc.expectedRequest.Method != ""
    62  	if tc.noRequest && hasMethod {
    63  		t.Fatalf("expected no content for %q but got: %v", tc.name, tc.expectedRequest)
    64  	}
    65  	return !tc.noRequest
    66  }
    67  
    68  func TestStatePersist(t *testing.T) {
    69  	testCases := []testCase{
    70  		// Refreshing state before we run the test loop causes a GET
    71  		{
    72  			name: "refresh state",
    73  			mutationFunc: func(mgr *State) (*states.State, func()) {
    74  				return mgr.State(), func() {}
    75  			},
    76  			expectedRequest: mockClientRequest{
    77  				Method: "Get",
    78  				Content: map[string]interface{}{
    79  					"version":           4.0, // encoding/json decodes this as float64 by default
    80  					"lineage":           "mock-lineage",
    81  					"serial":            1.0, // encoding/json decodes this as float64 by default
    82  					"terraform_version": "0.0.0",
    83  					"outputs":           map[string]interface{}{},
    84  					"resources":         []interface{}{},
    85  				},
    86  			},
    87  		},
    88  		{
    89  			name: "change lineage",
    90  			mutationFunc: func(mgr *State) (*states.State, func()) {
    91  				originalLineage := mgr.lineage
    92  				mgr.lineage = "some-new-lineage"
    93  				return mgr.State(), func() {
    94  					mgr.lineage = originalLineage
    95  				}
    96  			},
    97  			expectedRequest: mockClientRequest{
    98  				Method: "Put",
    99  				Content: map[string]interface{}{
   100  					"version":           4.0, // encoding/json decodes this as float64 by default
   101  					"lineage":           "some-new-lineage",
   102  					"serial":            2.0, // encoding/json decodes this as float64 by default
   103  					"terraform_version": version.Version,
   104  					"outputs":           map[string]interface{}{},
   105  					"resources":         []interface{}{},
   106  				},
   107  			},
   108  		},
   109  		{
   110  			name: "change serial",
   111  			mutationFunc: func(mgr *State) (*states.State, func()) {
   112  				originalSerial := mgr.serial
   113  				mgr.serial++
   114  				return mgr.State(), func() {
   115  					mgr.serial = originalSerial
   116  				}
   117  			},
   118  			expectedRequest: mockClientRequest{
   119  				Method: "Put",
   120  				Content: map[string]interface{}{
   121  					"version":           4.0, // encoding/json decodes this as float64 by default
   122  					"lineage":           "mock-lineage",
   123  					"serial":            4.0, // encoding/json decodes this as float64 by default
   124  					"terraform_version": version.Version,
   125  					"outputs":           map[string]interface{}{},
   126  					"resources":         []interface{}{},
   127  				},
   128  			},
   129  		},
   130  		{
   131  			name: "add output to state",
   132  			mutationFunc: func(mgr *State) (*states.State, func()) {
   133  				s := mgr.State()
   134  				s.RootModule().SetOutputValue("foo", cty.StringVal("bar"), false)
   135  				return s, func() {}
   136  			},
   137  			expectedRequest: mockClientRequest{
   138  				Method: "Put",
   139  				Content: map[string]interface{}{
   140  					"version":           4.0, // encoding/json decodes this as float64 by default
   141  					"lineage":           "mock-lineage",
   142  					"serial":            3.0, // encoding/json decodes this as float64 by default
   143  					"terraform_version": version.Version,
   144  					"outputs": map[string]interface{}{
   145  						"foo": map[string]interface{}{
   146  							"type":  "string",
   147  							"value": "bar",
   148  						},
   149  					},
   150  					"resources": []interface{}{},
   151  				},
   152  			},
   153  		},
   154  		{
   155  			name: "mutate state bar -> baz",
   156  			mutationFunc: func(mgr *State) (*states.State, func()) {
   157  				s := mgr.State()
   158  				s.RootModule().SetOutputValue("foo", cty.StringVal("baz"), false)
   159  				return s, func() {}
   160  			},
   161  			expectedRequest: mockClientRequest{
   162  				Method: "Put",
   163  				Content: map[string]interface{}{
   164  					"version":           4.0, // encoding/json decodes this as float64 by default
   165  					"lineage":           "mock-lineage",
   166  					"serial":            4.0, // encoding/json decodes this as float64 by default
   167  					"terraform_version": version.Version,
   168  					"outputs": map[string]interface{}{
   169  						"foo": map[string]interface{}{
   170  							"type":  "string",
   171  							"value": "baz",
   172  						},
   173  					},
   174  					"resources": []interface{}{},
   175  				},
   176  			},
   177  		},
   178  		{
   179  			name: "nothing changed",
   180  			mutationFunc: func(mgr *State) (*states.State, func()) {
   181  				s := mgr.State()
   182  				return s, func() {}
   183  			},
   184  			noRequest: true,
   185  		},
   186  		{
   187  			name: "reset serial (force push style)",
   188  			mutationFunc: func(mgr *State) (*states.State, func()) {
   189  				mgr.serial = 2
   190  				return mgr.State(), func() {}
   191  			},
   192  			expectedRequest: mockClientRequest{
   193  				Method: "Put",
   194  				Content: map[string]interface{}{
   195  					"version":           4.0, // encoding/json decodes this as float64 by default
   196  					"lineage":           "mock-lineage",
   197  					"serial":            3.0, // encoding/json decodes this as float64 by default
   198  					"terraform_version": version.Version,
   199  					"outputs": map[string]interface{}{
   200  						"foo": map[string]interface{}{
   201  							"type":  "string",
   202  							"value": "baz",
   203  						},
   204  					},
   205  					"resources": []interface{}{},
   206  				},
   207  			},
   208  		},
   209  	}
   210  
   211  	// Initial setup of state just to give us a fixed starting point for our
   212  	// test assertions below, or else we'd need to deal with
   213  	// random lineage.
   214  	mgr := &State{
   215  		Client: &mockClient{
   216  			current: []byte(`
   217  				{
   218  					"version": 4,
   219  					"lineage": "mock-lineage",
   220  					"serial": 1,
   221  					"terraform_version":"0.0.0",
   222  					"outputs": {},
   223  					"resources": []
   224  				}
   225  			`),
   226  		},
   227  	}
   228  
   229  	// In normal use (during a Terraform operation) we always refresh and read
   230  	// before any writes would happen, so we'll mimic that here for realism.
   231  	// NB This causes a GET to be logged so the first item in the test cases
   232  	// must account for this
   233  	if err := mgr.RefreshState(); err != nil {
   234  		t.Fatalf("failed to RefreshState: %s", err)
   235  	}
   236  
   237  	// Our client is a mockClient which has a log we
   238  	// use to check that operations generate expected requests
   239  	mockClient := mgr.Client.(*mockClient)
   240  
   241  	// logIdx tracks the current index of the log separate from
   242  	// the loop iteration so we can check operations that don't
   243  	// cause any requests to be generated
   244  	logIdx := 0
   245  
   246  	// Run tests in order.
   247  	for _, tc := range testCases {
   248  		s, cleanup := tc.mutationFunc(mgr)
   249  
   250  		if err := mgr.WriteState(s); err != nil {
   251  			t.Fatalf("failed to WriteState for %q: %s", tc.name, err)
   252  		}
   253  		if err := mgr.PersistState(); err != nil {
   254  			t.Fatalf("failed to PersistState for %q: %s", tc.name, err)
   255  		}
   256  
   257  		if tc.isRequested(t) {
   258  			// Get captured request from the mock client log
   259  			// based on the index of the current test
   260  			if logIdx >= len(mockClient.log) {
   261  				t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log))
   262  			}
   263  			loggedRequest := mockClient.log[logIdx]
   264  			logIdx++
   265  			if diff := cmp.Diff(tc.expectedRequest, loggedRequest); len(diff) > 0 {
   266  				t.Fatalf("incorrect client requests for %q:\n%s", tc.name, diff)
   267  			}
   268  		}
   269  		cleanup()
   270  	}
   271  	logCnt := len(mockClient.log)
   272  	if logIdx != logCnt {
   273  		log.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx)
   274  	}
   275  }
   276  
   277  type migrationTestCase struct {
   278  	name string
   279  	// A function to generate a statefile
   280  	stateFile func(*State) *statefile.File
   281  	// The expected request to have taken place
   282  	expectedRequest mockClientRequest
   283  	// Mark this case as not having a request
   284  	expectedError string
   285  	// force flag passed to client
   286  	force bool
   287  }
   288  
   289  func TestWriteStateForMigration(t *testing.T) {
   290  	mgr := &State{
   291  		Client: &mockClient{
   292  			current: []byte(`
   293  				{
   294  					"version": 4,
   295  					"lineage": "mock-lineage",
   296  					"serial": 3,
   297  					"terraform_version":"0.0.0",
   298  					"outputs": {"foo": {"value":"bar", "type": "string"}},
   299  					"resources": []
   300  				}
   301  			`),
   302  		},
   303  	}
   304  
   305  	testCases := []migrationTestCase{
   306  		// Refreshing state before we run the test loop causes a GET
   307  		{
   308  			name: "refresh state",
   309  			stateFile: func(mgr *State) *statefile.File {
   310  				return mgr.StateForMigration()
   311  			},
   312  			expectedRequest: mockClientRequest{
   313  				Method: "Get",
   314  				Content: map[string]interface{}{
   315  					"version":           4.0,
   316  					"lineage":           "mock-lineage",
   317  					"serial":            3.0,
   318  					"terraform_version": "0.0.0",
   319  					"outputs":           map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}},
   320  					"resources":         []interface{}{},
   321  				},
   322  			},
   323  		},
   324  		{
   325  			name: "cannot import lesser serial without force",
   326  			stateFile: func(mgr *State) *statefile.File {
   327  				return statefile.New(mgr.state, mgr.lineage, 1)
   328  			},
   329  			expectedError: "cannot import state with serial 1 over newer state with serial 3",
   330  		},
   331  		{
   332  			name: "cannot import differing lineage without force",
   333  			stateFile: func(mgr *State) *statefile.File {
   334  				return statefile.New(mgr.state, "different-lineage", mgr.serial)
   335  			},
   336  			expectedError: `cannot import state with lineage "different-lineage" over unrelated state with lineage "mock-lineage"`,
   337  		},
   338  		{
   339  			name: "can import lesser serial with force",
   340  			stateFile: func(mgr *State) *statefile.File {
   341  				return statefile.New(mgr.state, mgr.lineage, 1)
   342  			},
   343  			expectedRequest: mockClientRequest{
   344  				Method: "Force Put",
   345  				Content: map[string]interface{}{
   346  					"version":           4.0,
   347  					"lineage":           "mock-lineage",
   348  					"serial":            2.0,
   349  					"terraform_version": version.Version,
   350  					"outputs":           map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}},
   351  					"resources":         []interface{}{},
   352  				},
   353  			},
   354  			force: true,
   355  		},
   356  		{
   357  			name: "cannot import differing lineage without force",
   358  			stateFile: func(mgr *State) *statefile.File {
   359  				return statefile.New(mgr.state, "different-lineage", mgr.serial)
   360  			},
   361  			expectedRequest: mockClientRequest{
   362  				Method: "Force Put",
   363  				Content: map[string]interface{}{
   364  					"version":           4.0,
   365  					"lineage":           "different-lineage",
   366  					"serial":            3.0,
   367  					"terraform_version": version.Version,
   368  					"outputs":           map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}},
   369  					"resources":         []interface{}{},
   370  				},
   371  			},
   372  			force: true,
   373  		},
   374  	}
   375  
   376  	// In normal use (during a Terraform operation) we always refresh and read
   377  	// before any writes would happen, so we'll mimic that here for realism.
   378  	// NB This causes a GET to be logged so the first item in the test cases
   379  	// must account for this
   380  	if err := mgr.RefreshState(); err != nil {
   381  		t.Fatalf("failed to RefreshState: %s", err)
   382  	}
   383  
   384  	if err := mgr.WriteState(mgr.State()); err != nil {
   385  		t.Fatalf("failed to write initial state: %s", err)
   386  	}
   387  
   388  	// Our client is a mockClient which has a log we
   389  	// use to check that operations generate expected requests
   390  	mockClient := mgr.Client.(*mockClient)
   391  
   392  	if mockClient.force {
   393  		t.Fatalf("client should not default to force")
   394  	}
   395  
   396  	// logIdx tracks the current index of the log separate from
   397  	// the loop iteration so we can check operations that don't
   398  	// cause any requests to be generated
   399  	logIdx := 0
   400  
   401  	for _, tc := range testCases {
   402  		// Always reset client to not be force pushing
   403  		mockClient.force = false
   404  		sf := tc.stateFile(mgr)
   405  		err := mgr.WriteStateForMigration(sf, tc.force)
   406  		shouldError := tc.expectedError != ""
   407  
   408  		// If we are expecting and error check it and move on
   409  		if shouldError {
   410  			if err == nil {
   411  				t.Fatalf("test case %q should have failed with error %q", tc.name, tc.expectedError)
   412  			} else if err.Error() != tc.expectedError {
   413  				t.Fatalf("test case %q expected error %q but got %q", tc.name, tc.expectedError, err)
   414  			}
   415  			continue
   416  		}
   417  
   418  		if err != nil {
   419  			t.Fatalf("test case %q failed: %v", tc.name, err)
   420  		}
   421  
   422  		if tc.force && !mockClient.force {
   423  			t.Fatalf("test case %q should have enabled force push", tc.name)
   424  		}
   425  
   426  		// At this point we should just do a normal write and persist
   427  		// as would happen from the CLI
   428  		mgr.WriteState(mgr.State())
   429  		mgr.PersistState()
   430  
   431  		if logIdx >= len(mockClient.log) {
   432  			t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log))
   433  		}
   434  		loggedRequest := mockClient.log[logIdx]
   435  		logIdx++
   436  		if diff := cmp.Diff(tc.expectedRequest, loggedRequest); len(diff) > 0 {
   437  			t.Fatalf("incorrect client requests for %q:\n%s", tc.name, diff)
   438  		}
   439  	}
   440  
   441  	logCnt := len(mockClient.log)
   442  	if logIdx != logCnt {
   443  		log.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx)
   444  	}
   445  }