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