github.com/aznashwan/terraform@v0.4.3-0.20151118032030-21f93ca4558d/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-cleanhttp" 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 *http.Client 81 82 conflictHandlingAttempted bool 83 } 84 85 func (c *AtlasClient) Get() (*Payload, error) { 86 // Make the HTTP request 87 req, err := http.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 := http.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 := http.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 return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode) 222 } 223 224 func (c *AtlasClient) readBody(b io.Reader) string { 225 var buf bytes.Buffer 226 if _, err := io.Copy(&buf, b); err != nil { 227 return fmt.Sprintf("Error reading body: %s", err) 228 } 229 230 result := buf.String() 231 if result == "" { 232 result = "<empty>" 233 } 234 235 return result 236 } 237 238 func (c *AtlasClient) url() *url.URL { 239 values := url.Values{} 240 241 values.Add("atlas_run_id", c.RunId) 242 values.Add("access_token", c.AccessToken) 243 244 return &url.URL{ 245 Scheme: c.ServerURL.Scheme, 246 Host: c.ServerURL.Host, 247 Path: path.Join("api/v1/terraform/state", c.User, c.Name), 248 RawQuery: values.Encode(), 249 } 250 } 251 252 func (c *AtlasClient) http() *http.Client { 253 if c.HTTPClient != nil { 254 return c.HTTPClient 255 } 256 return cleanhttp.DefaultClient() 257 } 258 259 // Atlas returns an HTTP 409 - Conflict if the pushed state reports the same 260 // Serial number but the checksum of the raw content differs. This can 261 // sometimes happen when Terraform changes state representation internally 262 // between versions in a way that's semantically neutral but affects the JSON 263 // output and therefore the checksum. 264 // 265 // Here we detect and handle this situation by ticking the serial and retrying 266 // iff for the previous state and the proposed state: 267 // 268 // * the serials match 269 // * the parsed states are Equal (semantically equivalent) 270 // 271 // In other words, in this situation Terraform can override Atlas's detected 272 // conflict by asserting that the state it is pushing is indeed correct. 273 func (c *AtlasClient) handleConflict(msg string, state []byte) error { 274 log.Printf("[DEBUG] Handling Atlas conflict response: %s", msg) 275 276 if c.conflictHandlingAttempted { 277 log.Printf("[DEBUG] Already attempted conflict resolution; returning conflict.") 278 } else { 279 c.conflictHandlingAttempted = true 280 log.Printf("[DEBUG] Atlas reported conflict, checking for equivalent states.") 281 282 payload, err := c.Get() 283 if err != nil { 284 return conflictHandlingError(err) 285 } 286 287 currentState, err := terraform.ReadState(bytes.NewReader(payload.Data)) 288 if err != nil { 289 return conflictHandlingError(err) 290 } 291 292 proposedState, err := terraform.ReadState(bytes.NewReader(state)) 293 if err != nil { 294 return conflictHandlingError(err) 295 } 296 297 if statesAreEquivalent(currentState, proposedState) { 298 log.Printf("[DEBUG] States are equivalent, incrementing serial and retrying.") 299 proposedState.Serial++ 300 var buf bytes.Buffer 301 if err := terraform.WriteState(proposedState, &buf); err != nil { 302 return conflictHandlingError(err) 303 } 304 return c.Put(buf.Bytes()) 305 } else { 306 log.Printf("[DEBUG] States are not equivalent, returning conflict.") 307 } 308 } 309 310 return fmt.Errorf( 311 "Atlas detected a remote state conflict.\n\nMessage: %s", msg) 312 } 313 314 func conflictHandlingError(err error) error { 315 return fmt.Errorf( 316 "Error while handling a conflict response from Atlas: %s", err) 317 } 318 319 func statesAreEquivalent(current, proposed *terraform.State) bool { 320 return current.Serial == proposed.Serial && current.Equal(proposed) 321 }