github.com/tarrant/terraform@v0.3.8-0.20150402012457-f68c9eee638e/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'", name)
    52  	}
    53  
    54  	client.Server = server
    55  	client.ServerURL = url
    56  	client.AccessToken = token
    57  	client.User = parts[0]
    58  	client.Name = parts[1]
    59  
    60  	return &client, nil
    61  }
    62  
    63  // AtlasClient implements the Client interface for an Atlas compatible server.
    64  type AtlasClient struct {
    65  	Server      string
    66  	ServerURL   *url.URL
    67  	User        string
    68  	Name        string
    69  	AccessToken string
    70  }
    71  
    72  func (c *AtlasClient) Get() (*Payload, error) {
    73  	// Make the HTTP request
    74  	req, err := http.NewRequest("GET", c.url().String(), nil)
    75  	if err != nil {
    76  		return nil, fmt.Errorf("Failed to make HTTP request: %v", err)
    77  	}
    78  
    79  	// Request the url
    80  	resp, err := http.DefaultClient.Do(req)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	defer resp.Body.Close()
    85  
    86  	// Handle the common status codes
    87  	switch resp.StatusCode {
    88  	case http.StatusOK:
    89  		// Handled after
    90  	case http.StatusNoContent:
    91  		return nil, nil
    92  	case http.StatusNotFound:
    93  		return nil, nil
    94  	case http.StatusUnauthorized:
    95  		return nil, fmt.Errorf("HTTP remote state endpoint requires auth")
    96  	case http.StatusForbidden:
    97  		return nil, fmt.Errorf("HTTP remote state endpoint invalid auth")
    98  	case http.StatusInternalServerError:
    99  		return nil, fmt.Errorf("HTTP remote state internal server error")
   100  	default:
   101  		return nil, fmt.Errorf(
   102  			"Unexpected HTTP response code: %d\n\nBody: %s",
   103  			resp.StatusCode, c.readBody(resp.Body))
   104  	}
   105  
   106  	// Read in the body
   107  	buf := bytes.NewBuffer(nil)
   108  	if _, err := io.Copy(buf, resp.Body); err != nil {
   109  		return nil, fmt.Errorf("Failed to read remote state: %v", err)
   110  	}
   111  
   112  	// Create the payload
   113  	payload := &Payload{
   114  		Data: buf.Bytes(),
   115  	}
   116  
   117  	if len(payload.Data) == 0 {
   118  		return nil, nil
   119  	}
   120  
   121  	// Check for the MD5
   122  	if raw := resp.Header.Get("Content-MD5"); raw != "" {
   123  		md5, err := base64.StdEncoding.DecodeString(raw)
   124  		if err != nil {
   125  			return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err)
   126  		}
   127  
   128  		payload.MD5 = md5
   129  	} else {
   130  		// Generate the MD5
   131  		hash := md5.Sum(payload.Data)
   132  		payload.MD5 = hash[:]
   133  	}
   134  
   135  	return payload, nil
   136  }
   137  
   138  func (c *AtlasClient) Put(state []byte) error {
   139  	// Get the target URL
   140  	base := c.url()
   141  
   142  	// Generate the MD5
   143  	hash := md5.Sum(state)
   144  	b64 := base64.StdEncoding.EncodeToString(hash[:])
   145  
   146  	/*
   147  		// Set the force query parameter if needed
   148  		if force {
   149  			values := base.Query()
   150  			values.Set("force", "true")
   151  			base.RawQuery = values.Encode()
   152  		}
   153  	*/
   154  
   155  	// Make the HTTP client and request
   156  	req, err := http.NewRequest("PUT", base.String(), bytes.NewReader(state))
   157  	if err != nil {
   158  		return fmt.Errorf("Failed to make HTTP request: %v", err)
   159  	}
   160  
   161  	// Prepare the request
   162  	req.Header.Set("Content-MD5", b64)
   163  	req.Header.Set("Content-Type", "application/json")
   164  	req.ContentLength = int64(len(state))
   165  
   166  	// Make the request
   167  	resp, err := http.DefaultClient.Do(req)
   168  	if err != nil {
   169  		return fmt.Errorf("Failed to upload state: %v", err)
   170  	}
   171  	defer resp.Body.Close()
   172  
   173  	// Handle the error codes
   174  	switch resp.StatusCode {
   175  	case http.StatusOK:
   176  		return nil
   177  	default:
   178  		return fmt.Errorf(
   179  			"HTTP error: %d\n\nBody: %s",
   180  			resp.StatusCode, c.readBody(resp.Body))
   181  	}
   182  }
   183  
   184  func (c *AtlasClient) Delete() error {
   185  	// Make the HTTP request
   186  	req, err := http.NewRequest("DELETE", c.url().String(), nil)
   187  	if err != nil {
   188  		return fmt.Errorf("Failed to make HTTP request: %v", err)
   189  	}
   190  
   191  	// Make the request
   192  	resp, err := http.DefaultClient.Do(req)
   193  	if err != nil {
   194  		return fmt.Errorf("Failed to delete state: %v", err)
   195  	}
   196  	defer resp.Body.Close()
   197  
   198  	// Handle the error codes
   199  	switch resp.StatusCode {
   200  	case http.StatusOK:
   201  		return nil
   202  	case http.StatusNoContent:
   203  		return nil
   204  	case http.StatusNotFound:
   205  		return nil
   206  	default:
   207  		return fmt.Errorf(
   208  			"HTTP error: %d\n\nBody: %s",
   209  			resp.StatusCode, c.readBody(resp.Body))
   210  	}
   211  
   212  	return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   213  }
   214  
   215  func (c *AtlasClient) readBody(b io.Reader) string {
   216  	var buf bytes.Buffer
   217  	if _, err := io.Copy(&buf, b); err != nil {
   218  		return fmt.Sprintf("Error reading body: %s", err)
   219  	}
   220  
   221  	result := buf.String()
   222  	if result == "" {
   223  		result = "<empty>"
   224  	}
   225  
   226  	return result
   227  }
   228  
   229  func (c *AtlasClient) url() *url.URL {
   230  	return &url.URL{
   231  		Scheme:   c.ServerURL.Scheme,
   232  		Host:     c.ServerURL.Host,
   233  		Path:     path.Join("api/v1/terraform/state", c.User, c.Name),
   234  		RawQuery: fmt.Sprintf("access_token=%s", c.AccessToken),
   235  	}
   236  }