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