github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/consul/backend.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 consul
     7  
     8  import (
     9  	"context"
    10  	"net"
    11  	"strings"
    12  	"time"
    13  
    14  	consulapi "github.com/hashicorp/consul/api"
    15  	"github.com/opentofu/opentofu/internal/backend"
    16  	"github.com/opentofu/opentofu/internal/encryption"
    17  	"github.com/opentofu/opentofu/internal/legacy/helper/schema"
    18  )
    19  
    20  // New creates a new backend for Consul remote state.
    21  func New(enc encryption.StateEncryption) backend.Backend {
    22  	s := &schema.Backend{
    23  		Schema: map[string]*schema.Schema{
    24  			"path": &schema.Schema{
    25  				Type:        schema.TypeString,
    26  				Required:    true,
    27  				Description: "Path to store state in Consul",
    28  			},
    29  
    30  			"access_token": &schema.Schema{
    31  				Type:        schema.TypeString,
    32  				Optional:    true,
    33  				Description: "Access token for a Consul ACL",
    34  				Default:     "", // To prevent input
    35  			},
    36  
    37  			"address": &schema.Schema{
    38  				Type:        schema.TypeString,
    39  				Optional:    true,
    40  				Description: "Address to the Consul Cluster",
    41  				Default:     "", // To prevent input
    42  			},
    43  
    44  			"scheme": &schema.Schema{
    45  				Type:        schema.TypeString,
    46  				Optional:    true,
    47  				Description: "Scheme to communicate to Consul with",
    48  				Default:     "", // To prevent input
    49  			},
    50  
    51  			"datacenter": &schema.Schema{
    52  				Type:        schema.TypeString,
    53  				Optional:    true,
    54  				Description: "Datacenter to communicate with",
    55  				Default:     "", // To prevent input
    56  			},
    57  
    58  			"http_auth": &schema.Schema{
    59  				Type:        schema.TypeString,
    60  				Optional:    true,
    61  				Description: "HTTP Auth in the format of 'username:password'",
    62  				Default:     "", // To prevent input
    63  			},
    64  
    65  			"gzip": &schema.Schema{
    66  				Type:        schema.TypeBool,
    67  				Optional:    true,
    68  				Description: "Compress the state data using gzip",
    69  				Default:     false,
    70  			},
    71  
    72  			"lock": &schema.Schema{
    73  				Type:        schema.TypeBool,
    74  				Optional:    true,
    75  				Description: "Lock state access",
    76  				Default:     true,
    77  			},
    78  
    79  			"ca_file": &schema.Schema{
    80  				Type:        schema.TypeString,
    81  				Optional:    true,
    82  				Description: "A path to a PEM-encoded certificate authority used to verify the remote agent's certificate.",
    83  				DefaultFunc: schema.EnvDefaultFunc("CONSUL_CACERT", ""),
    84  			},
    85  
    86  			"cert_file": &schema.Schema{
    87  				Type:        schema.TypeString,
    88  				Optional:    true,
    89  				Description: "A path to a PEM-encoded certificate provided to the remote agent; requires use of key_file.",
    90  				DefaultFunc: schema.EnvDefaultFunc("CONSUL_CLIENT_CERT", ""),
    91  			},
    92  
    93  			"key_file": &schema.Schema{
    94  				Type:        schema.TypeString,
    95  				Optional:    true,
    96  				Description: "A path to a PEM-encoded private key, required if cert_file is specified.",
    97  				DefaultFunc: schema.EnvDefaultFunc("CONSUL_CLIENT_KEY", ""),
    98  			},
    99  		},
   100  	}
   101  
   102  	result := &Backend{Backend: s, encryption: enc}
   103  	result.Backend.ConfigureFunc = result.configure
   104  	return result
   105  }
   106  
   107  type Backend struct {
   108  	*schema.Backend
   109  	encryption encryption.StateEncryption
   110  
   111  	// The fields below are set from configure
   112  	client     *consulapi.Client
   113  	configData *schema.ResourceData
   114  	lock       bool
   115  }
   116  
   117  func (b *Backend) configure(ctx context.Context) error {
   118  	// Grab the resource data
   119  	b.configData = schema.FromContextBackendConfig(ctx)
   120  
   121  	// Store the lock information
   122  	b.lock = b.configData.Get("lock").(bool)
   123  
   124  	data := b.configData
   125  
   126  	// Configure the client
   127  	config := consulapi.DefaultConfig()
   128  
   129  	// replace the default Transport Dialer to reduce the KeepAlive
   130  	config.Transport.DialContext = dialContext
   131  
   132  	if v, ok := data.GetOk("access_token"); ok && v.(string) != "" {
   133  		config.Token = v.(string)
   134  	}
   135  	if v, ok := data.GetOk("address"); ok && v.(string) != "" {
   136  		config.Address = v.(string)
   137  	}
   138  	if v, ok := data.GetOk("scheme"); ok && v.(string) != "" {
   139  		config.Scheme = v.(string)
   140  	}
   141  	if v, ok := data.GetOk("datacenter"); ok && v.(string) != "" {
   142  		config.Datacenter = v.(string)
   143  	}
   144  
   145  	if v, ok := data.GetOk("ca_file"); ok && v.(string) != "" {
   146  		config.TLSConfig.CAFile = v.(string)
   147  	}
   148  	if v, ok := data.GetOk("cert_file"); ok && v.(string) != "" {
   149  		config.TLSConfig.CertFile = v.(string)
   150  	}
   151  	if v, ok := data.GetOk("key_file"); ok && v.(string) != "" {
   152  		config.TLSConfig.KeyFile = v.(string)
   153  	}
   154  
   155  	if v, ok := data.GetOk("http_auth"); ok && v.(string) != "" {
   156  		auth := v.(string)
   157  
   158  		var username, password string
   159  		if strings.Contains(auth, ":") {
   160  			split := strings.SplitN(auth, ":", 2)
   161  			username = split[0]
   162  			password = split[1]
   163  		} else {
   164  			username = auth
   165  		}
   166  
   167  		config.HttpAuth = &consulapi.HttpBasicAuth{
   168  			Username: username,
   169  			Password: password,
   170  		}
   171  	}
   172  
   173  	client, err := consulapi.NewClient(config)
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	b.client = client
   179  	return nil
   180  }
   181  
   182  // dialContext is the DialContext function for the consul client transport.
   183  // This is stored in a package var to inject a different dialer for tests.
   184  var dialContext = (&net.Dialer{
   185  	Timeout:   30 * time.Second,
   186  	KeepAlive: 17 * time.Second,
   187  }).DialContext