From 5868f994139db04a5f6989491f1dbb84486873ac Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 6 Mar 2024 14:24:00 -0800 Subject: [PATCH] backend/gcs: Stop using legacy helper/schema As part of our efforts to remove the large snapshot of the legacy SDK that we've been using, this replaces the uses of that package with our new lightweight "backendbase" package, which is a collection of helpers that can substitute for most of the parts of the legacy SDK that backends tend to use. For this backend for now we'll use the "SDK like" helpers which aim to retain some of the legacy-SDK-specific assumptions that aren't technically true anymore, such as the idea that null and empty string are equivalent. Hopefully one day we'll be able to wean this backend off of that more completely, but this is a pragmatic way to get away from the legacy SDK without a large rewrite. This also drew attention to the fact that this backend was previously relying on the weird context.Context value that the SDK was passing in to the configure method, even though that context isn't connected up to anything particularly useful. In the long run we should change the backend APIs to pass context.Context to each method that might make network requests, but for now we're using context.TODO so that we can more easily find these cases and update them later once there's a real context to use. --- internal/backend/remote-state/gcs/backend.go | 265 ++++++++++-------- .../backend/remote-state/gcs/backend_state.go | 18 +- .../backend/remote-state/gcs/backend_test.go | 7 +- internal/backend/remote-state/gcs/client.go | 40 +-- internal/backend/remote-state/gcs/go.mod | 8 +- internal/backend/remote-state/gcs/go.sum | 6 - 6 files changed, 187 insertions(+), 157 deletions(-) diff --git a/internal/backend/remote-state/gcs/backend.go b/internal/backend/remote-state/gcs/backend.go index a2cacd913b..f22b80805f 100644 --- a/internal/backend/remote-state/gcs/backend.go +++ b/internal/backend/remote-state/gcs/backend.go @@ -13,22 +13,25 @@ import ( "strings" "cloud.google.com/go/storage" - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/httpclient" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" + "github.com/zclconf/go-cty/cty" "golang.org/x/oauth2" "google.golang.org/api/impersonate" "google.golang.org/api/option" + + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendbase" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/httpclient" + "github.com/hashicorp/terraform/internal/tfdiags" ) // Backend implements "backend".Backend for GCS. -// Input(), Validate() and Configure() are implemented by embedding *schema.Backend. -// State(), DeleteState() and States() are implemented explicitly. +// Schema() and PrepareConfig() are implemented by embedding backendbase.Base. +// Configure(), State(), DeleteState() and States() are implemented explicitly. type Backend struct { - *schema.Backend + backendbase.Base - storageClient *storage.Client - storageContext context.Context + storageClient *storage.Client bucketName string prefix string @@ -38,104 +41,116 @@ type Backend struct { } func New() backend.Backend { - b := &Backend{} - b.Backend = &schema.Backend{ - ConfigureFunc: b.configure, - Schema: map[string]*schema.Schema{ - "bucket": { - Type: schema.TypeString, - Required: true, - Description: "The name of the Google Cloud Storage bucket", + return &Backend{ + Base: backendbase.Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bucket": { + Type: cty.String, + Required: true, + Description: "The name of the Google Cloud Storage bucket", + }, + + "prefix": { + Type: cty.String, + Optional: true, + Description: "The directory where state files will be saved inside the bucket", + }, + + "credentials": { + Type: cty.String, + Optional: true, + Description: "Google Cloud JSON Account Key", + }, + + "access_token": { + Type: cty.String, + Optional: true, + Description: "An OAuth2 token used for GCP authentication", + }, + + "impersonate_service_account": { + Type: cty.String, + Optional: true, + Description: "The service account to impersonate for all Google API Calls", + }, + + "impersonate_service_account_delegates": { + Type: cty.List(cty.String), + Optional: true, + Description: "The delegation chain for the impersonated service account", + }, + + "encryption_key": { + Type: cty.String, + Optional: true, + Description: "A 32 byte base64 encoded 'customer supplied encryption key' used when reading and writing state files in the bucket.", + }, + + "kms_encryption_key": { + Type: cty.String, + Optional: true, + Description: "A Cloud KMS key ('customer managed encryption key') used when reading and writing state files in the bucket. Format should be 'projects/{{project}}/locations/{{location}}/keyRings/{{keyRing}}/cryptoKeys/{{name}}'.", + }, + + "storage_custom_endpoint": { + Type: cty.String, + Optional: true, + }, + }, }, - - "prefix": { - Type: schema.TypeString, - Optional: true, - Description: "The directory where state files will be saved inside the bucket", - }, - - "credentials": { - Type: schema.TypeString, - Optional: true, - Description: "Google Cloud JSON Account Key", - Default: "", - }, - - "access_token": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_OAUTH_ACCESS_TOKEN", - }, nil), - Description: "An OAuth2 token used for GCP authentication", - }, - - "impersonate_service_account": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_BACKEND_IMPERSONATE_SERVICE_ACCOUNT", - "GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", - }, nil), - Description: "The service account to impersonate for all Google API Calls", - }, - - "impersonate_service_account_delegates": { - Type: schema.TypeList, - Optional: true, - Description: "The delegation chain for the impersonated service account", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - - "encryption_key": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_ENCRYPTION_KEY", - }, nil), - Description: "A 32 byte base64 encoded 'customer supplied encryption key' used when reading and writing state files in the bucket.", - ConflictsWith: []string{"kms_encryption_key"}, - }, - - "kms_encryption_key": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_KMS_ENCRYPTION_KEY", - }, nil), - Description: "A Cloud KMS key ('customer managed encryption key') used when reading and writing state files in the bucket. Format should be 'projects/{{project}}/locations/{{location}}/keyRings/{{keyRing}}/cryptoKeys/{{name}}'.", - ConflictsWith: []string{"encryption_key"}, - }, - - "storage_custom_endpoint": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_BACKEND_STORAGE_CUSTOM_ENDPOINT", - "GOOGLE_STORAGE_CUSTOM_ENDPOINT", - }, nil), + SDKLikeDefaults: backendbase.SDKLikeDefaults{ + "prefix": { + Fallback: "", + }, + "credentials": { + Fallback: "", + }, + "access_token": { + EnvVars: []string{"GOOGLE_OAUTH_ACCESS_TOKEN"}, + }, + "impersonate_service_account": { + EnvVars: []string{ + "GOOGLE_BACKEND_IMPERSONATE_SERVICE_ACCOUNT", + "GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", + }, + }, + "encryption_key": { + EnvVars: []string{"GOOGLE_ENCRYPTION_KEY"}, + }, + "kms_encryption_key": { + EnvVars: []string{"GOOGLE_KMS_ENCRYPTION_KEY"}, + }, + "storage_custom_endpoint": { + EnvVars: []string{ + "GOOGLE_BACKEND_STORAGE_CUSTOM_ENDPOINT", + "GOOGLE_STORAGE_CUSTOM_ENDPOINT", + }, + }, }, }, } - - return b } -func (b *Backend) configure(ctx context.Context) error { +func (b *Backend) Configure(configVal cty.Value) tfdiags.Diagnostics { if b.storageClient != nil { return nil } - // ctx is a background context with the backend config added. - // Since no context is passed to remoteClient.Get(), .Lock(), etc. but - // one is required for calling the GCP API, we're holding on to this - // context here and re-use it later. - b.storageContext = ctx + // TODO: Update the Backend API to pass the real context.Context from + // the running command. + ctx := context.TODO() + + data := backendbase.NewSDKLikeData(configVal) - data := schema.FromContextBackendConfig(b.storageContext) + if data.String("encryption_key") != "" && data.String("kms_encryption_key") != "" { + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("can't set both encryption_key and kms_encryption_key"), + ) + } - b.bucketName = data.Get("bucket").(string) - b.prefix = strings.TrimLeft(data.Get("prefix").(string), "/") + b.bucketName = data.String("bucket") + b.prefix = strings.TrimLeft(data.String("prefix"), "/") if b.prefix != "" && !strings.HasSuffix(b.prefix, "/") { b.prefix = b.prefix + "/" } @@ -147,12 +162,12 @@ func (b *Backend) configure(ctx context.Context) error { var creds string var tokenSource oauth2.TokenSource - if v, ok := data.GetOk("access_token"); ok { + if v := data.String("access_token"); v != "" { tokenSource = oauth2.StaticTokenSource(&oauth2.Token{ - AccessToken: v.(string), + AccessToken: v, }) - } else if v, ok := data.GetOk("credentials"); ok { - creds = v.(string) + } else if v := data.String("credentials"); v != "" { + creds = v } else if v := os.Getenv("GOOGLE_BACKEND_CREDENTIALS"); v != "" { creds = v } else { @@ -166,28 +181,36 @@ func (b *Backend) configure(ctx context.Context) error { // to mirror how the provider works, we accept the file path or the contents contents, err := readPathOrContents(creds) if err != nil { - return fmt.Errorf("Error loading credentials: %s", err) + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("Error loading credentials: %s", err), + ) } if !json.Valid([]byte(contents)) { - return fmt.Errorf("the string provided in credentials is neither valid json nor a valid file path") + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("the string provided in credentials is neither valid json nor a valid file path"), + ) } credOptions = append(credOptions, option.WithCredentialsJSON([]byte(contents))) } // Service Account Impersonation - if v, ok := data.GetOk("impersonate_service_account"); ok { - ServiceAccount := v.(string) + if v := data.String("impersonate_service_account"); v != "" { + ServiceAccount := v var delegates []string - if v, ok := data.GetOk("impersonate_service_account_delegates"); ok { - d := v.([]interface{}) - if len(delegates) > 0 { - delegates = make([]string, 0, len(d)) - } - for _, delegate := range d { - delegates = append(delegates, delegate.(string)) + delegatesVal := data.GetAttr("impersonate_service_account_delegates", cty.List(cty.String)) + if !delegatesVal.IsNull() && delegatesVal.LengthInt() != 0 { + delegates = make([]string, 0, delegatesVal.LengthInt()) + for it := delegatesVal.ElementIterator(); it.Next(); { + _, v := it.Element() + if v.IsNull() { + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("impersonate_service_account_delegates elements must not be null"), + ) + } + delegates = append(delegates, v.AsString()) } } @@ -198,7 +221,7 @@ func (b *Backend) configure(ctx context.Context) error { }, credOptions...) if err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } opts = append(opts, option.WithTokenSource(ts)) @@ -210,23 +233,27 @@ func (b *Backend) configure(ctx context.Context) error { opts = append(opts, option.WithUserAgent(httpclient.UserAgentString())) // Custom endpoint for storage API - if storageEndpoint, ok := data.GetOk("storage_custom_endpoint"); ok { - endpoint := option.WithEndpoint(storageEndpoint.(string)) + if storageEndpoint := data.String("storage_custom_endpoint"); storageEndpoint != "" { + endpoint := option.WithEndpoint(storageEndpoint) opts = append(opts, endpoint) } - client, err := storage.NewClient(b.storageContext, opts...) + client, err := storage.NewClient(ctx, opts...) if err != nil { - return fmt.Errorf("storage.NewClient() failed: %v", err) + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("storage.NewClient() failed: %v", err), + ) } b.storageClient = client // Customer-supplied encryption - key := data.Get("encryption_key").(string) + key := data.String("encryption_key") if key != "" { kc, err := readPathOrContents(key) if err != nil { - return fmt.Errorf("Error loading encryption key: %s", err) + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("Error loading encryption key: %s", err), + ) } // The GCS client expects a customer supplied encryption key to be @@ -237,13 +264,15 @@ func (b *Backend) configure(ctx context.Context) error { // https://github.com/GoogleCloudPlatform/google-cloud-go/blob/def681/storage/storage.go#L1181 k, err := base64.StdEncoding.DecodeString(kc) if err != nil { - return fmt.Errorf("Error decoding encryption key: %s", err) + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("Error decoding encryption key: %s", err), + ) } b.encryptionKey = k } // Customer-managed encryption - kmsName := data.Get("kms_encryption_key").(string) + kmsName := data.String("kms_encryption_key") if kmsName != "" { b.kmsKeyName = kmsName } diff --git a/internal/backend/remote-state/gcs/backend_state.go b/internal/backend/remote-state/gcs/backend_state.go index 94d7a5dd1c..cd67683e5c 100644 --- a/internal/backend/remote-state/gcs/backend_state.go +++ b/internal/backend/remote-state/gcs/backend_state.go @@ -4,6 +4,7 @@ package gcs import ( + "context" "fmt" "path" "sort" @@ -26,10 +27,12 @@ const ( // Workspaces returns a list of names for the workspaces found on GCS. The default // state is always returned as the first element in the slice. func (b *Backend) Workspaces() ([]string, error) { + ctx := context.TODO() + states := []string{backend.DefaultStateName} bucket := b.storageClient.Bucket(b.bucketName) - objs := bucket.Objects(b.storageContext, &storage.Query{ + objs := bucket.Objects(ctx, &storage.Query{ Delimiter: "/", Prefix: b.prefix, }) @@ -78,13 +81,12 @@ func (b *Backend) client(name string) (*remoteClient, error) { } return &remoteClient{ - storageContext: b.storageContext, - storageClient: b.storageClient, - bucketName: b.bucketName, - stateFilePath: b.stateFile(name), - lockFilePath: b.lockFile(name), - encryptionKey: b.encryptionKey, - kmsKeyName: b.kmsKeyName, + storageClient: b.storageClient, + bucketName: b.bucketName, + stateFilePath: b.stateFile(name), + lockFilePath: b.lockFile(name), + encryptionKey: b.encryptionKey, + kmsKeyName: b.kmsKeyName, }, nil } diff --git a/internal/backend/remote-state/gcs/backend_test.go b/internal/backend/remote-state/gcs/backend_test.go index 1cd06ab7ea..bcf2c0f591 100644 --- a/internal/backend/remote-state/gcs/backend_test.go +++ b/internal/backend/remote-state/gcs/backend_test.go @@ -214,6 +214,7 @@ func TestBackendWithCustomerManagedKMSEncryption(t *testing.T) { // setupBackend returns a new GCS backend. func setupBackend(t *testing.T, bucket, prefix, key, kmsName string) backend.Backend { t.Helper() + ctx := context.Background() projectID := os.Getenv("GOOGLE_PROJECT") if projectID == "" || os.Getenv("TF_ACC") == "" { @@ -240,7 +241,7 @@ func setupBackend(t *testing.T, bucket, prefix, key, kmsName string) backend.Bac // create the bucket if it doesn't exist bkt := be.storageClient.Bucket(bucket) - _, err := bkt.Attrs(be.storageContext) + _, err := bkt.Attrs(ctx) if err != nil { if err != storage.ErrBucketNotExist { t.Fatal(err) @@ -249,7 +250,7 @@ func setupBackend(t *testing.T, bucket, prefix, key, kmsName string) backend.Bac attrs := &storage.BucketAttrs{ Location: os.Getenv("GOOGLE_REGION"), } - err := bkt.Create(be.storageContext, projectID, attrs) + err := bkt.Create(ctx, projectID, attrs) if err != nil { t.Fatal(err) } @@ -379,7 +380,7 @@ func teardownBackend(t *testing.T, be backend.Backend, prefix string) { if !ok { t.Fatalf("be is a %T, want a *gcsBackend", be) } - ctx := gcsBE.storageContext + ctx := context.Background() bucket := gcsBE.storageClient.Bucket(gcsBE.bucketName) objs := bucket.Objects(ctx, nil) diff --git a/internal/backend/remote-state/gcs/client.go b/internal/backend/remote-state/gcs/client.go index 0a339a86e0..def842c148 100644 --- a/internal/backend/remote-state/gcs/client.go +++ b/internal/backend/remote-state/gcs/client.go @@ -4,6 +4,7 @@ package gcs import ( + "context" "encoding/json" "errors" "fmt" @@ -11,7 +12,6 @@ import ( "strconv" "cloud.google.com/go/storage" - "golang.org/x/net/context" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" @@ -21,17 +21,17 @@ import ( // blobs representing state. // Implements "state/remote".ClientLocker type remoteClient struct { - storageContext context.Context - storageClient *storage.Client - bucketName string - stateFilePath string - lockFilePath string - encryptionKey []byte - kmsKeyName string + storageClient *storage.Client + bucketName string + stateFilePath string + lockFilePath string + encryptionKey []byte + kmsKeyName string } func (c *remoteClient) Get() (payload *remote.Payload, err error) { - stateFileReader, err := c.stateFile().NewReader(c.storageContext) + ctx := context.TODO() + stateFileReader, err := c.stateFile().NewReader(ctx) if err != nil { if err == storage.ErrObjectNotExist { return nil, nil @@ -46,7 +46,7 @@ func (c *remoteClient) Get() (payload *remote.Payload, err error) { return nil, fmt.Errorf("Failed to read state file from %v: %v", c.stateFileURL(), err) } - stateFileAttrs, err := c.stateFile().Attrs(c.storageContext) + stateFileAttrs, err := c.stateFile().Attrs(ctx) if err != nil { return nil, fmt.Errorf("Failed to read state file attrs from %v: %v", c.stateFileURL(), err) } @@ -60,8 +60,9 @@ func (c *remoteClient) Get() (payload *remote.Payload, err error) { } func (c *remoteClient) Put(data []byte) error { + ctx := context.TODO() err := func() error { - stateFileWriter := c.stateFile().NewWriter(c.storageContext) + stateFileWriter := c.stateFile().NewWriter(ctx) if len(c.kmsKeyName) > 0 { stateFileWriter.KMSKeyName = c.kmsKeyName } @@ -78,7 +79,8 @@ func (c *remoteClient) Put(data []byte) error { } func (c *remoteClient) Delete() error { - if err := c.stateFile().Delete(c.storageContext); err != nil { + ctx := context.TODO() + if err := c.stateFile().Delete(ctx); err != nil { return fmt.Errorf("Failed to delete state file %v: %v", c.stateFileURL(), err) } @@ -88,6 +90,8 @@ func (c *remoteClient) Delete() error { // Lock writes to a lock file, ensuring file creation. Returns the generation // number, which must be passed to Unlock(). func (c *remoteClient) Lock(info *statemgr.LockInfo) (string, error) { + ctx := context.TODO() + // update the path we're using // we can't set the ID until the info is written info.Path = c.lockFileURL() @@ -98,7 +102,7 @@ func (c *remoteClient) Lock(info *statemgr.LockInfo) (string, error) { } lockFile := c.lockFile() - w := lockFile.If(storage.Conditions{DoesNotExist: true}).NewWriter(c.storageContext) + w := lockFile.If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx) err = func() error { if _, err := w.Write(infoJson); err != nil { return err @@ -116,12 +120,14 @@ func (c *remoteClient) Lock(info *statemgr.LockInfo) (string, error) { } func (c *remoteClient) Unlock(id string) error { + ctx := context.TODO() + gen, err := strconv.ParseInt(id, 10, 64) if err != nil { return fmt.Errorf("Lock ID should be numerical value, got '%s'", id) } - if err := c.lockFile().If(storage.Conditions{GenerationMatch: gen}).Delete(c.storageContext); err != nil { + if err := c.lockFile().If(storage.Conditions{GenerationMatch: gen}).Delete(ctx); err != nil { return c.lockError(err) } @@ -145,7 +151,9 @@ func (c *remoteClient) lockError(err error) *statemgr.LockError { // lockInfo reads the lock file, parses its contents and returns the parsed // LockInfo struct. func (c *remoteClient) lockInfo() (*statemgr.LockInfo, error) { - r, err := c.lockFile().NewReader(c.storageContext) + ctx := context.TODO() + + r, err := c.lockFile().NewReader(ctx) if err != nil { return nil, err } @@ -164,7 +172,7 @@ func (c *remoteClient) lockInfo() (*statemgr.LockInfo, error) { // We use the Generation as the ID, so overwrite the ID in the json. // This can't be written into the Info, since the generation isn't known // until it's written. - attrs, err := c.lockFile().Attrs(c.storageContext) + attrs, err := c.lockFile().Attrs(ctx) if err != nil { return nil, err } diff --git a/internal/backend/remote-state/gcs/go.mod b/internal/backend/remote-state/gcs/go.mod index 69c2fe3a49..c54b98f3d8 100644 --- a/internal/backend/remote-state/gcs/go.mod +++ b/internal/backend/remote-state/gcs/go.mod @@ -6,9 +6,8 @@ require ( cloud.google.com/go/kms v1.15.0 cloud.google.com/go/storage v1.30.1 github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 - github.com/hashicorp/terraform/internal/legacy v0.0.0-00010101000000-000000000000 github.com/mitchellh/go-homedir v1.1.0 - golang.org/x/net v0.22.0 + github.com/zclconf/go-cty v1.14.3 golang.org/x/oauth2 v0.18.0 google.golang.org/api v0.126.0 google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d @@ -38,17 +37,14 @@ require ( github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/kr/pretty v0.3.1 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/spf13/afero v1.9.3 // indirect - github.com/zclconf/go-cty v1.14.3 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/mod v0.16.0 // indirect + golang.org/x/net v0.22.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/internal/backend/remote-state/gcs/go.sum b/internal/backend/remote-state/gcs/go.sum index 09a2117e7c..ffd93b4884 100644 --- a/internal/backend/remote-state/gcs/go.sum +++ b/internal/backend/remote-state/gcs/go.sum @@ -223,18 +223,12 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=