github.com/ns1/terraform@v0.7.10-0.20161109153551-8949419bef40/state/remote/atlas_test.go (about)

     1  package remote
     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/helper/acctest"
    17  	"github.com/hashicorp/terraform/terraform"
    18  )
    19  
    20  func TestAtlasClient_impl(t *testing.T) {
    21  	var _ Client = new(AtlasClient)
    22  }
    23  
    24  func TestAtlasClient(t *testing.T) {
    25  	acctest.RemoteTestPrecheck(t)
    26  
    27  	token := os.Getenv("ATLAS_TOKEN")
    28  	if token == "" {
    29  		t.Skipf("skipping, ATLAS_TOKEN must be set")
    30  	}
    31  
    32  	client, err := atlasFactory(map[string]string{
    33  		"access_token": token,
    34  		"name":         "hashicorp/test-remote-state",
    35  	})
    36  	if err != nil {
    37  		t.Fatalf("bad: %s", err)
    38  	}
    39  
    40  	testClient(t, client)
    41  }
    42  
    43  func TestAtlasClient_noRetryOnBadCerts(t *testing.T) {
    44  	acctest.RemoteTestPrecheck(t)
    45  
    46  	client, err := atlasFactory(map[string]string{
    47  		"access_token": "NOT_REQUIRED",
    48  		"name":         "hashicorp/test-remote-state",
    49  	})
    50  	if err != nil {
    51  		t.Fatalf("bad: %s", err)
    52  	}
    53  
    54  	ac := client.(*AtlasClient)
    55  	// trigger the AtlasClient to build the http client and assign HTTPClient
    56  	httpClient, err := ac.http()
    57  	if err != nil {
    58  		t.Fatal(err)
    59  	}
    60  
    61  	// remove the CA certs from the client
    62  	brokenCfg := &tls.Config{
    63  		RootCAs: new(x509.CertPool),
    64  	}
    65  	httpClient.HTTPClient.Transport.(*http.Transport).TLSClientConfig = brokenCfg
    66  
    67  	// Instrument CheckRetry to make sure we didn't retry
    68  	retries := 0
    69  	oldCheck := httpClient.CheckRetry
    70  	httpClient.CheckRetry = func(resp *http.Response, err error) (bool, error) {
    71  		if retries > 0 {
    72  			t.Fatal("retried after certificate error")
    73  		}
    74  		retries++
    75  		return oldCheck(resp, err)
    76  	}
    77  
    78  	_, err = client.Get()
    79  	if err != nil {
    80  		if err, ok := err.(*url.Error); ok {
    81  			if _, ok := err.Err.(x509.UnknownAuthorityError); ok {
    82  				return
    83  			}
    84  		}
    85  	}
    86  
    87  	t.Fatalf("expected x509.UnknownAuthorityError, got %v", err)
    88  }
    89  
    90  func TestAtlasClient_ReportedConflictEqualStates(t *testing.T) {
    91  	fakeAtlas := newFakeAtlas(t, testStateModuleOrderChange)
    92  	srv := fakeAtlas.Server()
    93  	defer srv.Close()
    94  	client, err := atlasFactory(map[string]string{
    95  		"access_token": "sometoken",
    96  		"name":         "someuser/some-test-remote-state",
    97  		"address":      srv.URL,
    98  	})
    99  	if err != nil {
   100  		t.Fatalf("err: %s", err)
   101  	}
   102  
   103  	state, err := terraform.ReadState(bytes.NewReader(testStateModuleOrderChange))
   104  	if err != nil {
   105  		t.Fatalf("err: %s", err)
   106  	}
   107  
   108  	var stateJson bytes.Buffer
   109  	if err := terraform.WriteState(state, &stateJson); err != nil {
   110  		t.Fatalf("err: %s", err)
   111  	}
   112  	if err := client.Put(stateJson.Bytes()); err != nil {
   113  		t.Fatalf("err: %s", err)
   114  	}
   115  }
   116  
   117  func TestAtlasClient_NoConflict(t *testing.T) {
   118  	fakeAtlas := newFakeAtlas(t, testStateSimple)
   119  	srv := fakeAtlas.Server()
   120  	defer srv.Close()
   121  	client, err := atlasFactory(map[string]string{
   122  		"access_token": "sometoken",
   123  		"name":         "someuser/some-test-remote-state",
   124  		"address":      srv.URL,
   125  	})
   126  	if err != nil {
   127  		t.Fatalf("err: %s", err)
   128  	}
   129  
   130  	state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
   131  	if err != nil {
   132  		t.Fatalf("err: %s", err)
   133  	}
   134  
   135  	fakeAtlas.NoConflictAllowed(true)
   136  
   137  	var stateJson bytes.Buffer
   138  	if err := terraform.WriteState(state, &stateJson); err != nil {
   139  		t.Fatalf("err: %s", err)
   140  	}
   141  
   142  	if err := client.Put(stateJson.Bytes()); err != nil {
   143  		t.Fatalf("err: %s", err)
   144  	}
   145  }
   146  
   147  func TestAtlasClient_LegitimateConflict(t *testing.T) {
   148  	fakeAtlas := newFakeAtlas(t, testStateSimple)
   149  	srv := fakeAtlas.Server()
   150  	defer srv.Close()
   151  	client, err := atlasFactory(map[string]string{
   152  		"access_token": "sometoken",
   153  		"name":         "someuser/some-test-remote-state",
   154  		"address":      srv.URL,
   155  	})
   156  	if err != nil {
   157  		t.Fatalf("err: %s", err)
   158  	}
   159  
   160  	state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
   161  	if err != nil {
   162  		t.Fatalf("err: %s", err)
   163  	}
   164  
   165  	var buf bytes.Buffer
   166  	terraform.WriteState(state, &buf)
   167  
   168  	// Changing the state but not the serial. Should generate a conflict.
   169  	state.RootModule().Outputs["drift"] = &terraform.OutputState{
   170  		Type:      "string",
   171  		Sensitive: false,
   172  		Value:     "happens",
   173  	}
   174  
   175  	var stateJson bytes.Buffer
   176  	if err := terraform.WriteState(state, &stateJson); err != nil {
   177  		t.Fatalf("err: %s", err)
   178  	}
   179  	if err := client.Put(stateJson.Bytes()); err == nil {
   180  		t.Fatal("Expected error from state conflict, got none.")
   181  	}
   182  }
   183  
   184  func TestAtlasClient_UnresolvableConflict(t *testing.T) {
   185  	fakeAtlas := newFakeAtlas(t, testStateSimple)
   186  
   187  	// Something unexpected causes Atlas to conflict in a way that we can't fix.
   188  	fakeAtlas.AlwaysConflict(true)
   189  
   190  	srv := fakeAtlas.Server()
   191  	defer srv.Close()
   192  	client, err := atlasFactory(map[string]string{
   193  		"access_token": "sometoken",
   194  		"name":         "someuser/some-test-remote-state",
   195  		"address":      srv.URL,
   196  	})
   197  	if err != nil {
   198  		t.Fatalf("err: %s", err)
   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  `)