terraform/state/remote/s3.go

276 lines
6.5 KiB
Go

package remote
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"os"
"strconv"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-multierror"
terraformAws "github.com/hashicorp/terraform/builtin/providers/aws"
"github.com/hashicorp/terraform/state"
)
func s3Factory(conf map[string]string) (Client, error) {
bucketName, ok := conf["bucket"]
if !ok {
return nil, fmt.Errorf("missing 'bucket' configuration")
}
keyName, ok := conf["key"]
if !ok {
return nil, fmt.Errorf("missing 'key' configuration")
}
endpoint, ok := conf["endpoint"]
if !ok {
endpoint = os.Getenv("AWS_S3_ENDPOINT")
}
regionName, ok := conf["region"]
if !ok {
regionName = os.Getenv("AWS_DEFAULT_REGION")
if regionName == "" {
return nil, fmt.Errorf(
"missing 'region' configuration or AWS_DEFAULT_REGION environment variable")
}
}
serverSideEncryption := false
if raw, ok := conf["encrypt"]; ok {
v, err := strconv.ParseBool(raw)
if err != nil {
return nil, fmt.Errorf(
"'encrypt' field couldn't be parsed as bool: %s", err)
}
serverSideEncryption = v
}
acl := ""
if raw, ok := conf["acl"]; ok {
acl = raw
}
kmsKeyID := conf["kms_key_id"]
var errs []error
creds, err := terraformAws.GetCredentials(&terraformAws.Config{
AccessKey: conf["access_key"],
SecretKey: conf["secret_key"],
Token: conf["token"],
Profile: conf["profile"],
CredsFilename: conf["shared_credentials_file"],
AssumeRoleARN: conf["role_arn"],
})
if err != nil {
return nil, err
}
// Call Get to check for credential provider. If nothing found, we'll get an
// error, and we can present it nicely to the user
_, err = creds.Get()
if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" {
errs = append(errs, fmt.Errorf(`No valid credential sources found for AWS S3 remote.
Please see https://www.terraform.io/docs/state/remote/s3.html for more information on
providing credentials for the AWS S3 remote`))
} else {
errs = append(errs, fmt.Errorf("Error loading credentials for AWS S3 remote: %s", err))
}
return nil, &multierror.Error{Errors: errs}
}
awsConfig := &aws.Config{
Credentials: creds,
Endpoint: aws.String(endpoint),
Region: aws.String(regionName),
HTTPClient: cleanhttp.DefaultClient(),
}
sess := session.New(awsConfig)
nativeClient := s3.New(sess)
dynClient := dynamodb.New(sess)
return &S3Client{
nativeClient: nativeClient,
bucketName: bucketName,
keyName: keyName,
serverSideEncryption: serverSideEncryption,
acl: acl,
kmsKeyID: kmsKeyID,
dynClient: dynClient,
lockTable: conf["lock_table"],
}, nil
}
type S3Client struct {
nativeClient *s3.S3
bucketName string
keyName string
serverSideEncryption bool
acl string
kmsKeyID string
dynClient *dynamodb.DynamoDB
lockTable string
}
func (c *S3Client) Get() (*Payload, error) {
output, err := c.nativeClient.GetObject(&s3.GetObjectInput{
Bucket: &c.bucketName,
Key: &c.keyName,
})
if err != nil {
if awserr := err.(awserr.Error); awserr != nil {
if awserr.Code() == "NoSuchKey" {
return nil, nil
} else {
return nil, err
}
} else {
return nil, err
}
}
defer output.Body.Close()
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, output.Body); err != nil {
return nil, fmt.Errorf("Failed to read remote state: %s", err)
}
payload := &Payload{
Data: buf.Bytes(),
}
// If there was no data, then return nil
if len(payload.Data) == 0 {
return nil, nil
}
return payload, nil
}
func (c *S3Client) Put(data []byte) error {
contentType := "application/json"
contentLength := int64(len(data))
i := &s3.PutObjectInput{
ContentType: &contentType,
ContentLength: &contentLength,
Body: bytes.NewReader(data),
Bucket: &c.bucketName,
Key: &c.keyName,
}
if c.serverSideEncryption {
if c.kmsKeyID != "" {
i.SSEKMSKeyId = &c.kmsKeyID
i.ServerSideEncryption = aws.String("aws:kms")
} else {
i.ServerSideEncryption = aws.String("AES256")
}
}
if c.acl != "" {
i.ACL = aws.String(c.acl)
}
log.Printf("[DEBUG] Uploading remote state to S3: %#v", i)
if _, err := c.nativeClient.PutObject(i); err == nil {
return nil
} else {
return fmt.Errorf("Failed to upload state: %v", err)
}
}
func (c *S3Client) Delete() error {
_, err := c.nativeClient.DeleteObject(&s3.DeleteObjectInput{
Bucket: &c.bucketName,
Key: &c.keyName,
})
return err
}
func (c *S3Client) Lock(info string) error {
if c.lockTable == "" {
return nil
}
stateName := fmt.Sprintf("%s/%s", c.bucketName, c.keyName)
lockInfo := &state.LockInfo{
Path: stateName,
Created: time.Now().UTC(),
Info: info,
}
putParams := &dynamodb.PutItemInput{
Item: map[string]*dynamodb.AttributeValue{
"LockID": {S: aws.String(stateName)},
"Info": {S: aws.String(lockInfo.String())},
},
TableName: aws.String(c.lockTable),
ConditionExpression: aws.String("attribute_not_exists(LockID)"),
}
_, err := c.dynClient.PutItem(putParams)
if err != nil {
getParams := &dynamodb.GetItemInput{
Key: map[string]*dynamodb.AttributeValue{
"LockID": {S: aws.String(fmt.Sprintf("%s/%s", c.bucketName, c.keyName))},
},
ProjectionExpression: aws.String("LockID, Created, Info"),
TableName: aws.String(c.lockTable),
}
resp, err := c.dynClient.GetItem(getParams)
if err != nil {
return fmt.Errorf("s3 state file %q locked, failed to retrieve info: %s", stateName, err)
}
var infoData string
if v, ok := resp.Item["Info"]; ok && v.S != nil {
infoData = *v.S
}
lockInfo = &state.LockInfo{}
err = json.Unmarshal([]byte(infoData), lockInfo)
if err != nil {
return fmt.Errorf("s3 state file %q locked, failed get lock info: %s", stateName, err)
}
return lockInfo.Err()
}
return nil
}
func (c *S3Client) Unlock() error {
if c.lockTable == "" {
return nil
}
params := &dynamodb.DeleteItemInput{
Key: map[string]*dynamodb.AttributeValue{
"LockID": {S: aws.String(fmt.Sprintf("%s/%s", c.bucketName, c.keyName))},
},
TableName: aws.String(c.lockTable),
}
_, err := c.dynClient.DeleteItem(params)
if err != nil {
return err
}
return nil
}