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