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