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,
|
"local": fileFactory,
|
||||||
"s3": s3Factory,
|
"s3": s3Factory,
|
||||||
"swift": swiftFactory,
|
"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") %>>
|
<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>
|
||||||
|
|
Loading…
Reference in New Issue