Merge pull request #1723 from apparentlymart/s3remotestate
S3 Remote State Backend
This commit is contained in:
commit
af5ac59188
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue