github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/backend/atlas/state_client_test.go (about)

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