github.com/openshift/terraform@v0.11.12-beta1/backend/atlas/state_client_test.go (about)

     1  package atlas
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/md5"
     7  	"crypto/tls"
     8  	"crypto/x509"
     9  	"encoding/json"
    10  	"net/http"
    11  	"net/http/httptest"
    12  	"net/url"
    13  	"os"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/hashicorp/terraform/backend"
    18  	"github.com/hashicorp/terraform/helper/acctest"
    19  	"github.com/hashicorp/terraform/state/remote"
    20  	"github.com/hashicorp/terraform/terraform"
    21  )
    22  
    23  func testStateClient(t *testing.T, c map[string]interface{}) remote.Client {
    24  	b := backend.TestBackendConfig(t, New(), c)
    25  	raw, err := b.State(backend.DefaultStateName)
    26  	if err != nil {
    27  		t.Fatalf("err: %s", err)
    28  	}
    29  
    30  	s := raw.(*remote.State)
    31  	return s.Client
    32  }
    33  
    34  func TestStateClient_impl(t *testing.T) {
    35  	var _ remote.Client = new(stateClient)
    36  }
    37  
    38  func TestStateClient(t *testing.T) {
    39  	acctest.RemoteTestPrecheck(t)
    40  
    41  	token := os.Getenv("ATLAS_TOKEN")
    42  	if token == "" {
    43  		t.Skipf("skipping, ATLAS_TOKEN must be set")
    44  	}
    45  
    46  	client := testStateClient(t, map[string]interface{}{
    47  		"access_token": token,
    48  		"name":         "hashicorp/test-remote-state",
    49  	})
    50  
    51  	remote.TestClient(t, client)
    52  }
    53  
    54  func TestStateClient_noRetryOnBadCerts(t *testing.T) {
    55  	acctest.RemoteTestPrecheck(t)
    56  
    57  	client := testStateClient(t, map[string]interface{}{
    58  		"access_token": "NOT_REQUIRED",
    59  		"name":         "hashicorp/test-remote-state",
    60  	})
    61  
    62  	ac := client.(*stateClient)
    63  	// trigger the StateClient to build the http client and assign HTTPClient
    64  	httpClient, err := ac.http()
    65  	if err != nil {
    66  		t.Fatal(err)
    67  	}
    68  
    69  	// remove the CA certs from the client
    70  	brokenCfg := &tls.Config{
    71  		RootCAs: new(x509.CertPool),
    72  	}
    73  	httpClient.HTTPClient.Transport.(*http.Transport).TLSClientConfig = brokenCfg
    74  
    75  	// Instrument CheckRetry to make sure we didn't retry
    76  	retries := 0
    77  	oldCheck := httpClient.CheckRetry
    78  	httpClient.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
    79  		if retries > 0 {
    80  			t.Fatal("retried after certificate error")
    81  		}
    82  		retries++
    83  		return oldCheck(ctx, resp, err)
    84  	}
    85  
    86  	_, err = client.Get()
    87  	if err != nil {
    88  		if err, ok := err.(*url.Error); ok {
    89  			if _, ok := err.Err.(x509.UnknownAuthorityError); ok {
    90  				return
    91  			}
    92  		}
    93  	}
    94  
    95  	t.Fatalf("expected x509.UnknownAuthorityError, got %v", err)
    96  }
    97  
    98  func TestStateClient_ReportedConflictEqualStates(t *testing.T) {
    99  	fakeAtlas := newFakeAtlas(t, testStateModuleOrderChange)
   100  	srv := fakeAtlas.Server()
   101  	defer srv.Close()
   102  
   103  	client := testStateClient(t, map[string]interface{}{
   104  		"access_token": "sometoken",
   105  		"name":         "someuser/some-test-remote-state",
   106  		"address":      srv.URL,
   107  	})
   108  
   109  	state, err := terraform.ReadState(bytes.NewReader(testStateModuleOrderChange))
   110  	if err != nil {
   111  		t.Fatalf("err: %s", err)
   112  	}
   113  
   114  	var stateJson bytes.Buffer
   115  	if err := terraform.WriteState(state, &stateJson); err != nil {
   116  		t.Fatalf("err: %s", err)
   117  	}
   118  	if err := client.Put(stateJson.Bytes()); err != nil {
   119  		t.Fatalf("err: %s", err)
   120  	}
   121  }
   122  
   123  func TestStateClient_NoConflict(t *testing.T) {
   124  	fakeAtlas := newFakeAtlas(t, testStateSimple)
   125  	srv := fakeAtlas.Server()
   126  	defer srv.Close()
   127  
   128  	client := testStateClient(t, map[string]interface{}{
   129  		"access_token": "sometoken",
   130  		"name":         "someuser/some-test-remote-state",
   131  		"address":      srv.URL,
   132  	})
   133  
   134  	state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
   135  	if err != nil {
   136  		t.Fatalf("err: %s", err)
   137  	}
   138  
   139  	fakeAtlas.NoConflictAllowed(true)
   140  
   141  	var stateJson bytes.Buffer
   142  	if err := terraform.WriteState(state, &stateJson); err != nil {
   143  		t.Fatalf("err: %s", err)
   144  	}
   145  
   146  	if err := client.Put(stateJson.Bytes()); err != nil {
   147  		t.Fatalf("err: %s", err)
   148  	}
   149  }
   150  
   151  func TestStateClient_LegitimateConflict(t *testing.T) {
   152  	fakeAtlas := newFakeAtlas(t, testStateSimple)
   153  	srv := fakeAtlas.Server()
   154  	defer srv.Close()
   155  
   156  	client := testStateClient(t, map[string]interface{}{
   157  		"access_token": "sometoken",
   158  		"name":         "someuser/some-test-remote-state",
   159  		"address":      srv.URL,
   160  	})
   161  
   162  	state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
   163  	if err != nil {
   164  		t.Fatalf("err: %s", err)
   165  	}
   166  
   167  	var buf bytes.Buffer
   168  	terraform.WriteState(state, &buf)
   169  
   170  	// Changing the state but not the serial. Should generate a conflict.
   171  	state.RootModule().Outputs["drift"] = &terraform.OutputState{
   172  		Type:      "string",
   173  		Sensitive: false,
   174  		Value:     "happens",
   175  	}
   176  
   177  	var stateJson bytes.Buffer
   178  	if err := terraform.WriteState(state, &stateJson); err != nil {
   179  		t.Fatalf("err: %s", err)
   180  	}
   181  	if err := client.Put(stateJson.Bytes()); err == nil {
   182  		t.Fatal("Expected error from state conflict, got none.")
   183  	}
   184  }
   185  
   186  func TestStateClient_UnresolvableConflict(t *testing.T) {
   187  	fakeAtlas := newFakeAtlas(t, testStateSimple)
   188  
   189  	// Something unexpected causes Atlas to conflict in a way that we can't fix.
   190  	fakeAtlas.AlwaysConflict(true)
   191  
   192  	srv := fakeAtlas.Server()
   193  	defer srv.Close()
   194  
   195  	client := testStateClient(t, map[string]interface{}{
   196  		"access_token": "sometoken",
   197  		"name":         "someuser/some-test-remote-state",
   198  		"address":      srv.URL,
   199  	})
   200  
   201  	state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
   202  	if err != nil {
   203  		t.Fatalf("err: %s", err)
   204  	}
   205  
   206  	var stateJson bytes.Buffer
   207  	if err := terraform.WriteState(state, &stateJson); err != nil {
   208  		t.Fatalf("err: %s", err)
   209  	}
   210  	doneCh := make(chan struct{})
   211  	go func() {
   212  		defer close(doneCh)
   213  		if err := client.Put(stateJson.Bytes()); err == nil {
   214  			t.Fatal("Expected error from state conflict, got none.")
   215  		}
   216  	}()
   217  
   218  	select {
   219  	case <-doneCh:
   220  		// OK
   221  	case <-time.After(500 * time.Millisecond):
   222  		t.Fatalf("Timed out after 500ms, probably because retrying infinitely.")
   223  	}
   224  }
   225  
   226  // Stub Atlas HTTP API for a given state JSON string; does checksum-based
   227  // conflict detection equivalent to Atlas's.
   228  type fakeAtlas struct {
   229  	state []byte
   230  	t     *testing.T
   231  
   232  	// Used to test that we only do the special conflict handling retry once.
   233  	alwaysConflict bool
   234  
   235  	// Used to fail the test immediately if a conflict happens.
   236  	noConflictAllowed bool
   237  }
   238  
   239  func newFakeAtlas(t *testing.T, state []byte) *fakeAtlas {
   240  	return &fakeAtlas{
   241  		state: state,
   242  		t:     t,
   243  	}
   244  }
   245  
   246  func (f *fakeAtlas) Server() *httptest.Server {
   247  	return httptest.NewServer(http.HandlerFunc(f.handler))
   248  }
   249  
   250  func (f *fakeAtlas) CurrentState() *terraform.State {
   251  	// we read the state manually here, because terraform may alter state
   252  	// during read
   253  	currentState := &terraform.State{}
   254  	err := json.Unmarshal(f.state, currentState)
   255  	if err != nil {
   256  		f.t.Fatalf("err: %s", err)
   257  	}
   258  	return currentState
   259  }
   260  
   261  func (f *fakeAtlas) CurrentSerial() int64 {
   262  	return f.CurrentState().Serial
   263  }
   264  
   265  func (f *fakeAtlas) CurrentSum() [md5.Size]byte {
   266  	return md5.Sum(f.state)
   267  }
   268  
   269  func (f *fakeAtlas) AlwaysConflict(b bool) {
   270  	f.alwaysConflict = b
   271  }
   272  
   273  func (f *fakeAtlas) NoConflictAllowed(b bool) {
   274  	f.noConflictAllowed = b
   275  }
   276  
   277  func (f *fakeAtlas) handler(resp http.ResponseWriter, req *http.Request) {
   278  	// access tokens should only be sent as a header
   279  	if req.FormValue("access_token") != "" {
   280  		http.Error(resp, "access_token in request params", http.StatusBadRequest)
   281  		return
   282  	}
   283  
   284  	if req.Header.Get(atlasTokenHeader) == "" {
   285  		http.Error(resp, "missing access token", http.StatusBadRequest)
   286  		return
   287  	}
   288  
   289  	switch req.Method {
   290  	case "GET":
   291  		// Respond with the current stored state.
   292  		resp.Header().Set("Content-Type", "application/json")
   293  		resp.Write(f.state)
   294  	case "PUT":
   295  		var buf bytes.Buffer
   296  		buf.ReadFrom(req.Body)
   297  		sum := md5.Sum(buf.Bytes())
   298  
   299  		// we read the state manually here, because terraform may alter state
   300  		// during read
   301  		state := &terraform.State{}
   302  		err := json.Unmarshal(buf.Bytes(), state)
   303  		if err != nil {
   304  			f.t.Fatalf("err: %s", err)
   305  		}
   306  
   307  		conflict := f.CurrentSerial() == state.Serial && f.CurrentSum() != sum
   308  		conflict = conflict || f.alwaysConflict
   309  		if conflict {
   310  			if f.noConflictAllowed {
   311  				f.t.Fatal("Got conflict when NoConflictAllowed was set.")
   312  			}
   313  			http.Error(resp, "Conflict", 409)
   314  		} else {
   315  			f.state = buf.Bytes()
   316  			resp.WriteHeader(200)
   317  		}
   318  	}
   319  }
   320  
   321  // This is a tfstate file with the module order changed, which is a structural
   322  // but not a semantic difference. Terraform will sort these modules as it
   323  // loads the state.
   324  var testStateModuleOrderChange = []byte(
   325  	`{
   326      "version": 3,
   327      "serial": 1,
   328      "modules": [
   329          {
   330              "path": [
   331                  "root",
   332                  "child2",
   333                  "grandchild"
   334              ],
   335              "outputs": {
   336                  "foo": {
   337                      "sensitive": false,
   338                      "type": "string",
   339                      "value": "bar"
   340                  }
   341              },
   342              "resources": null
   343          },
   344          {
   345              "path": [
   346                  "root",
   347                  "child1",
   348                  "grandchild"
   349              ],
   350              "outputs": {
   351                  "foo": {
   352                      "sensitive": false,
   353                      "type": "string",
   354                      "value": "bar"
   355                  }
   356              },
   357              "resources": null
   358          }
   359      ]
   360  }
   361  `)
   362  
   363  var testStateSimple = []byte(
   364  	`{
   365      "version": 3,
   366      "serial": 2,
   367      "lineage": "c00ad9ac-9b35-42fe-846e-b06f0ef877e9",
   368      "modules": [
   369          {
   370              "path": [
   371                  "root"
   372              ],
   373              "outputs": {
   374                  "foo": {
   375                      "sensitive": false,
   376                      "type": "string",
   377                      "value": "bar"
   378                  }
   379              },
   380              "resources": {},
   381              "depends_on": []
   382          }
   383      ]
   384  }
   385  `)