409 lines
12 KiB
Go
409 lines
12 KiB
Go
package s3
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/service/dynamodb"
|
|
"github.com/aws/aws-sdk-go/service/s3"
|
|
awsbase "github.com/hashicorp/aws-sdk-go-base"
|
|
"github.com/hashicorp/terraform/backend"
|
|
"github.com/hashicorp/terraform/helper/logging"
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
"github.com/hashicorp/terraform/version"
|
|
)
|
|
|
|
// New creates a new backend for S3 remote state.
|
|
func New() backend.Backend {
|
|
s := &schema.Backend{
|
|
Schema: map[string]*schema.Schema{
|
|
"bucket": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
Description: "The name of the S3 bucket",
|
|
},
|
|
|
|
"key": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
Description: "The path to the state file inside the bucket",
|
|
ValidateFunc: func(v interface{}, s string) ([]string, []error) {
|
|
// s3 will strip leading slashes from an object, so while this will
|
|
// technically be accepted by s3, it will break our workspace hierarchy.
|
|
if strings.HasPrefix(v.(string), "/") {
|
|
return nil, []error{errors.New("key must not start with '/'")}
|
|
}
|
|
return nil, nil
|
|
},
|
|
},
|
|
|
|
"region": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
Description: "AWS region of the S3 Bucket and DynamoDB Table (if used).",
|
|
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
|
|
"AWS_REGION",
|
|
"AWS_DEFAULT_REGION",
|
|
}, nil),
|
|
},
|
|
|
|
"dynamodb_endpoint": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "A custom endpoint for the DynamoDB API",
|
|
DefaultFunc: schema.EnvDefaultFunc("AWS_DYNAMODB_ENDPOINT", ""),
|
|
},
|
|
|
|
"endpoint": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "A custom endpoint for the S3 API",
|
|
DefaultFunc: schema.EnvDefaultFunc("AWS_S3_ENDPOINT", ""),
|
|
},
|
|
|
|
"iam_endpoint": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "A custom endpoint for the IAM API",
|
|
DefaultFunc: schema.EnvDefaultFunc("AWS_IAM_ENDPOINT", ""),
|
|
},
|
|
|
|
"sts_endpoint": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "A custom endpoint for the STS API",
|
|
DefaultFunc: schema.EnvDefaultFunc("AWS_STS_ENDPOINT", ""),
|
|
},
|
|
|
|
"encrypt": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Description: "Whether to enable server side encryption of the state file",
|
|
Default: false,
|
|
},
|
|
|
|
"acl": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "Canned ACL to be applied to the state file",
|
|
Default: "",
|
|
},
|
|
|
|
"access_key": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "AWS access key",
|
|
Default: "",
|
|
},
|
|
|
|
"secret_key": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "AWS secret key",
|
|
Default: "",
|
|
},
|
|
|
|
"kms_key_id": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "The ARN of a KMS Key to use for encrypting the state",
|
|
Default: "",
|
|
},
|
|
|
|
"dynamodb_table": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "DynamoDB table for state locking and consistency",
|
|
Default: "",
|
|
},
|
|
|
|
"profile": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "AWS profile name",
|
|
Default: "",
|
|
},
|
|
|
|
"shared_credentials_file": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "Path to a shared credentials file",
|
|
Default: "",
|
|
},
|
|
|
|
"token": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "MFA token",
|
|
Default: "",
|
|
},
|
|
|
|
"skip_credentials_validation": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Description: "Skip the credentials validation via STS API.",
|
|
Default: false,
|
|
},
|
|
|
|
"skip_region_validation": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Description: "Skip static validation of region name.",
|
|
Default: false,
|
|
},
|
|
|
|
"skip_metadata_api_check": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Description: "Skip the AWS Metadata API check.",
|
|
Default: false,
|
|
},
|
|
|
|
"sse_customer_key": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "The base64-encoded encryption key to use for server-side encryption with customer-provided keys (SSE-C).",
|
|
DefaultFunc: schema.EnvDefaultFunc("AWS_SSE_CUSTOMER_KEY", ""),
|
|
Sensitive: true,
|
|
ValidateFunc: func(v interface{}, s string) ([]string, []error) {
|
|
key := v.(string)
|
|
if key != "" && len(key) != 44 {
|
|
return nil, []error{errors.New("sse_customer_key must be 44 characters in length (256 bits, base64 encoded)")}
|
|
}
|
|
return nil, nil
|
|
},
|
|
},
|
|
|
|
"role_arn": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "The role to be assumed",
|
|
Default: "",
|
|
},
|
|
|
|
"session_name": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "The session name to use when assuming the role.",
|
|
Default: "",
|
|
},
|
|
|
|
"external_id": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "The external ID to use when assuming the role",
|
|
Default: "",
|
|
},
|
|
|
|
"assume_role_duration_seconds": {
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Description: "Seconds to restrict the assume role session duration.",
|
|
},
|
|
|
|
"assume_role_policy": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.",
|
|
Default: "",
|
|
},
|
|
|
|
"assume_role_policy_arns": {
|
|
Type: schema.TypeSet,
|
|
Optional: true,
|
|
Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.",
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
},
|
|
|
|
"assume_role_tags": {
|
|
Type: schema.TypeMap,
|
|
Optional: true,
|
|
Description: "Assume role session tags.",
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
},
|
|
|
|
"assume_role_transitive_tag_keys": {
|
|
Type: schema.TypeSet,
|
|
Optional: true,
|
|
Description: "Assume role session tag keys to pass to any subsequent sessions.",
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
},
|
|
|
|
"workspace_key_prefix": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "The prefix applied to the non-default state path inside the bucket.",
|
|
Default: "env:",
|
|
ValidateFunc: func(v interface{}, s string) ([]string, []error) {
|
|
prefix := v.(string)
|
|
if strings.HasPrefix(prefix, "/") || strings.HasSuffix(prefix, "/") {
|
|
return nil, []error{errors.New("workspace_key_prefix must not start or end with '/'")}
|
|
}
|
|
return nil, nil
|
|
},
|
|
},
|
|
|
|
"force_path_style": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Description: "Force s3 to use path style api.",
|
|
Default: false,
|
|
},
|
|
|
|
"max_retries": {
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Description: "The maximum number of times an AWS API request is retried on retryable failure.",
|
|
Default: 5,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := &Backend{Backend: s}
|
|
result.Backend.ConfigureFunc = result.configure
|
|
return result
|
|
}
|
|
|
|
type Backend struct {
|
|
*schema.Backend
|
|
|
|
// The fields below are set from configure
|
|
s3Client *s3.S3
|
|
dynClient *dynamodb.DynamoDB
|
|
|
|
bucketName string
|
|
keyName string
|
|
serverSideEncryption bool
|
|
customerEncryptionKey []byte
|
|
acl string
|
|
kmsKeyID string
|
|
ddbTable string
|
|
workspaceKeyPrefix string
|
|
}
|
|
|
|
func (b *Backend) configure(ctx context.Context) error {
|
|
if b.s3Client != nil {
|
|
return nil
|
|
}
|
|
|
|
// Grab the resource data
|
|
data := schema.FromContextBackendConfig(ctx)
|
|
|
|
if !data.Get("skip_region_validation").(bool) {
|
|
if err := awsbase.ValidateRegion(data.Get("region").(string)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
b.bucketName = data.Get("bucket").(string)
|
|
b.keyName = data.Get("key").(string)
|
|
b.acl = data.Get("acl").(string)
|
|
b.workspaceKeyPrefix = data.Get("workspace_key_prefix").(string)
|
|
b.serverSideEncryption = data.Get("encrypt").(bool)
|
|
b.kmsKeyID = data.Get("kms_key_id").(string)
|
|
b.ddbTable = data.Get("dynamodb_table").(string)
|
|
|
|
customerKeyString := data.Get("sse_customer_key").(string)
|
|
if customerKeyString != "" {
|
|
if b.kmsKeyID != "" {
|
|
return errors.New(encryptionKeyConflictError)
|
|
}
|
|
|
|
var err error
|
|
b.customerEncryptionKey, err = base64.StdEncoding.DecodeString(customerKeyString)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to decode sse_customer_key: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
cfg := &awsbase.Config{
|
|
AccessKey: data.Get("access_key").(string),
|
|
AssumeRoleARN: data.Get("role_arn").(string),
|
|
AssumeRoleDurationSeconds: data.Get("assume_role_duration_seconds").(int),
|
|
AssumeRoleExternalID: data.Get("external_id").(string),
|
|
AssumeRolePolicy: data.Get("assume_role_policy").(string),
|
|
AssumeRoleSessionName: data.Get("session_name").(string),
|
|
CallerDocumentationURL: "https://www.terraform.io/docs/backends/types/s3.html",
|
|
CallerName: "S3 Backend",
|
|
CredsFilename: data.Get("shared_credentials_file").(string),
|
|
DebugLogging: logging.IsDebugOrHigher(),
|
|
IamEndpoint: data.Get("iam_endpoint").(string),
|
|
MaxRetries: data.Get("max_retries").(int),
|
|
Profile: data.Get("profile").(string),
|
|
Region: data.Get("region").(string),
|
|
SecretKey: data.Get("secret_key").(string),
|
|
SkipCredsValidation: data.Get("skip_credentials_validation").(bool),
|
|
SkipMetadataApiCheck: data.Get("skip_metadata_api_check").(bool),
|
|
StsEndpoint: data.Get("sts_endpoint").(string),
|
|
Token: data.Get("token").(string),
|
|
UserAgentProducts: []*awsbase.UserAgentProduct{
|
|
{Name: "APN", Version: "1.0"},
|
|
{Name: "HashiCorp", Version: "1.0"},
|
|
{Name: "Terraform", Version: version.String()},
|
|
},
|
|
}
|
|
|
|
if policyARNSet := data.Get("assume_role_policy_arns").(*schema.Set); policyARNSet.Len() > 0 {
|
|
for _, policyARNRaw := range policyARNSet.List() {
|
|
policyARN, ok := policyARNRaw.(string)
|
|
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
cfg.AssumeRolePolicyARNs = append(cfg.AssumeRolePolicyARNs, policyARN)
|
|
}
|
|
}
|
|
|
|
if tagMap := data.Get("assume_role_tags").(map[string]interface{}); len(tagMap) > 0 {
|
|
cfg.AssumeRoleTags = make(map[string]string)
|
|
|
|
for k, vRaw := range tagMap {
|
|
v, ok := vRaw.(string)
|
|
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
cfg.AssumeRoleTags[k] = v
|
|
}
|
|
}
|
|
|
|
if transitiveTagKeySet := data.Get("assume_role_transitive_tag_keys").(*schema.Set); transitiveTagKeySet.Len() > 0 {
|
|
for _, transitiveTagKeyRaw := range transitiveTagKeySet.List() {
|
|
transitiveTagKey, ok := transitiveTagKeyRaw.(string)
|
|
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
cfg.AssumeRoleTransitiveTagKeys = append(cfg.AssumeRoleTransitiveTagKeys, transitiveTagKey)
|
|
}
|
|
}
|
|
|
|
sess, err := awsbase.GetSession(cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("error configuring S3 Backend: %w", err)
|
|
}
|
|
|
|
b.dynClient = dynamodb.New(sess.Copy(&aws.Config{
|
|
Endpoint: aws.String(data.Get("dynamodb_endpoint").(string)),
|
|
}))
|
|
b.s3Client = s3.New(sess.Copy(&aws.Config{
|
|
Endpoint: aws.String(data.Get("endpoint").(string)),
|
|
S3ForcePathStyle: aws.Bool(data.Get("force_path_style").(bool)),
|
|
}))
|
|
|
|
return nil
|
|
}
|
|
|
|
const encryptionKeyConflictError = `Cannot have both kms_key_id and sse_customer_key set.
|
|
|
|
The kms_key_id is used for encryption with KMS-Managed Keys (SSE-KMS)
|
|
while sse_customer_key is used for encryption with customer-managed keys (SSE-C).
|
|
Please choose one or the other.`
|