github.com/kwoods/terraform@v0.6.11-0.20160809170336-13497db7138e/state/remote/atlas.go (about)

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