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  }