blob: be1af3f6fd190aeab6a1338b8d986ede05e6502c [file] [log] [blame]
// 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:]
}