github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/remote-state/s3/backend.go (about)

     1  package s3
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"errors"
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/aws/aws-sdk-go/aws"
    11  	"github.com/aws/aws-sdk-go/service/dynamodb"
    12  	"github.com/aws/aws-sdk-go/service/s3"
    13  	awsbase "github.com/hashicorp/aws-sdk-go-base"
    14  	"github.com/hashicorp/terraform/backend"
    15  	"github.com/hashicorp/terraform/helper/logging"
    16  	"github.com/hashicorp/terraform/helper/schema"
    17  	"github.com/hashicorp/terraform/version"
    18  )
    19  
    20  // New creates a new backend for S3 remote state.
    21  func New() backend.Backend {
    22  	s := &schema.Backend{
    23  		Schema: map[string]*schema.Schema{
    24  			"bucket": {
    25  				Type:        schema.TypeString,
    26  				Required:    true,
    27  				Description: "The name of the S3 bucket",
    28  			},
    29  
    30  			"key": {
    31  				Type:        schema.TypeString,
    32  				Required:    true,
    33  				Description: "The path to the state file inside the bucket",
    34  				ValidateFunc: func(v interface{}, s string) ([]string, []error) {
    35  					// s3 will strip leading slashes from an object, so while this will
    36  					// technically be accepted by s3, it will break our workspace hierarchy.
    37  					if strings.HasPrefix(v.(string), "/") {
    38  						return nil, []error{errors.New("key must not start with '/'")}
    39  					}
    40  					return nil, nil
    41  				},
    42  			},
    43  
    44  			"region": {
    45  				Type:        schema.TypeString,
    46  				Required:    true,
    47  				Description: "The region of the S3 bucket.",
    48  				DefaultFunc: schema.MultiEnvDefaultFunc([]string{
    49  					"AWS_REGION",
    50  					"AWS_DEFAULT_REGION",
    51  				}, nil),
    52  			},
    53  
    54  			"dynamodb_endpoint": {
    55  				Type:        schema.TypeString,
    56  				Optional:    true,
    57  				Description: "A custom endpoint for the DynamoDB API",
    58  				DefaultFunc: schema.EnvDefaultFunc("AWS_DYNAMODB_ENDPOINT", ""),
    59  			},
    60  
    61  			"endpoint": {
    62  				Type:        schema.TypeString,
    63  				Optional:    true,
    64  				Description: "A custom endpoint for the S3 API",
    65  				DefaultFunc: schema.EnvDefaultFunc("AWS_S3_ENDPOINT", ""),
    66  			},
    67  
    68  			"iam_endpoint": {
    69  				Type:        schema.TypeString,
    70  				Optional:    true,
    71  				Description: "A custom endpoint for the IAM API",
    72  				DefaultFunc: schema.EnvDefaultFunc("AWS_IAM_ENDPOINT", ""),
    73  			},
    74  
    75  			"sts_endpoint": {
    76  				Type:        schema.TypeString,
    77  				Optional:    true,
    78  				Description: "A custom endpoint for the STS API",
    79  				DefaultFunc: schema.EnvDefaultFunc("AWS_STS_ENDPOINT", ""),
    80  			},
    81  
    82  			"encrypt": {
    83  				Type:        schema.TypeBool,
    84  				Optional:    true,
    85  				Description: "Whether to enable server side encryption of the state file",
    86  				Default:     false,
    87  			},
    88  
    89  			"acl": {
    90  				Type:        schema.TypeString,
    91  				Optional:    true,
    92  				Description: "Canned ACL to be applied to the state file",
    93  				Default:     "",
    94  			},
    95  
    96  			"access_key": {
    97  				Type:        schema.TypeString,
    98  				Optional:    true,
    99  				Description: "AWS access key",
   100  				Default:     "",
   101  			},
   102  
   103  			"secret_key": {
   104  				Type:        schema.TypeString,
   105  				Optional:    true,
   106  				Description: "AWS secret key",
   107  				Default:     "",
   108  			},
   109  
   110  			"kms_key_id": {
   111  				Type:        schema.TypeString,
   112  				Optional:    true,
   113  				Description: "The ARN of a KMS Key to use for encrypting the state",
   114  				Default:     "",
   115  			},
   116  
   117  			"lock_table": {
   118  				Type:        schema.TypeString,
   119  				Optional:    true,
   120  				Description: "DynamoDB table for state locking",
   121  				Default:     "",
   122  				Deprecated:  "please use the dynamodb_table attribute",
   123  			},
   124  
   125  			"dynamodb_table": {
   126  				Type:        schema.TypeString,
   127  				Optional:    true,
   128  				Description: "DynamoDB table for state locking and consistency",
   129  				Default:     "",
   130  			},
   131  
   132  			"profile": {
   133  				Type:        schema.TypeString,
   134  				Optional:    true,
   135  				Description: "AWS profile name",
   136  				Default:     "",
   137  			},
   138  
   139  			"shared_credentials_file": {
   140  				Type:        schema.TypeString,
   141  				Optional:    true,
   142  				Description: "Path to a shared credentials file",
   143  				Default:     "",
   144  			},
   145  
   146  			"token": {
   147  				Type:        schema.TypeString,
   148  				Optional:    true,
   149  				Description: "MFA token",
   150  				Default:     "",
   151  			},
   152  
   153  			"skip_credentials_validation": {
   154  				Type:        schema.TypeBool,
   155  				Optional:    true,
   156  				Description: "Skip the credentials validation via STS API.",
   157  				Default:     false,
   158  			},
   159  
   160  			"skip_get_ec2_platforms": {
   161  				Type:        schema.TypeBool,
   162  				Optional:    true,
   163  				Description: "Skip getting the supported EC2 platforms.",
   164  				Default:     false,
   165  				Deprecated:  "The S3 Backend does not require EC2 functionality and this attribute is no longer used.",
   166  			},
   167  
   168  			"skip_region_validation": {
   169  				Type:        schema.TypeBool,
   170  				Optional:    true,
   171  				Description: "Skip static validation of region name.",
   172  				Default:     false,
   173  			},
   174  
   175  			"skip_requesting_account_id": {
   176  				Type:        schema.TypeBool,
   177  				Optional:    true,
   178  				Description: "Skip requesting the account ID.",
   179  				Default:     false,
   180  				Deprecated:  "The S3 Backend no longer automatically looks up the AWS Account ID and this attribute is no longer used.",
   181  			},
   182  
   183  			"skip_metadata_api_check": {
   184  				Type:        schema.TypeBool,
   185  				Optional:    true,
   186  				Description: "Skip the AWS Metadata API check.",
   187  				Default:     false,
   188  			},
   189  
   190  			"sse_customer_key": {
   191  				Type:        schema.TypeString,
   192  				Optional:    true,
   193  				Description: "The base64-encoded encryption key to use for server-side encryption with customer-provided keys (SSE-C).",
   194  				DefaultFunc: schema.EnvDefaultFunc("AWS_SSE_CUSTOMER_KEY", ""),
   195  				Sensitive:   true,
   196  				ValidateFunc: func(v interface{}, s string) ([]string, []error) {
   197  					key := v.(string)
   198  					if key != "" && len(key) != 44 {
   199  						return nil, []error{errors.New("sse_customer_key must be 44 characters in length (256 bits, base64 encoded)")}
   200  					}
   201  					return nil, nil
   202  				},
   203  			},
   204  
   205  			"role_arn": {
   206  				Type:        schema.TypeString,
   207  				Optional:    true,
   208  				Description: "The role to be assumed",
   209  				Default:     "",
   210  			},
   211  
   212  			"session_name": {
   213  				Type:        schema.TypeString,
   214  				Optional:    true,
   215  				Description: "The session name to use when assuming the role.",
   216  				Default:     "",
   217  			},
   218  
   219  			"external_id": {
   220  				Type:        schema.TypeString,
   221  				Optional:    true,
   222  				Description: "The external ID to use when assuming the role",
   223  				Default:     "",
   224  			},
   225  
   226  			"assume_role_policy": {
   227  				Type:        schema.TypeString,
   228  				Optional:    true,
   229  				Description: "The permissions applied when assuming a role.",
   230  				Default:     "",
   231  			},
   232  
   233  			"workspace_key_prefix": {
   234  				Type:        schema.TypeString,
   235  				Optional:    true,
   236  				Description: "The prefix applied to the non-default state path inside the bucket.",
   237  				Default:     "env:",
   238  				ValidateFunc: func(v interface{}, s string) ([]string, []error) {
   239  					prefix := v.(string)
   240  					if strings.HasPrefix(prefix, "/") || strings.HasSuffix(prefix, "/") {
   241  						return nil, []error{errors.New("workspace_key_prefix must not start or end with '/'")}
   242  					}
   243  					return nil, nil
   244  				},
   245  			},
   246  
   247  			"force_path_style": {
   248  				Type:        schema.TypeBool,
   249  				Optional:    true,
   250  				Description: "Force s3 to use path style api.",
   251  				Default:     false,
   252  			},
   253  
   254  			"max_retries": {
   255  				Type:        schema.TypeInt,
   256  				Optional:    true,
   257  				Description: "The maximum number of times an AWS API request is retried on retryable failure.",
   258  				Default:     5,
   259  			},
   260  		},
   261  	}
   262  
   263  	result := &Backend{Backend: s}
   264  	result.Backend.ConfigureFunc = result.configure
   265  	return result
   266  }
   267  
   268  type Backend struct {
   269  	*schema.Backend
   270  
   271  	// The fields below are set from configure
   272  	s3Client  *s3.S3
   273  	dynClient *dynamodb.DynamoDB
   274  
   275  	bucketName            string
   276  	keyName               string
   277  	serverSideEncryption  bool
   278  	customerEncryptionKey []byte
   279  	acl                   string
   280  	kmsKeyID              string
   281  	ddbTable              string
   282  	workspaceKeyPrefix    string
   283  }
   284  
   285  func (b *Backend) configure(ctx context.Context) error {
   286  	if b.s3Client != nil {
   287  		return nil
   288  	}
   289  
   290  	// Grab the resource data
   291  	data := schema.FromContextBackendConfig(ctx)
   292  
   293  	if !data.Get("skip_region_validation").(bool) {
   294  		if err := awsbase.ValidateRegion(data.Get("region").(string)); err != nil {
   295  			return err
   296  		}
   297  	}
   298  
   299  	b.bucketName = data.Get("bucket").(string)
   300  	b.keyName = data.Get("key").(string)
   301  	b.acl = data.Get("acl").(string)
   302  	b.workspaceKeyPrefix = data.Get("workspace_key_prefix").(string)
   303  	b.serverSideEncryption = data.Get("encrypt").(bool)
   304  	b.kmsKeyID = data.Get("kms_key_id").(string)
   305  
   306  	customerKeyString := data.Get("sse_customer_key").(string)
   307  	if customerKeyString != "" {
   308  		if b.kmsKeyID != "" {
   309  			return errors.New(encryptionKeyConflictError)
   310  		}
   311  
   312  		var err error
   313  		b.customerEncryptionKey, err = base64.StdEncoding.DecodeString(customerKeyString)
   314  		if err != nil {
   315  			return fmt.Errorf("Failed to decode sse_customer_key: %s", err.Error())
   316  		}
   317  	}
   318  
   319  	b.ddbTable = data.Get("dynamodb_table").(string)
   320  	if b.ddbTable == "" {
   321  		// try the deprecated field
   322  		b.ddbTable = data.Get("lock_table").(string)
   323  	}
   324  
   325  	cfg := &awsbase.Config{
   326  		AccessKey:             data.Get("access_key").(string),
   327  		AssumeRoleARN:         data.Get("role_arn").(string),
   328  		AssumeRoleExternalID:  data.Get("external_id").(string),
   329  		AssumeRolePolicy:      data.Get("assume_role_policy").(string),
   330  		AssumeRoleSessionName: data.Get("session_name").(string),
   331  		CredsFilename:         data.Get("shared_credentials_file").(string),
   332  		DebugLogging:          logging.IsDebugOrHigher(),
   333  		IamEndpoint:           data.Get("iam_endpoint").(string),
   334  		MaxRetries:            data.Get("max_retries").(int),
   335  		Profile:               data.Get("profile").(string),
   336  		Region:                data.Get("region").(string),
   337  		SecretKey:             data.Get("secret_key").(string),
   338  		SkipCredsValidation:   data.Get("skip_credentials_validation").(bool),
   339  		SkipMetadataApiCheck:  data.Get("skip_metadata_api_check").(bool),
   340  		StsEndpoint:           data.Get("sts_endpoint").(string),
   341  		Token:                 data.Get("token").(string),
   342  		UserAgentProducts: []*awsbase.UserAgentProduct{
   343  			{Name: "APN", Version: "1.0"},
   344  			{Name: "HashiCorp", Version: "1.0"},
   345  			{Name: "Terraform", Version: version.String()},
   346  		},
   347  	}
   348  
   349  	sess, err := awsbase.GetSession(cfg)
   350  	if err != nil {
   351  		return err
   352  	}
   353  
   354  	b.dynClient = dynamodb.New(sess.Copy(&aws.Config{
   355  		Endpoint: aws.String(data.Get("dynamodb_endpoint").(string)),
   356  	}))
   357  	b.s3Client = s3.New(sess.Copy(&aws.Config{
   358  		Endpoint:         aws.String(data.Get("endpoint").(string)),
   359  		S3ForcePathStyle: aws.Bool(data.Get("force_path_style").(bool)),
   360  	}))
   361  
   362  	return nil
   363  }
   364  
   365  const encryptionKeyConflictError = `Cannot have both kms_key_id and sse_customer_key set.
   366  
   367  The kms_key_id is used for encryption with KMS-Managed Keys (SSE-KMS)
   368  while sse_customer_key is used for encryption with customer-managed keys (SSE-C).
   369  Please choose one or the other.`