github.com/ojiry/terraform@v0.8.2-0.20161218223921-e50cec712c4a/state/remote/atlas.go (about) 1 package remote 2 3 import ( 4 "bytes" 5 "crypto/md5" 6 "crypto/tls" 7 "crypto/x509" 8 "encoding/base64" 9 "fmt" 10 "io" 11 "log" 12 "net/http" 13 "net/url" 14 "os" 15 "path" 16 "strings" 17 18 "github.com/hashicorp/go-cleanhttp" 19 "github.com/hashicorp/go-retryablehttp" 20 "github.com/hashicorp/go-rootcerts" 21 "github.com/hashicorp/terraform/terraform" 22 ) 23 24 const ( 25 // defaultAtlasServer is used when no address is given 26 defaultAtlasServer = "https://atlas.hashicorp.com/" 27 atlasTokenHeader = "X-Atlas-Token" 28 ) 29 30 func atlasFactory(conf map[string]string) (Client, error) { 31 var client AtlasClient 32 33 server, ok := conf["address"] 34 if !ok || server == "" { 35 server = defaultAtlasServer 36 } 37 38 url, err := url.Parse(server) 39 if err != nil { 40 return nil, err 41 } 42 43 token, ok := conf["access_token"] 44 if token == "" { 45 token = os.Getenv("ATLAS_TOKEN") 46 ok = true 47 } 48 if !ok || token == "" { 49 return nil, fmt.Errorf( 50 "missing 'access_token' configuration or ATLAS_TOKEN environmental variable") 51 } 52 53 name, ok := conf["name"] 54 if !ok || name == "" { 55 return nil, fmt.Errorf("missing 'name' configuration") 56 } 57 58 parts := strings.Split(name, "/") 59 if len(parts) != 2 { 60 return nil, fmt.Errorf("malformed name '%s', expected format '<account>/<name>'", name) 61 } 62 63 // If it exists, add the `ATLAS_RUN_ID` environment 64 // variable as a param, which is injected during Atlas Terraform 65 // runs. This is completely optional. 66 client.RunId = os.Getenv("ATLAS_RUN_ID") 67 68 client.Server = server 69 client.ServerURL = url 70 client.AccessToken = token 71 client.User = parts[0] 72 client.Name = parts[1] 73 74 return &client, nil 75 } 76 77 // AtlasClient implements the Client interface for an Atlas compatible server. 78 type AtlasClient struct { 79 Server string 80 ServerURL *url.URL 81 User string 82 Name string 83 AccessToken string 84 RunId string 85 HTTPClient *retryablehttp.Client 86 87 conflictHandlingAttempted bool 88 } 89 90 func (c *AtlasClient) Get() (*Payload, error) { 91 // Make the HTTP request 92 req, err := retryablehttp.NewRequest("GET", c.url().String(), nil) 93 if err != nil { 94 return nil, fmt.Errorf("Failed to make HTTP request: %v", err) 95 } 96 97 req.Header.Set(atlasTokenHeader, c.AccessToken) 98 99 // Request the url 100 client, err := c.http() 101 if err != nil { 102 return nil, err 103 } 104 resp, err := client.Do(req) 105 if err != nil { 106 return nil, err 107 } 108 defer resp.Body.Close() 109 110 // Handle the common status codes 111 switch resp.StatusCode { 112 case http.StatusOK: 113 // Handled after 114 case http.StatusNoContent: 115 return nil, nil 116 case http.StatusNotFound: 117 return nil, nil 118 case http.StatusUnauthorized: 119 return nil, fmt.Errorf("HTTP remote state endpoint requires auth") 120 case http.StatusForbidden: 121 return nil, fmt.Errorf("HTTP remote state endpoint invalid auth") 122 case http.StatusInternalServerError: 123 return nil, fmt.Errorf("HTTP remote state internal server error") 124 default: 125 return nil, fmt.Errorf( 126 "Unexpected HTTP response code: %d\n\nBody: %s", 127 resp.StatusCode, c.readBody(resp.Body)) 128 } 129 130 // Read in the body 131 buf := bytes.NewBuffer(nil) 132 if _, err := io.Copy(buf, resp.Body); err != nil { 133 return nil, fmt.Errorf("Failed to read remote state: %v", err) 134 } 135 136 // Create the payload 137 payload := &Payload{ 138 Data: buf.Bytes(), 139 } 140 141 if len(payload.Data) == 0 { 142 return nil, nil 143 } 144 145 // Check for the MD5 146 if raw := resp.Header.Get("Content-MD5"); raw != "" { 147 md5, err := base64.StdEncoding.DecodeString(raw) 148 if err != nil { 149 return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err) 150 } 151 152 payload.MD5 = md5 153 } else { 154 // Generate the MD5 155 hash := md5.Sum(payload.Data) 156 payload.MD5 = hash[:] 157 } 158 159 return payload, nil 160 } 161 162 func (c *AtlasClient) Put(state []byte) error { 163 // Get the target URL 164 base := c.url() 165 166 // Generate the MD5 167 hash := md5.Sum(state) 168 b64 := base64.StdEncoding.EncodeToString(hash[:]) 169 170 // Make the HTTP client and request 171 req, err := retryablehttp.NewRequest("PUT", base.String(), bytes.NewReader(state)) 172 if err != nil { 173 return fmt.Errorf("Failed to make HTTP request: %v", err) 174 } 175 176 // Prepare the request 177 req.Header.Set(atlasTokenHeader, c.AccessToken) 178 req.Header.Set("Content-MD5", b64) 179 req.Header.Set("Content-Type", "application/json") 180 req.ContentLength = int64(len(state)) 181 182 // Make the request 183 client, err := c.http() 184 if err != nil { 185 return err 186 } 187 resp, err := client.Do(req) 188 if err != nil { 189 return fmt.Errorf("Failed to upload state: %v", err) 190 } 191 defer resp.Body.Close() 192 193 // Handle the error codes 194 switch resp.StatusCode { 195 case http.StatusOK: 196 return nil 197 case http.StatusConflict: 198 return c.handleConflict(c.readBody(resp.Body), state) 199 default: 200 return fmt.Errorf( 201 "HTTP error: %d\n\nBody: %s", 202 resp.StatusCode, c.readBody(resp.Body)) 203 } 204 } 205 206 func (c *AtlasClient) Delete() error { 207 // Make the HTTP request 208 req, err := retryablehttp.NewRequest("DELETE", c.url().String(), nil) 209 if err != nil { 210 return fmt.Errorf("Failed to make HTTP request: %v", err) 211 } 212 req.Header.Set(atlasTokenHeader, c.AccessToken) 213 214 // Make the request 215 client, err := c.http() 216 if err != nil { 217 return err 218 } 219 resp, err := client.Do(req) 220 if err != nil { 221 return fmt.Errorf("Failed to delete state: %v", err) 222 } 223 defer resp.Body.Close() 224 225 // Handle the error codes 226 switch resp.StatusCode { 227 case http.StatusOK: 228 return nil 229 case http.StatusNoContent: 230 return nil 231 case http.StatusNotFound: 232 return nil 233 default: 234 return fmt.Errorf( 235 "HTTP error: %d\n\nBody: %s", 236 resp.StatusCode, c.readBody(resp.Body)) 237 } 238 } 239 240 func (c *AtlasClient) readBody(b io.Reader) string { 241 var buf bytes.Buffer 242 if _, err := io.Copy(&buf, b); err != nil { 243 return fmt.Sprintf("Error reading body: %s", err) 244 } 245 246 result := buf.String() 247 if result == "" { 248 result = "<empty>" 249 } 250 251 return result 252 } 253 254 func (c *AtlasClient) url() *url.URL { 255 values := url.Values{} 256 257 values.Add("atlas_run_id", c.RunId) 258 259 return &url.URL{ 260 Scheme: c.ServerURL.Scheme, 261 Host: c.ServerURL.Host, 262 Path: path.Join("api/v1/terraform/state", c.User, c.Name), 263 RawQuery: values.Encode(), 264 } 265 } 266 267 func (c *AtlasClient) http() (*retryablehttp.Client, error) { 268 if c.HTTPClient != nil { 269 return c.HTTPClient, nil 270 } 271 tlsConfig := &tls.Config{} 272 err := rootcerts.ConfigureTLS(tlsConfig, &rootcerts.Config{ 273 CAFile: os.Getenv("ATLAS_CAFILE"), 274 CAPath: os.Getenv("ATLAS_CAPATH"), 275 }) 276 if err != nil { 277 return nil, err 278 } 279 rc := retryablehttp.NewClient() 280 281 rc.CheckRetry = func(resp *http.Response, err error) (bool, error) { 282 if err != nil { 283 // don't bother retrying if the certs don't match 284 if err, ok := err.(*url.Error); ok { 285 if _, ok := err.Err.(x509.UnknownAuthorityError); ok { 286 return false, nil 287 } 288 } 289 // continue retrying 290 return true, nil 291 } 292 return retryablehttp.DefaultRetryPolicy(resp, err) 293 } 294 295 t := cleanhttp.DefaultTransport() 296 t.TLSClientConfig = tlsConfig 297 rc.HTTPClient.Transport = t 298 299 c.HTTPClient = rc 300 return rc, nil 301 } 302 303 // Atlas returns an HTTP 409 - Conflict if the pushed state reports the same 304 // Serial number but the checksum of the raw content differs. This can 305 // sometimes happen when Terraform changes state representation internally 306 // between versions in a way that's semantically neutral but affects the JSON 307 // output and therefore the checksum. 308 // 309 // Here we detect and handle this situation by ticking the serial and retrying 310 // iff for the previous state and the proposed state: 311 // 312 // * the serials match 313 // * the parsed states are Equal (semantically equivalent) 314 // 315 // In other words, in this situation Terraform can override Atlas's detected 316 // conflict by asserting that the state it is pushing is indeed correct. 317 func (c *AtlasClient) handleConflict(msg string, state []byte) error { 318 log.Printf("[DEBUG] Handling Atlas conflict response: %s", msg) 319 320 if c.conflictHandlingAttempted { 321 log.Printf("[DEBUG] Already attempted conflict resolution; returning conflict.") 322 } else { 323 c.conflictHandlingAttempted = true 324 log.Printf("[DEBUG] Atlas reported conflict, checking for equivalent states.") 325 326 payload, err := c.Get() 327 if err != nil { 328 return conflictHandlingError(err) 329 } 330 331 currentState, err := terraform.ReadState(bytes.NewReader(payload.Data)) 332 if err != nil { 333 return conflictHandlingError(err) 334 } 335 336 proposedState, err := terraform.ReadState(bytes.NewReader(state)) 337 if err != nil { 338 return conflictHandlingError(err) 339 } 340 341 if statesAreEquivalent(currentState, proposedState) { 342 log.Printf("[DEBUG] States are equivalent, incrementing serial and retrying.") 343 proposedState.Serial++ 344 var buf bytes.Buffer 345 if err := terraform.WriteState(proposedState, &buf); err != nil { 346 return conflictHandlingError(err) 347 348 } 349 return c.Put(buf.Bytes()) 350 } else { 351 log.Printf("[DEBUG] States are not equivalent, returning conflict.") 352 } 353 } 354 355 return fmt.Errorf( 356 "Atlas detected a remote state conflict.\n\nMessage: %s", msg) 357 } 358 359 func conflictHandlingError(err error) error { 360 return fmt.Errorf( 361 "Error while handling a conflict response from Atlas: %s", err) 362 } 363 364 func statesAreEquivalent(current, proposed *terraform.State) bool { 365 return current.Serial == proposed.Serial && current.Equal(proposed) 366 }