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