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