2017-03-21 18:43:31 +01:00
|
|
|
package s3
|
2015-04-30 18:21:49 +02:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2017-02-07 17:12:02 +01:00
|
|
|
"encoding/json"
|
2015-04-30 18:21:49 +02:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2015-09-14 11:38:29 +02:00
|
|
|
"log"
|
2015-04-30 18:21:49 +02:00
|
|
|
|
2015-06-03 20:36:57 +02:00
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
2017-01-12 22:55:42 +01:00
|
|
|
"github.com/aws/aws-sdk-go/service/dynamodb"
|
2015-06-03 20:36:57 +02:00
|
|
|
"github.com/aws/aws-sdk-go/service/s3"
|
2017-03-21 18:43:31 +01:00
|
|
|
multierror "github.com/hashicorp/go-multierror"
|
2017-02-15 00:00:59 +01:00
|
|
|
uuid "github.com/hashicorp/go-uuid"
|
2017-02-07 17:12:02 +01:00
|
|
|
"github.com/hashicorp/terraform/state"
|
2017-03-21 18:43:31 +01:00
|
|
|
"github.com/hashicorp/terraform/state/remote"
|
2015-04-30 18:21:49 +02:00
|
|
|
)
|
|
|
|
|
2017-03-22 20:52:55 +01:00
|
|
|
type RemoteClient struct {
|
|
|
|
s3Client *s3.S3
|
|
|
|
dynClient *dynamodb.DynamoDB
|
2015-06-19 20:33:03 +02:00
|
|
|
bucketName string
|
2017-03-22 20:52:55 +01:00
|
|
|
path string
|
2015-06-19 20:33:03 +02:00
|
|
|
serverSideEncryption bool
|
2015-09-14 11:38:29 +02:00
|
|
|
acl string
|
2015-07-31 09:09:28 +02:00
|
|
|
kmsKeyID string
|
2017-01-12 22:55:42 +01:00
|
|
|
lockTable string
|
2015-04-30 18:21:49 +02:00
|
|
|
}
|
|
|
|
|
2017-03-22 20:52:55 +01:00
|
|
|
func (c *RemoteClient) Get() (*remote.Payload, error) {
|
|
|
|
output, err := c.s3Client.GetObject(&s3.GetObjectInput{
|
2015-04-30 18:21:49 +02:00
|
|
|
Bucket: &c.bucketName,
|
2017-03-22 20:52:55 +01:00
|
|
|
Key: &c.path,
|
2015-04-30 18:21:49 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
2015-05-20 22:20:30 +02:00
|
|
|
if awserr := err.(awserr.Error); awserr != nil {
|
2015-05-20 13:21:23 +02:00
|
|
|
if awserr.Code() == "NoSuchKey" {
|
2015-04-30 18:21:49 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2017-03-21 18:43:31 +01:00
|
|
|
payload := &remote.Payload{
|
2015-04-30 18:21:49 +02:00
|
|
|
Data: buf.Bytes(),
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there was no data, then return nil
|
|
|
|
if len(payload.Data) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return payload, nil
|
|
|
|
}
|
|
|
|
|
2017-03-22 20:52:55 +01:00
|
|
|
func (c *RemoteClient) Put(data []byte) error {
|
2015-10-02 00:14:55 +02:00
|
|
|
contentType := "application/json"
|
2015-04-30 18:21:49 +02:00
|
|
|
contentLength := int64(len(data))
|
|
|
|
|
2015-06-19 20:33:03 +02:00
|
|
|
i := &s3.PutObjectInput{
|
2015-04-30 18:21:49 +02:00
|
|
|
ContentType: &contentType,
|
|
|
|
ContentLength: &contentLength,
|
|
|
|
Body: bytes.NewReader(data),
|
|
|
|
Bucket: &c.bucketName,
|
2017-03-22 20:52:55 +01:00
|
|
|
Key: &c.path,
|
2015-06-19 20:33:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if c.serverSideEncryption {
|
2015-07-31 09:09:28 +02:00
|
|
|
if c.kmsKeyID != "" {
|
2015-10-07 16:39:08 +02:00
|
|
|
i.SSEKMSKeyId = &c.kmsKeyID
|
2015-07-31 09:09:28 +02:00
|
|
|
i.ServerSideEncryption = aws.String("aws:kms")
|
|
|
|
} else {
|
|
|
|
i.ServerSideEncryption = aws.String("AES256")
|
|
|
|
}
|
2015-06-19 20:33:03 +02:00
|
|
|
}
|
|
|
|
|
2015-09-14 11:38:29 +02:00
|
|
|
if c.acl != "" {
|
|
|
|
i.ACL = aws.String(c.acl)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("[DEBUG] Uploading remote state to S3: %#v", i)
|
|
|
|
|
2017-03-22 20:52:55 +01:00
|
|
|
if _, err := c.s3Client.PutObject(i); err == nil {
|
2015-04-30 18:21:49 +02:00
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
return fmt.Errorf("Failed to upload state: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-22 20:52:55 +01:00
|
|
|
func (c *RemoteClient) Delete() error {
|
|
|
|
_, err := c.s3Client.DeleteObject(&s3.DeleteObjectInput{
|
2015-04-30 18:21:49 +02:00
|
|
|
Bucket: &c.bucketName,
|
2017-03-22 20:52:55 +01:00
|
|
|
Key: &c.path,
|
2015-04-30 18:21:49 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
2017-01-12 22:55:42 +01:00
|
|
|
|
2017-03-22 20:52:55 +01:00
|
|
|
func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) {
|
2017-01-12 22:55:42 +01:00
|
|
|
if c.lockTable == "" {
|
2017-02-15 00:00:59 +01:00
|
|
|
return "", nil
|
2017-01-12 22:55:42 +01:00
|
|
|
}
|
|
|
|
|
2017-03-30 19:36:54 +02:00
|
|
|
info.Path = c.lockPath()
|
2017-02-15 00:00:59 +01:00
|
|
|
|
2017-02-15 16:25:04 +01:00
|
|
|
if info.ID == "" {
|
|
|
|
lockID, err := uuid.GenerateUUID()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2017-01-12 22:55:42 +01:00
|
|
|
|
2017-02-15 16:25:04 +01:00
|
|
|
info.ID = lockID
|
|
|
|
}
|
2017-02-15 00:00:59 +01:00
|
|
|
|
2017-01-12 22:55:42 +01:00
|
|
|
putParams := &dynamodb.PutItemInput{
|
|
|
|
Item: map[string]*dynamodb.AttributeValue{
|
2017-03-30 19:36:54 +02:00
|
|
|
"LockID": {S: aws.String(c.lockPath())},
|
2017-02-15 20:01:18 +01:00
|
|
|
"Info": {S: aws.String(string(info.Marshal()))},
|
2017-01-12 22:55:42 +01:00
|
|
|
},
|
|
|
|
TableName: aws.String(c.lockTable),
|
|
|
|
ConditionExpression: aws.String("attribute_not_exists(LockID)"),
|
|
|
|
}
|
2017-02-15 16:25:04 +01:00
|
|
|
_, err := c.dynClient.PutItem(putParams)
|
2017-01-12 22:55:42 +01:00
|
|
|
|
|
|
|
if err != nil {
|
2017-02-15 20:01:18 +01:00
|
|
|
lockInfo, infoErr := c.getLockInfo()
|
|
|
|
if infoErr != nil {
|
|
|
|
err = multierror.Append(err, infoErr)
|
2017-02-07 17:12:02 +01:00
|
|
|
}
|
2017-01-12 22:55:42 +01:00
|
|
|
|
2017-02-15 20:01:18 +01:00
|
|
|
lockErr := &state.LockError{
|
|
|
|
Err: err,
|
|
|
|
Info: lockInfo,
|
|
|
|
}
|
|
|
|
return "", lockErr
|
2017-01-12 22:55:42 +01:00
|
|
|
}
|
2017-02-15 16:25:04 +01:00
|
|
|
return info.ID, nil
|
2017-01-12 22:55:42 +01:00
|
|
|
}
|
|
|
|
|
2017-03-22 20:52:55 +01:00
|
|
|
func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) {
|
2017-02-15 18:51:57 +01:00
|
|
|
getParams := &dynamodb.GetItemInput{
|
|
|
|
Key: map[string]*dynamodb.AttributeValue{
|
2017-03-30 19:36:54 +02:00
|
|
|
"LockID": {S: aws.String(c.lockPath())},
|
2017-02-15 18:51:57 +01:00
|
|
|
},
|
|
|
|
ProjectionExpression: aws.String("LockID, Info"),
|
|
|
|
TableName: aws.String(c.lockTable),
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := c.dynClient.GetItem(getParams)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 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 nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return lockInfo, nil
|
|
|
|
}
|
|
|
|
|
2017-03-22 20:52:55 +01:00
|
|
|
func (c *RemoteClient) Unlock(id string) error {
|
2017-01-12 22:55:42 +01:00
|
|
|
if c.lockTable == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-02-15 20:01:18 +01:00
|
|
|
lockErr := &state.LockError{}
|
|
|
|
|
2017-02-15 18:51:57 +01:00
|
|
|
// TODO: store the path and lock ID in separate fields, and have proper
|
|
|
|
// projection expression only delete the lock if both match, rather than
|
|
|
|
// checking the ID from the info field first.
|
|
|
|
lockInfo, err := c.getLockInfo()
|
|
|
|
if err != nil {
|
2017-02-15 20:01:18 +01:00
|
|
|
lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err)
|
|
|
|
return lockErr
|
2017-02-15 18:51:57 +01:00
|
|
|
}
|
2017-02-15 20:01:18 +01:00
|
|
|
lockErr.Info = lockInfo
|
2017-02-15 18:51:57 +01:00
|
|
|
|
|
|
|
if lockInfo.ID != id {
|
2017-02-15 20:01:18 +01:00
|
|
|
lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id)
|
|
|
|
return lockErr
|
2017-02-15 18:51:57 +01:00
|
|
|
}
|
|
|
|
|
2017-01-12 22:55:42 +01:00
|
|
|
params := &dynamodb.DeleteItemInput{
|
|
|
|
Key: map[string]*dynamodb.AttributeValue{
|
2017-03-30 19:36:54 +02:00
|
|
|
"LockID": {S: aws.String(c.lockPath())},
|
2017-01-12 22:55:42 +01:00
|
|
|
},
|
|
|
|
TableName: aws.String(c.lockTable),
|
|
|
|
}
|
2017-02-15 18:51:57 +01:00
|
|
|
_, err = c.dynClient.DeleteItem(params)
|
2017-01-12 22:55:42 +01:00
|
|
|
|
|
|
|
if err != nil {
|
2017-02-15 20:01:18 +01:00
|
|
|
lockErr.Err = err
|
|
|
|
return lockErr
|
2017-01-12 22:55:42 +01:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2017-03-30 19:36:54 +02:00
|
|
|
|
|
|
|
func (c *RemoteClient) lockPath() string {
|
|
|
|
return fmt.Sprintf("%s/%s", c.bucketName, c.path)
|
|
|
|
}
|