github.com/dougneal/terraform@v0.6.15-0.20170330092735-b6a3840768a4/backend/remote-state/s3/client.go (about)

     1  package s3
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  
    10  	"github.com/aws/aws-sdk-go/aws"
    11  	"github.com/aws/aws-sdk-go/aws/awserr"
    12  	"github.com/aws/aws-sdk-go/service/dynamodb"
    13  	"github.com/aws/aws-sdk-go/service/s3"
    14  	multierror "github.com/hashicorp/go-multierror"
    15  	uuid "github.com/hashicorp/go-uuid"
    16  	"github.com/hashicorp/terraform/state"
    17  	"github.com/hashicorp/terraform/state/remote"
    18  )
    19  
    20  type RemoteClient struct {
    21  	s3Client             *s3.S3
    22  	dynClient            *dynamodb.DynamoDB
    23  	bucketName           string
    24  	path                 string
    25  	serverSideEncryption bool
    26  	acl                  string
    27  	kmsKeyID             string
    28  	lockTable            string
    29  }
    30  
    31  func (c *RemoteClient) Get() (*remote.Payload, error) {
    32  	output, err := c.s3Client.GetObject(&s3.GetObjectInput{
    33  		Bucket: &c.bucketName,
    34  		Key:    &c.path,
    35  	})
    36  
    37  	if err != nil {
    38  		if awserr := err.(awserr.Error); awserr != nil {
    39  			if awserr.Code() == "NoSuchKey" {
    40  				return nil, nil
    41  			} else {
    42  				return nil, err
    43  			}
    44  		} else {
    45  			return nil, err
    46  		}
    47  	}
    48  
    49  	defer output.Body.Close()
    50  
    51  	buf := bytes.NewBuffer(nil)
    52  	if _, err := io.Copy(buf, output.Body); err != nil {
    53  		return nil, fmt.Errorf("Failed to read remote state: %s", err)
    54  	}
    55  
    56  	payload := &remote.Payload{
    57  		Data: buf.Bytes(),
    58  	}
    59  
    60  	// If there was no data, then return nil
    61  	if len(payload.Data) == 0 {
    62  		return nil, nil
    63  	}
    64  
    65  	return payload, nil
    66  }
    67  
    68  func (c *RemoteClient) Put(data []byte) error {
    69  	contentType := "application/json"
    70  	contentLength := int64(len(data))
    71  
    72  	i := &s3.PutObjectInput{
    73  		ContentType:   &contentType,
    74  		ContentLength: &contentLength,
    75  		Body:          bytes.NewReader(data),
    76  		Bucket:        &c.bucketName,
    77  		Key:           &c.path,
    78  	}
    79  
    80  	if c.serverSideEncryption {
    81  		if c.kmsKeyID != "" {
    82  			i.SSEKMSKeyId = &c.kmsKeyID
    83  			i.ServerSideEncryption = aws.String("aws:kms")
    84  		} else {
    85  			i.ServerSideEncryption = aws.String("AES256")
    86  		}
    87  	}
    88  
    89  	if c.acl != "" {
    90  		i.ACL = aws.String(c.acl)
    91  	}
    92  
    93  	log.Printf("[DEBUG] Uploading remote state to S3: %#v", i)
    94  
    95  	if _, err := c.s3Client.PutObject(i); err == nil {
    96  		return nil
    97  	} else {
    98  		return fmt.Errorf("Failed to upload state: %v", err)
    99  	}
   100  }
   101  
   102  func (c *RemoteClient) Delete() error {
   103  	_, err := c.s3Client.DeleteObject(&s3.DeleteObjectInput{
   104  		Bucket: &c.bucketName,
   105  		Key:    &c.path,
   106  	})
   107  
   108  	return err
   109  }
   110  
   111  func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) {
   112  	if c.lockTable == "" {
   113  		return "", nil
   114  	}
   115  
   116  	stateName := fmt.Sprintf("%s/%s", c.bucketName, c.path)
   117  	info.Path = stateName
   118  
   119  	if info.ID == "" {
   120  		lockID, err := uuid.GenerateUUID()
   121  		if err != nil {
   122  			return "", err
   123  		}
   124  
   125  		info.ID = lockID
   126  	}
   127  
   128  	putParams := &dynamodb.PutItemInput{
   129  		Item: map[string]*dynamodb.AttributeValue{
   130  			"LockID": {S: aws.String(stateName)},
   131  			"Info":   {S: aws.String(string(info.Marshal()))},
   132  		},
   133  		TableName:           aws.String(c.lockTable),
   134  		ConditionExpression: aws.String("attribute_not_exists(LockID)"),
   135  	}
   136  	_, err := c.dynClient.PutItem(putParams)
   137  
   138  	if err != nil {
   139  		lockInfo, infoErr := c.getLockInfo()
   140  		if infoErr != nil {
   141  			err = multierror.Append(err, infoErr)
   142  		}
   143  
   144  		lockErr := &state.LockError{
   145  			Err:  err,
   146  			Info: lockInfo,
   147  		}
   148  		return "", lockErr
   149  	}
   150  	return info.ID, nil
   151  }
   152  
   153  func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) {
   154  	getParams := &dynamodb.GetItemInput{
   155  		Key: map[string]*dynamodb.AttributeValue{
   156  			"LockID": {S: aws.String(fmt.Sprintf("%s/%s", c.bucketName, c.path))},
   157  		},
   158  		ProjectionExpression: aws.String("LockID, Info"),
   159  		TableName:            aws.String(c.lockTable),
   160  	}
   161  
   162  	resp, err := c.dynClient.GetItem(getParams)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	var infoData string
   168  	if v, ok := resp.Item["Info"]; ok && v.S != nil {
   169  		infoData = *v.S
   170  	}
   171  
   172  	lockInfo := &state.LockInfo{}
   173  	err = json.Unmarshal([]byte(infoData), lockInfo)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	return lockInfo, nil
   179  }
   180  
   181  func (c *RemoteClient) Unlock(id string) error {
   182  	if c.lockTable == "" {
   183  		return nil
   184  	}
   185  
   186  	lockErr := &state.LockError{}
   187  
   188  	// TODO: store the path and lock ID in separate fields, and have proper
   189  	// projection expression only delete the lock if both match, rather than
   190  	// checking the ID from the info field first.
   191  	lockInfo, err := c.getLockInfo()
   192  	if err != nil {
   193  		lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err)
   194  		return lockErr
   195  	}
   196  	lockErr.Info = lockInfo
   197  
   198  	if lockInfo.ID != id {
   199  		lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id)
   200  		return lockErr
   201  	}
   202  
   203  	params := &dynamodb.DeleteItemInput{
   204  		Key: map[string]*dynamodb.AttributeValue{
   205  			"LockID": {S: aws.String(fmt.Sprintf("%s/%s", c.bucketName, c.path))},
   206  		},
   207  		TableName: aws.String(c.lockTable),
   208  	}
   209  	_, err = c.dynClient.DeleteItem(params)
   210  
   211  	if err != nil {
   212  		lockErr.Err = err
   213  		return lockErr
   214  	}
   215  	return nil
   216  }