github.com/turtlemonvh/terraform@v0.6.9-0.20151204001754-8e40b6b855e8/state/remote/atlas.go (about)

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