github.com/hugorut/terraform@v1.1.3/src/backend/remote-state/http/client.go (about)

     1  package http
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"net/http"
    12  	"net/url"
    13  
    14  	"github.com/hashicorp/go-retryablehttp"
    15  	"github.com/hugorut/terraform/src/states/remote"
    16  	"github.com/hugorut/terraform/src/states/statemgr"
    17  )
    18  
    19  // httpClient is a remote client that stores data in Consul or HTTP REST.
    20  type httpClient struct {
    21  	// Update & Retrieve
    22  	URL          *url.URL
    23  	UpdateMethod string
    24  
    25  	// Locking
    26  	LockURL      *url.URL
    27  	LockMethod   string
    28  	UnlockURL    *url.URL
    29  	UnlockMethod string
    30  
    31  	// HTTP
    32  	Client   *retryablehttp.Client
    33  	Username string
    34  	Password string
    35  
    36  	lockID       string
    37  	jsonLockInfo []byte
    38  }
    39  
    40  func (c *httpClient) httpRequest(method string, url *url.URL, data *[]byte, what string) (*http.Response, error) {
    41  	// If we have data we need a reader
    42  	var reader io.Reader = nil
    43  	if data != nil {
    44  		reader = bytes.NewReader(*data)
    45  	}
    46  
    47  	// Create the request
    48  	req, err := retryablehttp.NewRequest(method, url.String(), reader)
    49  	if err != nil {
    50  		return nil, fmt.Errorf("Failed to make %s HTTP request: %s", what, err)
    51  	}
    52  	// Set up basic auth
    53  	if c.Username != "" {
    54  		req.SetBasicAuth(c.Username, c.Password)
    55  	}
    56  
    57  	// Work with data/body
    58  	if data != nil {
    59  		req.Header.Set("Content-Type", "application/json")
    60  		req.ContentLength = int64(len(*data))
    61  
    62  		// Generate the MD5
    63  		hash := md5.Sum(*data)
    64  		b64 := base64.StdEncoding.EncodeToString(hash[:])
    65  		req.Header.Set("Content-MD5", b64)
    66  	}
    67  
    68  	// Make the request
    69  	resp, err := c.Client.Do(req)
    70  	if err != nil {
    71  		return nil, fmt.Errorf("Failed to %s: %v", what, err)
    72  	}
    73  
    74  	return resp, nil
    75  }
    76  
    77  func (c *httpClient) Lock(info *statemgr.LockInfo) (string, error) {
    78  	if c.LockURL == nil {
    79  		return "", nil
    80  	}
    81  	c.lockID = ""
    82  
    83  	jsonLockInfo := info.Marshal()
    84  	resp, err := c.httpRequest(c.LockMethod, c.LockURL, &jsonLockInfo, "lock")
    85  	if err != nil {
    86  		return "", err
    87  	}
    88  	defer resp.Body.Close()
    89  
    90  	switch resp.StatusCode {
    91  	case http.StatusOK:
    92  		c.lockID = info.ID
    93  		c.jsonLockInfo = jsonLockInfo
    94  		return info.ID, nil
    95  	case http.StatusUnauthorized:
    96  		return "", fmt.Errorf("HTTP remote state endpoint requires auth")
    97  	case http.StatusForbidden:
    98  		return "", fmt.Errorf("HTTP remote state endpoint invalid auth")
    99  	case http.StatusConflict, http.StatusLocked:
   100  		defer resp.Body.Close()
   101  		body, err := ioutil.ReadAll(resp.Body)
   102  		if err != nil {
   103  			return "", fmt.Errorf("HTTP remote state already locked, failed to read body")
   104  		}
   105  		existing := statemgr.LockInfo{}
   106  		err = json.Unmarshal(body, &existing)
   107  		if err != nil {
   108  			return "", fmt.Errorf("HTTP remote state already locked, failed to unmarshal body")
   109  		}
   110  		return "", fmt.Errorf("HTTP remote state already locked: ID=%s", existing.ID)
   111  	default:
   112  		return "", fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   113  	}
   114  }
   115  
   116  func (c *httpClient) Unlock(id string) error {
   117  	if c.UnlockURL == nil {
   118  		return nil
   119  	}
   120  
   121  	resp, err := c.httpRequest(c.UnlockMethod, c.UnlockURL, &c.jsonLockInfo, "unlock")
   122  	if err != nil {
   123  		return err
   124  	}
   125  	defer resp.Body.Close()
   126  
   127  	switch resp.StatusCode {
   128  	case http.StatusOK:
   129  		return nil
   130  	default:
   131  		return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   132  	}
   133  }
   134  
   135  func (c *httpClient) Get() (*remote.Payload, error) {
   136  	resp, err := c.httpRequest("GET", c.URL, nil, "get state")
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	defer resp.Body.Close()
   141  
   142  	// Handle the common status codes
   143  	switch resp.StatusCode {
   144  	case http.StatusOK:
   145  		// Handled after
   146  	case http.StatusNoContent:
   147  		return nil, nil
   148  	case http.StatusNotFound:
   149  		return nil, nil
   150  	case http.StatusUnauthorized:
   151  		return nil, fmt.Errorf("HTTP remote state endpoint requires auth")
   152  	case http.StatusForbidden:
   153  		return nil, fmt.Errorf("HTTP remote state endpoint invalid auth")
   154  	case http.StatusInternalServerError:
   155  		return nil, fmt.Errorf("HTTP remote state src server error")
   156  	default:
   157  		return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   158  	}
   159  
   160  	// Read in the body
   161  	buf := bytes.NewBuffer(nil)
   162  	if _, err := io.Copy(buf, resp.Body); err != nil {
   163  		return nil, fmt.Errorf("Failed to read remote state: %s", err)
   164  	}
   165  
   166  	// Create the payload
   167  	payload := &remote.Payload{
   168  		Data: buf.Bytes(),
   169  	}
   170  
   171  	// If there was no data, then return nil
   172  	if len(payload.Data) == 0 {
   173  		return nil, nil
   174  	}
   175  
   176  	// Check for the MD5
   177  	if raw := resp.Header.Get("Content-MD5"); raw != "" {
   178  		md5, err := base64.StdEncoding.DecodeString(raw)
   179  		if err != nil {
   180  			return nil, fmt.Errorf(
   181  				"Failed to decode Content-MD5 '%s': %s", raw, err)
   182  		}
   183  
   184  		payload.MD5 = md5
   185  	} else {
   186  		// Generate the MD5
   187  		hash := md5.Sum(payload.Data)
   188  		payload.MD5 = hash[:]
   189  	}
   190  
   191  	return payload, nil
   192  }
   193  
   194  func (c *httpClient) Put(data []byte) error {
   195  	// Copy the target URL
   196  	base := *c.URL
   197  
   198  	if c.lockID != "" {
   199  		query := base.Query()
   200  		query.Set("ID", c.lockID)
   201  		base.RawQuery = query.Encode()
   202  	}
   203  
   204  	/*
   205  		// Set the force query parameter if needed
   206  		if force {
   207  			values := base.Query()
   208  			values.Set("force", "true")
   209  			base.RawQuery = values.Encode()
   210  		}
   211  	*/
   212  
   213  	var method string = "POST"
   214  	if c.UpdateMethod != "" {
   215  		method = c.UpdateMethod
   216  	}
   217  	resp, err := c.httpRequest(method, &base, &data, "upload state")
   218  	if err != nil {
   219  		return err
   220  	}
   221  	defer resp.Body.Close()
   222  
   223  	// Handle the error codes
   224  	switch resp.StatusCode {
   225  	case http.StatusOK, http.StatusCreated, http.StatusNoContent:
   226  		return nil
   227  	default:
   228  		return fmt.Errorf("HTTP error: %d", resp.StatusCode)
   229  	}
   230  }
   231  
   232  func (c *httpClient) Delete() error {
   233  	// Make the request
   234  	resp, err := c.httpRequest("DELETE", c.URL, nil, "delete state")
   235  	if err != nil {
   236  		return err
   237  	}
   238  	defer resp.Body.Close()
   239  
   240  	// Handle the error codes
   241  	switch resp.StatusCode {
   242  	case http.StatusOK:
   243  		return nil
   244  	default:
   245  		return fmt.Errorf("HTTP error: %d", resp.StatusCode)
   246  	}
   247  }