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