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=