github.com/jrasell/terraform@v0.6.17-0.20160523115548-2652f5232949/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 switch req.Method { 222 case "GET": 223 // Respond with the current stored state. 224 resp.Header().Set("Content-Type", "application/json") 225 resp.Write(f.state) 226 case "PUT": 227 var buf bytes.Buffer 228 buf.ReadFrom(req.Body) 229 sum := md5.Sum(buf.Bytes()) 230 state, err := terraform.ReadState(&buf) 231 if err != nil { 232 f.t.Fatalf("err: %s", err) 233 } 234 conflict := f.CurrentSerial() == state.Serial && f.CurrentSum() != sum 235 conflict = conflict || f.alwaysConflict 236 if conflict { 237 if f.noConflictAllowed { 238 f.t.Fatal("Got conflict when NoConflictAllowed was set.") 239 } 240 http.Error(resp, "Conflict", 409) 241 } else { 242 f.state = buf.Bytes() 243 resp.WriteHeader(200) 244 } 245 } 246 } 247 248 // This is a tfstate file with the module order changed, which is a structural 249 // but not a semantic difference. Terraform will sort these modules as it 250 // loads the state. 251 var testStateModuleOrderChange = []byte( 252 `{ 253 "version": 2, 254 "serial": 1, 255 "modules": [ 256 { 257 "path": [ 258 "root", 259 "child2", 260 "grandchild" 261 ], 262 "outputs": { 263 "foo": { 264 "sensitive": false, 265 "type": "string", 266 "value": "bar" 267 } 268 }, 269 "resources": null 270 }, 271 { 272 "path": [ 273 "root", 274 "child1", 275 "grandchild" 276 ], 277 "outputs": { 278 "foo": { 279 "sensitive": false, 280 "type": "string", 281 "value": "bar" 282 } 283 }, 284 "resources": null 285 } 286 ] 287 } 288 `) 289 290 var testStateSimple = []byte( 291 `{ 292 "version": 2, 293 "serial": 1, 294 "modules": [ 295 { 296 "path": [ 297 "root" 298 ], 299 "outputs": { 300 "foo": { 301 "sensitive": false, 302 "type": "string", 303 "value": "bar" 304 } 305 }, 306 "resources": null 307 } 308 ] 309 } 310 `)