github.com/ctrox/terraform@v0.11.12-beta1/state/remote/http.go (about)

     1  package remote
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"crypto/tls"
     7  	"encoding/base64"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"net/http"
    13  	"net/url"
    14  	"strconv"
    15  
    16  	"github.com/hashicorp/terraform/state"
    17  )
    18  
    19  func httpFactory(conf map[string]string) (Client, error) {
    20  	address, ok := conf["address"]
    21  	if !ok {
    22  		return nil, fmt.Errorf("missing 'address' configuration")
    23  	}
    24  
    25  	updateURL, err := url.Parse(address)
    26  	if err != nil {
    27  		return nil, fmt.Errorf("failed to parse address URL: %s", err)
    28  	}
    29  	if updateURL.Scheme != "http" && updateURL.Scheme != "https" {
    30  		return nil, fmt.Errorf("address must be HTTP or HTTPS")
    31  	}
    32  	updateMethod, ok := conf["update_method"]
    33  	if !ok {
    34  		updateMethod = "POST"
    35  	}
    36  
    37  	var lockURL *url.URL
    38  	if lockAddress, ok := conf["lock_address"]; ok {
    39  		var err error
    40  		lockURL, err = url.Parse(lockAddress)
    41  		if err != nil {
    42  			return nil, fmt.Errorf("failed to parse lockAddress URL: %s", err)
    43  		}
    44  		if lockURL.Scheme != "http" && lockURL.Scheme != "https" {
    45  			return nil, fmt.Errorf("lockAddress must be HTTP or HTTPS")
    46  		}
    47  	} else {
    48  		lockURL = nil
    49  	}
    50  	lockMethod, ok := conf["lock_method"]
    51  	if !ok {
    52  		lockMethod = "LOCK"
    53  	}
    54  
    55  	var unlockURL *url.URL
    56  	if unlockAddress, ok := conf["unlock_address"]; ok {
    57  		var err error
    58  		unlockURL, err = url.Parse(unlockAddress)
    59  		if err != nil {
    60  			return nil, fmt.Errorf("failed to parse unlockAddress URL: %s", err)
    61  		}
    62  		if unlockURL.Scheme != "http" && unlockURL.Scheme != "https" {
    63  			return nil, fmt.Errorf("unlockAddress must be HTTP or HTTPS")
    64  		}
    65  	} else {
    66  		unlockURL = nil
    67  	}
    68  	unlockMethod, ok := conf["unlock_method"]
    69  	if !ok {
    70  		unlockMethod = "UNLOCK"
    71  	}
    72  
    73  	client := &http.Client{}
    74  	if skipRaw, ok := conf["skip_cert_verification"]; ok {
    75  		skip, err := strconv.ParseBool(skipRaw)
    76  		if err != nil {
    77  			return nil, fmt.Errorf("skip_cert_verification must be boolean")
    78  		}
    79  		if skip {
    80  			// Replace the client with one that ignores TLS verification
    81  			client = &http.Client{
    82  				Transport: &http.Transport{
    83  					TLSClientConfig: &tls.Config{
    84  						InsecureSkipVerify: true,
    85  					},
    86  				},
    87  			}
    88  		}
    89  	}
    90  
    91  	ret := &HTTPClient{
    92  		URL:          updateURL,
    93  		UpdateMethod: updateMethod,
    94  
    95  		LockURL:      lockURL,
    96  		LockMethod:   lockMethod,
    97  		UnlockURL:    unlockURL,
    98  		UnlockMethod: unlockMethod,
    99  
   100  		Username: conf["username"],
   101  		Password: conf["password"],
   102  
   103  		// accessible only for testing use
   104  		Client: client,
   105  	}
   106  
   107  	return ret, nil
   108  }
   109  
   110  // HTTPClient is a remote client that stores data in Consul or HTTP REST.
   111  type HTTPClient struct {
   112  	// Update & Retrieve
   113  	URL          *url.URL
   114  	UpdateMethod string
   115  
   116  	// Locking
   117  	LockURL      *url.URL
   118  	LockMethod   string
   119  	UnlockURL    *url.URL
   120  	UnlockMethod string
   121  
   122  	// HTTP
   123  	Client   *http.Client
   124  	Username string
   125  	Password string
   126  
   127  	lockID       string
   128  	jsonLockInfo []byte
   129  }
   130  
   131  func (c *HTTPClient) httpRequest(method string, url *url.URL, data *[]byte, what string) (*http.Response, error) {
   132  	// If we have data we need a reader
   133  	var reader io.Reader = nil
   134  	if data != nil {
   135  		reader = bytes.NewReader(*data)
   136  	}
   137  
   138  	// Create the request
   139  	req, err := http.NewRequest(method, url.String(), reader)
   140  	if err != nil {
   141  		return nil, fmt.Errorf("Failed to make %s HTTP request: %s", what, err)
   142  	}
   143  	// Setup basic auth
   144  	if c.Username != "" {
   145  		req.SetBasicAuth(c.Username, c.Password)
   146  	}
   147  
   148  	// Work with data/body
   149  	if data != nil {
   150  		req.Header.Set("Content-Type", "application/json")
   151  		req.ContentLength = int64(len(*data))
   152  
   153  		// Generate the MD5
   154  		hash := md5.Sum(*data)
   155  		b64 := base64.StdEncoding.EncodeToString(hash[:])
   156  		req.Header.Set("Content-MD5", b64)
   157  	}
   158  
   159  	// Make the request
   160  	resp, err := c.Client.Do(req)
   161  	if err != nil {
   162  		return nil, fmt.Errorf("Failed to %s: %v", what, err)
   163  	}
   164  
   165  	return resp, nil
   166  }
   167  
   168  func (c *HTTPClient) Lock(info *state.LockInfo) (string, error) {
   169  	if c.LockURL == nil {
   170  		return "", nil
   171  	}
   172  	c.lockID = ""
   173  
   174  	jsonLockInfo := info.Marshal()
   175  	resp, err := c.httpRequest(c.LockMethod, c.LockURL, &jsonLockInfo, "lock")
   176  	if err != nil {
   177  		return "", err
   178  	}
   179  	defer resp.Body.Close()
   180  
   181  	switch resp.StatusCode {
   182  	case http.StatusOK:
   183  		c.lockID = info.ID
   184  		c.jsonLockInfo = jsonLockInfo
   185  		return info.ID, nil
   186  	case http.StatusUnauthorized:
   187  		return "", fmt.Errorf("HTTP remote state endpoint requires auth")
   188  	case http.StatusForbidden:
   189  		return "", fmt.Errorf("HTTP remote state endpoint invalid auth")
   190  	case http.StatusConflict, http.StatusLocked:
   191  		defer resp.Body.Close()
   192  		body, err := ioutil.ReadAll(resp.Body)
   193  		if err != nil {
   194  			return "", fmt.Errorf("HTTP remote state already locked, failed to read body")
   195  		}
   196  		existing := state.LockInfo{}
   197  		err = json.Unmarshal(body, &existing)
   198  		if err != nil {
   199  			return "", fmt.Errorf("HTTP remote state already locked, failed to unmarshal body")
   200  		}
   201  		return "", fmt.Errorf("HTTP remote state already locked: ID=%s", existing.ID)
   202  	default:
   203  		return "", fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   204  	}
   205  }
   206  
   207  func (c *HTTPClient) Unlock(id string) error {
   208  	if c.UnlockURL == nil {
   209  		return nil
   210  	}
   211  
   212  	resp, err := c.httpRequest(c.UnlockMethod, c.UnlockURL, &c.jsonLockInfo, "unlock")
   213  	if err != nil {
   214  		return err
   215  	}
   216  	defer resp.Body.Close()
   217  
   218  	switch resp.StatusCode {
   219  	case http.StatusOK:
   220  		return nil
   221  	default:
   222  		return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   223  	}
   224  }
   225  
   226  func (c *HTTPClient) Get() (*Payload, error) {
   227  	resp, err := c.httpRequest("GET", c.URL, nil, "get state")
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  	defer resp.Body.Close()
   232  
   233  	// Handle the common status codes
   234  	switch resp.StatusCode {
   235  	case http.StatusOK:
   236  		// Handled after
   237  	case http.StatusNoContent:
   238  		return nil, nil
   239  	case http.StatusNotFound:
   240  		return nil, nil
   241  	case http.StatusUnauthorized:
   242  		return nil, fmt.Errorf("HTTP remote state endpoint requires auth")
   243  	case http.StatusForbidden:
   244  		return nil, fmt.Errorf("HTTP remote state endpoint invalid auth")
   245  	case http.StatusInternalServerError:
   246  		return nil, fmt.Errorf("HTTP remote state internal server error")
   247  	default:
   248  		return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   249  	}
   250  
   251  	// Read in the body
   252  	buf := bytes.NewBuffer(nil)
   253  	if _, err := io.Copy(buf, resp.Body); err != nil {
   254  		return nil, fmt.Errorf("Failed to read remote state: %s", err)
   255  	}
   256  
   257  	// Create the payload
   258  	payload := &Payload{
   259  		Data: buf.Bytes(),
   260  	}
   261  
   262  	// If there was no data, then return nil
   263  	if len(payload.Data) == 0 {
   264  		return nil, nil
   265  	}
   266  
   267  	// Check for the MD5
   268  	if raw := resp.Header.Get("Content-MD5"); raw != "" {
   269  		md5, err := base64.StdEncoding.DecodeString(raw)
   270  		if err != nil {
   271  			return nil, fmt.Errorf(
   272  				"Failed to decode Content-MD5 '%s': %s", raw, err)
   273  		}
   274  
   275  		payload.MD5 = md5
   276  	} else {
   277  		// Generate the MD5
   278  		hash := md5.Sum(payload.Data)
   279  		payload.MD5 = hash[:]
   280  	}
   281  
   282  	return payload, nil
   283  }
   284  
   285  func (c *HTTPClient) Put(data []byte) error {
   286  	// Copy the target URL
   287  	base := *c.URL
   288  
   289  	if c.lockID != "" {
   290  		query := base.Query()
   291  		query.Set("ID", c.lockID)
   292  		base.RawQuery = query.Encode()
   293  	}
   294  
   295  	/*
   296  		// Set the force query parameter if needed
   297  		if force {
   298  			values := base.Query()
   299  			values.Set("force", "true")
   300  			base.RawQuery = values.Encode()
   301  		}
   302  	*/
   303  
   304  	var method string = "POST"
   305  	if c.UpdateMethod != "" {
   306  		method = c.UpdateMethod
   307  	}
   308  	resp, err := c.httpRequest(method, &base, &data, "upload state")
   309  	if err != nil {
   310  		return err
   311  	}
   312  	defer resp.Body.Close()
   313  
   314  	// Handle the error codes
   315  	switch resp.StatusCode {
   316  	case http.StatusOK, http.StatusCreated, http.StatusNoContent:
   317  		return nil
   318  	default:
   319  		return fmt.Errorf("HTTP error: %d", resp.StatusCode)
   320  	}
   321  }
   322  
   323  func (c *HTTPClient) Delete() error {
   324  	// Make the request
   325  	resp, err := c.httpRequest("DELETE", c.URL, nil, "delete state")
   326  	if err != nil {
   327  		return err
   328  	}
   329  	defer resp.Body.Close()
   330  
   331  	// Handle the error codes
   332  	switch resp.StatusCode {
   333  	case http.StatusOK:
   334  		return nil
   335  	default:
   336  		return fmt.Errorf("HTTP error: %d", resp.StatusCode)
   337  	}
   338  }