github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/http/backend.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package http
     5  
     6  import (
     7  	"context"
     8  	"crypto/tls"
     9  	"crypto/x509"
    10  	"errors"
    11  	"fmt"
    12  	"log"
    13  	"net/http"
    14  	"net/url"
    15  	"time"
    16  
    17  	"github.com/hashicorp/go-retryablehttp"
    18  
    19  	"github.com/terramate-io/tf/backend"
    20  	"github.com/terramate-io/tf/legacy/helper/schema"
    21  	"github.com/terramate-io/tf/logging"
    22  	"github.com/terramate-io/tf/states/remote"
    23  	"github.com/terramate-io/tf/states/statemgr"
    24  )
    25  
    26  func New() backend.Backend {
    27  	s := &schema.Backend{
    28  		Schema: map[string]*schema.Schema{
    29  			"address": &schema.Schema{
    30  				Type:        schema.TypeString,
    31  				Required:    true,
    32  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_ADDRESS", nil),
    33  				Description: "The address of the REST endpoint",
    34  			},
    35  			"update_method": &schema.Schema{
    36  				Type:        schema.TypeString,
    37  				Optional:    true,
    38  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UPDATE_METHOD", "POST"),
    39  				Description: "HTTP method to use when updating state",
    40  			},
    41  			"lock_address": &schema.Schema{
    42  				Type:        schema.TypeString,
    43  				Optional:    true,
    44  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_LOCK_ADDRESS", nil),
    45  				Description: "The address of the lock REST endpoint",
    46  			},
    47  			"unlock_address": &schema.Schema{
    48  				Type:        schema.TypeString,
    49  				Optional:    true,
    50  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_ADDRESS", nil),
    51  				Description: "The address of the unlock REST endpoint",
    52  			},
    53  			"lock_method": &schema.Schema{
    54  				Type:        schema.TypeString,
    55  				Optional:    true,
    56  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_LOCK_METHOD", "LOCK"),
    57  				Description: "The HTTP method to use when locking",
    58  			},
    59  			"unlock_method": &schema.Schema{
    60  				Type:        schema.TypeString,
    61  				Optional:    true,
    62  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_METHOD", "UNLOCK"),
    63  				Description: "The HTTP method to use when unlocking",
    64  			},
    65  			"username": &schema.Schema{
    66  				Type:        schema.TypeString,
    67  				Optional:    true,
    68  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_USERNAME", nil),
    69  				Description: "The username for HTTP basic authentication",
    70  			},
    71  			"password": &schema.Schema{
    72  				Type:        schema.TypeString,
    73  				Optional:    true,
    74  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_PASSWORD", nil),
    75  				Description: "The password for HTTP basic authentication",
    76  			},
    77  			"skip_cert_verification": &schema.Schema{
    78  				Type:        schema.TypeBool,
    79  				Optional:    true,
    80  				Default:     false,
    81  				Description: "Whether to skip TLS verification.",
    82  			},
    83  			"retry_max": &schema.Schema{
    84  				Type:        schema.TypeInt,
    85  				Optional:    true,
    86  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_MAX", 2),
    87  				Description: "The number of HTTP request retries.",
    88  			},
    89  			"retry_wait_min": &schema.Schema{
    90  				Type:        schema.TypeInt,
    91  				Optional:    true,
    92  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_WAIT_MIN", 1),
    93  				Description: "The minimum time in seconds to wait between HTTP request attempts.",
    94  			},
    95  			"retry_wait_max": &schema.Schema{
    96  				Type:        schema.TypeInt,
    97  				Optional:    true,
    98  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_WAIT_MAX", 30),
    99  				Description: "The maximum time in seconds to wait between HTTP request attempts.",
   100  			},
   101  			"client_ca_certificate_pem": &schema.Schema{
   102  				Type:        schema.TypeString,
   103  				Optional:    true,
   104  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_CA_CERTIFICATE_PEM", ""),
   105  				Description: "A PEM-encoded CA certificate chain used by the client to verify server certificates during TLS authentication.",
   106  			},
   107  			"client_certificate_pem": &schema.Schema{
   108  				Type:        schema.TypeString,
   109  				Optional:    true,
   110  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_CERTIFICATE_PEM", ""),
   111  				Description: "A PEM-encoded certificate used by the server to verify the client during mutual TLS (mTLS) authentication.",
   112  			},
   113  			"client_private_key_pem": &schema.Schema{
   114  				Type:        schema.TypeString,
   115  				Optional:    true,
   116  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_PRIVATE_KEY_PEM", ""),
   117  				Description: "A PEM-encoded private key, required if client_certificate_pem is specified.",
   118  			},
   119  		},
   120  	}
   121  
   122  	b := &Backend{Backend: s}
   123  	b.Backend.ConfigureFunc = b.configure
   124  	return b
   125  }
   126  
   127  type Backend struct {
   128  	*schema.Backend
   129  
   130  	client *httpClient
   131  }
   132  
   133  // configureTLS configures TLS when needed; if there are no conditions requiring TLS, no change is made.
   134  func (b *Backend) configureTLS(client *retryablehttp.Client, data *schema.ResourceData) error {
   135  	// If there are no conditions needing to configure TLS, leave the client untouched
   136  	skipCertVerification := data.Get("skip_cert_verification").(bool)
   137  	clientCACertificatePem := data.Get("client_ca_certificate_pem").(string)
   138  	clientCertificatePem := data.Get("client_certificate_pem").(string)
   139  	clientPrivateKeyPem := data.Get("client_private_key_pem").(string)
   140  	if !skipCertVerification && clientCACertificatePem == "" && clientCertificatePem == "" && clientPrivateKeyPem == "" {
   141  		return nil
   142  	}
   143  	if clientCertificatePem != "" && clientPrivateKeyPem == "" {
   144  		return fmt.Errorf("client_certificate_pem is set but client_private_key_pem is not")
   145  	}
   146  	if clientPrivateKeyPem != "" && clientCertificatePem == "" {
   147  		return fmt.Errorf("client_private_key_pem is set but client_certificate_pem is not")
   148  	}
   149  
   150  	// TLS configuration is needed; create an object and configure it
   151  	var tlsConfig tls.Config
   152  	client.HTTPClient.Transport.(*http.Transport).TLSClientConfig = &tlsConfig
   153  
   154  	if skipCertVerification {
   155  		// ignores TLS verification
   156  		tlsConfig.InsecureSkipVerify = true
   157  	}
   158  	if clientCACertificatePem != "" {
   159  		// trust servers based on a CA
   160  		tlsConfig.RootCAs = x509.NewCertPool()
   161  		if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(clientCACertificatePem)) {
   162  			return errors.New("failed to append certs")
   163  		}
   164  	}
   165  	if clientCertificatePem != "" && clientPrivateKeyPem != "" {
   166  		// attach a client certificate to the TLS handshake (aka mTLS)
   167  		certificate, err := tls.X509KeyPair([]byte(clientCertificatePem), []byte(clientPrivateKeyPem))
   168  		if err != nil {
   169  			return fmt.Errorf("cannot load client certificate: %w", err)
   170  		}
   171  		tlsConfig.Certificates = []tls.Certificate{certificate}
   172  	}
   173  
   174  	return nil
   175  }
   176  
   177  func (b *Backend) configure(ctx context.Context) error {
   178  	data := schema.FromContextBackendConfig(ctx)
   179  
   180  	address := data.Get("address").(string)
   181  	updateURL, err := url.Parse(address)
   182  	if err != nil {
   183  		return fmt.Errorf("failed to parse address URL: %s", err)
   184  	}
   185  	if updateURL.Scheme != "http" && updateURL.Scheme != "https" {
   186  		return fmt.Errorf("address must be HTTP or HTTPS")
   187  	}
   188  
   189  	updateMethod := data.Get("update_method").(string)
   190  
   191  	var lockURL *url.URL
   192  	if v, ok := data.GetOk("lock_address"); ok && v.(string) != "" {
   193  		var err error
   194  		lockURL, err = url.Parse(v.(string))
   195  		if err != nil {
   196  			return fmt.Errorf("failed to parse lockAddress URL: %s", err)
   197  		}
   198  		if lockURL.Scheme != "http" && lockURL.Scheme != "https" {
   199  			return fmt.Errorf("lockAddress must be HTTP or HTTPS")
   200  		}
   201  	}
   202  
   203  	lockMethod := data.Get("lock_method").(string)
   204  
   205  	var unlockURL *url.URL
   206  	if v, ok := data.GetOk("unlock_address"); ok && v.(string) != "" {
   207  		var err error
   208  		unlockURL, err = url.Parse(v.(string))
   209  		if err != nil {
   210  			return fmt.Errorf("failed to parse unlockAddress URL: %s", err)
   211  		}
   212  		if unlockURL.Scheme != "http" && unlockURL.Scheme != "https" {
   213  			return fmt.Errorf("unlockAddress must be HTTP or HTTPS")
   214  		}
   215  	}
   216  
   217  	unlockMethod := data.Get("unlock_method").(string)
   218  
   219  	rClient := retryablehttp.NewClient()
   220  	rClient.RetryMax = data.Get("retry_max").(int)
   221  	rClient.RetryWaitMin = time.Duration(data.Get("retry_wait_min").(int)) * time.Second
   222  	rClient.RetryWaitMax = time.Duration(data.Get("retry_wait_max").(int)) * time.Second
   223  	rClient.Logger = log.New(logging.LogOutput(), "", log.Flags())
   224  	if err = b.configureTLS(rClient, data); err != nil {
   225  		return err
   226  	}
   227  
   228  	b.client = &httpClient{
   229  		URL:          updateURL,
   230  		UpdateMethod: updateMethod,
   231  
   232  		LockURL:      lockURL,
   233  		LockMethod:   lockMethod,
   234  		UnlockURL:    unlockURL,
   235  		UnlockMethod: unlockMethod,
   236  
   237  		Username: data.Get("username").(string),
   238  		Password: data.Get("password").(string),
   239  
   240  		// accessible only for testing use
   241  		Client: rClient,
   242  	}
   243  	return nil
   244  }
   245  
   246  func (b *Backend) StateMgr(name string) (statemgr.Full, error) {
   247  	if name != backend.DefaultStateName {
   248  		return nil, backend.ErrWorkspacesNotSupported
   249  	}
   250  
   251  	return &remote.State{Client: b.client}, nil
   252  }
   253  
   254  func (b *Backend) Workspaces() ([]string, error) {
   255  	return nil, backend.ErrWorkspacesNotSupported
   256  }
   257  
   258  func (b *Backend) DeleteWorkspace(string, bool) error {
   259  	return backend.ErrWorkspacesNotSupported
   260  }