github.com/atsaki/terraform@v0.4.3-0.20150919165407-25bba5967654/state/remote/atlas.go (about)

     1  package remote
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"encoding/base64"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"path"
    13  	"strings"
    14  )
    15  
    16  const (
    17  	// defaultAtlasServer is used when no address is given
    18  	defaultAtlasServer = "https://atlas.hashicorp.com/"
    19  )
    20  
    21  func atlasFactory(conf map[string]string) (Client, error) {
    22  	var client AtlasClient
    23  
    24  	server, ok := conf["address"]
    25  	if !ok || server == "" {
    26  		server = defaultAtlasServer
    27  	}
    28  
    29  	url, err := url.Parse(server)
    30  	if err != nil {
    31  		return nil, err
    32  	}
    33  
    34  	token, ok := conf["access_token"]
    35  	if token == "" {
    36  		token = os.Getenv("ATLAS_TOKEN")
    37  		ok = true
    38  	}
    39  	if !ok || token == "" {
    40  		return nil, fmt.Errorf(
    41  			"missing 'access_token' configuration or ATLAS_TOKEN environmental variable")
    42  	}
    43  
    44  	name, ok := conf["name"]
    45  	if !ok || name == "" {
    46  		return nil, fmt.Errorf("missing 'name' configuration")
    47  	}
    48  
    49  	parts := strings.Split(name, "/")
    50  	if len(parts) != 2 {
    51  		return nil, fmt.Errorf("malformed name '%s', expected format '<account>/<name>'", name)
    52  	}
    53  
    54  	// If it exists, add the `ATLAS_RUN_ID` environment
    55  	// variable as a param, which is injected during Atlas Terraform
    56  	// runs. This is completely optional.
    57  	client.RunId = os.Getenv("ATLAS_RUN_ID")
    58  
    59  	client.Server = server
    60  	client.ServerURL = url
    61  	client.AccessToken = token
    62  	client.User = parts[0]
    63  	client.Name = parts[1]
    64  
    65  	return &client, nil
    66  }
    67  
    68  // AtlasClient implements the Client interface for an Atlas compatible server.
    69  type AtlasClient struct {
    70  	Server      string
    71  	ServerURL   *url.URL
    72  	User        string
    73  	Name        string
    74  	AccessToken string
    75  	RunId       string
    76  }
    77  
    78  func (c *AtlasClient) Get() (*Payload, error) {
    79  	// Make the HTTP request
    80  	req, err := http.NewRequest("GET", c.url().String(), nil)
    81  	if err != nil {
    82  		return nil, fmt.Errorf("Failed to make HTTP request: %v", err)
    83  	}
    84  
    85  	// Request the url
    86  	resp, err := http.DefaultClient.Do(req)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	defer resp.Body.Close()
    91  
    92  	// Handle the common status codes
    93  	switch resp.StatusCode {
    94  	case http.StatusOK:
    95  		// Handled after
    96  	case http.StatusNoContent:
    97  		return nil, nil
    98  	case http.StatusNotFound:
    99  		return nil, nil
   100  	case http.StatusUnauthorized:
   101  		return nil, fmt.Errorf("HTTP remote state endpoint requires auth")
   102  	case http.StatusForbidden:
   103  		return nil, fmt.Errorf("HTTP remote state endpoint invalid auth")
   104  	case http.StatusInternalServerError:
   105  		return nil, fmt.Errorf("HTTP remote state internal server error")
   106  	default:
   107  		return nil, fmt.Errorf(
   108  			"Unexpected HTTP response code: %d\n\nBody: %s",
   109  			resp.StatusCode, c.readBody(resp.Body))
   110  	}
   111  
   112  	// Read in the body
   113  	buf := bytes.NewBuffer(nil)
   114  	if _, err := io.Copy(buf, resp.Body); err != nil {
   115  		return nil, fmt.Errorf("Failed to read remote state: %v", err)
   116  	}
   117  
   118  	// Create the payload
   119  	payload := &Payload{
   120  		Data: buf.Bytes(),
   121  	}
   122  
   123  	if len(payload.Data) == 0 {
   124  		return nil, nil
   125  	}
   126  
   127  	// Check for the MD5
   128  	if raw := resp.Header.Get("Content-MD5"); raw != "" {
   129  		md5, err := base64.StdEncoding.DecodeString(raw)
   130  		if err != nil {
   131  			return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err)
   132  		}
   133  
   134  		payload.MD5 = md5
   135  	} else {
   136  		// Generate the MD5
   137  		hash := md5.Sum(payload.Data)
   138  		payload.MD5 = hash[:]
   139  	}
   140  
   141  	return payload, nil
   142  }
   143  
   144  func (c *AtlasClient) Put(state []byte) error {
   145  	// Get the target URL
   146  	base := c.url()
   147  
   148  	// Generate the MD5
   149  	hash := md5.Sum(state)
   150  	b64 := base64.StdEncoding.EncodeToString(hash[:])
   151  
   152  	// Make the HTTP client and request
   153  	req, err := http.NewRequest("PUT", base.String(), bytes.NewReader(state))
   154  	if err != nil {
   155  		return fmt.Errorf("Failed to make HTTP request: %v", err)
   156  	}
   157  
   158  	// Prepare the request
   159  	req.Header.Set("Content-MD5", b64)
   160  	req.Header.Set("Content-Type", "application/json")
   161  	req.ContentLength = int64(len(state))
   162  
   163  	// Make the request
   164  	resp, err := http.DefaultClient.Do(req)
   165  	if err != nil {
   166  		return fmt.Errorf("Failed to upload state: %v", err)
   167  	}
   168  	defer resp.Body.Close()
   169  
   170  	// Handle the error codes
   171  	switch resp.StatusCode {
   172  	case http.StatusOK:
   173  		return nil
   174  	default:
   175  		return fmt.Errorf(
   176  			"HTTP error: %d\n\nBody: %s",
   177  			resp.StatusCode, c.readBody(resp.Body))
   178  	}
   179  }
   180  
   181  func (c *AtlasClient) Delete() error {
   182  	// Make the HTTP request
   183  	req, err := http.NewRequest("DELETE", c.url().String(), nil)
   184  	if err != nil {
   185  		return fmt.Errorf("Failed to make HTTP request: %v", err)
   186  	}
   187  
   188  	// Make the request
   189  	resp, err := http.DefaultClient.Do(req)
   190  	if err != nil {
   191  		return fmt.Errorf("Failed to delete state: %v", err)
   192  	}
   193  	defer resp.Body.Close()
   194  
   195  	// Handle the error codes
   196  	switch resp.StatusCode {
   197  	case http.StatusOK:
   198  		return nil
   199  	case http.StatusNoContent:
   200  		return nil
   201  	case http.StatusNotFound:
   202  		return nil
   203  	default:
   204  		return fmt.Errorf(
   205  			"HTTP error: %d\n\nBody: %s",
   206  			resp.StatusCode, c.readBody(resp.Body))
   207  	}
   208  
   209  	return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   210  }
   211  
   212  func (c *AtlasClient) readBody(b io.Reader) string {
   213  	var buf bytes.Buffer
   214  	if _, err := io.Copy(&buf, b); err != nil {
   215  		return fmt.Sprintf("Error reading body: %s", err)
   216  	}
   217  
   218  	result := buf.String()
   219  	if result == "" {
   220  		result = "<empty>"
   221  	}
   222  
   223  	return result
   224  }
   225  
   226  func (c *AtlasClient) url() *url.URL {
   227  	values := url.Values{}
   228  
   229  	values.Add("atlas_run_id", c.RunId)
   230  	values.Add("access_token", c.AccessToken)
   231  
   232  	return &url.URL{
   233  		Scheme:   c.ServerURL.Scheme,
   234  		Host:     c.ServerURL.Host,
   235  		Path:     path.Join("api/v1/terraform/state", c.User, c.Name),
   236  		RawQuery: values.Encode(),
   237  	}
   238  }