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