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