| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package s3 |
| |
| import ( |
| "context" |
| "encoding/base64" |
| "fmt" |
| "os" |
| "regexp" |
| "strings" |
| "time" |
| "unicode" |
| "unicode/utf8" |
| |
| "github.com/aws/aws-sdk-go-v2/aws" |
| "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" |
| "github.com/aws/aws-sdk-go-v2/service/dynamodb" |
| "github.com/aws/aws-sdk-go-v2/service/s3" |
| awsbase "github.com/hashicorp/aws-sdk-go-base/v2" |
| baselogging "github.com/hashicorp/aws-sdk-go-base/v2/logging" |
| "github.com/hashicorp/aws-sdk-go-base/v2/validation" |
| "github.com/zclconf/go-cty/cty" |
| "github.com/zclconf/go-cty/cty/gocty" |
| |
| "github.com/hashicorp/terraform/internal/backend" |
| "github.com/hashicorp/terraform/internal/configs/configschema" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| "github.com/hashicorp/terraform/version" |
| ) |
| |
| func New() backend.Backend { |
| return &Backend{} |
| } |
| |
| type Backend struct { |
| awsConfig aws.Config |
| s3Client *s3.Client |
| dynClient *dynamodb.Client |
| |
| bucketName string |
| keyName string |
| serverSideEncryption bool |
| customerEncryptionKey []byte |
| acl string |
| kmsKeyID string |
| ddbTable string |
| useLockFile bool |
| workspaceKeyPrefix string |
| skipS3Checksum bool |
| } |
| |
| // ConfigSchema returns a description of the expected configuration |
| // structure for the receiving backend. |
| func (b *Backend) ConfigSchema() *configschema.Block { |
| return &configschema.Block{ |
| Attributes: map[string]*configschema.Attribute{ |
| "bucket": { |
| Type: cty.String, |
| Required: true, |
| Description: "The name of the S3 bucket", |
| }, |
| "key": { |
| Type: cty.String, |
| Required: true, |
| Description: "The path to the state file inside the bucket", |
| }, |
| "region": { |
| Type: cty.String, |
| Optional: true, |
| Description: "AWS region of the S3 Bucket and DynamoDB Table (if used).", |
| }, |
| "allowed_account_ids": { |
| Type: cty.Set(cty.String), |
| Optional: true, |
| Description: "List of allowed AWS account IDs.", |
| }, |
| "dynamodb_endpoint": { |
| Type: cty.String, |
| Optional: true, |
| Description: "A custom endpoint for the DynamoDB API", |
| Deprecated: true, |
| }, |
| "ec2_metadata_service_endpoint": { |
| Type: cty.String, |
| Optional: true, |
| Description: "Address of the EC2 metadata service (IMDS) endpoint to use.", |
| }, |
| "ec2_metadata_service_endpoint_mode": { |
| Type: cty.String, |
| Optional: true, |
| Description: "Mode to use in communicating with the metadata service.", |
| }, |
| "endpoint": { |
| Type: cty.String, |
| Optional: true, |
| Description: "A custom endpoint for the S3 API", |
| Deprecated: true, |
| }, |
| |
| "endpoints": endpointsSchema.SchemaAttribute(), |
| |
| "forbidden_account_ids": { |
| Type: cty.Set(cty.String), |
| Optional: true, |
| Description: "List of forbidden AWS account IDs.", |
| }, |
| "iam_endpoint": { |
| Type: cty.String, |
| Optional: true, |
| Description: "A custom endpoint for the IAM API", |
| Deprecated: true, |
| }, |
| "sts_endpoint": { |
| Type: cty.String, |
| Optional: true, |
| Description: "A custom endpoint for the STS API", |
| Deprecated: true, |
| }, |
| "sts_region": { |
| Type: cty.String, |
| Optional: true, |
| Description: "AWS region for STS.", |
| }, |
| "encrypt": { |
| Type: cty.Bool, |
| Optional: true, |
| Description: "Whether to enable server side encryption of the state file", |
| }, |
| "acl": { |
| Type: cty.String, |
| Optional: true, |
| Description: "Canned ACL to be applied to the state file", |
| }, |
| "access_key": { |
| Type: cty.String, |
| Optional: true, |
| Description: "AWS access key", |
| }, |
| "secret_key": { |
| Type: cty.String, |
| Optional: true, |
| Description: "AWS secret key", |
| }, |
| "kms_key_id": { |
| Type: cty.String, |
| Optional: true, |
| Description: "The ARN of a KMS Key to use for encrypting the state", |
| }, |
| "dynamodb_table": { |
| Type: cty.String, |
| Optional: true, |
| Description: "DynamoDB table for state locking and consistency", |
| Deprecated: true, |
| }, |
| "use_lockfile": { |
| Type: cty.Bool, |
| Optional: true, |
| Description: "Whether to use a lockfile for locking the state file.", |
| }, |
| "profile": { |
| Type: cty.String, |
| Optional: true, |
| Description: "AWS profile name", |
| }, |
| "retry_mode": { |
| Type: cty.String, |
| Optional: true, |
| Description: "Specifies how retries are attempted.", |
| }, |
| "shared_config_files": { |
| Type: cty.Set(cty.String), |
| Optional: true, |
| Description: "List of paths to shared config files", |
| }, |
| "shared_credentials_file": { |
| Type: cty.String, |
| Optional: true, |
| Description: "Path to a shared credentials file", |
| Deprecated: true, |
| }, |
| "shared_credentials_files": { |
| Type: cty.Set(cty.String), |
| Optional: true, |
| Description: "List of paths to shared credentials files", |
| }, |
| "token": { |
| Type: cty.String, |
| Optional: true, |
| Description: "MFA token", |
| }, |
| "skip_credentials_validation": { |
| Type: cty.Bool, |
| Optional: true, |
| Description: "Skip the credentials validation via STS API. Useful for testing and for AWS API implementations that do not have STS available.", |
| }, |
| "skip_requesting_account_id": { |
| Type: cty.Bool, |
| Optional: true, |
| Description: "Skip the requesting account ID. Useful for AWS API implementations that do not have the IAM, STS API, or metadata API.", |
| }, |
| "skip_metadata_api_check": { |
| Type: cty.Bool, |
| Optional: true, |
| Description: "Skip the AWS Metadata API check.", |
| }, |
| "skip_region_validation": { |
| Type: cty.Bool, |
| Optional: true, |
| Description: "Skip static validation of region name.", |
| }, |
| "skip_s3_checksum": { |
| Type: cty.Bool, |
| Optional: true, |
| Description: "Do not include checksum when uploading S3 Objects. Useful for some S3-Compatible APIs.", |
| }, |
| "sse_customer_key": { |
| Type: cty.String, |
| Optional: true, |
| Description: "The base64-encoded encryption key to use for server-side encryption with customer-provided keys (SSE-C).", |
| Sensitive: true, |
| }, |
| |
| "workspace_key_prefix": { |
| Type: cty.String, |
| Optional: true, |
| Description: "The prefix applied to the non-default state path inside the bucket.", |
| }, |
| |
| "force_path_style": { |
| Type: cty.Bool, |
| Optional: true, |
| Description: "Enable path-style S3 URLs.", |
| Deprecated: true, |
| }, |
| |
| "use_path_style": { |
| Type: cty.Bool, |
| Optional: true, |
| Description: "Enable path-style S3 URLs.", |
| }, |
| |
| "max_retries": { |
| Type: cty.Number, |
| Optional: true, |
| Description: "The maximum number of times an AWS API request is retried on retryable failure.", |
| }, |
| |
| "assume_role": assumeRoleSchema.SchemaAttribute(), |
| |
| "assume_role_with_web_identity": assumeRoleWithWebIdentitySchema.SchemaAttribute(), |
| |
| "custom_ca_bundle": { |
| Type: cty.String, |
| Optional: true, |
| Description: "File containing custom root and intermediate certificates.", |
| }, |
| |
| "http_proxy": { |
| Type: cty.String, |
| Optional: true, |
| Description: "URL of a proxy to use for HTTP requests when accessing the AWS API.", |
| }, |
| |
| "https_proxy": { |
| Type: cty.String, |
| Optional: true, |
| Description: "URL of a proxy to use for HTTPS requests when accessing the AWS API.", |
| }, |
| |
| "no_proxy": { |
| Type: cty.String, |
| Optional: true, |
| Description: "Comma-separated list of hosts that should not use HTTP or HTTPS proxies.", |
| }, |
| |
| "insecure": { |
| Type: cty.Bool, |
| Optional: true, |
| Description: "Whether to explicitly allow the backend to perform insecure SSL requests.", |
| }, |
| "use_fips_endpoint": { |
| Type: cty.Bool, |
| Optional: true, |
| Description: "Force the backend to resolve endpoints with FIPS capability.", |
| }, |
| "use_dualstack_endpoint": { |
| Type: cty.Bool, |
| Optional: true, |
| Description: "Force the backend to resolve endpoints with DualStack capability.", |
| }, |
| }, |
| } |
| } |
| |
| var assumeRoleSchema = singleNestedAttribute{ |
| Attributes: map[string]schemaAttribute{ |
| "role_arn": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Required: true, |
| Description: "The role to be assumed.", |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateStringNotEmpty, |
| validateARN( |
| validateIAMRoleARN, |
| ), |
| }, |
| }, |
| }, |
| |
| "duration": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.", |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateDuration( |
| validateDurationBetween(15*time.Minute, 12*time.Hour), |
| ), |
| }, |
| }, |
| }, |
| |
| "external_id": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "The external ID to use when assuming the role", |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateStringLenBetween(2, 1224), |
| validateStringMatches( |
| regexp.MustCompile(`^[\w+=,.@:\/\-]*$`), |
| `Value can only contain letters, numbers, or the following characters: =,.@/-`, |
| ), |
| }, |
| }, |
| }, |
| |
| "policy": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateStringNotEmpty, |
| validateIAMPolicyDocument, |
| }, |
| }, |
| }, |
| |
| "policy_arns": setAttribute{ |
| configschema.Attribute{ |
| Type: cty.Set(cty.String), |
| Optional: true, |
| Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.", |
| }, |
| validateSet{ |
| Validators: []setValidator{ |
| validateSetStringElements( |
| validateARN( |
| validateIAMPolicyARN, |
| ), |
| ), |
| }, |
| }, |
| }, |
| |
| "session_name": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "The session name to use when assuming the role.", |
| }, |
| validateString{ |
| Validators: assumeRoleNameValidator, |
| }, |
| }, |
| |
| "source_identity": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "Source identity specified by the principal assuming the role.", |
| }, |
| validateString{ |
| Validators: assumeRoleNameValidator, |
| }, |
| }, |
| |
| "tags": mapAttribute{ |
| configschema.Attribute{ |
| Type: cty.Map(cty.String), |
| Optional: true, |
| Description: "Assume role session tags.", |
| }, |
| validateMap{}, |
| }, |
| |
| "transitive_tag_keys": setAttribute{ |
| configschema.Attribute{ |
| Type: cty.Set(cty.String), |
| Optional: true, |
| Description: "Assume role session tag keys to pass to any subsequent sessions.", |
| }, |
| validateSet{}, |
| }, |
| }, |
| } |
| |
| var assumeRoleWithWebIdentitySchema = singleNestedAttribute{ |
| Attributes: map[string]schemaAttribute{ |
| "role_arn": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Required: true, |
| Description: "The role to be assumed.", |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateStringNotEmpty, |
| validateARN( |
| validateIAMRoleARN, |
| ), |
| }, |
| }, |
| }, |
| |
| "duration": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.", |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateDuration( |
| validateDurationBetween(15*time.Minute, 12*time.Hour), |
| ), |
| }, |
| }, |
| }, |
| |
| "policy": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateStringNotEmpty, |
| validateIAMPolicyDocument, |
| }, |
| }, |
| }, |
| |
| "policy_arns": setAttribute{ |
| configschema.Attribute{ |
| Type: cty.Set(cty.String), |
| Optional: true, |
| Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.", |
| }, |
| validateSet{ |
| Validators: []setValidator{ |
| validateSetStringElements( |
| validateARN( |
| validateIAMPolicyARN, |
| ), |
| ), |
| }, |
| }, |
| }, |
| |
| "session_name": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "The session name to use when assuming the role.", |
| }, |
| validateString{ |
| Validators: assumeRoleNameValidator, |
| }, |
| }, |
| |
| "web_identity_token": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "Value of a web identity token from an OpenID Connect (OIDC) or OAuth provider.", |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateStringLenBetween(4, 20000), |
| }, |
| }, |
| }, |
| |
| "web_identity_token_file": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "File containing a web identity token from an OpenID Connect (OIDC) or OAuth provider.", |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateStringLenBetween(4, 20000), |
| }, |
| }, |
| }, |
| }, |
| validateObject: validateObject{ |
| Validators: []objectValidator{ |
| validateExactlyOneOfAttributes( |
| cty.GetAttrPath("web_identity_token"), |
| cty.GetAttrPath("web_identity_token_file"), |
| ), |
| }, |
| }, |
| } |
| |
| var endpointsSchema = singleNestedAttribute{ |
| Attributes: map[string]schemaAttribute{ |
| "dynamodb": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "A custom endpoint for the DynamoDB API", |
| Deprecated: true, |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateStringLegacyURL, |
| }, |
| }, |
| }, |
| |
| "iam": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "A custom endpoint for the IAM API", |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateStringLegacyURL, |
| }, |
| }, |
| }, |
| |
| "s3": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "A custom endpoint for the S3 API", |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateStringLegacyURL, |
| }, |
| }, |
| }, |
| |
| "sso": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "A custom endpoint for the IAM Identity Center (formerly known as SSO) API", |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateStringValidURL, |
| }, |
| }, |
| }, |
| |
| "sts": stringAttribute{ |
| configschema.Attribute{ |
| Type: cty.String, |
| Optional: true, |
| Description: "A custom endpoint for the STS API", |
| }, |
| validateString{ |
| Validators: []stringValidator{ |
| validateStringLegacyURL, |
| }, |
| }, |
| }, |
| }, |
| } |
| |
| // PrepareConfig checks the validity of the values in the given |
| // configuration, and inserts any missing defaults, assuming that its |
| // structure has already been validated per the schema returned by |
| // ConfigSchema. |
| func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| if obj.IsNull() { |
| return obj, diags |
| } |
| |
| var attrPath cty.Path |
| |
| attrPath = cty.GetAttrPath("bucket") |
| if val := obj.GetAttr("bucket"); val.IsNull() { |
| diags = diags.Append(requiredAttributeErrDiag(attrPath)) |
| } else { |
| bucketValidators := validateString{ |
| Validators: []stringValidator{ |
| validateStringNotEmpty, |
| }, |
| } |
| bucketValidators.ValidateAttr(val, attrPath, &diags) |
| } |
| |
| attrPath = cty.GetAttrPath("key") |
| if val := obj.GetAttr("key"); val.IsNull() { |
| diags = diags.Append(requiredAttributeErrDiag(attrPath)) |
| } else { |
| keyValidators := validateString{ |
| Validators: []stringValidator{ |
| validateStringNotEmpty, |
| validateStringS3Path, |
| validateStringDoesNotContain("//"), |
| }, |
| } |
| keyValidators.ValidateAttr(val, attrPath, &diags) |
| } |
| |
| // Not updating region handling, because validation will be handled by `aws-sdk-go-base` once it is updated |
| if val := obj.GetAttr("region"); val.IsNull() || val.AsString() == "" { |
| if os.Getenv("AWS_REGION") == "" && os.Getenv("AWS_DEFAULT_REGION") == "" { |
| diags = diags.Append(tfdiags.AttributeValue( |
| tfdiags.Error, |
| "Missing region value", |
| `The "region" attribute or the "AWS_REGION" or "AWS_DEFAULT_REGION" environment variables must be set.`, |
| cty.GetAttrPath("region"), |
| )) |
| } |
| } |
| |
| validateAttributesConflict( |
| cty.GetAttrPath("kms_key_id"), |
| cty.GetAttrPath("sse_customer_key"), |
| )(obj, cty.Path{}, &diags) |
| |
| attrPath = cty.GetAttrPath("kms_key_id") |
| if val := obj.GetAttr("kms_key_id"); !val.IsNull() { |
| kmsKeyIDValidators := validateString{ |
| Validators: []stringValidator{ |
| validateStringKMSKey, |
| }, |
| } |
| kmsKeyIDValidators.ValidateAttr(val, attrPath, &diags) |
| } |
| |
| attrPath = cty.GetAttrPath("workspace_key_prefix") |
| if val := obj.GetAttr("workspace_key_prefix"); !val.IsNull() { |
| keyPrefixValidators := validateString{ |
| Validators: []stringValidator{ |
| validateStringS3Path, |
| }, |
| } |
| keyPrefixValidators.ValidateAttr(val, attrPath, &diags) |
| } |
| |
| if val := obj.GetAttr("assume_role"); !val.IsNull() { |
| validateNestedAttribute(assumeRoleSchema, val, cty.GetAttrPath("assume_role"), &diags) |
| } |
| |
| if val := obj.GetAttr("assume_role_with_web_identity"); !val.IsNull() { |
| validateNestedAttribute(assumeRoleWithWebIdentitySchema, val, cty.GetAttrPath("assume_role_with_web_identity"), &diags) |
| } |
| |
| validateAttributesConflict( |
| cty.GetAttrPath("shared_credentials_file"), |
| cty.GetAttrPath("shared_credentials_files"), |
| )(obj, cty.Path{}, &diags) |
| |
| attrPath = cty.GetAttrPath("shared_credentials_file") |
| if val := obj.GetAttr("shared_credentials_file"); !val.IsNull() { |
| diags = diags.Append(deprecatedAttrDiag(attrPath, cty.GetAttrPath("shared_credentials_files"))) |
| } |
| |
| attrPath = cty.GetAttrPath("dynamodb_table") |
| if val := obj.GetAttr("dynamodb_table"); !val.IsNull() { |
| diags = diags.Append(deprecatedAttrDiag(attrPath, cty.GetAttrPath("use_lockfile"))) |
| } |
| |
| endpointFields := map[string]string{ |
| "dynamodb_endpoint": "dynamodb", |
| "iam_endpoint": "iam", |
| "endpoint": "s3", |
| "sts_endpoint": "sts", |
| } |
| endpoints := make(map[string]string) |
| if val := obj.GetAttr("endpoints"); !val.IsNull() { |
| for _, k := range []string{"dynamodb", "iam", "s3", "sts"} { |
| if v := val.GetAttr(k); !v.IsNull() { |
| endpoints[k] = v.AsString() |
| } |
| } |
| } |
| for k, v := range endpointFields { |
| if val := obj.GetAttr(k); !val.IsNull() { |
| diags = diags.Append(deprecatedAttrDiag(cty.GetAttrPath(k), cty.GetAttrPath("endpoints").GetAttr(v))) |
| if _, ok := endpoints[v]; ok { |
| diags = diags.Append(wholeBodyErrDiag( |
| "Conflicting Parameters", |
| fmt.Sprintf(`The parameters "%s" and "%s" cannot be configured together.`, |
| pathString(cty.GetAttrPath(k)), |
| pathString(cty.GetAttrPath("endpoints").GetAttr(v)), |
| ), |
| )) |
| } |
| } |
| } |
| |
| if val := obj.GetAttr("endpoints"); !val.IsNull() { |
| validateNestedAttribute(endpointsSchema, val, cty.GetAttrPath("endpoints"), &diags) |
| } |
| |
| endpointValidators := validateString{ |
| Validators: []stringValidator{ |
| validateStringLegacyURL, |
| }, |
| } |
| for k := range endpointFields { |
| if val := obj.GetAttr(k); !val.IsNull() { |
| attrPath := cty.GetAttrPath(k) |
| endpointValidators.ValidateAttr(val, attrPath, &diags) |
| } |
| } |
| |
| if val := obj.GetAttr("ec2_metadata_service_endpoint"); !val.IsNull() { |
| attrPath := cty.GetAttrPath("ec2_metadata_service_endpoint") |
| ec2MetadataEndpointValidators := validateString{ |
| Validators: []stringValidator{ |
| validateStringValidURL, |
| }, |
| } |
| ec2MetadataEndpointValidators.ValidateAttr(val, attrPath, &diags) |
| } |
| |
| validateAttributesConflict( |
| cty.GetAttrPath("force_path_style"), |
| cty.GetAttrPath("use_path_style"), |
| )(obj, cty.Path{}, &diags) |
| |
| attrPath = cty.GetAttrPath("force_path_style") |
| if val := obj.GetAttr("force_path_style"); !val.IsNull() { |
| diags = diags.Append(deprecatedAttrDiag(attrPath, cty.GetAttrPath("use_path_style"))) |
| } |
| |
| attrPath = cty.GetAttrPath("retry_mode") |
| if val := obj.GetAttr("retry_mode"); !val.IsNull() { |
| retryModeValidators := validateString{ |
| Validators: []stringValidator{ |
| validateStringRetryMode, |
| }, |
| } |
| retryModeValidators.ValidateAttr(val, attrPath, &diags) |
| } |
| |
| attrPath = cty.GetAttrPath("ec2_metadata_service_endpoint_mode") |
| if val := obj.GetAttr("ec2_metadata_service_endpoint_mode"); !val.IsNull() { |
| endpointModeValidators := validateString{ |
| Validators: []stringValidator{ |
| validateStringInSlice(awsbase.EC2MetadataEndpointMode_Values()), |
| }, |
| } |
| endpointModeValidators.ValidateAttr(val, attrPath, &diags) |
| } |
| |
| validateAttributesConflict( |
| cty.GetAttrPath("allowed_account_ids"), |
| cty.GetAttrPath("forbidden_account_ids"), |
| )(obj, cty.Path{}, &diags) |
| |
| return obj, diags |
| } |
| |
| // Configure uses the provided configuration to set configuration fields |
| // within the backend. |
| // |
| // The given configuration is assumed to have already been validated |
| // against the schema returned by ConfigSchema and passed validation |
| // via PrepareConfig. |
| func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { |
| ctx := context.TODO() |
| log := logger() |
| log = logWithOperation(log, operationBackendConfigure) |
| |
| var diags tfdiags.Diagnostics |
| if obj.IsNull() { |
| return diags |
| } |
| |
| var region string |
| if v, ok := stringAttrOk(obj, "region"); ok { |
| region = v |
| } |
| |
| if region != "" && !boolAttr(obj, "skip_region_validation") { |
| if err := validation.SupportedRegion(region); err != nil { |
| diags = diags.Append(tfdiags.AttributeValue( |
| tfdiags.Error, |
| "Invalid region value", |
| firstToUpper(err.Error()), |
| cty.GetAttrPath("region"), |
| )) |
| return diags |
| } |
| } |
| |
| b.bucketName = stringAttr(obj, "bucket") |
| b.keyName = stringAttr(obj, "key") |
| |
| log = log.With( |
| logKeyBucket, b.bucketName, |
| logKeyPath, b.keyName, |
| ) |
| |
| b.acl = stringAttr(obj, "acl") |
| b.workspaceKeyPrefix = stringAttrDefault(obj, "workspace_key_prefix", defaultWorkspaceKeyPrefix) |
| b.serverSideEncryption = boolAttr(obj, "encrypt") |
| b.kmsKeyID = stringAttr(obj, "kms_key_id") |
| b.ddbTable = stringAttr(obj, "dynamodb_table") |
| b.useLockFile = boolAttr(obj, "use_lockfile") |
| b.skipS3Checksum = boolAttr(obj, "skip_s3_checksum") |
| |
| if _, ok := stringAttrOk(obj, "kms_key_id"); ok { |
| if customerKey := os.Getenv("AWS_SSE_CUSTOMER_KEY"); customerKey != "" { |
| diags = diags.Append(wholeBodyErrDiag( |
| "Invalid encryption configuration", |
| encryptionKeyConflictEnvVarError, |
| )) |
| } |
| } |
| |
| if customerKey, ok := stringAttrOk(obj, "sse_customer_key"); ok { |
| if len(customerKey) != 44 { |
| diags = diags.Append(tfdiags.AttributeValue( |
| tfdiags.Error, |
| "Invalid sse_customer_key value", |
| "sse_customer_key must be 44 characters in length", |
| cty.GetAttrPath("sse_customer_key"), |
| )) |
| } else { |
| var err error |
| if b.customerEncryptionKey, err = base64.StdEncoding.DecodeString(customerKey); err != nil { |
| diags = diags.Append(tfdiags.AttributeValue( |
| tfdiags.Error, |
| "Invalid sse_customer_key value", |
| fmt.Sprintf("sse_customer_key must be base64 encoded: %s", err), |
| cty.GetAttrPath("sse_customer_key"), |
| )) |
| } |
| } |
| } else if customerKey := os.Getenv("AWS_SSE_CUSTOMER_KEY"); customerKey != "" { |
| if len(customerKey) != 44 { |
| diags = diags.Append(tfdiags.WholeContainingBody( |
| tfdiags.Error, |
| "Invalid AWS_SSE_CUSTOMER_KEY value", |
| `The environment variable "AWS_SSE_CUSTOMER_KEY" must be 44 characters in length`, |
| )) |
| } else { |
| var err error |
| if b.customerEncryptionKey, err = base64.StdEncoding.DecodeString(customerKey); err != nil { |
| diags = diags.Append(tfdiags.WholeContainingBody( |
| tfdiags.Error, |
| "Invalid AWS_SSE_CUSTOMER_KEY value", |
| fmt.Sprintf(`The environment variable "AWS_SSE_CUSTOMER_KEY" must be base64 encoded: %s`, err), |
| )) |
| } |
| } |
| } |
| |
| endpointEnvvars := map[string]string{ |
| "AWS_DYNAMODB_ENDPOINT": "AWS_ENDPOINT_URL_DYNAMODB", |
| "AWS_IAM_ENDPOINT": "AWS_ENDPOINT_URL_IAM", |
| "AWS_S3_ENDPOINT": "AWS_ENDPOINT_URL_S3", |
| "AWS_STS_ENDPOINT": "AWS_ENDPOINT_URL_STS", |
| "AWS_METADATA_URL": "AWS_EC2_METADATA_SERVICE_ENDPOINT", |
| } |
| for envvar, replacement := range endpointEnvvars { |
| if val := os.Getenv(envvar); val != "" { |
| diags = diags.Append(deprecatedEnvVarDiag(envvar, replacement)) |
| } |
| } |
| |
| ctx, baselog := baselogging.NewHcLogger(ctx, log) |
| |
| cfg := &awsbase.Config{ |
| AccessKey: stringAttr(obj, "access_key"), |
| APNInfo: stdUserAgentProducts(), |
| CallerDocumentationURL: "https://developer.hashicorp.com/terraform/language/backend/s3", |
| CallerName: "S3 Backend", |
| Logger: baselog, |
| MaxRetries: intAttrDefault(obj, "max_retries", 5), |
| Profile: stringAttr(obj, "profile"), |
| HTTPProxyMode: awsbase.HTTPProxyModeLegacy, |
| Region: stringAttr(obj, "region"), |
| SecretKey: stringAttr(obj, "secret_key"), |
| SkipCredsValidation: boolAttr(obj, "skip_credentials_validation"), |
| SkipRequestingAccountId: boolAttr(obj, "skip_requesting_account_id"), |
| Token: stringAttr(obj, "token"), |
| } |
| |
| if val, ok := boolAttrOk(obj, "skip_metadata_api_check"); ok { |
| if val { |
| cfg.EC2MetadataServiceEnableState = imds.ClientDisabled |
| } else { |
| cfg.EC2MetadataServiceEnableState = imds.ClientEnabled |
| } |
| } |
| |
| if v, ok := retrieveArgument(&diags, |
| newAttributeRetriever(obj, cty.GetAttrPath("ec2_metadata_service_endpoint")), |
| newEnvvarRetriever("AWS_EC2_METADATA_SERVICE_ENDPOINT"), |
| newEnvvarRetriever("AWS_METADATA_URL"), |
| ); ok { |
| cfg.EC2MetadataServiceEndpoint = v |
| } |
| |
| if v, ok := retrieveArgument(&diags, |
| newAttributeRetriever(obj, cty.GetAttrPath("ec2_metadata_service_endpoint_mode")), |
| newEnvvarRetriever("AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE"), |
| ); ok { |
| cfg.EC2MetadataServiceEndpointMode = v |
| } |
| |
| if val, ok := stringAttrOk(obj, "shared_credentials_file"); ok { |
| cfg.SharedCredentialsFiles = []string{ |
| val, |
| } |
| } |
| if val, ok := stringSetAttrDefaultEnvVarOk(obj, "shared_credentials_files", "AWS_SHARED_CREDENTIALS_FILE"); ok { |
| cfg.SharedCredentialsFiles = val |
| } |
| if val, ok := stringSetAttrDefaultEnvVarOk(obj, "shared_config_files", "AWS_SHARED_CONFIG_FILE"); ok { |
| cfg.SharedConfigFiles = val |
| } |
| |
| if v, ok := retrieveArgument(&diags, |
| newAttributeRetriever(obj, cty.GetAttrPath("custom_ca_bundle")), |
| newEnvvarRetriever("AWS_CA_BUNDLE"), |
| ); ok { |
| cfg.CustomCABundle = v |
| } |
| |
| if v, ok := retrieveArgument(&diags, |
| newAttributeRetriever(obj, cty.GetAttrPath("endpoints").GetAttr("iam")), |
| newAttributeRetriever(obj, cty.GetAttrPath("iam_endpoint")), |
| newEnvvarRetriever("AWS_ENDPOINT_URL_IAM"), |
| newEnvvarRetriever("AWS_IAM_ENDPOINT"), |
| ); ok { |
| cfg.IamEndpoint = v |
| } |
| |
| if v, ok := retrieveArgument(&diags, |
| newAttributeRetriever(obj, cty.GetAttrPath("endpoints").GetAttr("sso")), |
| newEnvvarRetriever("AWS_ENDPOINT_URL_SSO"), |
| ); ok { |
| cfg.SsoEndpoint = v |
| } |
| |
| if v, ok := retrieveArgument(&diags, |
| newAttributeRetriever(obj, cty.GetAttrPath("endpoints").GetAttr("sts")), |
| newAttributeRetriever(obj, cty.GetAttrPath("sts_endpoint")), |
| newEnvvarRetriever("AWS_ENDPOINT_URL_STS"), |
| newEnvvarRetriever("AWS_STS_ENDPOINT"), |
| ); ok { |
| cfg.StsEndpoint = v |
| } |
| |
| if v, ok := retrieveArgument(&diags, newAttributeRetriever(obj, cty.GetAttrPath("sts_region"))); ok { |
| cfg.StsRegion = v |
| } |
| |
| if assumeRole := obj.GetAttr("assume_role"); !assumeRole.IsNull() { |
| ar := awsbase.AssumeRole{} |
| if val, ok := stringAttrOk(assumeRole, "role_arn"); ok { |
| ar.RoleARN = val |
| } |
| if val, ok := stringAttrOk(assumeRole, "duration"); ok { |
| duration, _ := time.ParseDuration(val) |
| ar.Duration = duration |
| } |
| if val, ok := stringAttrOk(assumeRole, "external_id"); ok { |
| ar.ExternalID = val |
| } |
| if val, ok := stringAttrOk(assumeRole, "policy"); ok { |
| ar.Policy = strings.TrimSpace(val) |
| } |
| if val, ok := stringSetAttrOk(assumeRole, "policy_arns"); ok { |
| ar.PolicyARNs = val |
| } |
| if val, ok := stringAttrOk(assumeRole, "session_name"); ok { |
| ar.SessionName = val |
| } |
| if val, ok := stringAttrOk(assumeRole, "source_identity"); ok { |
| ar.SourceIdentity = val |
| } |
| if val, ok := stringMapAttrOk(assumeRole, "tags"); ok { |
| ar.Tags = val |
| } |
| if val, ok := stringSetAttrOk(assumeRole, "transitive_tag_keys"); ok { |
| ar.TransitiveTagKeys = val |
| } |
| cfg.AssumeRole = []awsbase.AssumeRole{ar} |
| } |
| |
| if assumeRoleWithWebIdentity := obj.GetAttr("assume_role_with_web_identity"); !assumeRoleWithWebIdentity.IsNull() { |
| ar := &awsbase.AssumeRoleWithWebIdentity{} |
| if val, ok := stringAttrOk(assumeRoleWithWebIdentity, "role_arn"); ok { |
| ar.RoleARN = val |
| } |
| if val, ok := stringAttrOk(assumeRoleWithWebIdentity, "duration"); ok { |
| duration, _ := time.ParseDuration(val) |
| ar.Duration = duration |
| } |
| if val, ok := stringAttrOk(assumeRoleWithWebIdentity, "policy"); ok { |
| ar.Policy = strings.TrimSpace(val) |
| } |
| if val, ok := stringSetAttrOk(assumeRoleWithWebIdentity, "policy_arns"); ok { |
| ar.PolicyARNs = val |
| } |
| if val, ok := stringAttrOk(assumeRoleWithWebIdentity, "session_name"); ok { |
| ar.SessionName = val |
| } |
| if val, ok := stringAttrOk(assumeRoleWithWebIdentity, "web_identity_token"); ok { |
| ar.WebIdentityToken = val |
| } |
| if val, ok := stringAttrOk(assumeRoleWithWebIdentity, "web_identity_token_file"); ok { |
| ar.WebIdentityTokenFile = val |
| } |
| cfg.AssumeRoleWithWebIdentity = ar |
| } |
| |
| if v, ok := retrieveArgument(&diags, |
| newAttributeRetriever(obj, cty.GetAttrPath("http_proxy")), |
| ); ok { |
| cfg.HTTPProxy = aws.String(v) |
| } |
| if v, ok := retrieveArgument(&diags, |
| newAttributeRetriever(obj, cty.GetAttrPath("https_proxy")), |
| ); ok { |
| cfg.HTTPSProxy = aws.String(v) |
| } |
| if val, ok := stringAttrOk(obj, "no_proxy"); ok { |
| cfg.NoProxy = val |
| } |
| |
| if val, ok := boolAttrOk(obj, "insecure"); ok { |
| cfg.Insecure = val |
| } |
| if val, ok := boolAttrDefaultEnvVarOk(obj, "use_fips_endpoint", "AWS_USE_FIPS_ENDPOINT"); ok { |
| cfg.UseFIPSEndpoint = val |
| } |
| if val, ok := boolAttrDefaultEnvVarOk(obj, "use_dualstack_endpoint", "AWS_USE_DUALSTACK_ENDPOINT"); ok { |
| cfg.UseDualStackEndpoint = val |
| } |
| |
| if v, ok := retrieveArgument(&diags, |
| newAttributeRetriever(obj, cty.GetAttrPath("retry_mode")), |
| newEnvvarRetriever("AWS_RETRY_MODE"), |
| ); ok { |
| cfg.RetryMode = aws.RetryMode(v) |
| } |
| |
| if val, ok := stringSetAttrOk(obj, "allowed_account_ids"); ok { |
| cfg.AllowedAccountIds = val |
| } |
| if val, ok := stringSetAttrOk(obj, "forbidden_account_ids"); ok { |
| cfg.ForbiddenAccountIds = val |
| } |
| |
| _ /* ctx */, awsConfig, cfgDiags := awsbase.GetAwsConfig(ctx, cfg) |
| for _, d := range cfgDiags { |
| diags = diags.Append(tfdiags.Sourceless( |
| baseSeverityToTerraformSeverity(d.Severity()), |
| d.Summary(), |
| d.Detail(), |
| )) |
| } |
| if diags.HasErrors() { |
| return diags |
| } |
| b.awsConfig = awsConfig |
| |
| accountID, _, awsDiags := awsbase.GetAwsAccountIDAndPartition(ctx, awsConfig, cfg) |
| for _, d := range awsDiags { |
| diags = append(diags, tfdiags.Sourceless( |
| baseSeverityToTerraformSeverity(d.Severity()), |
| fmt.Sprintf("Retrieving AWS account details: %s", d.Summary()), |
| d.Detail(), |
| )) |
| } |
| |
| err := cfg.VerifyAccountIDAllowed(accountID) |
| if err != nil { |
| diags = append(diags, tfdiags.Sourceless( |
| tfdiags.Error, |
| "Invalid account ID", |
| err.Error(), |
| )) |
| } |
| |
| b.dynClient = dynamodb.NewFromConfig(awsConfig, func(opts *dynamodb.Options) { |
| if v, ok := retrieveArgument(&diags, |
| newAttributeRetriever(obj, cty.GetAttrPath("endpoints").GetAttr("dynamodb")), |
| newAttributeRetriever(obj, cty.GetAttrPath("dynamodb_endpoint")), |
| newEnvvarRetriever("AWS_ENDPOINT_URL_DYNAMODB"), |
| newEnvvarRetriever("AWS_DYNAMODB_ENDPOINT"), |
| ); ok { |
| opts.EndpointResolver = dynamodb.EndpointResolverFromURL(v) //nolint:staticcheck // The replacement is not documented yet (2023/08/03) |
| } |
| }) |
| |
| b.s3Client = s3.NewFromConfig(awsConfig, s3.WithAPIOptions(addS3WrongRegionErrorMiddleware), |
| func(opts *s3.Options) { |
| if v, ok := retrieveArgument(&diags, |
| newAttributeRetriever(obj, cty.GetAttrPath("endpoints").GetAttr("s3")), |
| newAttributeRetriever(obj, cty.GetAttrPath("endpoint")), |
| newEnvvarRetriever("AWS_ENDPOINT_URL_S3"), |
| newEnvvarRetriever("AWS_S3_ENDPOINT"), |
| ); ok { |
| opts.EndpointResolver = s3.EndpointResolverFromURL(v) //nolint:staticcheck // The replacement is not documented yet (2023/08/03) |
| } |
| if v, ok := boolAttrOk(obj, "force_path_style"); ok { // deprecated |
| opts.UsePathStyle = v |
| } |
| if v, ok := boolAttrOk(obj, "use_path_style"); ok { |
| opts.UsePathStyle = v |
| } |
| }) |
| |
| return diags |
| } |
| |
| func stdUserAgentProducts() *awsbase.APNInfo { |
| return &awsbase.APNInfo{ |
| PartnerName: "HashiCorp", |
| Products: []awsbase.UserAgentProduct{ |
| {Name: "Terraform", Version: version.String(), Comment: "+https://www.terraform.io"}, |
| }, |
| } |
| } |
| |
| type argumentRetriever interface { |
| Retrieve(diags *tfdiags.Diagnostics) (string, bool) |
| } |
| |
| type attributeRetriever struct { |
| obj cty.Value |
| objPath cty.Path |
| attrPath cty.Path |
| } |
| |
| var _ argumentRetriever = attributeRetriever{} |
| |
| func newAttributeRetriever(obj cty.Value, attrPath cty.Path) attributeRetriever { |
| return attributeRetriever{ |
| obj: obj, |
| objPath: cty.Path{}, // Assumes that we're working relative to the root object |
| attrPath: attrPath, |
| } |
| } |
| |
| func (r attributeRetriever) Retrieve(diags *tfdiags.Diagnostics) (string, bool) { |
| val, err := pathSafeApply(r.attrPath, r.obj) |
| if err != nil { |
| *diags = diags.Append(attributeErrDiag( |
| "Invalid Path for Schema", |
| "The S3 Backend unexpectedly provided a path that does not match the schema. "+ |
| "Please report this to the developers.\n\n"+ |
| "Path: "+pathString(r.attrPath)+"\n\n"+ |
| "Error: "+err.Error(), |
| r.objPath, |
| )) |
| } |
| return stringValueOk(val) |
| } |
| |
| // pathSafeApply applies a `cty.Path` to a `cty.Value`. |
| // Unlike `path.Apply`, it does not return an error if it encounters a Null value |
| func pathSafeApply(path cty.Path, obj cty.Value) (cty.Value, error) { |
| if obj == cty.NilVal || obj.IsNull() { |
| return obj, nil |
| } |
| val := obj |
| var err error |
| for _, step := range path { |
| val, err = step.Apply(val) |
| if err != nil { |
| return cty.NilVal, err |
| } |
| if val == cty.NilVal || val.IsNull() { |
| return val, nil |
| } |
| } |
| return val, nil |
| } |
| |
| type envvarRetriever struct { |
| name string |
| } |
| |
| var _ argumentRetriever = envvarRetriever{} |
| |
| func newEnvvarRetriever(name string) envvarRetriever { |
| return envvarRetriever{ |
| name: name, |
| } |
| } |
| |
| func (r envvarRetriever) Retrieve(_ *tfdiags.Diagnostics) (string, bool) { |
| if v := os.Getenv(r.name); v != "" { |
| return v, true |
| } |
| return "", false |
| } |
| |
| func retrieveArgument(diags *tfdiags.Diagnostics, retrievers ...argumentRetriever) (string, bool) { |
| for _, retriever := range retrievers { |
| if v, ok := retriever.Retrieve(diags); ok { |
| return v, true |
| } |
| } |
| return "", false |
| } |
| |
| func stringValue(val cty.Value) string { |
| v, _ := stringValueOk(val) |
| return v |
| } |
| |
| func stringValueOk(val cty.Value) (string, bool) { |
| if val.IsNull() { |
| return "", false |
| } else { |
| return val.AsString(), true |
| } |
| } |
| |
| func stringAttr(obj cty.Value, name string) string { |
| return stringValue(obj.GetAttr(name)) |
| } |
| |
| func stringAttrOk(obj cty.Value, name string) (string, bool) { |
| return stringValueOk(obj.GetAttr(name)) |
| } |
| |
| func stringAttrDefault(obj cty.Value, name, def string) string { |
| if v, ok := stringAttrOk(obj, name); !ok { |
| return def |
| } else { |
| return v |
| } |
| } |
| |
| func stringSetValueOk(val cty.Value) ([]string, bool) { |
| var list []string |
| typ := val.Type() |
| if !typ.IsSetType() { |
| return nil, false |
| } |
| err := gocty.FromCtyValue(val, &list) |
| if err != nil { |
| return nil, false |
| } |
| return list, true |
| } |
| |
| func stringSetAttrOk(obj cty.Value, name string) ([]string, bool) { |
| return stringSetValueOk(obj.GetAttr(name)) |
| } |
| |
| // stringSetAttrDefaultEnvVarOk checks for a configured set of strings |
| // in the provided argument name or environment variables. A configured |
| // argument takes precedent over environment variables. An environment |
| // variable is assumed to be as a single item, such as how the singular |
| // AWS_SHARED_CONFIG_FILE variable aligns with the underlying |
| // shared_config_files argument. |
| func stringSetAttrDefaultEnvVarOk(obj cty.Value, name string, envvars ...string) ([]string, bool) { |
| if v, ok := stringSetValueOk(obj.GetAttr(name)); !ok { |
| for _, envvar := range envvars { |
| if v := os.Getenv(envvar); v != "" { |
| return []string{v}, true |
| } |
| } |
| return nil, false |
| } else { |
| return v, true |
| } |
| } |
| |
| func stringMapValueOk(val cty.Value) (map[string]string, bool) { |
| var m map[string]string |
| err := gocty.FromCtyValue(val, &m) |
| if err != nil { |
| return nil, false |
| } |
| return m, true |
| } |
| |
| func stringMapAttrOk(obj cty.Value, name string) (map[string]string, bool) { |
| return stringMapValueOk(obj.GetAttr(name)) |
| } |
| |
| func boolAttr(obj cty.Value, name string) bool { |
| v, _ := boolAttrOk(obj, name) |
| return v |
| } |
| |
| func boolAttrOk(obj cty.Value, name string) (bool, bool) { |
| if val := obj.GetAttr(name); val.IsNull() { |
| return false, false |
| } else { |
| return val.True(), true |
| } |
| } |
| |
| // boolAttrDefaultEnvVarOk checks for a configured bool argument or a non-empty |
| // value in any of the provided environment variables. If any of the environment |
| // variables are non-empty, to boolean is considered true. |
| func boolAttrDefaultEnvVarOk(obj cty.Value, name string, envvars ...string) (bool, bool) { |
| if val := obj.GetAttr(name); val.IsNull() { |
| for _, envvar := range envvars { |
| if v := os.Getenv(envvar); v != "" { |
| return true, true |
| } |
| } |
| return false, false |
| } else { |
| return val.True(), true |
| } |
| } |
| |
| func intAttr(obj cty.Value, name string) int { |
| v, _ := intAttrOk(obj, name) |
| return v |
| } |
| |
| func intAttrOk(obj cty.Value, name string) (int, bool) { |
| if val := obj.GetAttr(name); val.IsNull() { |
| return 0, false |
| } else { |
| var v int |
| if err := gocty.FromCtyValue(val, &v); err != nil { |
| return 0, false |
| } |
| return v, true |
| } |
| } |
| |
| func intAttrDefault(obj cty.Value, name string, def int) int { |
| if v, ok := intAttrOk(obj, name); !ok { |
| return def |
| } else { |
| return v |
| } |
| } |
| |
| const encryptionKeyConflictEnvVarError = `Only one of "kms_key_id" and the environment variable "AWS_SSE_CUSTOMER_KEY" can be set. |
| |
| The "kms_key_id" is used for encryption with KMS-Managed Keys (SSE-KMS) |
| while "AWS_SSE_CUSTOMER_KEY" is used for encryption with customer-managed keys (SSE-C). |
| Please choose one or the other.` |
| |
| func validateNestedAttribute(objSchema schemaAttribute, obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics) { |
| if obj.IsNull() { |
| return |
| } |
| |
| na, ok := objSchema.(singleNestedAttribute) |
| if !ok { |
| return |
| } |
| |
| validator := objSchema.Validator() |
| validator.ValidateAttr(obj, objPath, diags) |
| |
| for name, attrSchema := range na.Attributes { |
| attrPath := objPath.GetAttr(name) |
| attrVal := obj.GetAttr(name) |
| |
| if attrVal.IsNull() { |
| if attrSchema.SchemaAttribute().Required { |
| *diags = diags.Append(requiredAttributeErrDiag(attrPath)) |
| } |
| continue |
| } |
| |
| if a, e := attrVal.Type(), attrSchema.SchemaAttribute().Type; a != e { |
| *diags = diags.Append(attributeErrDiag( |
| "Internal Error", |
| fmt.Sprintf(`Expected type to be %s, got: %s`, e.FriendlyName(), a.FriendlyName()), |
| attrPath, |
| )) |
| continue |
| } |
| |
| if attrVal.IsNull() { |
| if attrSchema.SchemaAttribute().Required { |
| *diags = diags.Append(requiredAttributeErrDiag(attrPath)) |
| } |
| continue |
| } |
| |
| validator := attrSchema.Validator() |
| validator.ValidateAttr(attrVal, attrPath, diags) |
| } |
| } |
| |
| func requiredAttributeErrDiag(path cty.Path) tfdiags.Diagnostic { |
| return attributeErrDiag( |
| "Missing Required Value", |
| fmt.Sprintf("The attribute %q is required by the backend.\n\n", pathString(path))+ |
| "Refer to the backend documentation for additional information which attributes are required.", |
| path, |
| ) |
| } |
| |
| func pathString(path cty.Path) string { |
| var buf strings.Builder |
| for i, step := range path { |
| switch x := step.(type) { |
| case cty.GetAttrStep: |
| if i != 0 { |
| buf.WriteString(".") |
| } |
| buf.WriteString(x.Name) |
| case cty.IndexStep: |
| val := x.Key |
| typ := val.Type() |
| var s string |
| switch { |
| case typ == cty.String: |
| s = val.AsString() |
| case typ == cty.Number: |
| num := val.AsBigFloat() |
| s = num.String() |
| default: |
| s = fmt.Sprintf("<unexpected index: %s>", typ.FriendlyName()) |
| } |
| buf.WriteString(fmt.Sprintf("[%s]", s)) |
| default: |
| if i != 0 { |
| buf.WriteString(".") |
| } |
| buf.WriteString(fmt.Sprintf("<unexpected step: %[1]T %[1]v>", x)) |
| } |
| } |
| return buf.String() |
| } |
| |
| type validateSchema interface { |
| ValidateAttr(cty.Value, cty.Path, *tfdiags.Diagnostics) |
| } |
| |
| type validateString struct { |
| Validators []stringValidator |
| } |
| |
| func (v validateString) ValidateAttr(val cty.Value, attrPath cty.Path, diags *tfdiags.Diagnostics) { |
| s := val.AsString() |
| for _, validator := range v.Validators { |
| validator(s, attrPath, diags) |
| if diags.HasErrors() { |
| return |
| } |
| } |
| } |
| |
| type validateMap struct{} |
| |
| func (v validateMap) ValidateAttr(val cty.Value, attrPath cty.Path, diags *tfdiags.Diagnostics) {} |
| |
| type validateSet struct { |
| Validators []setValidator |
| } |
| |
| func (v validateSet) ValidateAttr(val cty.Value, attrPath cty.Path, diags *tfdiags.Diagnostics) { |
| for _, validator := range v.Validators { |
| validator(val, attrPath, diags) |
| if diags.HasErrors() { |
| return |
| } |
| } |
| } |
| |
| type validateObject struct { |
| Validators []objectValidator |
| } |
| |
| func (v validateObject) ValidateAttr(val cty.Value, attrPath cty.Path, diags *tfdiags.Diagnostics) { |
| for _, validator := range v.Validators { |
| validator(val, attrPath, diags) |
| } |
| } |
| |
| type schemaAttribute interface { |
| SchemaAttribute() *configschema.Attribute |
| Validator() validateSchema |
| } |
| |
| var _ schemaAttribute = stringAttribute{} |
| |
| type stringAttribute struct { |
| configschema.Attribute |
| validateString |
| } |
| |
| func (a stringAttribute) SchemaAttribute() *configschema.Attribute { |
| return &a.Attribute |
| } |
| |
| func (a stringAttribute) Validator() validateSchema { |
| return a.validateString |
| } |
| |
| var _ schemaAttribute = setAttribute{} |
| |
| type setAttribute struct { |
| configschema.Attribute |
| validateSet |
| } |
| |
| func (a setAttribute) SchemaAttribute() *configschema.Attribute { |
| return &a.Attribute |
| } |
| |
| func (a setAttribute) Validator() validateSchema { |
| return a.validateSet |
| } |
| |
| var _ schemaAttribute = mapAttribute{} |
| |
| type mapAttribute struct { |
| configschema.Attribute |
| validateMap |
| } |
| |
| func (a mapAttribute) SchemaAttribute() *configschema.Attribute { |
| return &a.Attribute |
| } |
| |
| func (a mapAttribute) Validator() validateSchema { |
| return a.validateMap |
| } |
| |
| type objectSchema map[string]schemaAttribute |
| |
| func (s objectSchema) SchemaAttributes() map[string]*configschema.Attribute { |
| m := make(map[string]*configschema.Attribute, len(s)) |
| for k, v := range s { |
| m[k] = v.SchemaAttribute() |
| } |
| return m |
| } |
| |
| var _ schemaAttribute = singleNestedAttribute{} |
| |
| type singleNestedAttribute struct { |
| Attributes objectSchema |
| Required bool |
| validateObject |
| } |
| |
| func (a singleNestedAttribute) SchemaAttribute() *configschema.Attribute { |
| return &configschema.Attribute{ |
| NestedType: &configschema.Object{ |
| Nesting: configschema.NestingSingle, |
| Attributes: a.Attributes.SchemaAttributes(), |
| }, |
| Required: a.Required, |
| Optional: !a.Required, |
| } |
| } |
| |
| func (a singleNestedAttribute) Validator() validateSchema { |
| return a.validateObject |
| } |
| |
| func deprecatedAttrDiag(attr, replacement cty.Path) tfdiags.Diagnostic { |
| return attributeWarningDiag( |
| "Deprecated Parameter", |
| fmt.Sprintf(`The parameter "%s" is deprecated. Use parameter "%s" instead.`, pathString(attr), pathString(replacement)), |
| attr, |
| ) |
| } |
| |
| func deprecatedEnvVarDiag(envvar, replacement string) tfdiags.Diagnostic { |
| return wholeBodyWarningDiag( |
| "Deprecated Environment Variable", |
| fmt.Sprintf(`The environment variable "%s" is deprecated. Use environment variable "%s" instead.`, envvar, replacement), |
| ) |
| } |
| |
| func firstToUpper(s string) string { |
| r, size := utf8.DecodeRuneInString(s) |
| if r == utf8.RuneError && size <= 1 { |
| return s |
| } |
| lc := unicode.ToUpper(r) |
| if r == lc { |
| return s |
| } |
| return string(lc) + s[size:] |
| } |