github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/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/hashicorp/terraform/internal/states/remote"
    16  	"github.com/hashicorp/terraform/internal/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 "", &statemgr.LockError{
   104  				Info: info,
   105  				Err:  fmt.Errorf("HTTP remote state already locked, failed to read body"),
   106  			}
   107  		}
   108  		existing := statemgr.LockInfo{}
   109  		err = json.Unmarshal(body, &existing)
   110  		if err != nil {
   111  			return "", &statemgr.LockError{
   112  				Info: info,
   113  				Err:  fmt.Errorf("HTTP remote state already locked, failed to unmarshal body"),
   114  			}
   115  		}
   116  		return "", &statemgr.LockError{
   117  			Info: info,
   118  			Err:  fmt.Errorf("HTTP remote state already locked: ID=%s", existing.ID),
   119  		}
   120  	default:
   121  		return "", fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   122  	}
   123  }
   124  
   125  func (c *httpClient) Unlock(id string) error {
   126  	if c.UnlockURL == nil {
   127  		return nil
   128  	}
   129  
   130  	resp, err := c.httpRequest(c.UnlockMethod, c.UnlockURL, &c.jsonLockInfo, "unlock")
   131  	if err != nil {
   132  		return err
   133  	}
   134  	defer resp.Body.Close()
   135  
   136  	switch resp.StatusCode {
   137  	case http.StatusOK:
   138  		return nil
   139  	default:
   140  		return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   141  	}
   142  }
   143  
   144  func (c *httpClient) Get() (*remote.Payload, error) {
   145  	resp, err := c.httpRequest("GET", c.URL, nil, "get state")
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  	defer resp.Body.Close()
   150  
   151  	// Handle the common status codes
   152  	switch resp.StatusCode {
   153  	case http.StatusOK:
   154  		// Handled after
   155  	case http.StatusNoContent:
   156  		return nil, nil
   157  	case http.StatusNotFound:
   158  		return nil, nil
   159  	case http.StatusUnauthorized:
   160  		return nil, fmt.Errorf("HTTP remote state endpoint requires auth")
   161  	case http.StatusForbidden:
   162  		return nil, fmt.Errorf("HTTP remote state endpoint invalid auth")
   163  	case http.StatusInternalServerError:
   164  		return nil, fmt.Errorf("HTTP remote state internal server error")
   165  	default:
   166  		return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
   167  	}
   168  
   169  	// Read in the body
   170  	buf := bytes.NewBuffer(nil)
   171  	if _, err := io.Copy(buf, resp.Body); err != nil {
   172  		return nil, fmt.Errorf("Failed to read remote state: %s", err)
   173  	}
   174  
   175  	// Create the payload
   176  	payload := &remote.Payload{
   177  		Data: buf.Bytes(),
   178  	}
   179  
   180  	// If there was no data, then return nil
   181  	if len(payload.Data) == 0 {
   182  		return nil, nil
   183  	}
   184  
   185  	// Check for the MD5
   186  	if raw := resp.Header.Get("Content-MD5"); raw != "" {
   187  		md5, err := base64.StdEncoding.DecodeString(raw)
   188  		if err != nil {
   189  			return nil, fmt.Errorf(
   190  				"Failed to decode Content-MD5 '%s': %s", raw, err)
   191  		}
   192  
   193  		payload.MD5 = md5
   194  	} else {
   195  		// Generate the MD5
   196  		hash := md5.Sum(payload.Data)
   197  		payload.MD5 = hash[:]
   198  	}
   199  
   200  	return payload, nil
   201  }
   202  
   203  func (c *httpClient) Put(data []byte) error {
   204  	// Copy the target URL
   205  	base := *c.URL
   206  
   207  	if c.lockID != "" {
   208  		query := base.Query()
   209  		query.Set("ID", c.lockID)
   210  		base.RawQuery = query.Encode()
   211  	}
   212  
   213  	/*
   214  		// Set the force query parameter if needed
   215  		if force {
   216  			values := base.Query()
   217  			values.Set("force", "true")
   218  			base.RawQuery = values.Encode()
   219  		}
   220  	*/
   221  
   222  	var method string = "POST"
   223  	if c.UpdateMethod != "" {
   224  		method = c.UpdateMethod
   225  	}
   226  	resp, err := c.httpRequest(method, &base, &data, "upload state")
   227  	if err != nil {
   228  		return err
   229  	}
   230  	defer resp.Body.Close()
   231  
   232  	// Handle the error codes
   233  	switch resp.StatusCode {
   234  	case http.StatusOK, http.StatusCreated, http.StatusNoContent:
   235  		return nil
   236  	default:
   237  		return fmt.Errorf("HTTP error: %d", resp.StatusCode)
   238  	}
   239  }
   240  
   241  func (c *httpClient) Delete() error {
   242  	// Make the request
   243  	resp, err := c.httpRequest("DELETE", c.URL, nil, "delete state")
   244  	if err != nil {
   245  		return err
   246  	}
   247  	defer resp.Body.Close()
   248  
   249  	// Handle the error codes
   250  	switch resp.StatusCode {
   251  	case http.StatusOK:
   252  		return nil
   253  	default:
   254  		return fmt.Errorf("HTTP error: %d", resp.StatusCode)
   255  	}
   256  }