github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/http/client.go (about)

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