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