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