github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/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  	info.Path = c.lockPath()
   117  
   118  	if info.ID == "" {
   119  		lockID, err := uuid.GenerateUUID()
   120  		if err != nil {
   121  			return "", err
   122  		}
   123  
   124  		info.ID = lockID
   125  	}
   126  
   127  	putParams := &dynamodb.PutItemInput{
   128  		Item: map[string]*dynamodb.AttributeValue{
   129  			"LockID": {S: aws.String(c.lockPath())},
   130  			"Info":   {S: aws.String(string(info.Marshal()))},
   131  		},
   132  		TableName:           aws.String(c.lockTable),
   133  		ConditionExpression: aws.String("attribute_not_exists(LockID)"),
   134  	}
   135  	_, err := c.dynClient.PutItem(putParams)
   136  
   137  	if err != nil {
   138  		lockInfo, infoErr := c.getLockInfo()
   139  		if infoErr != nil {
   140  			err = multierror.Append(err, infoErr)
   141  		}
   142  
   143  		lockErr := &state.LockError{
   144  			Err:  err,
   145  			Info: lockInfo,
   146  		}
   147  		return "", lockErr
   148  	}
   149  	return info.ID, nil
   150  }
   151  
   152  func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) {
   153  	getParams := &dynamodb.GetItemInput{
   154  		Key: map[string]*dynamodb.AttributeValue{
   155  			"LockID": {S: aws.String(c.lockPath())},
   156  		},
   157  		ProjectionExpression: aws.String("LockID, Info"),
   158  		TableName:            aws.String(c.lockTable),
   159  	}
   160  
   161  	resp, err := c.dynClient.GetItem(getParams)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	var infoData string
   167  	if v, ok := resp.Item["Info"]; ok && v.S != nil {
   168  		infoData = *v.S
   169  	}
   170  
   171  	lockInfo := &state.LockInfo{}
   172  	err = json.Unmarshal([]byte(infoData), lockInfo)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	return lockInfo, nil
   178  }
   179  
   180  func (c *RemoteClient) Unlock(id string) error {
   181  	if c.lockTable == "" {
   182  		return nil
   183  	}
   184  
   185  	lockErr := &state.LockError{}
   186  
   187  	// TODO: store the path and lock ID in separate fields, and have proper
   188  	// projection expression only delete the lock if both match, rather than
   189  	// checking the ID from the info field first.
   190  	lockInfo, err := c.getLockInfo()
   191  	if err != nil {
   192  		lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err)
   193  		return lockErr
   194  	}
   195  	lockErr.Info = lockInfo
   196  
   197  	if lockInfo.ID != id {
   198  		lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id)
   199  		return lockErr
   200  	}
   201  
   202  	params := &dynamodb.DeleteItemInput{
   203  		Key: map[string]*dynamodb.AttributeValue{
   204  			"LockID": {S: aws.String(c.lockPath())},
   205  		},
   206  		TableName: aws.String(c.lockTable),
   207  	}
   208  	_, err = c.dynClient.DeleteItem(params)
   209  
   210  	if err != nil {
   211  		lockErr.Err = err
   212  		return lockErr
   213  	}
   214  	return nil
   215  }
   216  
   217  func (c *RemoteClient) lockPath() string {
   218  	return fmt.Sprintf("%s/%s", c.bucketName, c.path)
   219  }