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