github.com/chalford/terraform@v0.3.7-0.20150113080010-a78c69a8c81f/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  // AtlasRemoteClient implements the RemoteClient interface
    22  // for an Atlas compatible server.
    23  type AtlasRemoteClient struct {
    24  	server      string
    25  	serverURL   *url.URL
    26  	user        string
    27  	name        string
    28  	accessToken string
    29  }
    30  
    31  func NewAtlasRemoteClient(conf map[string]string) (*AtlasRemoteClient, error) {
    32  	client := &AtlasRemoteClient{}
    33  	if err := client.validateConfig(conf); err != nil {
    34  		return nil, err
    35  	}
    36  	return client, nil
    37  }
    38  
    39  func (c *AtlasRemoteClient) validateConfig(conf map[string]string) error {
    40  	server, ok := conf["address"]
    41  	if !ok || server == "" {
    42  		server = defaultAtlasServer
    43  	}
    44  	url, err := url.Parse(server)
    45  	if err != nil {
    46  		return err
    47  	}
    48  	c.server = server
    49  	c.serverURL = url
    50  
    51  	token, ok := conf["access_token"]
    52  	if token == "" {
    53  		token = os.Getenv("ATLAS_TOKEN")
    54  		ok = true
    55  	}
    56  	if !ok || token == "" {
    57  		return fmt.Errorf(
    58  			"missing 'access_token' configuration or ATLAS_TOKEN environmental variable")
    59  	}
    60  	c.accessToken = token
    61  
    62  	name, ok := conf["name"]
    63  	if !ok || name == "" {
    64  		return fmt.Errorf("missing 'name' configuration")
    65  	}
    66  
    67  	parts := strings.Split(name, "/")
    68  	if len(parts) != 2 {
    69  		return fmt.Errorf("malformed name '%s'", name)
    70  	}
    71  	c.user = parts[0]
    72  	c.name = parts[1]
    73  	return nil
    74  }
    75  
    76  func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) {
    77  	// Make the HTTP request
    78  	req, err := http.NewRequest("GET", c.url().String(), nil)
    79  	if err != nil {
    80  		return nil, fmt.Errorf("Failed to make HTTP request: %v", err)
    81  	}
    82  
    83  	// Request the url
    84  	resp, err := http.DefaultClient.Do(req)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	defer resp.Body.Close()
    89  
    90  	// Handle the common status codes
    91  	switch resp.StatusCode {
    92  	case http.StatusOK:
    93  		// Handled after
    94  	case http.StatusNoContent:
    95  		return nil, nil
    96  	case http.StatusNotFound:
    97  		return nil, nil
    98  	case http.StatusUnauthorized:
    99  		return nil, ErrRequireAuth
   100  	case http.StatusForbidden:
   101  		return nil, ErrInvalidAuth
   102  	case http.StatusInternalServerError:
   103  		return nil, ErrRemoteInternal
   104  	default:
   105  		return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   106  	}
   107  
   108  	// Read in the body
   109  	buf := bytes.NewBuffer(nil)
   110  	if _, err := io.Copy(buf, resp.Body); err != nil {
   111  		return nil, fmt.Errorf("Failed to read remote state: %v", err)
   112  	}
   113  
   114  	// Create the payload
   115  	payload := &RemoteStatePayload{
   116  		State: buf.Bytes(),
   117  	}
   118  
   119  	// Check for the MD5
   120  	if raw := resp.Header.Get("Content-MD5"); raw != "" {
   121  		md5, err := base64.StdEncoding.DecodeString(raw)
   122  		if err != nil {
   123  			return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err)
   124  		}
   125  		payload.MD5 = md5
   126  
   127  	} else {
   128  		// Generate the MD5
   129  		hash := md5.Sum(payload.State)
   130  		payload.MD5 = hash[:md5.Size]
   131  	}
   132  
   133  	return payload, nil
   134  }
   135  
   136  func (c *AtlasRemoteClient) PutState(state []byte, force bool) error {
   137  	// Get the target URL
   138  	base := c.url()
   139  
   140  	// Generate the MD5
   141  	hash := md5.Sum(state)
   142  	b64 := base64.StdEncoding.EncodeToString(hash[:md5.Size])
   143  
   144  	// Set the force query parameter if needed
   145  	if force {
   146  		values := base.Query()
   147  		values.Set("force", "true")
   148  		base.RawQuery = values.Encode()
   149  	}
   150  
   151  	// Make the HTTP client and request
   152  	req, err := http.NewRequest("PUT", base.String(), bytes.NewReader(state))
   153  	if err != nil {
   154  		return fmt.Errorf("Failed to make HTTP request: %v", err)
   155  	}
   156  
   157  	// Prepare the request
   158  	req.Header.Set("Content-MD5", b64)
   159  	req.Header.Set("Content-Type", "application/json")
   160  	req.ContentLength = int64(len(state))
   161  
   162  	// Make the request
   163  	resp, err := http.DefaultClient.Do(req)
   164  	if err != nil {
   165  		return fmt.Errorf("Failed to upload state: %v", err)
   166  	}
   167  	defer resp.Body.Close()
   168  
   169  	// Handle the error codes
   170  	switch resp.StatusCode {
   171  	case http.StatusOK:
   172  		return nil
   173  	case http.StatusConflict:
   174  		return ErrConflict
   175  	case http.StatusPreconditionFailed:
   176  		return ErrServerNewer
   177  	case http.StatusUnauthorized:
   178  		return ErrRequireAuth
   179  	case http.StatusForbidden:
   180  		return ErrInvalidAuth
   181  	case http.StatusInternalServerError:
   182  		return ErrRemoteInternal
   183  	default:
   184  		return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   185  	}
   186  }
   187  
   188  func (c *AtlasRemoteClient) DeleteState() error {
   189  	// Make the HTTP request
   190  	req, err := http.NewRequest("DELETE", c.url().String(), nil)
   191  	if err != nil {
   192  		return fmt.Errorf("Failed to make HTTP request: %v", err)
   193  	}
   194  
   195  	// Make the request
   196  	resp, err := http.DefaultClient.Do(req)
   197  	if err != nil {
   198  		return fmt.Errorf("Failed to delete state: %v", err)
   199  	}
   200  	defer resp.Body.Close()
   201  
   202  	// Handle the error codes
   203  	switch resp.StatusCode {
   204  	case http.StatusOK:
   205  		return nil
   206  	case http.StatusNoContent:
   207  		return nil
   208  	case http.StatusNotFound:
   209  		return nil
   210  	case http.StatusUnauthorized:
   211  		return ErrRequireAuth
   212  	case http.StatusForbidden:
   213  		return ErrInvalidAuth
   214  	case http.StatusInternalServerError:
   215  		return ErrRemoteInternal
   216  	default:
   217  		return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   218  	}
   219  	return nil
   220  }
   221  
   222  func (c *AtlasRemoteClient) url() *url.URL {
   223  	return &url.URL{
   224  		Scheme:   c.serverURL.Scheme,
   225  		Host:     c.serverURL.Host,
   226  		Path:     path.Join("api/v1/terraform/state", c.user, c.name),
   227  		RawQuery: fmt.Sprintf("access_token=%s", c.accessToken),
   228  	}
   229  }