github.com/rjeczalik/terraform@v0.6.7-0.20160812060014-e251d5c7bd39/state/remote/atlas_test.go (about)

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