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