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:
parent
72a341ba56
commit
b4eb63d710
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -46,4 +46,5 @@ var BuiltinClients = map[string]Factory{
|
|||
"local": fileFactory,
|
||||
"s3": s3Factory,
|
||||
"swift": swiftFactory,
|
||||
"manta": mantaFactory,
|
||||
}
|
||||
|
|
|
@ -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.
|
|
@ -37,6 +37,9 @@
|
|||
<li<%= sidebar_current("docs-state-remote-local") %>>
|
||||
<a href="/docs/state/remote/local.html">local</a>
|
||||
</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") %>>
|
||||
<a href="/docs/state/remote/s3.html">s3</a>
|
||||
</li>
|
||||
|
|
Loading…
Reference in New Issue