github.com/kwoods/terraform@v0.6.11-0.20160809170336-13497db7138e/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 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"] = &terraform.OutputState{ 116 Type: "string", 117 Sensitive: false, 118 Value: "happens", 119 } 120 121 var stateJson bytes.Buffer 122 if err := terraform.WriteState(state, &stateJson); err != nil { 123 t.Fatalf("err: %s", err) 124 } 125 if err := client.Put(stateJson.Bytes()); err == nil { 126 t.Fatal("Expected error from state conflict, got none.") 127 } 128 } 129 130 func TestAtlasClient_UnresolvableConflict(t *testing.T) { 131 fakeAtlas := newFakeAtlas(t, testStateSimple) 132 133 // Something unexpected causes Atlas to conflict in a way that we can't fix. 134 fakeAtlas.AlwaysConflict(true) 135 136 srv := fakeAtlas.Server() 137 defer srv.Close() 138 client, err := atlasFactory(map[string]string{ 139 "access_token": "sometoken", 140 "name": "someuser/some-test-remote-state", 141 "address": srv.URL, 142 }) 143 if err != nil { 144 t.Fatalf("err: %s", err) 145 } 146 147 state, err := terraform.ReadState(bytes.NewReader(testStateSimple)) 148 if err != nil { 149 t.Fatalf("err: %s", err) 150 } 151 152 var stateJson bytes.Buffer 153 if err := terraform.WriteState(state, &stateJson); err != nil { 154 t.Fatalf("err: %s", err) 155 } 156 doneCh := make(chan struct{}) 157 go func() { 158 defer close(doneCh) 159 if err := client.Put(stateJson.Bytes()); err == nil { 160 t.Fatal("Expected error from state conflict, got none.") 161 } 162 }() 163 164 select { 165 case <-doneCh: 166 // OK 167 case <-time.After(500 * time.Millisecond): 168 t.Fatalf("Timed out after 500ms, probably because retrying infinitely.") 169 } 170 } 171 172 // Stub Atlas HTTP API for a given state JSON string; does checksum-based 173 // conflict detection equivalent to Atlas's. 174 type fakeAtlas struct { 175 state []byte 176 t *testing.T 177 178 // Used to test that we only do the special conflict handling retry once. 179 alwaysConflict bool 180 181 // Used to fail the test immediately if a conflict happens. 182 noConflictAllowed bool 183 } 184 185 func newFakeAtlas(t *testing.T, state []byte) *fakeAtlas { 186 return &fakeAtlas{ 187 state: state, 188 t: t, 189 } 190 } 191 192 func (f *fakeAtlas) Server() *httptest.Server { 193 return httptest.NewServer(http.HandlerFunc(f.handler)) 194 } 195 196 func (f *fakeAtlas) CurrentState() *terraform.State { 197 currentState, err := terraform.ReadState(bytes.NewReader(f.state)) 198 if err != nil { 199 f.t.Fatalf("err: %s", err) 200 } 201 return currentState 202 } 203 204 func (f *fakeAtlas) CurrentSerial() int64 { 205 return f.CurrentState().Serial 206 } 207 208 func (f *fakeAtlas) CurrentSum() [md5.Size]byte { 209 return md5.Sum(f.state) 210 } 211 212 func (f *fakeAtlas) AlwaysConflict(b bool) { 213 f.alwaysConflict = b 214 } 215 216 func (f *fakeAtlas) NoConflictAllowed(b bool) { 217 f.noConflictAllowed = b 218 } 219 220 func (f *fakeAtlas) handler(resp http.ResponseWriter, req *http.Request) { 221 // access tokens should only be sent as a header 222 if req.FormValue("access_token") != "" { 223 http.Error(resp, "access_token in request params", http.StatusBadRequest) 224 return 225 } 226 227 if req.Header.Get(atlasTokenHeader) == "" { 228 http.Error(resp, "missing access token", http.StatusBadRequest) 229 return 230 } 231 232 switch req.Method { 233 case "GET": 234 // Respond with the current stored state. 235 resp.Header().Set("Content-Type", "application/json") 236 resp.Write(f.state) 237 case "PUT": 238 var buf bytes.Buffer 239 buf.ReadFrom(req.Body) 240 sum := md5.Sum(buf.Bytes()) 241 state, err := terraform.ReadState(&buf) 242 if err != nil { 243 f.t.Fatalf("err: %s", err) 244 } 245 conflict := f.CurrentSerial() == state.Serial && f.CurrentSum() != sum 246 conflict = conflict || f.alwaysConflict 247 if conflict { 248 if f.noConflictAllowed { 249 f.t.Fatal("Got conflict when NoConflictAllowed was set.") 250 } 251 http.Error(resp, "Conflict", 409) 252 } else { 253 f.state = buf.Bytes() 254 resp.WriteHeader(200) 255 } 256 } 257 } 258 259 // This is a tfstate file with the module order changed, which is a structural 260 // but not a semantic difference. Terraform will sort these modules as it 261 // loads the state. 262 var testStateModuleOrderChange = []byte( 263 `{ 264 "version": 3, 265 "serial": 1, 266 "modules": [ 267 { 268 "path": [ 269 "root", 270 "child2", 271 "grandchild" 272 ], 273 "outputs": { 274 "foo": { 275 "sensitive": false, 276 "type": "string", 277 "value": "bar" 278 } 279 }, 280 "resources": null 281 }, 282 { 283 "path": [ 284 "root", 285 "child1", 286 "grandchild" 287 ], 288 "outputs": { 289 "foo": { 290 "sensitive": false, 291 "type": "string", 292 "value": "bar" 293 } 294 }, 295 "resources": null 296 } 297 ] 298 } 299 `) 300 301 var testStateSimple = []byte( 302 `{ 303 "version": 3, 304 "serial": 1, 305 "modules": [ 306 { 307 "path": [ 308 "root" 309 ], 310 "outputs": { 311 "foo": { 312 "sensitive": false, 313 "type": "string", 314 "value": "bar" 315 } 316 }, 317 "resources": null 318 } 319 ] 320 } 321 `)