Merge pull request #1723 from apparentlymart/s3remotestate

S3 Remote State Backend
This commit is contained in:
Mitchell Hashimoto 2015-04-30 13:25:33 -07:00
commit af5ac59188
4 changed files with 254 additions and 0 deletions

View File

@ -39,6 +39,7 @@ var BuiltinClients = map[string]Factory{
"atlas": atlasFactory, "atlas": atlasFactory,
"consul": consulFactory, "consul": consulFactory,
"http": httpFactory, "http": httpFactory,
"s3": s3Factory,
// This is used for development purposes only. // This is used for development purposes only.
"_local": fileFactory, "_local": fileFactory,

125
state/remote/s3.go Normal file
View File

@ -0,0 +1,125 @@
package remote
import (
"bytes"
"fmt"
"io"
"os"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/s3"
)
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")
}
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")
}
}
accessKeyId := conf["access_key"]
secretAccessKey := conf["secret_key"]
credentialsProvider := aws.DetectCreds(accessKeyId, secretAccessKey, "")
// Make sure we got some sort of working credentials.
_, err := credentialsProvider.Credentials()
if err != nil {
return nil, fmt.Errorf("Unable to determine AWS credentials. Set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.\n(error was: %s)", err)
}
awsConfig := &aws.Config{
Credentials: credentialsProvider,
Region: regionName,
}
nativeClient := s3.New(awsConfig)
return &S3Client{
nativeClient: nativeClient,
bucketName: bucketName,
keyName: keyName,
}, nil
}
type S3Client struct {
nativeClient *s3.S3
bucketName string
keyName 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 := aws.Error(err); 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/octet-stream"
contentLength := int64(len(data))
_, err := c.nativeClient.PutObject(&s3.PutObjectInput{
ContentType: &contentType,
ContentLength: &contentLength,
Body: bytes.NewReader(data),
Bucket: &c.bucketName,
Key: &c.keyName,
})
if 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
}

120
state/remote/s3_test.go Normal file
View File

@ -0,0 +1,120 @@
package remote
import (
"fmt"
"os"
"testing"
"time"
"github.com/awslabs/aws-sdk-go/service/s3"
)
func TestS3Client_impl(t *testing.T) {
var _ Client = new(S3Client)
}
func TestS3Factory(t *testing.T) {
// This test just instantiates the client. Shouldn't make any actual
// requests nor incur any costs.
config := make(map[string]string)
// Empty config is an error
_, err := s3Factory(config)
if err == nil {
t.Fatalf("Empty config should be error")
}
config["region"] = "us-west-1"
config["bucket"] = "foo"
config["key"] = "bar"
// For this test we'll provide the credentials as config. The
// acceptance tests implicitly test passing credentials as
// environment variables.
config["access_key"] = "bazkey"
config["secret_key"] = "bazsecret"
client, err := s3Factory(config)
if err != nil {
t.Fatalf("Error for valid config")
}
s3Client := client.(*S3Client)
if s3Client.nativeClient.Config.Region != "us-west-1" {
t.Fatalf("Incorrect region was populated")
}
if s3Client.bucketName != "foo" {
t.Fatalf("Incorrect bucketName was populated")
}
if s3Client.keyName != "bar" {
t.Fatalf("Incorrect keyName was populated")
}
credentials, err := s3Client.nativeClient.Config.Credentials.Credentials()
if err != nil {
t.Fatalf("Error when requesting credentials")
}
if credentials.AccessKeyID != "bazkey" {
t.Fatalf("Incorrect Access Key Id was populated")
}
if credentials.SecretAccessKey != "bazsecret" {
t.Fatalf("Incorrect Secret Access Key was populated")
}
}
func TestS3Client(t *testing.T) {
// This test creates a bucket in S3 and populates it.
// It may incur costs, so it will only run if AWS credential environment
// variables are present.
accessKeyId := os.Getenv("AWS_ACCESS_KEY_ID")
if accessKeyId == "" {
t.Skipf("skipping; AWS_ACCESS_KEY_ID must be set")
}
regionName := os.Getenv("AWS_DEFAULT_REGION")
if regionName == "" {
regionName = "us-west-2"
}
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
keyName := "testState"
config := make(map[string]string)
config["region"] = regionName
config["bucket"] = bucketName
config["key"] = keyName
client, err := s3Factory(config)
if err != nil {
t.Fatalf("Error for valid config")
}
s3Client := client.(*S3Client)
nativeClient := s3Client.nativeClient
createBucketReq := &s3.CreateBucketInput{
Bucket: &bucketName,
}
// Be clear about what we're doing in case the user needs to clean
// this up later.
t.Logf("Creating S3 bucket %s in %s", bucketName, regionName)
_, err = nativeClient.CreateBucket(createBucketReq)
if err != nil {
t.Skipf("Failed to create test S3 bucket, so skipping")
}
defer func () {
deleteBucketReq := &s3.DeleteBucketInput{
Bucket: &bucketName,
}
_, err := nativeClient.DeleteBucket(deleteBucketReq)
if err != nil {
t.Logf("WARNING: Failed to delete the test S3 bucket. It has been left in your AWS account and may incur storage charges. (error was %s)", err)
}
}()
testClient(t, client)
}

View File

@ -50,6 +50,14 @@ The following backends are supported:
variables can optionally be provided. Address is assumed to be the variables can optionally be provided. Address is assumed to be the
local agent if not provided. local agent if not provided.
* S3 - Stores the state as a given key in a given bucket on Amazon S3.
Requires the `bucket` and `key` variables. Supports and honors the standard
AWS environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`
and `AWS_DEFAULT_REGION`. These can optionally be provided as parameters
in the `aws_access_key`, `aws_secret_key` and `region` variables
respectively, but passing credentials this way is not recommended since they
will be included in cleartext inside the persisted state.
* HTTP - Stores the state using a simple REST client. State will be fetched * HTTP - Stores the state using a simple REST client. State will be fetched
via GET, updated via POST, and purged with DELETE. Requires the `address` variable. via GET, updated via POST, and purged with DELETE. Requires the `address` variable.