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