github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/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  }