github.com/ojiry/terraform@v0.8.2-0.20161218223921-e50cec712c4a/state/remote/atlas.go (about)

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