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
This commit is contained in:
Cameron Watters 2016-09-13 11:49:09 -07:00 committed by James Nugent
parent 72a341ba56
commit b4eb63d710
5 changed files with 232 additions and 0 deletions

124
state/remote/manta.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -46,4 +46,5 @@ var BuiltinClients = map[string]Factory{
"local": fileFactory, "local": fileFactory,
"s3": s3Factory, "s3": s3Factory,
"swift": swiftFactory, "swift": swiftFactory,
"manta": mantaFactory,
} }

View File

@ -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.

View File

@ -37,6 +37,9 @@
<li<%= sidebar_current("docs-state-remote-local") %>> <li<%= sidebar_current("docs-state-remote-local") %>>
<a href="/docs/state/remote/local.html">local</a> <a href="/docs/state/remote/local.html">local</a>
</li> </li>
<li<%= sidebar_current("docs-state-remote-manta") %>>
<a href="/docs/state/remote/manta.html">manta</a>
</li>
<li<%= sidebar_current("docs-state-remote-s3") %>> <li<%= sidebar_current("docs-state-remote-s3") %>>
<a href="/docs/state/remote/s3.html">s3</a> <a href="/docs/state/remote/s3.html">s3</a>
</li> </li>