Migrate Manta Remote state to be a backend

This PR changes manta from being a legacy remote state client to a new backend type. This also includes creating a simple lock within manta

This PR also unifies the way the triton client is configured (the schema) and also uses the same env vars to set the backend up

It is important to note that if the remote state path does not exist, then the backend will create that path. This means the user doesn't need to fall into a chicken and egg situation of creating the directory in advance before interacting with it
This commit is contained in:
stack72 2017-10-09 14:46:54 +03:00
parent 4a01bf0b97
commit 1fd0f803e4
29 changed files with 3331 additions and 185 deletions

View File

@ -16,6 +16,7 @@ import (
backendetcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3"
backendGCS "github.com/hashicorp/terraform/backend/remote-state/gcs"
backendinmem "github.com/hashicorp/terraform/backend/remote-state/inmem"
backendManta "github.com/hashicorp/terraform/backend/remote-state/manta"
backendS3 "github.com/hashicorp/terraform/backend/remote-state/s3"
backendSwift "github.com/hashicorp/terraform/backend/remote-state/swift"
)
@ -49,6 +50,7 @@ func init() {
"azurerm": func() backend.Backend { return backendAzure.New() },
"etcdv3": func() backend.Backend { return backendetcdv3.New() },
"gcs": func() backend.Backend { return backendGCS.New() },
"manta": func() backend.Backend { return backendManta.New() },
}
// Add the legacy remote backends that haven't yet been convertd to

View File

@ -0,0 +1,177 @@
package manta
import (
"context"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/schema"
triton "github.com/joyent/triton-go"
"github.com/joyent/triton-go/authentication"
"github.com/joyent/triton-go/storage"
)
func New() backend.Backend {
s := &schema.Backend{
Schema: map[string]*schema.Schema{
"account": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"TRITON_ACCOUNT", "SDC_ACCOUNT"}, ""),
},
"url": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"MANTA_URL"}, "https://us-east.manta.joyent.com"),
},
"key_material": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"TRITON_KEY_MATERIAL", "SDC_KEY_MATERIAL"}, ""),
},
"key_id": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"TRITON_KEY_ID", "SDC_KEY_ID"}, ""),
},
"insecure_skip_tls_verify": {
Type: schema.TypeBool,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TRITON_SKIP_TLS_VERIFY", ""),
},
"path": {
Type: schema.TypeString,
Required: true,
},
"objectName": {
Type: schema.TypeString,
Optional: true,
Default: "terraform.tfstate",
},
},
}
result := &Backend{Backend: s}
result.Backend.ConfigureFunc = result.configure
return result
}
type Backend struct {
*schema.Backend
data *schema.ResourceData
// The fields below are set from configure
storageClient *storage.StorageClient
path string
objectName string
}
type BackendConfig struct {
AccountId string
KeyId string
AccountUrl string
KeyMaterial string
SkipTls bool
}
func (b *Backend) configure(ctx context.Context) error {
if b.path != "" {
return nil
}
data := schema.FromContextBackendConfig(ctx)
config := &BackendConfig{
AccountId: data.Get("account").(string),
AccountUrl: data.Get("url").(string),
KeyId: data.Get("key_id").(string),
SkipTls: data.Get("insecure_skip_tls_verify").(bool),
}
if v, ok := data.GetOk("key_material"); ok {
config.KeyMaterial = v.(string)
}
b.path = data.Get("path").(string)
b.objectName = data.Get("objectName").(string)
var validationError *multierror.Error
if data.Get("account").(string) == "" {
validationError = multierror.Append(validationError, errors.New("`Account` must be configured for the Triton provider"))
}
if data.Get("key_id").(string) == "" {
validationError = multierror.Append(validationError, errors.New("`Key ID` must be configured for the Triton provider"))
}
if b.path == "" {
validationError = multierror.Append(validationError, errors.New("`Path` must be configured for the Triton provider"))
}
if validationError != nil {
return validationError
}
var signer authentication.Signer
var err error
if config.KeyMaterial == "" {
signer, err = authentication.NewSSHAgentSigner(config.KeyId, config.AccountId)
if err != nil {
return errwrap.Wrapf("Error Creating SSH Agent Signer: {{err}}", err)
}
} else {
var keyBytes []byte
if _, err = os.Stat(config.KeyMaterial); err == nil {
keyBytes, err = ioutil.ReadFile(config.KeyMaterial)
if err != nil {
return fmt.Errorf("Error reading key material from %s: %s",
config.KeyMaterial, err)
}
block, _ := pem.Decode(keyBytes)
if block == nil {
return fmt.Errorf(
"Failed to read key material '%s': no key found", config.KeyMaterial)
}
if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
return fmt.Errorf(
"Failed to read key '%s': password protected keys are\n"+
"not currently supported. Please decrypt the key prior to use.", config.KeyMaterial)
}
} else {
keyBytes = []byte(config.KeyMaterial)
}
signer, err = authentication.NewPrivateKeySigner(config.KeyId, keyBytes, config.AccountId)
if err != nil {
return errwrap.Wrapf("Error Creating SSH Private Key Signer: {{err}}", err)
}
}
clientConfig := &triton.ClientConfig{
MantaURL: config.AccountUrl,
AccountName: config.AccountId,
Signers: []authentication.Signer{signer},
}
triton, err := storage.NewClient(clientConfig)
if err != nil {
return err
}
b.storageClient = triton
return nil
}

View File

@ -0,0 +1,144 @@
package manta
import (
"context"
"errors"
"fmt"
"path"
"sort"
"strings"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
"github.com/joyent/triton-go/storage"
)
func (b *Backend) States() ([]string, error) {
result := []string{backend.DefaultStateName}
objs, err := b.storageClient.Dir().List(context.Background(), &storage.ListDirectoryInput{
DirectoryName: path.Join(mantaDefaultRootStore, b.path),
})
if err != nil {
if strings.Contains(err.Error(), "ResourceNotFound") {
return result, nil
}
return nil, err
}
for _, obj := range objs.Entries {
if obj.Type == "directory" && obj.Name != "" {
result = append(result, obj.Name)
}
}
sort.Strings(result[1:])
return result, nil
}
func (b *Backend) DeleteState(name string) error {
if name == backend.DefaultStateName || name == "" {
return fmt.Errorf("can't delete default state")
}
//firstly we need to delete the state file
err := b.storageClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{
ObjectPath: path.Join(mantaDefaultRootStore, b.statePath(name), b.objectName),
})
if err != nil {
return err
}
//then we need to delete the state folder
err = b.storageClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{
ObjectPath: path.Join(mantaDefaultRootStore, b.statePath(name)),
})
if err != nil {
return err
}
return nil
}
func (b *Backend) State(name string) (state.State, error) {
if name == "" {
return nil, errors.New("missing state name")
}
client := &RemoteClient{
storageClient: b.storageClient,
directoryName: b.statePath(name),
keyName: b.objectName,
}
stateMgr := &remote.State{Client: client}
//if this isn't the default state name, we need to create the object so
//it's listed by States.
if name != backend.DefaultStateName {
// take a lock on this state while we write it
lockInfo := state.NewLockInfo()
lockInfo.Operation = "init"
lockId, err := client.Lock(lockInfo)
if err != nil {
return nil, fmt.Errorf("failed to lock manta state: %s", err)
}
// Local helper function so we can call it multiple places
lockUnlock := func(parent error) error {
if err := stateMgr.Unlock(lockId); err != nil {
return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)
}
return parent
}
// Grab the value
if err := stateMgr.RefreshState(); err != nil {
err = lockUnlock(err)
return nil, err
}
// If we have no state, we have to create an empty state
if v := stateMgr.State(); v == nil {
if err := stateMgr.WriteState(terraform.NewState()); err != nil {
err = lockUnlock(err)
return nil, err
}
if err := stateMgr.PersistState(); err != nil {
err = lockUnlock(err)
return nil, err
}
}
// Unlock, the state should now be initialized
if err := lockUnlock(nil); err != nil {
return nil, err
}
}
return stateMgr, nil
}
func (b *Backend) client() *RemoteClient {
return &RemoteClient{}
}
func (b *Backend) statePath(name string) string {
if name == backend.DefaultStateName {
return b.path
}
return path.Join(b.path, name)
}
const errStateUnlock = `
Error unlocking Manta state. Lock ID: %s
Error: %s
You may have to force-unlock this state in order to use it again.
`

View File

@ -0,0 +1,123 @@
package manta
import (
"context"
"fmt"
"os"
"path"
"testing"
"time"
"github.com/hashicorp/terraform/backend"
"github.com/joyent/triton-go/storage"
)
func testACC(t *testing.T) {
skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_MANTA_TEST") == ""
if skip {
t.Log("Manta backend tests require setting TF_ACC or TF_MANTA_TEST")
t.Skip()
}
}
func TestBackend_impl(t *testing.T) {
var _ backend.Backend = new(Backend)
}
func TestBackend(t *testing.T) {
testACC(t)
directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix())
keyName := "testState"
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
"path": directory,
"objectName": keyName,
}).(*Backend)
createMantaFolder(t, b.storageClient, directory)
defer deleteMantaFolder(t, b.storageClient, directory)
backend.TestBackend(t, b, nil)
}
func TestBackendLocked(t *testing.T) {
testACC(t)
directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix())
keyName := "testState"
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"path": directory,
"objectName": keyName,
}).(*Backend)
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"path": directory,
"objectName": keyName,
}).(*Backend)
createMantaFolder(t, b1.storageClient, directory)
defer deleteMantaFolder(t, b1.storageClient, directory)
backend.TestBackend(t, b1, b2)
}
func createMantaFolder(t *testing.T, mantaClient *storage.StorageClient, directoryName string) {
// Be clear about what we're doing in case the user needs to clean
// this up later.
//t.Logf("creating Manta directory %s", directoryName)
err := mantaClient.Dir().Put(context.Background(), &storage.PutDirectoryInput{
DirectoryName: path.Join(mantaDefaultRootStore, directoryName),
})
if err != nil {
t.Fatal("failed to create test Manta directory:", err)
}
}
func deleteMantaFolder(t *testing.T, mantaClient *storage.StorageClient, directoryName string) {
//warning := "WARNING: Failed to delete the test Manta directory. It may have been left in your Manta account and may incur storage charges. (error was %s)"
// first we have to get rid of the env objects, or we can't delete the directory
objs, err := mantaClient.Dir().List(context.Background(), &storage.ListDirectoryInput{
DirectoryName: path.Join(mantaDefaultRootStore, directoryName),
})
if err != nil {
t.Fatal("failed to retrieve directory listing")
}
for _, obj := range objs.Entries {
if obj.Type == "directory" {
ojs, err := mantaClient.Dir().List(context.Background(), &storage.ListDirectoryInput{
DirectoryName: path.Join(mantaDefaultRootStore, directoryName, obj.Name),
})
if err != nil {
t.Fatal("failed to retrieve directory listing")
}
for _, oj := range ojs.Entries {
err := mantaClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{
ObjectPath: path.Join(mantaDefaultRootStore, directoryName, obj.Name, oj.Name),
})
if err != nil {
t.Fatal(err)
}
}
}
err := mantaClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{
ObjectPath: path.Join(mantaDefaultRootStore, directoryName, obj.Name),
})
if err != nil {
t.Fatal(err)
}
}
err = mantaClient.Dir().Delete(context.Background(), &storage.DeleteDirectoryInput{
DirectoryName: path.Join(mantaDefaultRootStore, directoryName),
})
if err != nil {
t.Fatal("failed to delete manta directory")
}
}

View File

@ -0,0 +1,201 @@
package manta
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"path"
"strings"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/joyent/triton-go/storage"
)
const (
mantaDefaultRootStore = "/stor"
lockFileName = "tflock"
)
type RemoteClient struct {
storageClient *storage.StorageClient
directoryName string
keyName string
statePath string
}
func (c *RemoteClient) Get() (*remote.Payload, error) {
output, err := c.storageClient.Objects().Get(context.Background(), &storage.GetObjectInput{
ObjectPath: path.Join(mantaDefaultRootStore, c.directoryName, c.keyName),
})
if err != nil {
if strings.Contains(err.Error(), "ResourceNotFound") {
return nil, nil
}
return nil, err
}
defer output.ObjectReader.Close()
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, output.ObjectReader); err != nil {
return nil, fmt.Errorf("Failed to read remote state: %s", err)
}
payload := &remote.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 *RemoteClient) Put(data []byte) error {
contentType := "application/json"
contentLength := int64(len(data))
params := &storage.PutObjectInput{
ContentType: contentType,
ContentLength: uint64(contentLength),
ObjectPath: path.Join(mantaDefaultRootStore, c.directoryName, c.keyName),
ObjectReader: bytes.NewReader(data),
}
log.Printf("[DEBUG] Uploading remote state to Manta: %#v", params)
err := c.storageClient.Objects().Put(context.Background(), params)
if err != nil {
return err
}
return nil
}
func (c *RemoteClient) Delete() error {
err := c.storageClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{
ObjectPath: path.Join(mantaDefaultRootStore, c.directoryName, c.keyName),
})
return err
}
func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) {
//At Joyent, we want to make sure that the State directory exists before we interact with it
//We don't expect users to have to create it in advance
//The order of operations of Backend State as follows:
// * Get - if this doesn't exist then we continue as though it's new
// * Lock - we make sure that the state directory exists as it's the entrance to writing to Manta
// * Put - put the state up there
// * Unlock - unlock the directory
//We can always guarantee that the user can put their state in the specified location because of this
err := c.storageClient.Dir().Put(context.Background(), &storage.PutDirectoryInput{
DirectoryName: path.Join(mantaDefaultRootStore, c.directoryName),
})
if err != nil {
return "", err
}
//firstly we want to check that a lock doesn't already exist
lockErr := &state.LockError{}
lockInfo, err := c.getLockInfo()
if err != nil {
if !strings.Contains(err.Error(), "ResourceNotFound") {
lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err)
return "", lockErr
}
}
if lockInfo != nil {
lockErr := &state.LockError{
Err: fmt.Errorf("A lock is already acquired"),
Info: lockInfo,
}
return "", lockErr
}
info.Path = path.Join(c.directoryName, lockFileName)
if info.ID == "" {
lockID, err := uuid.GenerateUUID()
if err != nil {
return "", err
}
info.ID = lockID
}
data := info.Marshal()
contentType := "application/json"
contentLength := int64(len(data))
params := &storage.PutObjectInput{
ContentType: contentType,
ContentLength: uint64(contentLength),
ObjectPath: path.Join(mantaDefaultRootStore, c.directoryName, lockFileName),
ObjectReader: bytes.NewReader(data),
}
log.Printf("[DEBUG] Creating manta state lock: %#v", params)
err = c.storageClient.Objects().Put(context.Background(), params)
if err != nil {
return "", err
}
return info.ID, nil
}
func (c *RemoteClient) Unlock(id string) error {
lockErr := &state.LockError{}
lockInfo, err := c.getLockInfo()
if err != nil {
lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err)
return lockErr
}
lockErr.Info = lockInfo
if lockInfo.ID != id {
lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id)
return lockErr
}
err = c.storageClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{
ObjectPath: path.Join(mantaDefaultRootStore, c.directoryName, lockFileName),
})
return err
}
func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) {
output, err := c.storageClient.Objects().Get(context.Background(), &storage.GetObjectInput{
ObjectPath: path.Join(mantaDefaultRootStore, c.directoryName, lockFileName),
})
if err != nil {
return nil, err
}
defer output.ObjectReader.Close()
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, output.ObjectReader); err != nil {
return nil, fmt.Errorf("Failed to read lock info: %s", err)
}
lockInfo := &state.LockInfo{}
err = json.Unmarshal(buf.Bytes(), lockInfo)
if err != nil {
return nil, err
}
return lockInfo, nil
}

View File

@ -0,0 +1,68 @@
package manta
import (
"testing"
"fmt"
"time"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state/remote"
)
func TestRemoteClient_impl(t *testing.T) {
var _ remote.Client = new(RemoteClient)
var _ remote.ClientLocker = new(RemoteClient)
}
func TestRemoteClient(t *testing.T) {
testACC(t)
directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix())
keyName := "testState"
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
"path": directory,
"objectName": keyName,
}).(*Backend)
createMantaFolder(t, b.storageClient, directory)
defer deleteMantaFolder(t, b.storageClient, directory)
state, err := b.State(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}
remote.TestClient(t, state.(*remote.State).Client)
}
func TestRemoteClientLocks(t *testing.T) {
testACC(t)
directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix())
keyName := "testState"
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"path": directory,
"objectName": keyName,
}).(*Backend)
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"path": directory,
"objectName": keyName,
}).(*Backend)
createMantaFolder(t, b1.storageClient, directory)
defer deleteMantaFolder(t, b1.storageClient, directory)
s1, err := b1.State(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}
s2, err := b2.State(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}
remote.TestRemoteLocks(t, s1.(*remote.State).Client, s2.(*remote.State).Client)
}

View File

@ -1,124 +0,0 @@
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

@ -1,58 +0,0 @@
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

@ -49,5 +49,4 @@ var BuiltinClients = map[string]Factory{
"etcd": etcdFactory,
"http": httpFactory,
"local": fileFactory,
"manta": mantaFactory,
}

373
vendor/github.com/joyent/triton-go/LICENSE generated vendored Normal file
View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

215
vendor/github.com/joyent/triton-go/README.md generated vendored Normal file
View File

@ -0,0 +1,215 @@
# triton-go
`triton-go` is an idiomatic library exposing a client SDK for Go applications
using Joyent's Triton Compute and Storage (Manta) APIs.
## Usage
Triton uses [HTTP Signature][4] to sign the Date header in each HTTP request
made to the Triton API. Currently, requests can be signed using either a private
key file loaded from disk (using an [`authentication.PrivateKeySigner`][5]), or
using a key stored with the local SSH Agent (using an [`SSHAgentSigner`][6].
To construct a Signer, use the `New*` range of methods in the `authentication`
package. In the case of `authentication.NewSSHAgentSigner`, the parameters are
the fingerprint of the key with which to sign, and the account name (normally
stored in the `SDC_ACCOUNT` environment variable). For example:
```
const fingerprint := "a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11"
sshKeySigner, err := authentication.NewSSHAgentSigner(fingerprint, "AccountName")
if err != nil {
log.Fatalf("NewSSHAgentSigner: %s", err)
}
```
An appropriate key fingerprint can be generated using `ssh-keygen`.
```
ssh-keygen -Emd5 -lf ~/.ssh/id_rsa.pub | cut -d " " -f 2 | sed 's/MD5://'
```
Each top level package, `account`, `compute`, `identity`, `network`, all have
their own seperate client. In order to initialize a package client, simply pass
the global `triton.ClientConfig` struct into the client's constructor function.
```go
config := &triton.ClientConfig{
TritonURL: os.Getenv("SDC_URL"),
MantaURL: os.Getenv("MANTA_URL"),
AccountName: accountName,
Signers: []authentication.Signer{sshKeySigner},
}
c, err := compute.NewClient(config)
if err != nil {
log.Fatalf("compute.NewClient: %s", err)
}
```
Constructing `compute.Client` returns an interface which exposes `compute` API
resources. The same goes for all other packages. Reference their unique
documentation for more information.
The same `triton.ClientConfig` will initialize the Manta `storage` client as
well...
```go
c, err := storage.NewClient(config)
if err != nil {
log.Fatalf("storage.NewClient: %s", err)
}
```
## Error Handling
If an error is returned by the HTTP API, the `error` returned from the function
will contain an instance of `compute.TritonError` in the chain. Error wrapping
is performed using the [errwrap][7] library from HashiCorp.
## Acceptance Tests
Acceptance Tests run directly against the Triton API, so you will need either a
local installation of Triton or an account with Joyent's Public Cloud offering
in order to run them. The tests create real resources (and thus cost real
money)!
In order to run acceptance tests, the following environment variables must be
set:
- `TRITON_TEST` - must be set to any value in order to indicate desire to create
resources
- `SDC_URL` - the base endpoint for the Triton API
- `SDC_ACCOUNT` - the account name for the Triton API
- `SDC_KEY_ID` - the fingerprint of the SSH key identifying the key
Additionally, you may set `SDC_KEY_MATERIAL` to the contents of an unencrypted
private key. If this is set, the PrivateKeySigner (see above) will be used - if
not the SSHAgentSigner will be used.
### Example Run
The verbose output has been removed for brevity here.
```
$ HTTP_PROXY=http://localhost:8888 \
TRITON_TEST=1 \
SDC_URL=https://us-sw-1.api.joyent.com \
SDC_ACCOUNT=AccountName \
SDC_KEY_ID=a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11 \
go test -v -run "TestAccKey"
=== RUN TestAccKey_Create
--- PASS: TestAccKey_Create (12.46s)
=== RUN TestAccKey_Get
--- PASS: TestAccKey_Get (4.30s)
=== RUN TestAccKey_Delete
--- PASS: TestAccKey_Delete (15.08s)
PASS
ok github.com/joyent/triton-go 31.861s
```
## Example API
There's an `examples/` directory available with sample code setup for many of
the APIs within this library. Most of these can be run using `go run` and
referencing your SSH key file use by your active `triton` CLI profile.
```sh
$ eval "$(triton env us-sw-1)"
$ SDC_KEY_FILE=~/.ssh/triton-id_rsa go run examples/compute/instances.go
```
The following is a complete example of how to initialize the `compute` package
client and list all instances under an account. More detailed usage of this
library follows.
```go
package main
import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"time"
triton "github.com/joyent/triton-go"
"github.com/joyent/triton-go/authentication"
"github.com/joyent/triton-go/compute"
)
func main() {
keyID := os.Getenv("SDC_KEY_ID")
accountName := os.Getenv("SDC_ACCOUNT")
keyMaterial := os.Getenv("SDC_KEY_MATERIAL")
var signer authentication.Signer
var err error
if keyMaterial == "" {
signer, err = authentication.NewSSHAgentSigner(keyID, accountName)
if err != nil {
log.Fatalf("Error Creating SSH Agent Signer: {{err}}", err)
}
} else {
var keyBytes []byte
if _, err = os.Stat(keyMaterial); err == nil {
keyBytes, err = ioutil.ReadFile(keyMaterial)
if err != nil {
log.Fatalf("Error reading key material from %s: %s",
keyMaterial, err)
}
block, _ := pem.Decode(keyBytes)
if block == nil {
log.Fatalf(
"Failed to read key material '%s': no key found", keyMaterial)
}
if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
log.Fatalf(
"Failed to read key '%s': password protected keys are\n"+
"not currently supported. Please decrypt the key prior to use.", keyMaterial)
}
} else {
keyBytes = []byte(keyMaterial)
}
signer, err = authentication.NewPrivateKeySigner(keyID, []byte(keyMaterial), accountName)
if err != nil {
log.Fatalf("Error Creating SSH Private Key Signer: {{err}}", err)
}
}
config := &triton.ClientConfig{
TritonURL: os.Getenv("SDC_URL"),
AccountName: accountName,
Signers: []authentication.Signer{signer},
}
c, err := compute.NewClient(config)
if err != nil {
log.Fatalf("compute.NewClient: %s", err)
}
listInput := &compute.ListInstancesInput{}
instances, err := c.Instances().List(context.Background(), listInput)
if err != nil {
log.Fatalf("compute.Instances.List: %v", err)
}
numInstances := 0
for _, instance := range instances {
numInstances++
fmt.Println(fmt.Sprintf("-- Instance: %v", instance.Name))
}
}
```
[4]: https://github.com/joyent/node-http-signature/blob/master/http_signing.md
[5]: https://godoc.org/github.com/joyent/triton-go/authentication
[6]: https://godoc.org/github.com/joyent/triton-go/authentication
[7]: https://github.com/hashicorp/go-errwrap

View File

@ -0,0 +1,66 @@
package authentication
import (
"encoding/asn1"
"encoding/base64"
"fmt"
"math/big"
"github.com/hashicorp/errwrap"
"golang.org/x/crypto/ssh"
)
type ecdsaSignature struct {
hashAlgorithm string
R *big.Int
S *big.Int
}
func (s *ecdsaSignature) SignatureType() string {
return fmt.Sprintf("ecdsa-%s", s.hashAlgorithm)
}
func (s *ecdsaSignature) String() string {
toEncode := struct {
R *big.Int
S *big.Int
}{
R: s.R,
S: s.S,
}
signatureBytes, err := asn1.Marshal(toEncode)
if err != nil {
panic(fmt.Sprintf("Error marshaling signature: %s", err))
}
return base64.StdEncoding.EncodeToString(signatureBytes)
}
func newECDSASignature(signatureBlob []byte) (*ecdsaSignature, error) {
var ecSig struct {
R *big.Int
S *big.Int
}
if err := ssh.Unmarshal(signatureBlob, &ecSig); err != nil {
return nil, errwrap.Wrapf("Error unmarshaling signature: {{err}}", err)
}
rValue := ecSig.R.Bytes()
var hashAlgorithm string
switch len(rValue) {
case 31, 32:
hashAlgorithm = "sha256"
case 65, 66:
hashAlgorithm = "sha512"
default:
return nil, fmt.Errorf("Unsupported key length: %d", len(rValue))
}
return &ecdsaSignature{
hashAlgorithm: hashAlgorithm,
R: ecSig.R,
S: ecSig.S,
}, nil
}

View File

@ -0,0 +1,106 @@
package authentication
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"strings"
"github.com/hashicorp/errwrap"
"golang.org/x/crypto/ssh"
)
type PrivateKeySigner struct {
formattedKeyFingerprint string
keyFingerprint string
algorithm string
accountName string
hashFunc crypto.Hash
privateKey *rsa.PrivateKey
}
func NewPrivateKeySigner(keyFingerprint string, privateKeyMaterial []byte, accountName string) (*PrivateKeySigner, error) {
keyFingerprintMD5 := strings.Replace(keyFingerprint, ":", "", -1)
block, _ := pem.Decode(privateKeyMaterial)
if block == nil {
return nil, errors.New("Error PEM-decoding private key material: nil block received")
}
rsakey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, errwrap.Wrapf("Error parsing private key: {{err}}", err)
}
sshPublicKey, err := ssh.NewPublicKey(rsakey.Public())
if err != nil {
return nil, errwrap.Wrapf("Error parsing SSH key from private key: {{err}}", err)
}
matchKeyFingerprint := formatPublicKeyFingerprint(sshPublicKey, false)
displayKeyFingerprint := formatPublicKeyFingerprint(sshPublicKey, true)
if matchKeyFingerprint != keyFingerprintMD5 {
return nil, errors.New("Private key file does not match public key fingerprint")
}
signer := &PrivateKeySigner{
formattedKeyFingerprint: displayKeyFingerprint,
keyFingerprint: keyFingerprint,
accountName: accountName,
hashFunc: crypto.SHA1,
privateKey: rsakey,
}
_, algorithm, err := signer.SignRaw("HelloWorld")
if err != nil {
return nil, fmt.Errorf("Cannot sign using ssh agent: %s", err)
}
signer.algorithm = algorithm
return signer, nil
}
func (s *PrivateKeySigner) Sign(dateHeader string) (string, error) {
const headerName = "date"
hash := s.hashFunc.New()
hash.Write([]byte(fmt.Sprintf("%s: %s", headerName, dateHeader)))
digest := hash.Sum(nil)
signed, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, s.hashFunc, digest)
if err != nil {
return "", errwrap.Wrapf("Error signing date header: {{err}}", err)
}
signedBase64 := base64.StdEncoding.EncodeToString(signed)
keyID := fmt.Sprintf("/%s/keys/%s", s.accountName, s.formattedKeyFingerprint)
return fmt.Sprintf(authorizationHeaderFormat, keyID, "rsa-sha1", headerName, signedBase64), nil
}
func (s *PrivateKeySigner) SignRaw(toSign string) (string, string, error) {
hash := s.hashFunc.New()
hash.Write([]byte(toSign))
digest := hash.Sum(nil)
signed, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, s.hashFunc, digest)
if err != nil {
return "", "", errwrap.Wrapf("Error signing date header: {{err}}", err)
}
signedBase64 := base64.StdEncoding.EncodeToString(signed)
return signedBase64, "rsa-sha1", nil
}
func (s *PrivateKeySigner) KeyFingerprint() string {
return s.formattedKeyFingerprint
}
func (s *PrivateKeySigner) DefaultAlgorithm() string {
return s.algorithm
}

View File

@ -0,0 +1,25 @@
package authentication
import (
"encoding/base64"
)
type rsaSignature struct {
hashAlgorithm string
signature []byte
}
func (s *rsaSignature) SignatureType() string {
return s.hashAlgorithm
}
func (s *rsaSignature) String() string {
return base64.StdEncoding.EncodeToString(s.signature)
}
func newRSASignature(signatureBlob []byte) (*rsaSignature, error) {
return &rsaSignature{
hashAlgorithm: "rsa-sha1",
signature: signatureBlob,
}, nil
}

View File

@ -0,0 +1,27 @@
package authentication
import (
"regexp"
"fmt"
)
type httpAuthSignature interface {
SignatureType() string
String() string
}
func keyFormatToKeyType(keyFormat string) (string, error) {
if keyFormat == "ssh-rsa" {
return "rsa", nil
}
if keyFormat == "ssh-ed25519" {
return "ed25519", nil
}
if regexp.MustCompile("^ecdsa-sha2-*").Match([]byte(keyFormat)) {
return "ecdsa", nil
}
return "", fmt.Errorf("Unknown key format: %s", keyFormat)
}

View File

@ -0,0 +1,10 @@
package authentication
const authorizationHeaderFormat = `Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"`
type Signer interface {
DefaultAlgorithm() string
KeyFingerprint() string
Sign(dateHeader string) (string, error)
SignRaw(toSign string) (string, string, error)
}

View File

@ -0,0 +1,159 @@
package authentication
import (
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"net"
"os"
"strings"
"github.com/hashicorp/errwrap"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
type SSHAgentSigner struct {
formattedKeyFingerprint string
keyFingerprint string
algorithm string
accountName string
keyIdentifier string
agent agent.Agent
key ssh.PublicKey
}
func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, error) {
sshAgentAddress := os.Getenv("SSH_AUTH_SOCK")
if sshAgentAddress == "" {
return nil, errors.New("SSH_AUTH_SOCK is not set")
}
conn, err := net.Dial("unix", sshAgentAddress)
if err != nil {
return nil, errwrap.Wrapf("Error dialing SSH agent: {{err}}", err)
}
ag := agent.NewClient(conn)
keys, err := ag.List()
if err != nil {
return nil, errwrap.Wrapf("Error listing keys in SSH Agent: %s", err)
}
keyFingerprintStripped := strings.TrimPrefix(keyFingerprint, "MD5:")
keyFingerprintStripped = strings.TrimPrefix(keyFingerprintStripped, "SHA256:")
keyFingerprintStripped = strings.Replace(keyFingerprintStripped, ":", "", -1)
var matchingKey ssh.PublicKey
for _, key := range keys {
keyMD5 := md5.New()
keyMD5.Write(key.Marshal())
finalizedMD5 := fmt.Sprintf("%x", keyMD5.Sum(nil))
keySHA256 := sha256.New()
keySHA256.Write(key.Marshal())
finalizedSHA256 := base64.RawStdEncoding.EncodeToString(keySHA256.Sum(nil))
if keyFingerprintStripped == finalizedMD5 || keyFingerprintStripped == finalizedSHA256 {
matchingKey = key
}
}
if matchingKey == nil {
return nil, fmt.Errorf("No key in the SSH Agent matches fingerprint: %s", keyFingerprint)
}
formattedKeyFingerprint := formatPublicKeyFingerprint(matchingKey, true)
signer := &SSHAgentSigner{
formattedKeyFingerprint: formattedKeyFingerprint,
keyFingerprint: keyFingerprint,
accountName: accountName,
agent: ag,
key: matchingKey,
keyIdentifier: fmt.Sprintf("/%s/keys/%s", accountName, formattedKeyFingerprint),
}
_, algorithm, err := signer.SignRaw("HelloWorld")
if err != nil {
return nil, fmt.Errorf("Cannot sign using ssh agent: %s", err)
}
signer.algorithm = algorithm
return signer, nil
}
func (s *SSHAgentSigner) Sign(dateHeader string) (string, error) {
const headerName = "date"
signature, err := s.agent.Sign(s.key, []byte(fmt.Sprintf("%s: %s", headerName, dateHeader)))
if err != nil {
return "", errwrap.Wrapf("Error signing date header: {{err}}", err)
}
keyFormat, err := keyFormatToKeyType(signature.Format)
if err != nil {
return "", errwrap.Wrapf("Error reading signature: {{err}}", err)
}
var authSignature httpAuthSignature
switch keyFormat {
case "rsa":
authSignature, err = newRSASignature(signature.Blob)
if err != nil {
return "", errwrap.Wrapf("Error reading signature: {{err}}", err)
}
case "ecdsa":
authSignature, err = newECDSASignature(signature.Blob)
if err != nil {
return "", errwrap.Wrapf("Error reading signature: {{err}}", err)
}
default:
return "", fmt.Errorf("Unsupported algorithm from SSH agent: %s", signature.Format)
}
return fmt.Sprintf(authorizationHeaderFormat, s.keyIdentifier,
authSignature.SignatureType(), headerName, authSignature.String()), nil
}
func (s *SSHAgentSigner) SignRaw(toSign string) (string, string, error) {
signature, err := s.agent.Sign(s.key, []byte(toSign))
if err != nil {
return "", "", errwrap.Wrapf("Error signing string: {{err}}", err)
}
keyFormat, err := keyFormatToKeyType(signature.Format)
if err != nil {
return "", "", errwrap.Wrapf("Error reading signature: {{err}}", err)
}
var authSignature httpAuthSignature
switch keyFormat {
case "rsa":
authSignature, err = newRSASignature(signature.Blob)
if err != nil {
return "", "", errwrap.Wrapf("Error reading signature: {{err}}", err)
}
case "ecdsa":
authSignature, err = newECDSASignature(signature.Blob)
if err != nil {
return "", "", errwrap.Wrapf("Error reading signature: {{err}}", err)
}
default:
return "", "", fmt.Errorf("Unsupported algorithm from SSH agent: %s", signature.Format)
}
return authSignature.String(), authSignature.SignatureType(), nil
}
func (s *SSHAgentSigner) KeyFingerprint() string {
return s.formattedKeyFingerprint
}
func (s *SSHAgentSigner) DefaultAlgorithm() string {
return s.algorithm
}

View File

@ -0,0 +1,29 @@
package authentication
import (
"crypto/md5"
"fmt"
"strings"
"golang.org/x/crypto/ssh"
)
// formatPublicKeyFingerprint produces the MD5 fingerprint of the given SSH
// public key. If display is true, the fingerprint is formatted with colons
// between each byte, as per the output of OpenSSL.
func formatPublicKeyFingerprint(key ssh.PublicKey, display bool) string {
publicKeyFingerprint := md5.New()
publicKeyFingerprint.Write(key.Marshal())
publicKeyFingerprintString := fmt.Sprintf("%x", publicKeyFingerprint.Sum(nil))
if !display {
return publicKeyFingerprintString
}
formatted := ""
for i := 0; i < len(publicKeyFingerprintString); i = i + 2 {
formatted = fmt.Sprintf("%s%s:", formatted, publicKeyFingerprintString[i:i+2])
}
return strings.TrimSuffix(formatted, ":")
}

397
vendor/github.com/joyent/triton-go/client/client.go generated vendored Normal file
View File

@ -0,0 +1,397 @@
package client
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"io"
"net"
"net/http"
"net/url"
"os"
"time"
"github.com/hashicorp/errwrap"
"github.com/joyent/triton-go/authentication"
)
const nilContext = "nil context"
var MissingKeyIdError = errors.New("Default SSH agent authentication requires SDC_KEY_ID")
// Client represents a connection to the Triton Compute or Object Storage APIs.
type Client struct {
HTTPClient *http.Client
Authorizers []authentication.Signer
TritonURL url.URL
MantaURL url.URL
AccountName string
Endpoint string
}
// New is used to construct a Client in order to make API
// requests to the Triton API.
//
// At least one signer must be provided - example signers include
// authentication.PrivateKeySigner and authentication.SSHAgentSigner.
func New(tritonURL string, mantaURL string, accountName string, signers ...authentication.Signer) (*Client, error) {
cloudURL, err := url.Parse(tritonURL)
if err != nil {
return nil, errwrap.Wrapf("invalid endpoint URL: {{err}}", err)
}
storageURL, err := url.Parse(mantaURL)
if err != nil {
return nil, errwrap.Wrapf("invalid manta URL: {{err}}", err)
}
if accountName == "" {
return nil, errors.New("account name can not be empty")
}
httpClient := &http.Client{
Transport: httpTransport(false),
CheckRedirect: doNotFollowRedirects,
}
newClient := &Client{
HTTPClient: httpClient,
Authorizers: signers,
TritonURL: *cloudURL,
MantaURL: *storageURL,
AccountName: accountName,
// TODO(justinwr): Deprecated?
// Endpoint: tritonURL,
}
var authorizers []authentication.Signer
for _, key := range signers {
if key != nil {
authorizers = append(authorizers, key)
}
}
// Default to constructing an SSHAgentSigner if there are no other signers
// passed into NewClient and there's an SDC_KEY_ID value available in the
// user environ.
if len(authorizers) == 0 {
keyID := os.Getenv("SDC_KEY_ID")
if len(keyID) != 0 {
keySigner, err := authentication.NewSSHAgentSigner(keyID, accountName)
if err != nil {
return nil, errwrap.Wrapf("Problem initializing NewSSHAgentSigner: {{err}}", err)
}
newClient.Authorizers = append(authorizers, keySigner)
} else {
return nil, MissingKeyIdError
}
}
return newClient, nil
}
// InsecureSkipTLSVerify turns off TLS verification for the client connection. This
// allows connection to an endpoint with a certificate which was signed by a non-
// trusted CA, such as self-signed certificates. This can be useful when connecting
// to temporary Triton installations such as Triton Cloud-On-A-Laptop.
func (c *Client) InsecureSkipTLSVerify() {
if c.HTTPClient == nil {
return
}
c.HTTPClient.Transport = httpTransport(true)
}
func httpTransport(insecureSkipTLSVerify bool) *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
DisableKeepAlives: true,
MaxIdleConnsPerHost: -1,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecureSkipTLSVerify,
},
}
}
func doNotFollowRedirects(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}
// TODO(justinwr): Deprecated?
// func (c *Client) FormatURL(path string) string {
// return fmt.Sprintf("%s%s", c.Endpoint, path)
// }
func (c *Client) DecodeError(statusCode int, body io.Reader) error {
err := &TritonError{
StatusCode: statusCode,
}
errorDecoder := json.NewDecoder(body)
if err := errorDecoder.Decode(err); err != nil {
return errwrap.Wrapf("Error decoding error response: {{err}}", err)
}
return err
}
// -----------------------------------------------------------------------------
type RequestInput struct {
Method string
Path string
Query *url.Values
Headers *http.Header
Body interface{}
}
func (c *Client) ExecuteRequestURIParams(ctx context.Context, inputs RequestInput) (io.ReadCloser, error) {
method := inputs.Method
path := inputs.Path
body := inputs.Body
query := inputs.Query
var requestBody io.ReadSeeker
if body != nil {
marshaled, err := json.MarshalIndent(body, "", " ")
if err != nil {
return nil, err
}
requestBody = bytes.NewReader(marshaled)
}
endpoint := c.TritonURL
endpoint.Path = path
if query != nil {
endpoint.RawQuery = query.Encode()
}
req, err := http.NewRequest(method, endpoint.String(), requestBody)
if err != nil {
return nil, errwrap.Wrapf("Error constructing HTTP request: {{err}}", err)
}
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
// NewClient ensures there's always an authorizer (unless this is called
// outside that constructor).
authHeader, err := c.Authorizers[0].Sign(dateHeader)
if err != nil {
return nil, errwrap.Wrapf("Error signing HTTP request: {{err}}", err)
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "application/json")
req.Header.Set("Accept-Version", "8")
req.Header.Set("User-Agent", "triton-go Client API")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, errwrap.Wrapf("Error executing HTTP request: {{err}}", err)
}
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
return resp.Body, nil
}
return nil, c.DecodeError(resp.StatusCode, resp.Body)
}
func (c *Client) ExecuteRequest(ctx context.Context, inputs RequestInput) (io.ReadCloser, error) {
return c.ExecuteRequestURIParams(ctx, inputs)
}
func (c *Client) ExecuteRequestRaw(ctx context.Context, inputs RequestInput) (*http.Response, error) {
method := inputs.Method
path := inputs.Path
body := inputs.Body
var requestBody io.ReadSeeker
if body != nil {
marshaled, err := json.MarshalIndent(body, "", " ")
if err != nil {
return nil, err
}
requestBody = bytes.NewReader(marshaled)
}
endpoint := c.TritonURL
endpoint.Path = path
req, err := http.NewRequest(method, endpoint.String(), requestBody)
if err != nil {
return nil, errwrap.Wrapf("Error constructing HTTP request: {{err}}", err)
}
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
// NewClient ensures there's always an authorizer (unless this is called
// outside that constructor).
authHeader, err := c.Authorizers[0].Sign(dateHeader)
if err != nil {
return nil, errwrap.Wrapf("Error signing HTTP request: {{err}}", err)
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "application/json")
req.Header.Set("Accept-Version", "8")
req.Header.Set("User-Agent", "triton-go c API")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, errwrap.Wrapf("Error executing HTTP request: {{err}}", err)
}
return resp, nil
}
func (c *Client) ExecuteRequestStorage(ctx context.Context, inputs RequestInput) (io.ReadCloser, http.Header, error) {
method := inputs.Method
path := inputs.Path
query := inputs.Query
headers := inputs.Headers
body := inputs.Body
endpoint := c.MantaURL
endpoint.Path = path
var requestBody io.ReadSeeker
if body != nil {
marshaled, err := json.MarshalIndent(body, "", " ")
if err != nil {
return nil, nil, err
}
requestBody = bytes.NewReader(marshaled)
}
req, err := http.NewRequest(method, endpoint.String(), requestBody)
if err != nil {
return nil, nil, errwrap.Wrapf("Error constructing HTTP request: {{err}}", err)
}
if body != nil && (headers == nil || headers.Get("Content-Type") == "") {
req.Header.Set("Content-Type", "application/json")
}
if headers != nil {
for key, values := range *headers {
for _, value := range values {
req.Header.Set(key, value)
}
}
}
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
authHeader, err := c.Authorizers[0].Sign(dateHeader)
if err != nil {
return nil, nil, errwrap.Wrapf("Error signing HTTP request: {{err}}", err)
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "*/*")
req.Header.Set("User-Agent", "manta-go client API")
if query != nil {
req.URL.RawQuery = query.Encode()
}
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, nil, errwrap.Wrapf("Error executing HTTP request: {{err}}", err)
}
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
return resp.Body, resp.Header, nil
}
mantaError := &MantaError{
StatusCode: resp.StatusCode,
}
errorDecoder := json.NewDecoder(resp.Body)
if err := errorDecoder.Decode(mantaError); err != nil {
return nil, nil, errwrap.Wrapf("Error decoding error response: {{err}}", err)
}
return nil, nil, mantaError
}
type RequestNoEncodeInput struct {
Method string
Path string
Query *url.Values
Headers *http.Header
Body io.ReadSeeker
}
func (c *Client) ExecuteRequestNoEncode(ctx context.Context, inputs RequestNoEncodeInput) (io.ReadCloser, http.Header, error) {
method := inputs.Method
path := inputs.Path
query := inputs.Query
headers := inputs.Headers
body := inputs.Body
endpoint := c.MantaURL
endpoint.Path = path
req, err := http.NewRequest(method, endpoint.String(), body)
if err != nil {
return nil, nil, errwrap.Wrapf("Error constructing HTTP request: {{err}}", err)
}
if headers != nil {
for key, values := range *headers {
for _, value := range values {
req.Header.Set(key, value)
}
}
}
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
authHeader, err := c.Authorizers[0].Sign(dateHeader)
if err != nil {
return nil, nil, errwrap.Wrapf("Error signing HTTP request: {{err}}", err)
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "*/*")
req.Header.Set("User-Agent", "manta-go client API")
if query != nil {
req.URL.RawQuery = query.Encode()
}
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, nil, errwrap.Wrapf("Error executing HTTP request: {{err}}", err)
}
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
return resp.Body, resp.Header, nil
}
mantaError := &MantaError{
StatusCode: resp.StatusCode,
}
errorDecoder := json.NewDecoder(resp.Body)
if err := errorDecoder.Decode(mantaError); err != nil {
return nil, nil, errwrap.Wrapf("Error decoding error response: {{err}}", err)
}
return nil, nil, mantaError
}

190
vendor/github.com/joyent/triton-go/client/errors.go generated vendored Normal file
View File

@ -0,0 +1,190 @@
package client
import (
"fmt"
"github.com/hashicorp/errwrap"
)
// ClientError represents an error code and message along with the status code
// of the HTTP request which resulted in the error message.
type ClientError struct {
StatusCode int
Code string
Message string
}
// Error implements interface Error on the TritonError type.
func (e ClientError) Error() string {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
// MantaError represents an error code and message along with
// the status code of the HTTP request which resulted in the error
// message. Error codes used by the Manta API are listed at
// https://apidocs.joyent.com/manta/api.html#errors
type MantaError struct {
StatusCode int
Code string `json:"code"`
Message string `json:"message"`
}
// Error implements interface Error on the MantaError type.
func (e MantaError) Error() string {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
// TritonError represents an error code and message along with
// the status code of the HTTP request which resulted in the error
// message. Error codes used by the Triton API are listed at
// https://apidocs.joyent.com/cloudapi/#cloudapi-http-responses
type TritonError struct {
StatusCode int
Code string `json:"code"`
Message string `json:"message"`
}
// Error implements interface Error on the TritonError type.
func (e TritonError) Error() string {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
func IsAuthSchemeError(err error) bool {
return isSpecificError(err, "AuthScheme")
}
func IsAuthorizationError(err error) bool {
return isSpecificError(err, "Authorization")
}
func IsBadRequestError(err error) bool {
return isSpecificError(err, "BadRequest")
}
func IsChecksumError(err error) bool {
return isSpecificError(err, "Checksum")
}
func IsConcurrentRequestError(err error) bool {
return isSpecificError(err, "ConcurrentRequest")
}
func IsContentLengthError(err error) bool {
return isSpecificError(err, "ContentLength")
}
func IsContentMD5MismatchError(err error) bool {
return isSpecificError(err, "ContentMD5Mismatch")
}
func IsEntityExistsError(err error) bool {
return isSpecificError(err, "EntityExists")
}
func IsInvalidArgumentError(err error) bool {
return isSpecificError(err, "InvalidArgument")
}
func IsInvalidAuthTokenError(err error) bool {
return isSpecificError(err, "InvalidAuthToken")
}
func IsInvalidCredentialsError(err error) bool {
return isSpecificError(err, "InvalidCredentials")
}
func IsInvalidDurabilityLevelError(err error) bool {
return isSpecificError(err, "InvalidDurabilityLevel")
}
func IsInvalidKeyIdError(err error) bool {
return isSpecificError(err, "InvalidKeyId")
}
func IsInvalidJobError(err error) bool {
return isSpecificError(err, "InvalidJob")
}
func IsInvalidLinkError(err error) bool {
return isSpecificError(err, "InvalidLink")
}
func IsInvalidLimitError(err error) bool {
return isSpecificError(err, "InvalidLimit")
}
func IsInvalidSignatureError(err error) bool {
return isSpecificError(err, "InvalidSignature")
}
func IsInvalidUpdateError(err error) bool {
return isSpecificError(err, "InvalidUpdate")
}
func IsDirectoryDoesNotExistError(err error) bool {
return isSpecificError(err, "DirectoryDoesNotExist")
}
func IsDirectoryExistsError(err error) bool {
return isSpecificError(err, "DirectoryExists")
}
func IsDirectoryNotEmptyError(err error) bool {
return isSpecificError(err, "DirectoryNotEmpty")
}
func IsDirectoryOperationError(err error) bool {
return isSpecificError(err, "DirectoryOperation")
}
func IsInternalError(err error) bool {
return isSpecificError(err, "Internal")
}
func IsJobNotFoundError(err error) bool {
return isSpecificError(err, "JobNotFound")
}
func IsJobStateError(err error) bool {
return isSpecificError(err, "JobState")
}
func IsKeyDoesNotExistError(err error) bool {
return isSpecificError(err, "KeyDoesNotExist")
}
func IsNotAcceptableError(err error) bool {
return isSpecificError(err, "NotAcceptable")
}
func IsNotEnoughSpaceError(err error) bool {
return isSpecificError(err, "NotEnoughSpace")
}
func IsLinkNotFoundError(err error) bool {
return isSpecificError(err, "LinkNotFound")
}
func IsLinkNotObjectError(err error) bool {
return isSpecificError(err, "LinkNotObject")
}
func IsLinkRequiredError(err error) bool {
return isSpecificError(err, "LinkRequired")
}
func IsParentNotDirectoryError(err error) bool {
return isSpecificError(err, "ParentNotDirectory")
}
func IsPreconditionFailedError(err error) bool {
return isSpecificError(err, "PreconditionFailed")
}
func IsPreSignedRequestError(err error) bool {
return isSpecificError(err, "PreSignedRequest")
}
func IsRequestEntityTooLargeError(err error) bool {
return isSpecificError(err, "RequestEntityTooLarge")
}
func IsResourceNotFoundError(err error) bool {
return isSpecificError(err, "ResourceNotFound")
}
func IsRootDirectoryError(err error) bool {
return isSpecificError(err, "RootDirectory")
}
func IsServiceUnavailableError(err error) bool {
return isSpecificError(err, "ServiceUnavailable")
}
func IsSSLRequiredError(err error) bool {
return isSpecificError(err, "SSLRequired")
}
func IsUploadTimeoutError(err error) bool {
return isSpecificError(err, "UploadTimeout")
}
func IsUserDoesNotExistError(err error) bool {
return isSpecificError(err, "UserDoesNotExist")
}
// isSpecificError checks whether the error represented by err wraps
// an underlying MantaError with code errorCode.
func isSpecificError(err error, errorCode string) bool {
tritonErrorInterface := errwrap.GetType(err.(error), &MantaError{})
if tritonErrorInterface == nil {
return false
}
tritonErr := tritonErrorInterface.(*MantaError)
if tritonErr.Code == errorCode {
return true
}
return false
}

51
vendor/github.com/joyent/triton-go/storage/client.go generated vendored Normal file
View File

@ -0,0 +1,51 @@
package storage
import (
triton "github.com/joyent/triton-go"
"github.com/joyent/triton-go/client"
)
type StorageClient struct {
Client *client.Client
}
func newStorageClient(client *client.Client) *StorageClient {
return &StorageClient{
Client: client,
}
}
// NewClient returns a new client for working with Storage endpoints and
// resources within CloudAPI
func NewClient(config *triton.ClientConfig) (*StorageClient, error) {
// TODO: Utilize config interface within the function itself
client, err := client.New(config.TritonURL, config.MantaURL, config.AccountName, config.Signers...)
if err != nil {
return nil, err
}
return newStorageClient(client), nil
}
// Dir returns a DirectoryClient used for accessing functions pertaining to
// Directories functionality of the Manta API.
func (c *StorageClient) Dir() *DirectoryClient {
return &DirectoryClient{c.Client}
}
// Jobs returns a JobClient used for accessing functions pertaining to Jobs
// functionality of the Triton Object Storage API.
func (c *StorageClient) Jobs() *JobClient {
return &JobClient{c.Client}
}
// Objects returns an ObjectsClient used for accessing functions pertaining to
// Objects functionality of the Triton Object Storage API.
func (c *StorageClient) Objects() *ObjectsClient {
return &ObjectsClient{c.Client}
}
// SnapLinks returns an SnapLinksClient used for accessing functions pertaining to
// SnapLinks functionality of the Triton Object Storage API.
func (c *StorageClient) SnapLinks() *SnapLinksClient {
return &SnapLinksClient{c.Client}
}

144
vendor/github.com/joyent/triton-go/storage/directory.go generated vendored Normal file
View File

@ -0,0 +1,144 @@
package storage
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
"github.com/hashicorp/errwrap"
"github.com/joyent/triton-go/client"
)
type DirectoryClient struct {
client *client.Client
}
// DirectoryEntry represents an object or directory in Manta.
type DirectoryEntry struct {
ETag string `json:"etag"`
ModifiedTime time.Time `json:"mtime"`
Name string `json:"name"`
Size uint64 `json:"size"`
Type string `json:"type"`
}
// ListDirectoryInput represents parameters to a ListDirectory operation.
type ListDirectoryInput struct {
DirectoryName string
Limit uint64
Marker string
}
// ListDirectoryOutput contains the outputs of a ListDirectory operation.
type ListDirectoryOutput struct {
Entries []*DirectoryEntry
ResultSetSize uint64
}
// List lists the contents of a directory on the Triton Object Store service.
func (s *DirectoryClient) List(ctx context.Context, input *ListDirectoryInput) (*ListDirectoryOutput, error) {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.DirectoryName)
query := &url.Values{}
if input.Limit != 0 {
query.Set("limit", strconv.FormatUint(input.Limit, 10))
}
if input.Marker != "" {
query.Set("manta_path", input.Marker)
}
reqInput := client.RequestInput{
Method: http.MethodGet,
Path: path,
Query: query,
}
respBody, respHeader, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return nil, errwrap.Wrapf("Error executing ListDirectory request: {{err}}", err)
}
var results []*DirectoryEntry
for {
current := &DirectoryEntry{}
decoder := json.NewDecoder(respBody)
if err = decoder.Decode(&current); err != nil {
if err == io.EOF {
break
}
return nil, errwrap.Wrapf("Error decoding ListDirectory response: {{err}}", err)
}
results = append(results, current)
}
output := &ListDirectoryOutput{
Entries: results,
}
resultSetSize, err := strconv.ParseUint(respHeader.Get("Result-Set-Size"), 10, 64)
if err == nil {
output.ResultSetSize = resultSetSize
}
return output, nil
}
// PutDirectoryInput represents parameters to a PutDirectory operation.
type PutDirectoryInput struct {
DirectoryName string
}
// Put puts a directoy into the Triton Object Storage service is an idempotent
// create-or-update operation. Your private namespace starts at /:login, and you
// can create any nested set of directories or objects within it.
func (s *DirectoryClient) Put(ctx context.Context, input *PutDirectoryInput) error {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.DirectoryName)
headers := &http.Header{}
headers.Set("Content-Type", "application/json; type=directory")
reqInput := client.RequestInput{
Method: http.MethodPut,
Path: path,
Headers: headers,
}
respBody, _, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return errwrap.Wrapf("Error executing PutDirectory request: {{err}}", err)
}
return nil
}
// DeleteDirectoryInput represents parameters to a DeleteDirectory operation.
type DeleteDirectoryInput struct {
DirectoryName string
}
// Delete deletes a directory on the Triton Object Storage. The directory must
// be empty.
func (s *DirectoryClient) Delete(ctx context.Context, input *DeleteDirectoryInput) error {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.DirectoryName)
reqInput := client.RequestInput{
Method: http.MethodDelete,
Path: path,
}
respBody, _, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return errwrap.Wrapf("Error executing DeleteDirectory request: {{err}}", err)
}
return nil
}

440
vendor/github.com/joyent/triton-go/storage/job.go generated vendored Normal file
View File

@ -0,0 +1,440 @@
package storage
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/hashicorp/errwrap"
"github.com/joyent/triton-go/client"
)
type JobClient struct {
client *client.Client
}
const (
JobStateDone = "done"
JobStateRunning = "running"
)
// JobPhase represents the specification for a map or reduce phase of a Manta
// job.
type JobPhase struct {
// Type is the type of phase. Must be `map` or `reduce`.
Type string `json:"type,omitempty"`
// Assets is an array of objects to be placed in your compute zones.
Assets []string `json:"assets,omitempty"`
// Exec is the shell statement to execute. It may be any valid shell
// command, including pipelines and other shell syntax. You can also
// execute programs stored in the service by including them in "assets"
// and referencing them as /assets/$manta_path.
Exec string `json:"exec"`
// Init is a shell statement to execute in each compute zone before
// any tasks are executed. The same constraints apply as to Exec.
Init string `json:"init"`
// ReducerCount is an optional number of reducers for this phase. The
// default value if not specified is 1. The maximum value is 1024.
ReducerCount uint `json:"count,omitempty"`
// Memory is the amount of DRAM in MB to be allocated to the compute
// zone. Valid values are 256, 512, 1024, 2048, 4096 or 8192.
Memory uint64 `json:"memory,omitempty"`
// Disk is the amount of disk space in GB to be allocated to the compute
// zone. Valid values are 2, 4, 8, 16, 32, 64, 128, 256, 512 or 1024.
Disk uint64 `json:"disk,omitempty"`
}
// JobSummary represents the summary of a compute job in Manta.
type JobSummary struct {
ModifiedTime time.Time `json:"mtime"`
ID string `json:"name"`
}
// Job represents a compute job in Manta.
type Job struct {
ID string `json:"id"`
Name string `json:"name"`
Phases []*JobPhase `json:"phases"`
State string `json:"state"`
Cancelled bool `json:"cancelled"`
InputDone bool `json:"inputDone"`
CreatedTime time.Time `json:"timeCreated"`
DoneTime time.Time `json:"timeDone"`
Transient bool `json:"transient"`
Stats *JobStats `json:"stats"`
}
// JobStats represents statistics for a compute job in Manta.
type JobStats struct {
Errors uint64 `json:"errors"`
Outputs uint64 `json:"outputs"`
Retries uint64 `json:"retries"`
Tasks uint64 `json:"tasks"`
TasksDone uint64 `json:"tasksDone"`
}
// CreateJobInput represents parameters to a CreateJob operation.
type CreateJobInput struct {
Name string `json:"name"`
Phases []*JobPhase `json:"phases"`
}
// CreateJobOutput contains the outputs of a CreateJob operation.
type CreateJobOutput struct {
JobID string
}
// CreateJob submits a new job to be executed. This call is not
// idempotent, so calling it twice will create two jobs.
func (s *JobClient) Create(ctx context.Context, input *CreateJobInput) (*CreateJobOutput, error) {
path := fmt.Sprintf("/%s/jobs", s.client.AccountName)
reqInput := client.RequestInput{
Method: http.MethodPost,
Path: path,
Body: input,
}
respBody, respHeaders, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return nil, errwrap.Wrapf("Error executing CreateJob request: {{err}}", err)
}
jobURI := respHeaders.Get("Location")
parts := strings.Split(jobURI, "/")
jobID := parts[len(parts)-1]
response := &CreateJobOutput{
JobID: jobID,
}
return response, nil
}
// AddJobInputs represents parameters to a AddJobInputs operation.
type AddJobInputsInput struct {
JobID string
ObjectPaths []string
}
// AddJobInputs submits inputs to an already created job.
func (s *JobClient) AddInputs(ctx context.Context, input *AddJobInputsInput) error {
path := fmt.Sprintf("/%s/jobs/%s/live/in", s.client.AccountName, input.JobID)
headers := &http.Header{}
headers.Set("Content-Type", "text/plain")
reader := strings.NewReader(strings.Join(input.ObjectPaths, "\n"))
reqInput := client.RequestNoEncodeInput{
Method: http.MethodPost,
Path: path,
Headers: headers,
Body: reader,
}
respBody, _, err := s.client.ExecuteRequestNoEncode(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return errwrap.Wrapf("Error executing AddJobInputs request: {{err}}", err)
}
return nil
}
// EndJobInputInput represents parameters to a EndJobInput operation.
type EndJobInputInput struct {
JobID string
}
// EndJobInput submits inputs to an already created job.
func (s *JobClient) EndInput(ctx context.Context, input *EndJobInputInput) error {
path := fmt.Sprintf("/%s/jobs/%s/live/in/end", s.client.AccountName, input.JobID)
reqInput := client.RequestNoEncodeInput{
Method: http.MethodPost,
Path: path,
}
respBody, _, err := s.client.ExecuteRequestNoEncode(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return errwrap.Wrapf("Error executing EndJobInput request: {{err}}", err)
}
return nil
}
// CancelJobInput represents parameters to a CancelJob operation.
type CancelJobInput struct {
JobID string
}
// CancelJob cancels a job from doing any further work. Cancellation
// is asynchronous and "best effort"; there is no guarantee the job
// will actually stop. For example, short jobs where input is already
// closed will likely still run to completion.
//
// This is however useful when:
// - input is still open
// - you have a long-running job
func (s *JobClient) Cancel(ctx context.Context, input *CancelJobInput) error {
path := fmt.Sprintf("/%s/jobs/%s/live/cancel", s.client.AccountName, input.JobID)
reqInput := client.RequestNoEncodeInput{
Method: http.MethodPost,
Path: path,
}
respBody, _, err := s.client.ExecuteRequestNoEncode(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return errwrap.Wrapf("Error executing CancelJob request: {{err}}", err)
}
return nil
}
// ListJobsInput represents parameters to a ListJobs operation.
type ListJobsInput struct {
RunningOnly bool
Limit uint64
Marker string
}
// ListJobsOutput contains the outputs of a ListJobs operation.
type ListJobsOutput struct {
Jobs []*JobSummary
ResultSetSize uint64
}
// ListJobs returns the list of jobs you currently have.
func (s *JobClient) List(ctx context.Context, input *ListJobsInput) (*ListJobsOutput, error) {
path := fmt.Sprintf("/%s/jobs", s.client.AccountName)
query := &url.Values{}
if input.RunningOnly {
query.Set("state", "running")
}
if input.Limit != 0 {
query.Set("limit", strconv.FormatUint(input.Limit, 10))
}
if input.Marker != "" {
query.Set("manta_path", input.Marker)
}
reqInput := client.RequestInput{
Method: http.MethodGet,
Path: path,
Query: query,
}
respBody, respHeader, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return nil, errwrap.Wrapf("Error executing ListJobs request: {{err}}", err)
}
var results []*JobSummary
for {
current := &JobSummary{}
decoder := json.NewDecoder(respBody)
if err = decoder.Decode(&current); err != nil {
if err == io.EOF {
break
}
return nil, errwrap.Wrapf("Error decoding ListJobs response: {{err}}", err)
}
results = append(results, current)
}
output := &ListJobsOutput{
Jobs: results,
}
resultSetSize, err := strconv.ParseUint(respHeader.Get("Result-Set-Size"), 10, 64)
if err == nil {
output.ResultSetSize = resultSetSize
}
return output, nil
}
// GetJobInput represents parameters to a GetJob operation.
type GetJobInput struct {
JobID string
}
// GetJobOutput contains the outputs of a GetJob operation.
type GetJobOutput struct {
Job *Job
}
// GetJob returns the list of jobs you currently have.
func (s *JobClient) Get(ctx context.Context, input *GetJobInput) (*GetJobOutput, error) {
path := fmt.Sprintf("/%s/jobs/%s/live/status", s.client.AccountName, input.JobID)
reqInput := client.RequestInput{
Method: http.MethodGet,
Path: path,
}
respBody, _, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return nil, errwrap.Wrapf("Error executing GetJob request: {{err}}", err)
}
job := &Job{}
decoder := json.NewDecoder(respBody)
if err = decoder.Decode(&job); err != nil {
return nil, errwrap.Wrapf("Error decoding GetJob response: {{err}}", err)
}
return &GetJobOutput{
Job: job,
}, nil
}
// GetJobOutputInput represents parameters to a GetJobOutput operation.
type GetJobOutputInput struct {
JobID string
}
// GetJobOutputOutput contains the outputs for a GetJobOutput operation. It is your
// responsibility to ensure that the io.ReadCloser Items is closed.
type GetJobOutputOutput struct {
ResultSetSize uint64
Items io.ReadCloser
}
// GetJobOutput returns the current "live" set of outputs from a job. Think of
// this like `tail -f`. If error is nil (i.e. the operation is successful), it is
// your responsibility to close the io.ReadCloser named Items in the output.
func (s *JobClient) GetOutput(ctx context.Context, input *GetJobOutputInput) (*GetJobOutputOutput, error) {
path := fmt.Sprintf("/%s/jobs/%s/live/out", s.client.AccountName, input.JobID)
reqInput := client.RequestInput{
Method: http.MethodGet,
Path: path,
}
respBody, respHeader, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return nil, errwrap.Wrapf("Error executing GetJobOutput request: {{err}}", err)
}
output := &GetJobOutputOutput{
Items: respBody,
}
resultSetSize, err := strconv.ParseUint(respHeader.Get("Result-Set-Size"), 10, 64)
if err == nil {
output.ResultSetSize = resultSetSize
}
return output, nil
}
// GetJobInputInput represents parameters to a GetJobOutput operation.
type GetJobInputInput struct {
JobID string
}
// GetJobInputOutput contains the outputs for a GetJobOutput operation. It is your
// responsibility to ensure that the io.ReadCloser Items is closed.
type GetJobInputOutput struct {
ResultSetSize uint64
Items io.ReadCloser
}
// GetJobInput returns the current "live" set of inputs from a job. Think of
// this like `tail -f`. If error is nil (i.e. the operation is successful), it is
// your responsibility to close the io.ReadCloser named Items in the output.
func (s *JobClient) GetInput(ctx context.Context, input *GetJobInputInput) (*GetJobInputOutput, error) {
path := fmt.Sprintf("/%s/jobs/%s/live/in", s.client.AccountName, input.JobID)
reqInput := client.RequestInput{
Method: http.MethodGet,
Path: path,
}
respBody, respHeader, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return nil, errwrap.Wrapf("Error executing GetJobInput request: {{err}}", err)
}
output := &GetJobInputOutput{
Items: respBody,
}
resultSetSize, err := strconv.ParseUint(respHeader.Get("Result-Set-Size"), 10, 64)
if err == nil {
output.ResultSetSize = resultSetSize
}
return output, nil
}
// GetJobFailuresInput represents parameters to a GetJobFailures operation.
type GetJobFailuresInput struct {
JobID string
}
// GetJobFailuresOutput contains the outputs for a GetJobFailures operation. It is your
// responsibility to ensure that the io.ReadCloser Items is closed.
type GetJobFailuresOutput struct {
ResultSetSize uint64
Items io.ReadCloser
}
// GetJobFailures returns the current "live" set of outputs from a job. Think of
// this like `tail -f`. If error is nil (i.e. the operation is successful), it is
// your responsibility to close the io.ReadCloser named Items in the output.
func (s *JobClient) GetFailures(ctx context.Context, input *GetJobFailuresInput) (*GetJobFailuresOutput, error) {
path := fmt.Sprintf("/%s/jobs/%s/live/fail", s.client.AccountName, input.JobID)
reqInput := client.RequestInput{
Method: http.MethodGet,
Path: path,
}
respBody, respHeader, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return nil, errwrap.Wrapf("Error executing GetJobFailures request: {{err}}", err)
}
output := &GetJobFailuresOutput{
Items: respBody,
}
resultSetSize, err := strconv.ParseUint(respHeader.Get("Result-Set-Size"), 10, 64)
if err == nil {
output.ResultSetSize = resultSetSize
}
return output, nil
}

208
vendor/github.com/joyent/triton-go/storage/objects.go generated vendored Normal file
View File

@ -0,0 +1,208 @@
package storage
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/hashicorp/errwrap"
"github.com/joyent/triton-go/client"
)
type ObjectsClient struct {
client *client.Client
}
// GetObjectInput represents parameters to a GetObject operation.
type GetObjectInput struct {
ObjectPath string
}
// GetObjectOutput contains the outputs for a GetObject operation. It is your
// responsibility to ensure that the io.ReadCloser ObjectReader is closed.
type GetObjectOutput struct {
ContentLength uint64
ContentType string
LastModified time.Time
ContentMD5 string
ETag string
Metadata map[string]string
ObjectReader io.ReadCloser
}
// GetObject retrieves an object from the Manta service. If error is nil (i.e.
// the call returns successfully), it is your responsibility to close the io.ReadCloser
// named ObjectReader in the operation output.
func (s *ObjectsClient) Get(ctx context.Context, input *GetObjectInput) (*GetObjectOutput, error) {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.ObjectPath)
reqInput := client.RequestInput{
Method: http.MethodGet,
Path: path,
}
respBody, respHeaders, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if err != nil {
return nil, errwrap.Wrapf("Error executing GetDirectory request: {{err}}", err)
}
response := &GetObjectOutput{
ContentType: respHeaders.Get("Content-Type"),
ContentMD5: respHeaders.Get("Content-MD5"),
ETag: respHeaders.Get("Etag"),
ObjectReader: respBody,
}
lastModified, err := time.Parse(time.RFC1123, respHeaders.Get("Last-Modified"))
if err == nil {
response.LastModified = lastModified
}
contentLength, err := strconv.ParseUint(respHeaders.Get("Content-Length"), 10, 64)
if err == nil {
response.ContentLength = contentLength
}
metadata := map[string]string{}
for key, values := range respHeaders {
if strings.HasPrefix(key, "m-") {
metadata[key] = strings.Join(values, ", ")
}
}
response.Metadata = metadata
return response, nil
}
// DeleteObjectInput represents parameters to a DeleteObject operation.
type DeleteObjectInput struct {
ObjectPath string
}
// DeleteObject deletes an object.
func (s *ObjectsClient) Delete(ctx context.Context, input *DeleteObjectInput) error {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.ObjectPath)
reqInput := client.RequestInput{
Method: http.MethodDelete,
Path: path,
}
respBody, _, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return errwrap.Wrapf("Error executing DeleteObject request: {{err}}", err)
}
return nil
}
// PutObjectMetadataInput represents parameters to a PutObjectMetadata operation.
type PutObjectMetadataInput struct {
ObjectPath string
ContentType string
Metadata map[string]string
}
// PutObjectMetadata allows you to overwrite the HTTP headers for an already
// existing object, without changing the data. Note this is an idempotent "replace"
// operation, so you must specify the complete set of HTTP headers you want
// stored on each request.
//
// You cannot change "critical" headers:
// - Content-Length
// - Content-MD5
// - Durability-Level
func (s *ObjectsClient) PutMetadata(ctx context.Context, input *PutObjectMetadataInput) error {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.ObjectPath)
query := &url.Values{}
query.Set("metadata", "true")
headers := &http.Header{}
headers.Set("Content-Type", input.ContentType)
for key, value := range input.Metadata {
headers.Set(key, value)
}
reqInput := client.RequestInput{
Method: http.MethodPut,
Path: path,
Query: query,
Headers: headers,
}
respBody, _, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return errwrap.Wrapf("Error executing PutObjectMetadata request: {{err}}", err)
}
return nil
}
// PutObjectInput represents parameters to a PutObject operation.
type PutObjectInput struct {
ObjectPath string
DurabilityLevel uint64
ContentType string
ContentMD5 string
IfMatch string
IfModifiedSince *time.Time
ContentLength uint64
MaxContentLength uint64
ObjectReader io.ReadSeeker
}
func (s *ObjectsClient) Put(ctx context.Context, input *PutObjectInput) error {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.ObjectPath)
if input.MaxContentLength != 0 && input.ContentLength != 0 {
return errors.New("ContentLength and MaxContentLength may not both be set to non-zero values.")
}
headers := &http.Header{}
if input.DurabilityLevel != 0 {
headers.Set("Durability-Level", strconv.FormatUint(input.DurabilityLevel, 10))
}
if input.ContentType != "" {
headers.Set("Content-Type", input.ContentType)
}
if input.ContentMD5 != "" {
headers.Set("Content-MD$", input.ContentMD5)
}
if input.IfMatch != "" {
headers.Set("If-Match", input.IfMatch)
}
if input.IfModifiedSince != nil {
headers.Set("If-Modified-Since", input.IfModifiedSince.Format(time.RFC1123))
}
if input.ContentLength != 0 {
headers.Set("Content-Length", strconv.FormatUint(input.ContentLength, 10))
}
if input.MaxContentLength != 0 {
headers.Set("Max-Content-Length", strconv.FormatUint(input.MaxContentLength, 10))
}
reqInput := client.RequestNoEncodeInput{
Method: http.MethodPut,
Path: path,
Headers: headers,
Body: input.ObjectReader,
}
respBody, _, err := s.client.ExecuteRequestNoEncode(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return errwrap.Wrapf("Error executing PutObjectMetadata request: {{err}}", err)
}
return nil
}

81
vendor/github.com/joyent/triton-go/storage/signing.go generated vendored Normal file
View File

@ -0,0 +1,81 @@
package storage
import (
"bytes"
"fmt"
"net/url"
"strconv"
"strings"
"time"
"github.com/hashicorp/errwrap"
)
// SignURLInput represents parameters to a SignURL operation.
type SignURLInput struct {
ValidityPeriod time.Duration
Method string
ObjectPath string
}
// SignURLOutput contains the outputs of a SignURL operation. To simply
// access the signed URL, use the SignedURL method.
type SignURLOutput struct {
host string
objectPath string
Method string
Algorithm string
Signature string
Expires string
KeyID string
}
// SignedURL returns a signed URL for the given scheme. Valid schemes are
// `http` and `https`.
func (output *SignURLOutput) SignedURL(scheme string) string {
query := &url.Values{}
query.Set("algorithm", output.Algorithm)
query.Set("expires", output.Expires)
query.Set("keyId", output.KeyID)
query.Set("signature", output.Signature)
sUrl := url.URL{}
sUrl.Scheme = scheme
sUrl.Host = output.host
sUrl.Path = output.objectPath
sUrl.RawQuery = query.Encode()
return sUrl.String()
}
// SignURL creates a time-expiring URL that can be shared with others.
// This is useful to generate HTML links, for example.
func (s *StorageClient) SignURL(input *SignURLInput) (*SignURLOutput, error) {
output := &SignURLOutput{
host: s.Client.MantaURL.Host,
objectPath: fmt.Sprintf("/%s%s", s.Client.AccountName, input.ObjectPath),
Method: input.Method,
Algorithm: strings.ToUpper(s.Client.Authorizers[0].DefaultAlgorithm()),
Expires: strconv.FormatInt(time.Now().Add(input.ValidityPeriod).Unix(), 10),
KeyID: fmt.Sprintf("/%s/keys/%s", s.Client.AccountName, s.Client.Authorizers[0].KeyFingerprint()),
}
toSign := bytes.Buffer{}
toSign.WriteString(input.Method + "\n")
toSign.WriteString(s.Client.MantaURL.Host + "\n")
toSign.WriteString(fmt.Sprintf("/%s%s\n", s.Client.AccountName, input.ObjectPath))
query := &url.Values{}
query.Set("algorithm", output.Algorithm)
query.Set("expires", output.Expires)
query.Set("keyId", output.KeyID)
toSign.WriteString(query.Encode())
signature, _, err := s.Client.Authorizers[0].SignRaw(toSign.String())
if err != nil {
return nil, errwrap.Wrapf("Error signing string: {{err}}", err)
}
output.Signature = signature
return output, nil
}

46
vendor/github.com/joyent/triton-go/storage/snaplink.go generated vendored Normal file
View File

@ -0,0 +1,46 @@
package storage
import (
"context"
"fmt"
"net/http"
"github.com/hashicorp/errwrap"
"github.com/joyent/triton-go/client"
)
type SnapLinksClient struct {
client *client.Client
}
// PutSnapLinkInput represents parameters to a PutSnapLink operation.
type PutSnapLinkInput struct {
LinkPath string
SourcePath string
}
// PutSnapLink creates a SnapLink to an object.
func (s *SnapLinksClient) Put(ctx context.Context, input *PutSnapLinkInput) error {
linkPath := fmt.Sprintf("/%s%s", s.client.AccountName, input.LinkPath)
sourcePath := fmt.Sprintf("/%s%s", s.client.AccountName, input.SourcePath)
headers := &http.Header{}
headers.Set("Content-Type", "application/json; type=link")
headers.Set("location", sourcePath)
headers.Set("Accept", "~1.0")
headers.Set("Accept-Version", "application/json, */*")
reqInput := client.RequestInput{
Method: http.MethodPut,
Path: linkPath,
Headers: headers,
}
respBody, _, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return errwrap.Wrapf("Error executing PutSnapLink request: {{err}}", err)
}
return nil
}

18
vendor/github.com/joyent/triton-go/triton.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
package triton
import (
"github.com/joyent/triton-go/authentication"
)
// Universal package used for defining configuration used across all client
// constructors.
// ClientConfig is a placeholder/input struct around the behavior of configuring
// a client constructor through the implementation's runtime environment
// (SDC/MANTA env vars).
type ClientConfig struct {
TritonURL string
MantaURL string
AccountName string
Signers []authentication.Signer
}

24
vendor/vendor.json vendored
View File

@ -1767,6 +1767,30 @@
"revision": "8978c75ffefb3f63a977ad9cbfce40caeb40177e",
"revisionTime": "2016-06-16T18:50:15Z"
},
{
"checksumSHA1": "EqvUu0Ku0Ec5Tk6yhGNOuOr8yeA=",
"path": "github.com/joyent/triton-go",
"revision": "5a58ad2cdec95cddd1e0a2e56f559341044b04f0",
"revisionTime": "2017-10-17T16:55:58Z"
},
{
"checksumSHA1": "JKf97EAAAZFQ6Wf8qN9X7TWqNBY=",
"path": "github.com/joyent/triton-go/authentication",
"revision": "5a58ad2cdec95cddd1e0a2e56f559341044b04f0",
"revisionTime": "2017-10-17T16:55:58Z"
},
{
"checksumSHA1": "dlO1or0cyVMAmZzyLcBuoy+M0xU=",
"path": "github.com/joyent/triton-go/client",
"revision": "5a58ad2cdec95cddd1e0a2e56f559341044b04f0",
"revisionTime": "2017-10-17T16:55:58Z"
},
{
"checksumSHA1": "9VONvM4aQL088cLPgg+Z0K0dshc=",
"path": "github.com/joyent/triton-go/storage",
"revision": "5a58ad2cdec95cddd1e0a2e56f559341044b04f0",
"revisionTime": "2017-10-17T16:55:58Z"
},
{
"checksumSHA1": "g+afVQQVopBLiLB5pFZp/8s6aBs=",
"path": "github.com/kardianos/osext",

View File

@ -8,7 +8,7 @@ description: |-
# manta
**Kind: Standard (with no locking)**
**Kind: Standard (with locking within Manta)**
Stores the state as an artifact in [Manta](https://www.joyent.com/manta).
@ -42,7 +42,12 @@ data "terraform_remote_state" "foo" {
The following configuration options are supported:
* `path` - (Required) The path relative to your private storage directory (`/$MANTA_USER/stor`) where the state file will be stored
* `account` - (Required) This is the name of the Manta account. It can also be provided via the `SDC_ACCOUNT` or `TRITON_ACCOUNT` environment variables.
* `url` - (Optional) The Manta API Endpoint. It can also be provided via the `MANTA_URL` environment variable. Defaults to `https://us-east.manta.joyent.com`.
* `key_material` - (Optional) This is the private key of an SSH key associated with the Triton account to be used. If this is not set, the private key corresponding to the fingerprint in key_id must be available via an SSH Agent. Can be set via the `SDC_KEY_MATERIAL` or `TRITON_KEY_MATERIAL` environment variables.
* `key_id` - (Required) This is the fingerprint of the public key matching the key specified in key_path. It can be obtained via the command ssh-keygen -l -E md5 -f /path/to/key. Can be set via the `SDC_KEY_ID` or `TRITON_KEY_ID` environment variables.
* `insecure_skip_tls_verify` - (Optional) This allows skipping TLS verification of the Triton endpoint. It is useful when connecting to a temporary Triton installation such as Cloud-On-A-Laptop which does not generally use a certificate signed by a trusted root CA. Defaults to `false`.
* `path` - (Required) The path relative to your private storage directory (`/$MANTA_USER/stor`) where the state file will be stored. **Please Note:** If this path does not exist, then the backend will create this folder location as part of backend creation.
* `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: