github.com/jdextraze/terraform@v0.6.17-0.20160511153921-e33847c8a8af/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(500 * 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": 2, 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": 2, 280 "serial": 1, 281 "modules": [ 282 { 283 "path": [ 284 "root" 285 ], 286 "outputs": { 287 "foo": "bar" 288 }, 289 "resources": null 290 } 291 ] 292 } 293 `)