github.com/erriapo/terraform@v0.6.12-0.20160203182612-0340ea72354f/state/remote/atlas_test.go (about)

     1  package remote
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"os"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/hashicorp/terraform/helper/acctest"
    13  	"github.com/hashicorp/terraform/terraform"
    14  )
    15  
    16  func TestAtlasClient_impl(t *testing.T) {
    17  	var _ Client = new(AtlasClient)
    18  }
    19  
    20  func TestAtlasClient(t *testing.T) {
    21  	acctest.RemoteTestPrecheck(t)
    22  
    23  	token := os.Getenv("ATLAS_TOKEN")
    24  	if token == "" {
    25  		t.Skipf("skipping, ATLAS_TOKEN must be set")
    26  	}
    27  
    28  	client, err := atlasFactory(map[string]string{
    29  		"access_token": token,
    30  		"name":         "hashicorp/test-remote-state",
    31  	})
    32  	if err != nil {
    33  		t.Fatalf("bad: %s", err)
    34  	}
    35  
    36  	testClient(t, client)
    37  }
    38  
    39  func TestAtlasClient_ReportedConflictEqualStates(t *testing.T) {
    40  	fakeAtlas := newFakeAtlas(t, testStateModuleOrderChange)
    41  	srv := fakeAtlas.Server()
    42  	defer srv.Close()
    43  	client, err := atlasFactory(map[string]string{
    44  		"access_token": "sometoken",
    45  		"name":         "someuser/some-test-remote-state",
    46  		"address":      srv.URL,
    47  	})
    48  	if err != nil {
    49  		t.Fatalf("err: %s", err)
    50  	}
    51  
    52  	state, err := terraform.ReadState(bytes.NewReader(testStateModuleOrderChange))
    53  	if err != nil {
    54  		t.Fatalf("err: %s", err)
    55  	}
    56  
    57  	var stateJson bytes.Buffer
    58  	if err := terraform.WriteState(state, &stateJson); err != nil {
    59  		t.Fatalf("err: %s", err)
    60  	}
    61  	if err := client.Put(stateJson.Bytes()); err != nil {
    62  		t.Fatalf("err: %s", err)
    63  	}
    64  }
    65  
    66  func TestAtlasClient_NoConflict(t *testing.T) {
    67  	fakeAtlas := newFakeAtlas(t, testStateSimple)
    68  	srv := fakeAtlas.Server()
    69  	defer srv.Close()
    70  	client, err := atlasFactory(map[string]string{
    71  		"access_token": "sometoken",
    72  		"name":         "someuser/some-test-remote-state",
    73  		"address":      srv.URL,
    74  	})
    75  	if err != nil {
    76  		t.Fatalf("err: %s", err)
    77  	}
    78  
    79  	state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
    80  	if err != nil {
    81  		t.Fatalf("err: %s", err)
    82  	}
    83  
    84  	fakeAtlas.NoConflictAllowed(true)
    85  
    86  	var stateJson bytes.Buffer
    87  	if err := terraform.WriteState(state, &stateJson); err != nil {
    88  		t.Fatalf("err: %s", err)
    89  	}
    90  	if err := client.Put(stateJson.Bytes()); err != nil {
    91  		t.Fatalf("err: %s", err)
    92  	}
    93  }
    94  
    95  func TestAtlasClient_LegitimateConflict(t *testing.T) {
    96  	fakeAtlas := newFakeAtlas(t, testStateSimple)
    97  	srv := fakeAtlas.Server()
    98  	defer srv.Close()
    99  	client, err := atlasFactory(map[string]string{
   100  		"access_token": "sometoken",
   101  		"name":         "someuser/some-test-remote-state",
   102  		"address":      srv.URL,
   103  	})
   104  	if err != nil {
   105  		t.Fatalf("err: %s", err)
   106  	}
   107  
   108  	state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
   109  	if err != nil {
   110  		t.Fatalf("err: %s", err)
   111  	}
   112  
   113  	// Changing the state but not the serial. Should generate a conflict.
   114  	state.RootModule().Outputs["drift"] = "happens"
   115  
   116  	var stateJson bytes.Buffer
   117  	if err := terraform.WriteState(state, &stateJson); err != nil {
   118  		t.Fatalf("err: %s", err)
   119  	}
   120  	if err := client.Put(stateJson.Bytes()); err == nil {
   121  		t.Fatal("Expected error from state conflict, got none.")
   122  	}
   123  }
   124  
   125  func TestAtlasClient_UnresolvableConflict(t *testing.T) {
   126  	fakeAtlas := newFakeAtlas(t, testStateSimple)
   127  
   128  	// Something unexpected causes Atlas to conflict in a way that we can't fix.
   129  	fakeAtlas.AlwaysConflict(true)
   130  
   131  	srv := fakeAtlas.Server()
   132  	defer srv.Close()
   133  	client, err := atlasFactory(map[string]string{
   134  		"access_token": "sometoken",
   135  		"name":         "someuser/some-test-remote-state",
   136  		"address":      srv.URL,
   137  	})
   138  	if err != nil {
   139  		t.Fatalf("err: %s", err)
   140  	}
   141  
   142  	state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
   143  	if err != nil {
   144  		t.Fatalf("err: %s", err)
   145  	}
   146  
   147  	var stateJson bytes.Buffer
   148  	if err := terraform.WriteState(state, &stateJson); err != nil {
   149  		t.Fatalf("err: %s", err)
   150  	}
   151  	doneCh := make(chan struct{})
   152  	go func() {
   153  		defer close(doneCh)
   154  		if err := client.Put(stateJson.Bytes()); err == nil {
   155  			t.Fatal("Expected error from state conflict, got none.")
   156  		}
   157  	}()
   158  
   159  	select {
   160  	case <-doneCh:
   161  		// OK
   162  	case <-time.After(50 * time.Millisecond):
   163  		t.Fatalf("Timed out after 50ms, probably because retrying infinitely.")
   164  	}
   165  }
   166  
   167  // Stub Atlas HTTP API for a given state JSON string; does checksum-based
   168  // conflict detection equivalent to Atlas's.
   169  type fakeAtlas struct {
   170  	state []byte
   171  	t     *testing.T
   172  
   173  	// Used to test that we only do the special conflict handling retry once.
   174  	alwaysConflict bool
   175  
   176  	// Used to fail the test immediately if a conflict happens.
   177  	noConflictAllowed bool
   178  }
   179  
   180  func newFakeAtlas(t *testing.T, state []byte) *fakeAtlas {
   181  	return &fakeAtlas{
   182  		state: state,
   183  		t:     t,
   184  	}
   185  }
   186  
   187  func (f *fakeAtlas) Server() *httptest.Server {
   188  	return httptest.NewServer(http.HandlerFunc(f.handler))
   189  }
   190  
   191  func (f *fakeAtlas) CurrentState() *terraform.State {
   192  	currentState, err := terraform.ReadState(bytes.NewReader(f.state))
   193  	if err != nil {
   194  		f.t.Fatalf("err: %s", err)
   195  	}
   196  	return currentState
   197  }
   198  
   199  func (f *fakeAtlas) CurrentSerial() int64 {
   200  	return f.CurrentState().Serial
   201  }
   202  
   203  func (f *fakeAtlas) CurrentSum() [md5.Size]byte {
   204  	return md5.Sum(f.state)
   205  }
   206  
   207  func (f *fakeAtlas) AlwaysConflict(b bool) {
   208  	f.alwaysConflict = b
   209  }
   210  
   211  func (f *fakeAtlas) NoConflictAllowed(b bool) {
   212  	f.noConflictAllowed = b
   213  }
   214  
   215  func (f *fakeAtlas) handler(resp http.ResponseWriter, req *http.Request) {
   216  	switch req.Method {
   217  	case "GET":
   218  		// Respond with the current stored state.
   219  		resp.Header().Set("Content-Type", "application/json")
   220  		resp.Write(f.state)
   221  	case "PUT":
   222  		var buf bytes.Buffer
   223  		buf.ReadFrom(req.Body)
   224  		sum := md5.Sum(buf.Bytes())
   225  		state, err := terraform.ReadState(&buf)
   226  		if err != nil {
   227  			f.t.Fatalf("err: %s", err)
   228  		}
   229  		conflict := f.CurrentSerial() == state.Serial && f.CurrentSum() != sum
   230  		conflict = conflict || f.alwaysConflict
   231  		if conflict {
   232  			if f.noConflictAllowed {
   233  				f.t.Fatal("Got conflict when NoConflictAllowed was set.")
   234  			}
   235  			http.Error(resp, "Conflict", 409)
   236  		} else {
   237  			f.state = buf.Bytes()
   238  			resp.WriteHeader(200)
   239  		}
   240  	}
   241  }
   242  
   243  // This is a tfstate file with the module order changed, which is a structural
   244  // but not a semantic difference. Terraform will sort these modules as it
   245  // loads the state.
   246  var testStateModuleOrderChange = []byte(
   247  	`{
   248      "version": 1,
   249      "serial": 1,
   250      "modules": [
   251          {
   252              "path": [
   253                  "root",
   254                  "child2",
   255                  "grandchild"
   256              ],
   257              "outputs": {
   258                  "foo": "bar2"
   259              },
   260              "resources": null
   261          },
   262          {
   263              "path": [
   264                  "root",
   265                  "child1",
   266                  "grandchild"
   267              ],
   268              "outputs": {
   269                  "foo": "bar1"
   270              },
   271              "resources": null
   272          }
   273      ]
   274  }
   275  `)
   276  
   277  var testStateSimple = []byte(
   278  	`{
   279      "version": 1,
   280      "serial": 1,
   281      "modules": [
   282          {
   283              "path": [
   284                  "root"
   285              ],
   286              "outputs": {
   287                  "foo": "bar"
   288              },
   289              "resources": null
   290          }
   291      ]
   292  }
   293  `)