From b4eb63d71038896679d8ac54bb8fadd8a28769ef Mon Sep 17 00:00:00 2001 From: Cameron Watters Date: Tue, 13 Sep 2016 11:49:09 -0700 Subject: [PATCH] state/remote: New provider - manta - add remote state provider backed by Joyent's Manta - add documentation of Manta remote state provider - explicitly check for passphrase-protected SSH keys, which are currently unsupported, and generate a more helpful error (borrowed from Packer's solution to the same problem): https://github.com/mitchellh/packer/blob/master/common/ssh/key.go#L27 --- state/remote/manta.go | 124 ++++++++++++++++++ state/remote/manta_test.go | 58 ++++++++ state/remote/remote.go | 1 + .../source/docs/state/remote/manta.html.md | 46 +++++++ website/source/layouts/remotestate.erb | 3 + 5 files changed, 232 insertions(+) create mode 100644 state/remote/manta.go create mode 100644 state/remote/manta_test.go create mode 100644 website/source/docs/state/remote/manta.html.md diff --git a/state/remote/manta.go b/state/remote/manta.go new file mode 100644 index 000000000..37782c433 --- /dev/null +++ b/state/remote/manta.go @@ -0,0 +1,124 @@ +package remote + +import ( + "crypto/md5" + "encoding/pem" + "fmt" + "io/ioutil" + "log" + "os" + + joyentclient "github.com/joyent/gocommon/client" + joyenterrors "github.com/joyent/gocommon/errors" + "github.com/joyent/gomanta/manta" + joyentauth "github.com/joyent/gosign/auth" +) + +const DEFAULT_OBJECT_NAME = "terraform.tfstate" + +func mantaFactory(conf map[string]string) (Client, error) { + path, ok := conf["path"] + if !ok { + return nil, fmt.Errorf("missing 'path' configuration") + } + + objectName, ok := conf["objectName"] + if !ok { + objectName = DEFAULT_OBJECT_NAME + } + + creds, err := getCredentialsFromEnvironment() + + if err != nil { + return nil, fmt.Errorf("Error getting Manta credentials: %s", err.Error()) + } + + client := manta.New(joyentclient.NewClient( + creds.MantaEndpoint.URL, + "", + creds, + log.New(os.Stderr, "", log.LstdFlags), + )) + + return &MantaClient{ + Client: client, + Path: path, + ObjectName: objectName, + }, nil +} + +type MantaClient struct { + Client *manta.Client + Path string + ObjectName string +} + +func (c *MantaClient) Get() (*Payload, error) { + bytes, err := c.Client.GetObject(c.Path, c.ObjectName) + if err != nil { + if joyenterrors.IsResourceNotFound(err.(joyenterrors.Error).Cause()) { + return nil, nil + } + + return nil, err + } + + md5 := md5.Sum(bytes) + + return &Payload{ + Data: bytes, + MD5: md5[:], + }, nil +} + +func (c *MantaClient) Put(data []byte) error { + return c.Client.PutObject(c.Path, c.ObjectName, data) +} + +func (c *MantaClient) Delete() error { + return c.Client.DeleteObject(c.Path, c.ObjectName) +} + +func getCredentialsFromEnvironment() (cred *joyentauth.Credentials, err error) { + + user := os.Getenv("MANTA_USER") + keyId := os.Getenv("MANTA_KEY_ID") + url := os.Getenv("MANTA_URL") + keyMaterial := os.Getenv("MANTA_KEY_MATERIAL") + + if _, err := os.Stat(keyMaterial); err == nil { + // key material is a file path; try to read it + keyBytes, err := ioutil.ReadFile(keyMaterial) + if err != nil { + return nil, fmt.Errorf("Error reading key material from %s: %s", + keyMaterial, err) + } else { + block, _ := pem.Decode(keyBytes) + if block == nil { + return nil, fmt.Errorf( + "Failed to read key material '%s': no key found", keyMaterial) + } + + if block.Headers["Proc-Type"] == "4,ENCRYPTED" { + return nil, fmt.Errorf( + "Failed to read key '%s': password protected keys are\n"+ + "not currently supported. Please decrypt the key prior to use.", keyMaterial) + } + + keyMaterial = string(keyBytes) + } + } + + authentication, err := joyentauth.NewAuth(user, keyMaterial, "rsa-sha256") + if err != nil { + return nil, fmt.Errorf("Error constructing authentication for %s: %s", user, err) + } + + return &joyentauth.Credentials{ + UserAuthentication: authentication, + SdcKeyId: "", + SdcEndpoint: joyentauth.Endpoint{}, + MantaKeyId: keyId, + MantaEndpoint: joyentauth.Endpoint{URL: url}, + }, nil +} diff --git a/state/remote/manta_test.go b/state/remote/manta_test.go new file mode 100644 index 000000000..8888e68b9 --- /dev/null +++ b/state/remote/manta_test.go @@ -0,0 +1,58 @@ +package remote + +import ( + "os" + "testing" +) + +func TestMantaClient_impl(t *testing.T) { + var _ Client = new(MantaClient) +} + +func TestMantaClient(t *testing.T) { + // This test creates an object in Manta in the root directory of + // the current MANTA_USER. + // + // It may incur costs, so it will only run if Manta credential environment + // variables are present. + + mantaUser := os.Getenv("MANTA_USER") + mantaKeyId := os.Getenv("MANTA_KEY_ID") + mantaUrl := os.Getenv("MANTA_URL") + mantaKeyMaterial := os.Getenv("MANTA_KEY_MATERIAL") + + if mantaUser == "" || mantaKeyId == "" || mantaUrl == "" || mantaKeyMaterial == "" { + t.Skipf("skipping; MANTA_USER, MANTA_KEY_ID, MANTA_URL and MANTA_KEY_MATERIAL must all be set") + } + + if _, err := os.Stat(mantaKeyMaterial); err == nil { + t.Logf("[DEBUG] MANTA_KEY_MATERIAL is a file path %s", mantaKeyMaterial) + } + + testPath := "terraform-remote-state-test" + + client, err := mantaFactory(map[string]string{ + "path": testPath, + "objectName": "terraform-test-state.tfstate", + }) + + if err != nil { + t.Fatalf("bad: %s", err) + } + + mantaClient := client.(*MantaClient) + + err = mantaClient.Client.PutDirectory(mantaClient.Path) + if err != nil { + t.Fatalf("bad: %s", err) + } + + defer func() { + err = mantaClient.Client.DeleteDirectory(mantaClient.Path) + if err != nil { + t.Fatalf("bad: %s", err) + } + }() + + testClient(t, client) +} diff --git a/state/remote/remote.go b/state/remote/remote.go index 173ab00ca..752dc16cc 100644 --- a/state/remote/remote.go +++ b/state/remote/remote.go @@ -46,4 +46,5 @@ var BuiltinClients = map[string]Factory{ "local": fileFactory, "s3": s3Factory, "swift": swiftFactory, + "manta": mantaFactory, } diff --git a/website/source/docs/state/remote/manta.html.md b/website/source/docs/state/remote/manta.html.md new file mode 100644 index 000000000..7875a4472 --- /dev/null +++ b/website/source/docs/state/remote/manta.html.md @@ -0,0 +1,46 @@ +--- +layout: "remotestate" +page_title: "Remote State Backend: manta" +sidebar_current: "docs-state-remote-manta" +description: |- + Terraform can store the state remotely, making it easier to version and work with in a team. +--- + +# manta + +Stores the state as an artifact in [Manta](https://www.joyent.com/manta). + +## Example Usage + +``` +terraform remote config \ + -backend=manta \ + -backend-config="path=random/path" \ + -backend-config="objecName=terraform.tfstate" +``` + +## Example Referencing + +``` +data "terraform_remote_state" "foo" { + backend = "manta" + config { + path = "random/path" + objectName = "terraform.tfstate" + } +} +``` + +## Configuration variables + +The following configuration options are supported: + + * `path` - (Required) The path where to store the state file + * `objectName` - (Optional) The name of the state file (defaults to `terraform.tfstate`) + +The following [Manta environment variables](https://apidocs.joyent.com/manta/#setting-up-your-environment) are supported: + + * `MANTA_URL` - (Required) The API endpoint + * `MANTA_USER` - (Required) The Manta user + * `MANTA_KEY_ID` - (Required) The MD5 fingerprint of your SSH key + * `MANTA_KEY_MATERIAL` - (Required) The path to the private key for accessing Manta (must align with the `MANTA_KEY_ID`). This key must *not* be protected by passphrase. diff --git a/website/source/layouts/remotestate.erb b/website/source/layouts/remotestate.erb index 3a3a27cbb..6b34f2a9b 100644 --- a/website/source/layouts/remotestate.erb +++ b/website/source/layouts/remotestate.erb @@ -37,6 +37,9 @@ > local + > + manta + > s3