github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/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/internal/backend"
    15  	"github.com/hashicorp/terraform/internal/legacy/helper/schema"
    16  	"github.com/hashicorp/terraform/internal/logging"
    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  					// s3 will recognize objects with a trailing slash as a directory
    41  					// so they should not be valid keys
    42  					if strings.HasSuffix(v.(string), "/") {
    43  						return nil, []error{errors.New("key must not end with '/'")}
    44  					}
    45  					return nil, nil
    46  				},
    47  			},
    48  
    49  			"region": {
    50  				Type:        schema.TypeString,
    51  				Required:    true,
    52  				Description: "AWS region of the S3 Bucket and DynamoDB Table (if used).",
    53  				DefaultFunc: schema.MultiEnvDefaultFunc([]string{
    54  					"AWS_REGION",
    55  					"AWS_DEFAULT_REGION",
    56  				}, nil),
    57  			},
    58  
    59  			"dynamodb_endpoint": {
    60  				Type:        schema.TypeString,
    61  				Optional:    true,
    62  				Description: "A custom endpoint for the DynamoDB API",
    63  				DefaultFunc: schema.EnvDefaultFunc("AWS_DYNAMODB_ENDPOINT", ""),
    64  			},
    65  
    66  			"endpoint": {
    67  				Type:        schema.TypeString,
    68  				Optional:    true,
    69  				Description: "A custom endpoint for the S3 API",
    70  				DefaultFunc: schema.EnvDefaultFunc("AWS_S3_ENDPOINT", ""),
    71  			},
    72  
    73  			"iam_endpoint": {
    74  				Type:        schema.TypeString,
    75  				Optional:    true,
    76  				Description: "A custom endpoint for the IAM API",
    77  				DefaultFunc: schema.EnvDefaultFunc("AWS_IAM_ENDPOINT", ""),
    78  			},
    79  
    80  			"sts_endpoint": {
    81  				Type:        schema.TypeString,
    82  				Optional:    true,
    83  				Description: "A custom endpoint for the STS API",
    84  				DefaultFunc: schema.EnvDefaultFunc("AWS_STS_ENDPOINT", ""),
    85  			},
    86  
    87  			"encrypt": {
    88  				Type:        schema.TypeBool,
    89  				Optional:    true,
    90  				Description: "Whether to enable server side encryption of the state file",
    91  				Default:     false,
    92  			},
    93  
    94  			"acl": {
    95  				Type:        schema.TypeString,
    96  				Optional:    true,
    97  				Description: "Canned ACL to be applied to the state file",
    98  				Default:     "",
    99  			},
   100  
   101  			"access_key": {
   102  				Type:        schema.TypeString,
   103  				Optional:    true,
   104  				Description: "AWS access key",
   105  				Default:     "",
   106  			},
   107  
   108  			"secret_key": {
   109  				Type:        schema.TypeString,
   110  				Optional:    true,
   111  				Description: "AWS secret key",
   112  				Default:     "",
   113  			},
   114  
   115  			"kms_key_id": {
   116  				Type:        schema.TypeString,
   117  				Optional:    true,
   118  				Description: "The ARN of a KMS Key to use for encrypting the state",
   119  				Default:     "",
   120  			},
   121  
   122  			"dynamodb_table": {
   123  				Type:        schema.TypeString,
   124  				Optional:    true,
   125  				Description: "DynamoDB table for state locking and consistency",
   126  				Default:     "",
   127  			},
   128  
   129  			"profile": {
   130  				Type:        schema.TypeString,
   131  				Optional:    true,
   132  				Description: "AWS profile name",
   133  				Default:     "",
   134  			},
   135  
   136  			"shared_credentials_file": {
   137  				Type:        schema.TypeString,
   138  				Optional:    true,
   139  				Description: "Path to a shared credentials file",
   140  				Default:     "",
   141  			},
   142  
   143  			"token": {
   144  				Type:        schema.TypeString,
   145  				Optional:    true,
   146  				Description: "MFA token",
   147  				Default:     "",
   148  			},
   149  
   150  			"skip_credentials_validation": {
   151  				Type:        schema.TypeBool,
   152  				Optional:    true,
   153  				Description: "Skip the credentials validation via STS API.",
   154  				Default:     false,
   155  			},
   156  
   157  			"skip_region_validation": {
   158  				Type:        schema.TypeBool,
   159  				Optional:    true,
   160  				Description: "Skip static validation of region name.",
   161  				Default:     false,
   162  			},
   163  
   164  			"skip_metadata_api_check": {
   165  				Type:        schema.TypeBool,
   166  				Optional:    true,
   167  				Description: "Skip the AWS Metadata API check.",
   168  				Default:     false,
   169  			},
   170  
   171  			"sse_customer_key": {
   172  				Type:        schema.TypeString,
   173  				Optional:    true,
   174  				Description: "The base64-encoded encryption key to use for server-side encryption with customer-provided keys (SSE-C).",
   175  				DefaultFunc: schema.EnvDefaultFunc("AWS_SSE_CUSTOMER_KEY", ""),
   176  				Sensitive:   true,
   177  				ValidateFunc: func(v interface{}, s string) ([]string, []error) {
   178  					key := v.(string)
   179  					if key != "" && len(key) != 44 {
   180  						return nil, []error{errors.New("sse_customer_key must be 44 characters in length (256 bits, base64 encoded)")}
   181  					}
   182  					return nil, nil
   183  				},
   184  			},
   185  
   186  			"role_arn": {
   187  				Type:        schema.TypeString,
   188  				Optional:    true,
   189  				Description: "The role to be assumed",
   190  				Default:     "",
   191  			},
   192  
   193  			"session_name": {
   194  				Type:        schema.TypeString,
   195  				Optional:    true,
   196  				Description: "The session name to use when assuming the role.",
   197  				Default:     "",
   198  			},
   199  
   200  			"external_id": {
   201  				Type:        schema.TypeString,
   202  				Optional:    true,
   203  				Description: "The external ID to use when assuming the role",
   204  				Default:     "",
   205  			},
   206  
   207  			"assume_role_duration_seconds": {
   208  				Type:        schema.TypeInt,
   209  				Optional:    true,
   210  				Description: "Seconds to restrict the assume role session duration.",
   211  			},
   212  
   213  			"assume_role_policy": {
   214  				Type:        schema.TypeString,
   215  				Optional:    true,
   216  				Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.",
   217  				Default:     "",
   218  			},
   219  
   220  			"assume_role_policy_arns": {
   221  				Type:        schema.TypeSet,
   222  				Optional:    true,
   223  				Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.",
   224  				Elem:        &schema.Schema{Type: schema.TypeString},
   225  			},
   226  
   227  			"assume_role_tags": {
   228  				Type:        schema.TypeMap,
   229  				Optional:    true,
   230  				Description: "Assume role session tags.",
   231  				Elem:        &schema.Schema{Type: schema.TypeString},
   232  			},
   233  
   234  			"assume_role_transitive_tag_keys": {
   235  				Type:        schema.TypeSet,
   236  				Optional:    true,
   237  				Description: "Assume role session tag keys to pass to any subsequent sessions.",
   238  				Elem:        &schema.Schema{Type: schema.TypeString},
   239  			},
   240  
   241  			"workspace_key_prefix": {
   242  				Type:        schema.TypeString,
   243  				Optional:    true,
   244  				Description: "The prefix applied to the non-default state path inside the bucket.",
   245  				Default:     "env:",
   246  				ValidateFunc: func(v interface{}, s string) ([]string, []error) {
   247  					prefix := v.(string)
   248  					if strings.HasPrefix(prefix, "/") || strings.HasSuffix(prefix, "/") {
   249  						return nil, []error{errors.New("workspace_key_prefix must not start or end with '/'")}
   250  					}
   251  					return nil, nil
   252  				},
   253  			},
   254  
   255  			"force_path_style": {
   256  				Type:        schema.TypeBool,
   257  				Optional:    true,
   258  				Description: "Force s3 to use path style api.",
   259  				Default:     false,
   260  			},
   261  
   262  			"max_retries": {
   263  				Type:        schema.TypeInt,
   264  				Optional:    true,
   265  				Description: "The maximum number of times an AWS API request is retried on retryable failure.",
   266  				Default:     5,
   267  			},
   268  		},
   269  	}
   270  
   271  	result := &Backend{Backend: s}
   272  	result.Backend.ConfigureFunc = result.configure
   273  	return result
   274  }
   275  
   276  type Backend struct {
   277  	*schema.Backend
   278  
   279  	// The fields below are set from configure
   280  	s3Client  *s3.S3
   281  	dynClient *dynamodb.DynamoDB
   282  
   283  	bucketName            string
   284  	keyName               string
   285  	serverSideEncryption  bool
   286  	customerEncryptionKey []byte
   287  	acl                   string
   288  	kmsKeyID              string
   289  	ddbTable              string
   290  	workspaceKeyPrefix    string
   291  }
   292  
   293  func (b *Backend) configure(ctx context.Context) error {
   294  	if b.s3Client != nil {
   295  		return nil
   296  	}
   297  
   298  	// Grab the resource data
   299  	data := schema.FromContextBackendConfig(ctx)
   300  
   301  	if !data.Get("skip_region_validation").(bool) {
   302  		if err := awsbase.ValidateRegion(data.Get("region").(string)); err != nil {
   303  			return err
   304  		}
   305  	}
   306  
   307  	b.bucketName = data.Get("bucket").(string)
   308  	b.keyName = data.Get("key").(string)
   309  	b.acl = data.Get("acl").(string)
   310  	b.workspaceKeyPrefix = data.Get("workspace_key_prefix").(string)
   311  	b.serverSideEncryption = data.Get("encrypt").(bool)
   312  	b.kmsKeyID = data.Get("kms_key_id").(string)
   313  	b.ddbTable = data.Get("dynamodb_table").(string)
   314  
   315  	customerKeyString := data.Get("sse_customer_key").(string)
   316  	if customerKeyString != "" {
   317  		if b.kmsKeyID != "" {
   318  			return errors.New(encryptionKeyConflictError)
   319  		}
   320  
   321  		var err error
   322  		b.customerEncryptionKey, err = base64.StdEncoding.DecodeString(customerKeyString)
   323  		if err != nil {
   324  			return fmt.Errorf("Failed to decode sse_customer_key: %s", err.Error())
   325  		}
   326  	}
   327  
   328  	cfg := &awsbase.Config{
   329  		AccessKey:                 data.Get("access_key").(string),
   330  		AssumeRoleARN:             data.Get("role_arn").(string),
   331  		AssumeRoleDurationSeconds: data.Get("assume_role_duration_seconds").(int),
   332  		AssumeRoleExternalID:      data.Get("external_id").(string),
   333  		AssumeRolePolicy:          data.Get("assume_role_policy").(string),
   334  		AssumeRoleSessionName:     data.Get("session_name").(string),
   335  		CallerDocumentationURL:    "https://www.terraform.io/docs/language/settings/backends/s3.html",
   336  		CallerName:                "S3 Backend",
   337  		CredsFilename:             data.Get("shared_credentials_file").(string),
   338  		DebugLogging:              logging.IsDebugOrHigher(),
   339  		IamEndpoint:               data.Get("iam_endpoint").(string),
   340  		MaxRetries:                data.Get("max_retries").(int),
   341  		Profile:                   data.Get("profile").(string),
   342  		Region:                    data.Get("region").(string),
   343  		SecretKey:                 data.Get("secret_key").(string),
   344  		SkipCredsValidation:       data.Get("skip_credentials_validation").(bool),
   345  		SkipMetadataApiCheck:      data.Get("skip_metadata_api_check").(bool),
   346  		StsEndpoint:               data.Get("sts_endpoint").(string),
   347  		Token:                     data.Get("token").(string),
   348  		UserAgentProducts: []*awsbase.UserAgentProduct{
   349  			{Name: "APN", Version: "1.0"},
   350  			{Name: "HashiCorp", Version: "1.0"},
   351  			{Name: "Terraform", Version: version.String()},
   352  		},
   353  	}
   354  
   355  	if policyARNSet := data.Get("assume_role_policy_arns").(*schema.Set); policyARNSet.Len() > 0 {
   356  		for _, policyARNRaw := range policyARNSet.List() {
   357  			policyARN, ok := policyARNRaw.(string)
   358  
   359  			if !ok {
   360  				continue
   361  			}
   362  
   363  			cfg.AssumeRolePolicyARNs = append(cfg.AssumeRolePolicyARNs, policyARN)
   364  		}
   365  	}
   366  
   367  	if tagMap := data.Get("assume_role_tags").(map[string]interface{}); len(tagMap) > 0 {
   368  		cfg.AssumeRoleTags = make(map[string]string)
   369  
   370  		for k, vRaw := range tagMap {
   371  			v, ok := vRaw.(string)
   372  
   373  			if !ok {
   374  				continue
   375  			}
   376  
   377  			cfg.AssumeRoleTags[k] = v
   378  		}
   379  	}
   380  
   381  	if transitiveTagKeySet := data.Get("assume_role_transitive_tag_keys").(*schema.Set); transitiveTagKeySet.Len() > 0 {
   382  		for _, transitiveTagKeyRaw := range transitiveTagKeySet.List() {
   383  			transitiveTagKey, ok := transitiveTagKeyRaw.(string)
   384  
   385  			if !ok {
   386  				continue
   387  			}
   388  
   389  			cfg.AssumeRoleTransitiveTagKeys = append(cfg.AssumeRoleTransitiveTagKeys, transitiveTagKey)
   390  		}
   391  	}
   392  
   393  	sess, err := awsbase.GetSession(cfg)
   394  	if err != nil {
   395  		return fmt.Errorf("error configuring S3 Backend: %w", err)
   396  	}
   397  
   398  	b.dynClient = dynamodb.New(sess.Copy(&aws.Config{
   399  		Endpoint: aws.String(data.Get("dynamodb_endpoint").(string)),
   400  	}))
   401  	b.s3Client = s3.New(sess.Copy(&aws.Config{
   402  		Endpoint:         aws.String(data.Get("endpoint").(string)),
   403  		S3ForcePathStyle: aws.Bool(data.Get("force_path_style").(bool)),
   404  	}))
   405  
   406  	return nil
   407  }
   408  
   409  const encryptionKeyConflictError = `Cannot have both kms_key_id and sse_customer_key set.
   410  
   411  The kms_key_id is used for encryption with KMS-Managed Keys (SSE-KMS)
   412  while sse_customer_key is used for encryption with customer-managed keys (SSE-C).
   413  Please choose one or the other.`