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.`