Adding Manta Storage Backend (#3720)
This PR adds a new Storage Backend for Triton's Object Storage - Manta ``` make testacc TEST=./physical/manta ==> Checking that code complies with gofmt requirements... ==> Checking that build is using go version >= 1.9.1... go generate VAULT_ACC=1 go test -tags='vault' ./physical/manta -v -timeout 45m === RUN TestMantaBackend --- PASS: TestMantaBackend (61.18s) PASS ok github.com/hashicorp/vault/physical/manta 61.210s ``` Manta behaves differently to how S3 works - it has no such concepts of Buckets - it is merely a filesystem style object store Therefore, we have chosen the approach of when writing a secret `foo` it will actually map (on disk) as foo/.vault_value The reason for this is because if we write the secret `foo/bar` and then try and Delete a key using the name `foo` then Manta will complain that the folder is not empty because `foo/bar` exists. Therefore, `foo/bar` is written as `foo/bar/.vault_value` The value of the key is *always* written to a directory tree of the name and put in a `.vault_value` file.
This commit is contained in:
parent
60732577f5
commit
3c683dba92
|
@ -56,6 +56,7 @@ import (
|
|||
physFile "github.com/hashicorp/vault/physical/file"
|
||||
physGCS "github.com/hashicorp/vault/physical/gcs"
|
||||
physInmem "github.com/hashicorp/vault/physical/inmem"
|
||||
physManta "github.com/hashicorp/vault/physical/manta"
|
||||
physMSSQL "github.com/hashicorp/vault/physical/mssql"
|
||||
physMySQL "github.com/hashicorp/vault/physical/mysql"
|
||||
physPostgreSQL "github.com/hashicorp/vault/physical/postgresql"
|
||||
|
@ -128,6 +129,7 @@ var (
|
|||
"inmem_transactional_ha": physInmem.NewTransactionalInmemHA,
|
||||
"inmem_transactional": physInmem.NewTransactionalInmem,
|
||||
"inmem": physInmem.NewInmem,
|
||||
"manta": physManta.NewMantaBackend,
|
||||
"mssql": physMSSQL.NewMSSQLBackend,
|
||||
"mysql": physMySQL.NewMySQLBackend,
|
||||
"postgresql": physPostgreSQL.NewPostgreSQLBackend,
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
package manta
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/vault/physical"
|
||||
"github.com/joyent/triton-go"
|
||||
"github.com/joyent/triton-go/authentication"
|
||||
"github.com/joyent/triton-go/errors"
|
||||
"github.com/joyent/triton-go/storage"
|
||||
log "github.com/mgutz/logxi/v1"
|
||||
)
|
||||
|
||||
const mantaDefaultRootStore = "/stor"
|
||||
|
||||
type MantaBackend struct {
|
||||
logger log.Logger
|
||||
permitPool *physical.PermitPool
|
||||
client *storage.StorageClient
|
||||
directory string
|
||||
}
|
||||
|
||||
func NewMantaBackend(conf map[string]string, logger log.Logger) (physical.Backend, error) {
|
||||
|
||||
user := os.Getenv("MANTA_USER")
|
||||
if user == "" {
|
||||
user = conf["user"]
|
||||
}
|
||||
|
||||
keyId := os.Getenv("MANTA_KEY_ID")
|
||||
if keyId == "" {
|
||||
keyId = conf["key_id"]
|
||||
}
|
||||
|
||||
url := os.Getenv("MANTA_URL")
|
||||
if url == "" {
|
||||
url = conf["url"]
|
||||
} else {
|
||||
url = "https://us-east.manta.joyent.com"
|
||||
}
|
||||
|
||||
subuser := os.Getenv("MANTA_SUBUSER")
|
||||
if subuser == "" {
|
||||
if confUser, ok := conf["subuser"]; ok {
|
||||
subuser = confUser
|
||||
}
|
||||
}
|
||||
|
||||
input := authentication.SSHAgentSignerInput{
|
||||
KeyID: keyId,
|
||||
AccountName: user,
|
||||
Username: subuser,
|
||||
}
|
||||
signer, err := authentication.NewSSHAgentSigner(input)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error Creating SSH Agent Signer: {{err}}", err)
|
||||
}
|
||||
|
||||
maxParStr, ok := conf["max_parallel"]
|
||||
var maxParInt int
|
||||
if ok {
|
||||
maxParInt, err = strconv.Atoi(maxParStr)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("failed parsing max_parallel parameter: {{err}}", err)
|
||||
}
|
||||
if logger.IsDebug() {
|
||||
logger.Debug("manta: max_parallel set", "max_parallel", maxParInt)
|
||||
}
|
||||
}
|
||||
|
||||
config := &triton.ClientConfig{
|
||||
MantaURL: url,
|
||||
AccountName: user,
|
||||
Signers: []authentication.Signer{signer},
|
||||
}
|
||||
|
||||
client, err := storage.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("failed initialising Storage client: {{err}}", err)
|
||||
}
|
||||
|
||||
return &MantaBackend{
|
||||
client: client,
|
||||
directory: conf["directory"],
|
||||
logger: logger,
|
||||
permitPool: physical.NewPermitPool(maxParInt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Put is used to insert or update an entry
|
||||
func (m *MantaBackend) Put(entry *physical.Entry) error {
|
||||
defer metrics.MeasureSince([]string{"manta", "put"}, time.Now())
|
||||
|
||||
m.permitPool.Acquire()
|
||||
defer m.permitPool.Release()
|
||||
|
||||
r := bytes.NewReader(entry.Value)
|
||||
r.Seek(0, 0)
|
||||
|
||||
return m.client.Objects().Put(context.Background(), &storage.PutObjectInput{
|
||||
ObjectPath: path.Join(mantaDefaultRootStore, m.directory, entry.Key, ".vault_value"),
|
||||
ObjectReader: r,
|
||||
ContentLength: uint64(len(entry.Value)),
|
||||
ForceInsert: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Get is used to fetch an entry
|
||||
func (m *MantaBackend) Get(key string) (*physical.Entry, error) {
|
||||
defer metrics.MeasureSince([]string{"manta", "get"}, time.Now())
|
||||
|
||||
m.permitPool.Acquire()
|
||||
defer m.permitPool.Release()
|
||||
|
||||
output, err := m.client.Objects().Get(context.Background(), &storage.GetObjectInput{
|
||||
ObjectPath: path.Join(mantaDefaultRootStore, m.directory, key, ".vault_value"),
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "ResourceNotFound") {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer output.ObjectReader.Close()
|
||||
|
||||
data := make([]byte, output.ContentLength)
|
||||
_, err = io.ReadFull(output.ObjectReader, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ent := &physical.Entry{
|
||||
Key: key,
|
||||
Value: data,
|
||||
}
|
||||
|
||||
return ent, nil
|
||||
}
|
||||
|
||||
// Delete is used to permanently delete an entry
|
||||
func (m *MantaBackend) Delete(key string) error {
|
||||
defer metrics.MeasureSince([]string{"manta", "delete"}, time.Now())
|
||||
|
||||
m.permitPool.Acquire()
|
||||
defer m.permitPool.Release()
|
||||
|
||||
if strings.HasSuffix(key, "/") {
|
||||
err := m.client.Dir().Delete(context.Background(), &storage.DeleteDirectoryInput{
|
||||
DirectoryName: path.Join(mantaDefaultRootStore, m.directory, key),
|
||||
ForceDelete: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err := m.client.Objects().Delete(context.Background(), &storage.DeleteObjectInput{
|
||||
ObjectPath: path.Join(mantaDefaultRootStore, m.directory, key, ".vault_value"),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.IsResourceNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return tryDeleteDirectory(m, path.Join(mantaDefaultRootStore, m.directory, key))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func tryDeleteDirectory(m *MantaBackend, directoryPath string) error {
|
||||
objs, err := m.client.Dir().List(context.Background(), &storage.ListDirectoryInput{
|
||||
DirectoryName: directoryPath,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.IsResourceNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if objs != nil && len(objs.Entries) == 0 {
|
||||
err := m.client.Dir().Delete(context.Background(), &storage.DeleteDirectoryInput{
|
||||
DirectoryName: directoryPath,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tryDeleteDirectory(m, path.Dir(directoryPath))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List is used to list all the keys under a given
|
||||
// prefix, up to the next prefix.
|
||||
func (m *MantaBackend) List(prefix string) ([]string, error) {
|
||||
defer metrics.MeasureSince([]string{"manta", "list"}, time.Now())
|
||||
|
||||
m.permitPool.Acquire()
|
||||
defer m.permitPool.Release()
|
||||
|
||||
objs, err := m.client.Dir().List(context.Background(), &storage.ListDirectoryInput{
|
||||
DirectoryName: path.Join(mantaDefaultRootStore, m.directory, prefix),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.IsResourceNotFound(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys := []string{}
|
||||
for _, obj := range objs.Entries {
|
||||
if obj.Type == "directory" {
|
||||
objs, err := m.client.Dir().List(context.Background(), &storage.ListDirectoryInput{
|
||||
DirectoryName: path.Join(mantaDefaultRootStore, m.directory, prefix, obj.Name),
|
||||
})
|
||||
if err != nil {
|
||||
if !errors.IsResourceNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
//We need to check to see if there is something more than just the `value` file
|
||||
//if the length of the children is:
|
||||
// > 1 and includes the value `index` then we need to add foo and foo/
|
||||
// = 1 and the value is `index` then we need to add foo
|
||||
// = 1 and the value is not `index` then we need to add foo/
|
||||
if len(objs.Entries) == 1 {
|
||||
if objs.Entries[0].Name != ".vault_value" {
|
||||
keys = append(keys, fmt.Sprintf("%s/", obj.Name))
|
||||
} else {
|
||||
keys = append(keys, obj.Name)
|
||||
}
|
||||
} else if len(objs.Entries) > 1 {
|
||||
for _, childObj := range objs.Entries {
|
||||
if childObj.Name == ".vault_value" {
|
||||
keys = append(keys, obj.Name)
|
||||
} else {
|
||||
keys = append(keys, fmt.Sprintf("%s/", obj.Name))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
keys = append(keys, obj.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
return keys, nil
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package manta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/helper/logformat"
|
||||
"github.com/hashicorp/vault/physical"
|
||||
"github.com/joyent/triton-go"
|
||||
"github.com/joyent/triton-go/authentication"
|
||||
tclient "github.com/joyent/triton-go/client"
|
||||
"github.com/joyent/triton-go/storage"
|
||||
log "github.com/mgutz/logxi/v1"
|
||||
)
|
||||
|
||||
func TestMantaBackend(t *testing.T) {
|
||||
user := os.Getenv("MANTA_USER")
|
||||
keyId := os.Getenv("MANTA_KEY_ID")
|
||||
url := "https://us-east.manta.joyent.com"
|
||||
testHarnessBucket := fmt.Sprintf("test-bucket-%d", randInt())
|
||||
|
||||
if user == "" || keyId == "" {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
input := authentication.SSHAgentSignerInput{
|
||||
KeyID: keyId,
|
||||
AccountName: user,
|
||||
Username: "",
|
||||
}
|
||||
signer, err := authentication.NewSSHAgentSigner(input)
|
||||
if err != nil {
|
||||
t.Fatalf("Error Creating SSH Agent Signer: %s", err.Error())
|
||||
}
|
||||
|
||||
config := &triton.ClientConfig{
|
||||
MantaURL: url,
|
||||
AccountName: user,
|
||||
Signers: []authentication.Signer{signer},
|
||||
}
|
||||
|
||||
client, err := storage.NewClient(config)
|
||||
if err != nil {
|
||||
t.Fatalf("failed initialising Storage client: %s", err.Error())
|
||||
}
|
||||
|
||||
logger := logformat.NewVaultLogger(log.LevelTrace)
|
||||
mb := &MantaBackend{
|
||||
client: client,
|
||||
directory: testHarnessBucket,
|
||||
logger: logger,
|
||||
permitPool: physical.NewPermitPool(128),
|
||||
}
|
||||
|
||||
err = mb.client.Dir().Put(context.Background(), &storage.PutDirectoryInput{
|
||||
DirectoryName: path.Join(mantaDefaultRootStore),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("Error creating test harness directory")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = mb.client.Dir().Delete(context.Background(), &storage.DeleteDirectoryInput{
|
||||
DirectoryName: path.Join(mantaDefaultRootStore, testHarnessBucket),
|
||||
ForceDelete: true,
|
||||
})
|
||||
if err != nil {
|
||||
if !tclient.IsResourceNotFoundError(err) {
|
||||
t.Fatal("failed to delete test harness directory")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
physical.ExerciseBackend(t, mb)
|
||||
physical.ExerciseBackend_ListPrefix(t, mb)
|
||||
}
|
||||
|
||||
func randInt() int {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
return rand.New(rand.NewSource(time.Now().UnixNano())).Int()
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
## Unreleased
|
||||
|
||||
- Add support for managing columes in Triton [#100]
|
||||
- identity/policies: Add support for managing policies in Triton [#86]
|
||||
- addition of triton-go errors package to expose unwraping of internal errors
|
||||
- Migration from hashicorp/errwrap to pkg/errors
|
||||
- Using path.Join() for URL structures rather than fmt.Sprintf()
|
||||
|
||||
## 0.5.2 (December 28)
|
||||
|
||||
- Standardise the API SSH Signers input casing and naming
|
||||
|
||||
## 0.5.1 (December 28)
|
||||
|
||||
- Include leading '/' when working with SSH Agent signers
|
||||
|
||||
## 0.5.0 (December 28)
|
||||
|
||||
- Add support for RBAC in triton-go [#82]
|
||||
This is a breaking change. No longer do we pass individual parameters to the SSH Signer funcs, but we now pass an input Struct. This will guard from from additional parameter changes in the future.
|
||||
We also now add support for using `SDC_*` and `TRITON_*` env vars when working with the Default agent signer
|
||||
|
||||
## 0.4.2 (December 22)
|
||||
|
||||
- Fixing a panic when the user loses network connectivity when making a GET request to instance [#81]
|
||||
|
||||
## 0.4.1 (December 15)
|
||||
|
||||
- Clean up the handling of directory sanitization. Use abs paths everywhere [#79]
|
||||
|
||||
## 0.4.0 (December 15)
|
||||
|
||||
- Fix an issue where Manta HEAD requests do not return an error resp body [#77]
|
||||
- Add support for recursively creating child directories [#78]
|
||||
|
||||
## 0.3.0 (December 14)
|
||||
|
||||
- Introduce CloudAPI's ListRulesMachines under networking
|
||||
- Enable HTTP KeepAlives by default in the client. 15s idle timeout, 2x
|
||||
connections per host, total of 10x connections per client.
|
||||
- Expose an optional Headers attribute to clients to allow them to customize
|
||||
HTTP headers when making Object requests.
|
||||
- Fix a bug in Directory ListIndex [#69](https://github.com/joyent/issues/69)
|
||||
- Inputs to Object inputs have been relaxed to `io.Reader` (formerly a
|
||||
`io.ReadSeeker`) [#73](https://github.com/joyent/issues/73).
|
||||
- Add support for ForceDelete of all children of a directory [#71](https://github.com/joyent/issues/71)
|
||||
- storage: Introduce `Objects.GetInfo` and `Objects.IsDir` using HEAD requests [#74](https://github.com/joyent/triton-go/issues/74)
|
||||
|
||||
## 0.2.1 (November 8)
|
||||
|
||||
- Fixing a bug where CreateUser and UpdateUser didn't return the UserID
|
||||
|
||||
## 0.2.0 (November 7)
|
||||
|
||||
- Introduce CloudAPI's Ping under compute
|
||||
- Introduce CloudAPI's RebootMachine under compute instances
|
||||
- Introduce CloudAPI's ListUsers, GetUser, CreateUser, UpdateUser and DeleteUser under identity package
|
||||
- Introduce CloudAPI's ListMachineSnapshots, GetMachineSnapshot, CreateSnapshot, DeleteMachineSnapshot and StartMachineFromSnapshot under compute package
|
||||
- tools: Introduce unit testing and scripts for linting, etc.
|
||||
- bug: Fix the `compute.ListMachineRules` endpoint
|
||||
|
||||
## 0.1.0 (November 2)
|
||||
|
||||
- Initial release of a versioned SDK
|
|
@ -0,0 +1,46 @@
|
|||
TEST?=$$(go list ./... |grep -Ev 'vendor|examples|testutils')
|
||||
GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor)
|
||||
|
||||
default: vet errcheck test
|
||||
|
||||
tools:: ## Download and install all dev/code tools
|
||||
@echo "==> Installing dev tools"
|
||||
go get -u github.com/golang/dep/cmd/dep
|
||||
go get -u github.com/golang/lint/golint
|
||||
go get -u github.com/kisielk/errcheck
|
||||
@echo "==> Installing test package dependencies"
|
||||
go test -i $(TEST) || exit 1
|
||||
|
||||
test:: ## Run unit tests
|
||||
@echo "==> Running unit test with coverage"
|
||||
@./scripts/go-test-with-coverage.sh
|
||||
|
||||
testacc:: ## Run acceptance tests
|
||||
@echo "==> Running acceptance tests"
|
||||
TRITON_TEST=1 go test $(TEST) -v $(TESTARGS) -run -timeout 120m
|
||||
|
||||
vet:: ## Check for unwanted code constructs
|
||||
@echo "go vet ."
|
||||
@go vet $$(go list ./... | grep -v vendor/) ; if [ $$? -eq 1 ]; then \
|
||||
echo ""; \
|
||||
echo "Vet found suspicious constructs. Please check the reported constructs"; \
|
||||
echo "and fix them if necessary before submitting the code for review."; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
lint:: ## Lint and vet code by common Go standards
|
||||
@bash $(CURDIR)/scripts/lint.sh
|
||||
|
||||
fmt:: ## Format as canonical Go code
|
||||
gofmt -w $(GOFMT_FILES)
|
||||
|
||||
fmtcheck:: ## Check if code format is canonical Go
|
||||
@bash $(CURDIR)/scripts/gofmtcheck.sh
|
||||
|
||||
errcheck:: ## Check for unhandled errors
|
||||
@bash $(CURDIR)/scripts/errcheck.sh
|
||||
|
||||
.PHONY: help
|
||||
help:: ## Display this help message
|
||||
@echo "GNU make(1) targets:"
|
||||
@grep -E '^[a-zA-Z_.-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
|
|
@ -0,0 +1,33 @@
|
|||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/abdullin/seq"
|
||||
packages = ["."]
|
||||
revision = "d5467c17e7afe8d8f08f556c6c811a50c3feb28d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "e881fd58d78e04cf6d0de1217f8707c8cc2249bc"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/sean-/seed"
|
||||
packages = ["."]
|
||||
revision = "e2103e2c35297fb7e17febb81e49b312087a2372"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["curve25519","ed25519","ed25519/internal/edwards25519","ssh","ssh/agent"]
|
||||
revision = "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "f7efd974ae38e2ee077c4d2698df74128a04797460b5f9c833853ddfaa86a0a0"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
|
@ -0,0 +1,38 @@
|
|||
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/abdullin/seq"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/sean-/seed"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/pkg/errors"
|
|
@ -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.
|
|
@ -0,0 +1,238 @@
|
|||
# triton-go
|
||||
|
||||
`triton-go` is an idiomatic library exposing a client SDK for Go applications
|
||||
using Joyent's Triton Compute and Storage (Manta) APIs.
|
||||
|
||||
[![Build Status](https://travis-ci.org/joyent/triton-go.svg?branch=master)](https://travis-ci.org/joyent/triton-go) [![Go Report Card](https://goreportcard.com/badge/github.com/joyent/triton-go)](https://goreportcard.com/report/github.com/joyent/triton-go)
|
||||
|
||||
## 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 `TRITON_ACCOUNT` environment variable). There is also support for
|
||||
passing in a username, this will allow you to use an account other than the main
|
||||
Triton account. For example:
|
||||
|
||||
```go
|
||||
input := authentication.SSHAgentSignerInput{
|
||||
KeyID: "a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11",
|
||||
AccountName: "AccountName",
|
||||
Username: "Username",
|
||||
}
|
||||
sshKeySigner, err := authentication.NewSSHAgentSigner(input)
|
||||
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("TRITON_URL"),
|
||||
MantaURL: os.Getenv("MANTA_URL"),
|
||||
AccountName: accountName,
|
||||
Username: os.Getenv("TRITON_USER"),
|
||||
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
|
||||
- `TRITON_URL` - the base endpoint for the Triton API
|
||||
- `TRITON_ACCOUNT` - the account name for the Triton API
|
||||
- `TRITON_KEY_ID` - the fingerprint of the SSH key identifying the key
|
||||
|
||||
Additionally, you may set `TRITON_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. You can also set `TRITON_USER` to run the tests
|
||||
against an account other than the main Triton account
|
||||
|
||||
### Example Run
|
||||
|
||||
The verbose output has been removed for brevity here.
|
||||
|
||||
```
|
||||
$ HTTP_PROXY=http://localhost:8888 \
|
||||
TRITON_TEST=1 \
|
||||
TRITON_URL=https://us-sw-1.api.joyent.com \
|
||||
TRITON_ACCOUNT=AccountName \
|
||||
TRITON_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)"
|
||||
$ TRITON_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("TRITON_KEY_ID")
|
||||
accountName := os.Getenv("TRITON_ACCOUNT")
|
||||
keyMaterial := os.Getenv("TRITON_KEY_MATERIAL")
|
||||
userName := os.Getenv("TRITON_USER")
|
||||
|
||||
var signer authentication.Signer
|
||||
var err error
|
||||
|
||||
if keyMaterial == "" {
|
||||
input := authentication.SSHAgentSignerInput{
|
||||
KeyID: keyID,
|
||||
AccountName: accountName,
|
||||
Username: userName,
|
||||
}
|
||||
signer, err = authentication.NewSSHAgentSigner(input)
|
||||
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)
|
||||
}
|
||||
|
||||
input := authentication.PrivateKeySignerInput{
|
||||
KeyID: keyID,
|
||||
PrivateKeyMaterial: keyBytes,
|
||||
AccountName: accountName,
|
||||
Username: userName,
|
||||
}
|
||||
signer, err = authentication.NewPrivateKeySigner(input)
|
||||
if err != nil {
|
||||
log.Fatalf("Error Creating SSH Private Key Signer: {{err}}", err)
|
||||
}
|
||||
}
|
||||
|
||||
config := &triton.ClientConfig{
|
||||
TritonURL: os.Getenv("TRITON_URL"),
|
||||
AccountName: accountName,
|
||||
Username: userName,
|
||||
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
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package authentication
|
||||
|
||||
// DON'T USE THIS OUTSIDE TESTING ~ This key was only created to use for
|
||||
// internal unit testing. It should never be used for acceptance testing either.
|
||||
//
|
||||
// This is just a randomly generated key pair.
|
||||
var Dummy = struct {
|
||||
Fingerprint string
|
||||
PrivateKey []byte
|
||||
PublicKey []byte
|
||||
Signer Signer
|
||||
}{
|
||||
"9f:d6:65:fc:d6:60:dc:d0:4e:db:2d:75:f7:92:8c:31",
|
||||
[]byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIJKAIBAAKCAgEAui9lNjCJahHeFSFC6HXi/CNX588C/L2gJUx65bnNphVC98hW
|
||||
1wzoRvPXHx5aWnb7lEbpNhP6B0UoCBDTaPgt9hHfD/oNQ+6HT1QpDIGfZmXI91/t
|
||||
cjGVSBbxN7WaYt/HsPrGjbalwvQPChN53sMVmFkMTEDR5G3zOBOAGrOimlCT80wI
|
||||
2S5Xg0spd8jjKM5I1swDR0xtuDWnHTR1Ohin+pEQIE6glLTfYq7oQx6nmMXXBNmk
|
||||
+SaPD1FAyjkF/81im2EHXBygNEwraVrDcAxK2mKlU2XMJiogQKNYWlm3UkbNB6WP
|
||||
Le12+Ka02rmIVsSqIpc/ZCBraAlCaSWlYCkU+vJ2hH/+ypy5bXNlbaTiWZK+vuI7
|
||||
PC87T50yLNeXVuNZAynzDpBCvsjiiHrB/ZFRfVfF6PviV8CV+m7GTzfAwJhVeSbl
|
||||
rR6nts16K0HTD48v57DU0b0t5VOvC7cWPShs+afdSL3Z8ReL5EWMgU1wfvtycRKe
|
||||
hiDVGj3Ms2cf83RIANr387G+1LcTQYP7JJuB7Svy5j+R6+HjI0cgu4EMUPdWfCNG
|
||||
GyrlxwJNtPmUSfasH1xUKpqr7dC+0sN4/gfJw75WTAYrATkPzexoYNaMsGDfhuoh
|
||||
kYa3Tn2q1g3kqhsX/R0Fd5d8d5qc137qcRCxiZYz9f3bVkXQbhYmO9da3KsCAwEA
|
||||
AQKCAgAeEAURqOinPddUJhi9nDtYZwSMo3piAORY4W5+pW+1P32esLSE6MqgmkLD
|
||||
/YytSsT4fjKtzq/yeJIsKztXmasiLmSMGd4Gd/9VKcuu/0cTq5+1gcG/TI5EI6Az
|
||||
VJlnGacOxo9E1pcRUYMUJ2zoMSvNe6NmtJivf6lkBpIKvbKlpBkfkclj9/2db4d0
|
||||
lfVH43cTZ8Gnw4l70v320z+Sb+S/qqil7swy9rmTH5bVL5/0JQ3A9LuUl0tGN+J0
|
||||
RJzZXvprCFG958leaGYiDsu7zeBQPtlfC/LYvriSd02O2SmmmVQFxg/GZK9vGsvc
|
||||
/VQsXnjyOOW9bxaop8YXYELBsiB21ipTHzOwoqHT8wFnjgU9Y/7iZIv7YbZKQsCS
|
||||
DrwdlZ/Yw90wiif+ldYryIVinWfytt6ERv4Dgezc98+1XPi1Z/WB74/lIaDXFl3M
|
||||
3ypjtvLYbKew2IkIjeAwjvZJg/QpC/50RrrPtVDgeAI1Ni01ikixUhMYsHJ1kRih
|
||||
0tqLvLqSPoHmr6luFlaoKdc2eBqb+8U6K/TrXhKtT7BeUFiSbvnVfdbrH9r+AY/2
|
||||
zYtG6llzkE5DH8ZR3Qp+dx7QEDtvYhGftWhx9uasd79AN7CuGYnL54YFLKGRrWKN
|
||||
ylysqfUyOQYiitdWdNCw9PP2vGRx5JAsMMSy+ft18jjTJvNQ0QKCAQEA28M11EE6
|
||||
MpnHxfyP00Dl1+3wl2lRyNXZnZ4hgkk1f83EJGpoB2amiMTF8P1qJb7US1fXtf7l
|
||||
gkJMMk6t6iccexV1/NBh/7tDZHH/v4HPirFTXQFizflaghD8dEADy9DY4BpQYFRe
|
||||
8zGsv4/4U0txCXkUIfKcENt/FtXv2T9blJT6cDV0yTx9IAyd4Kor7Ly2FIYroSME
|
||||
uqnOQt5PwB+2qkE+9hdg4xBhFs9sW5dvyBvQvlBfX/xOmMw2ygH6vsaJlNfZ5VPa
|
||||
EP/wFP/qHyhDlCfbHdL6qF2//wUoM2QM9RgBdZNhcKU7zWuf7Ev199tmlLC5O14J
|
||||
PkQxUGftMfmWxQKCAQEA2OLKD8dwOzpwGJiPQdBmGpwCamfcCY4nDwqEaCu4vY1R
|
||||
OJR+rpYdC2hgl5PTXWH7qzJVdT/ZAz2xUQOgB1hD3Ltk7DQ+EZIA8+vJdaicQOme
|
||||
vfpMPNDxCEX9ee0AXAmAC3aET82B4cMFnjXjl1WXLLTowF/Jp/hMorm6tl2m15A2
|
||||
oTyWlB/i/W/cxHl2HFWK7o8uCNoKpKJjheNYn+emEcH1bkwrk8sxQ78cBNmqe/gk
|
||||
MLgu8qfXQ0LLKIL7wqmIUHeUpkepOod8uXcTmmN2X9saCIwFKx4Jal5hh5v5cy0G
|
||||
MkyZcUIhhnmzr7lXbepauE5V2Sj5Qp040AfRVjZcrwKCAQANe8OwuzPL6P2F20Ij
|
||||
zwaLIhEx6QdYkC5i6lHaAY3jwoc3SMQLODQdjh0q9RFvMW8rFD+q7fG89T5hk8w9
|
||||
4ppvvthXY52vqBixcAEmCdvnAYxA15XtV1BDTLGAnHDfL3gu/85QqryMpU6ZDkdJ
|
||||
LQbJcwFWN+F1c1Iv335w0N9YlW9sNQtuUWTH8544K5i4VLfDOJwyrchbf5GlLqir
|
||||
/AYkGg634KVUKSwbzywxzm/QUkyTcLD5Xayg2V6/NDHjRKEqXbgDxwpJIrrjPvRp
|
||||
ZvoGfA+Im+o/LElcZz+ZL5lP7GIiiaFf3PN3XhQY1mxIAdEgbFthFhrxFBQGf+ng
|
||||
uBSVAoIBAHl12K8pg8LHoUtE9MVoziWMxRWOAH4ha+JSg4BLK/SLlbbYAnIHg1CG
|
||||
LcH1eWNMokJnt9An54KXJBw4qYAzgB23nHdjcncoivwPSg1oVclMjCfcaqGMac+2
|
||||
UpPblF32vAyvXL3MWzZxn03Q5Bo2Rqk0zzwc6LP2rARdeyDyJaOHEfEOG03s5ZQE
|
||||
91/YnbqUdW/QI3m1kkxM3Ot4PIOgmTJMqwQQCD+GhZppBmn49C7k8m+OVkxyjm0O
|
||||
lPOlFxUXGE3oCgltDGrIwaKj+wh1Ny/LZjLvJ13UPnWhUYE+al6EEnpMx4nT/S5w
|
||||
LZ71bu8RVajtxcoN1jnmDpECL8vWOeUCggEBAIEuKoY7pVHfs5gr5dXfQeVZEtqy
|
||||
LnSdsd37/aqQZRlUpVmBrPNl1JBLiEVhk2SL3XJIDU4Er7f0idhtYLY3eE7wqZ4d
|
||||
38Iaj5tv3zBc/wb1bImPgOgXCH7QrrbW7uTiYMLScuUbMR4uSpfubLaV8Zc9WHT8
|
||||
kTJ2pKKtA1GPJ4V7HCIxuTjD2iyOK1CRkaqSC+5VUuq5gHf92CEstv9AIvvy5cWg
|
||||
gnfBQoS89m3aO035henSfRFKVJkHaEoasj8hB3pwl9FGZUJp1c2JxiKzONqZhyGa
|
||||
6tcIAM3od0QtAfDJ89tWJ5D31W8KNNysobFSQxZ62WgLUUtXrkN1LGodxGQ=
|
||||
-----END RSA PRIVATE KEY-----`),
|
||||
[]byte(`ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC6L2U2MIlqEd4VIULodeL8I1fnzwL8vaAlTHrluc2mFUL3yFbXDOhG89cfHlpadvuURuk2E/oHRSgIENNo+C32Ed8P+g1D7odPVCkMgZ9mZcj3X+1yMZVIFvE3tZpi38ew+saNtqXC9A8KE3newxWYWQxMQNHkbfM4E4Aas6KaUJPzTAjZLleDSyl3yOMozkjWzANHTG24NacdNHU6GKf6kRAgTqCUtN9iruhDHqeYxdcE2aT5Jo8PUUDKOQX/zWKbYQdcHKA0TCtpWsNwDEraYqVTZcwmKiBAo1haWbdSRs0HpY8t7Xb4prTauYhWxKoilz9kIGtoCUJpJaVgKRT68naEf/7KnLltc2VtpOJZkr6+4js8LztPnTIs15dW41kDKfMOkEK+yOKIesH9kVF9V8Xo++JXwJX6bsZPN8DAmFV5JuWtHqe2zXorQdMPjy/nsNTRvS3lU68LtxY9KGz5p91IvdnxF4vkRYyBTXB++3JxEp6GINUaPcyzZx/zdEgA2vfzsb7UtxNBg/skm4HtK/LmP5Hr4eMjRyC7gQxQ91Z8I0YbKuXHAk20+ZRJ9qwfXFQqmqvt0L7Sw3j+B8nDvlZMBisBOQ/N7Ghg1oywYN+G6iGRhrdOfarWDeSqGxf9HQV3l3x3mpzXfupxELGJljP1/dtWRdBuFiY711rcqw== test-dummy-20171002140848`),
|
||||
nil,
|
||||
}
|
||||
|
||||
func init() {
|
||||
testSigner, _ := NewTestSigner()
|
||||
Dummy.Signer = testSigner
|
||||
}
|
74
vendor/github.com/joyent/triton-go/authentication/ecdsa_signature.go
generated
vendored
Normal file
74
vendor/github.com/joyent/triton-go/authentication/ecdsa_signature.go
generated
vendored
Normal file
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"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, errors.Wrap(err, "unable to unmarshall signature")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
132
vendor/github.com/joyent/triton-go/authentication/private_key_signer.go
generated
vendored
Normal file
132
vendor/github.com/joyent/triton-go/authentication/private_key_signer.go
generated
vendored
Normal file
|
@ -0,0 +1,132 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type PrivateKeySigner struct {
|
||||
formattedKeyFingerprint string
|
||||
keyFingerprint string
|
||||
algorithm string
|
||||
accountName string
|
||||
userName string
|
||||
hashFunc crypto.Hash
|
||||
|
||||
privateKey *rsa.PrivateKey
|
||||
}
|
||||
|
||||
type PrivateKeySignerInput struct {
|
||||
KeyID string
|
||||
PrivateKeyMaterial []byte
|
||||
AccountName string
|
||||
Username string
|
||||
}
|
||||
|
||||
func NewPrivateKeySigner(input PrivateKeySignerInput) (*PrivateKeySigner, error) {
|
||||
keyFingerprintMD5 := strings.Replace(input.KeyID, ":", "", -1)
|
||||
|
||||
block, _ := pem.Decode(input.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, errors.Wrap(err, "unable to parse private key")
|
||||
}
|
||||
|
||||
sshPublicKey, err := ssh.NewPublicKey(rsakey.Public())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to parse SSH key from private key")
|
||||
}
|
||||
|
||||
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: input.KeyID,
|
||||
accountName: input.AccountName,
|
||||
|
||||
hashFunc: crypto.SHA1,
|
||||
privateKey: rsakey,
|
||||
}
|
||||
|
||||
if input.Username != "" {
|
||||
signer.userName = input.Username
|
||||
}
|
||||
|
||||
_, 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 "", errors.Wrap(err, "unable to sign date header")
|
||||
}
|
||||
signedBase64 := base64.StdEncoding.EncodeToString(signed)
|
||||
|
||||
var keyID string
|
||||
if s.userName != "" {
|
||||
|
||||
keyID = path.Join("/", s.accountName, "users", s.userName, "keys", s.formattedKeyFingerprint)
|
||||
} else {
|
||||
keyID = path.Join("/", s.accountName, "keys", 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 "", "", errors.Wrap(err, "unable to sign date header")
|
||||
}
|
||||
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
|
||||
}
|
33
vendor/github.com/joyent/triton-go/authentication/rsa_signature.go
generated
vendored
Normal file
33
vendor/github.com/joyent/triton-go/authentication/rsa_signature.go
generated
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
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)
|
||||
}
|
190
vendor/github.com/joyent/triton-go/authentication/ssh_agent_signer.go
generated
vendored
Normal file
190
vendor/github.com/joyent/triton-go/authentication/ssh_agent_signer.go
generated
vendored
Normal file
|
@ -0,0 +1,190 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
pkgerrors "github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnsetEnvVar = pkgerrors.New("environment variable SSH_AUTH_SOCK not set")
|
||||
)
|
||||
|
||||
type SSHAgentSigner struct {
|
||||
formattedKeyFingerprint string
|
||||
keyFingerprint string
|
||||
algorithm string
|
||||
accountName string
|
||||
userName string
|
||||
keyIdentifier string
|
||||
|
||||
agent agent.Agent
|
||||
key ssh.PublicKey
|
||||
}
|
||||
|
||||
type SSHAgentSignerInput struct {
|
||||
KeyID string
|
||||
AccountName string
|
||||
Username string
|
||||
}
|
||||
|
||||
func NewSSHAgentSigner(input SSHAgentSignerInput) (*SSHAgentSigner, error) {
|
||||
sshAgentAddress, agentOk := os.LookupEnv("SSH_AUTH_SOCK")
|
||||
if !agentOk {
|
||||
return nil, ErrUnsetEnvVar
|
||||
}
|
||||
|
||||
conn, err := net.Dial("unix", sshAgentAddress)
|
||||
if err != nil {
|
||||
return nil, pkgerrors.Wrap(err, "unable to dial SSH agent")
|
||||
}
|
||||
|
||||
ag := agent.NewClient(conn)
|
||||
|
||||
signer := &SSHAgentSigner{
|
||||
keyFingerprint: input.KeyID,
|
||||
accountName: input.AccountName,
|
||||
agent: ag,
|
||||
}
|
||||
|
||||
matchingKey, err := signer.MatchKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signer.key = matchingKey
|
||||
signer.formattedKeyFingerprint = formatPublicKeyFingerprint(signer.key, true)
|
||||
if input.Username != "" {
|
||||
signer.userName = input.Username
|
||||
signer.keyIdentifier = path.Join("/", signer.accountName, "users", input.Username, "keys", signer.formattedKeyFingerprint)
|
||||
} else {
|
||||
signer.keyIdentifier = path.Join("/", signer.accountName, "keys", signer.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) MatchKey() (ssh.PublicKey, error) {
|
||||
keys, err := s.agent.List()
|
||||
if err != nil {
|
||||
return nil, pkgerrors.Wrap(err, "unable to list keys in SSH Agent")
|
||||
}
|
||||
|
||||
keyFingerprintStripped := strings.TrimPrefix(s.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", s.keyFingerprint)
|
||||
}
|
||||
|
||||
return matchingKey, 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 "", pkgerrors.Wrap(err, "unable to sign date header")
|
||||
}
|
||||
|
||||
keyFormat, err := keyFormatToKeyType(signature.Format)
|
||||
if err != nil {
|
||||
return "", pkgerrors.Wrap(err, "unable to format signature")
|
||||
}
|
||||
|
||||
var authSignature httpAuthSignature
|
||||
switch keyFormat {
|
||||
case "rsa":
|
||||
authSignature, err = newRSASignature(signature.Blob)
|
||||
if err != nil {
|
||||
return "", pkgerrors.Wrap(err, "unable to read RSA signature")
|
||||
}
|
||||
case "ecdsa":
|
||||
authSignature, err = newECDSASignature(signature.Blob)
|
||||
if err != nil {
|
||||
return "", pkgerrors.Wrap(err, "unable to read ECDSA signature")
|
||||
}
|
||||
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 "", "", pkgerrors.Wrap(err, "unable to sign string")
|
||||
}
|
||||
|
||||
keyFormat, err := keyFormatToKeyType(signature.Format)
|
||||
if err != nil {
|
||||
return "", "", pkgerrors.Wrap(err, "unable to format key")
|
||||
}
|
||||
|
||||
var authSignature httpAuthSignature
|
||||
switch keyFormat {
|
||||
case "rsa":
|
||||
authSignature, err = newRSASignature(signature.Blob)
|
||||
if err != nil {
|
||||
return "", "", pkgerrors.Wrap(err, "unable to read RSA signature")
|
||||
}
|
||||
case "ecdsa":
|
||||
authSignature, err = newECDSASignature(signature.Blob)
|
||||
if err != nil {
|
||||
return "", "", pkgerrors.Wrap(err, "unable to read ECDSA signature")
|
||||
}
|
||||
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
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package authentication
|
||||
|
||||
// TestSigner represents an authentication key signer which we can use for
|
||||
// testing purposes only. This will largely be a stub to send through client
|
||||
// unit tests.
|
||||
type TestSigner struct{}
|
||||
|
||||
// NewTestSigner constructs a new instance of test signer
|
||||
func NewTestSigner() (Signer, error) {
|
||||
return &TestSigner{}, nil
|
||||
}
|
||||
|
||||
func (s *TestSigner) DefaultAlgorithm() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *TestSigner) KeyFingerprint() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *TestSigner) Sign(dateHeader string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (s *TestSigner) SignRaw(toSign string) (string, string, error) {
|
||||
return "", "", nil
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
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, ":")
|
||||
}
|
|
@ -0,0 +1,444 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/joyent/triton-go"
|
||||
"github.com/joyent/triton-go/authentication"
|
||||
"github.com/joyent/triton-go/errors"
|
||||
pkgerrors "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const nilContext = "nil context"
|
||||
|
||||
var (
|
||||
ErrDefaultAuth = pkgerrors.New("default SSH agent authentication requires SDC_KEY_ID / TRITON_KEY_ID and SSH_AUTH_SOCK")
|
||||
ErrAccountName = pkgerrors.New("missing account name")
|
||||
ErrMissingURL = pkgerrors.New("missing API URL")
|
||||
|
||||
InvalidTritonURL = "invalid format of Triton URL"
|
||||
InvalidMantaURL = "invalid format of Manta URL"
|
||||
)
|
||||
|
||||
// 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
|
||||
Username 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) {
|
||||
if accountName == "" {
|
||||
return nil, ErrAccountName
|
||||
}
|
||||
|
||||
if tritonURL == "" && mantaURL == "" {
|
||||
return nil, ErrMissingURL
|
||||
}
|
||||
|
||||
cloudURL, err := url.Parse(tritonURL)
|
||||
if err != nil {
|
||||
return nil, pkgerrors.Wrapf(err, InvalidTritonURL)
|
||||
}
|
||||
|
||||
storageURL, err := url.Parse(mantaURL)
|
||||
if err != nil {
|
||||
return nil, pkgerrors.Wrapf(err, InvalidMantaURL)
|
||||
}
|
||||
|
||||
authorizers := make([]authentication.Signer, 0)
|
||||
for _, key := range signers {
|
||||
if key != nil {
|
||||
authorizers = append(authorizers, key)
|
||||
}
|
||||
}
|
||||
|
||||
newClient := &Client{
|
||||
HTTPClient: &http.Client{
|
||||
Transport: httpTransport(false),
|
||||
CheckRedirect: doNotFollowRedirects,
|
||||
},
|
||||
Authorizers: authorizers,
|
||||
TritonURL: *cloudURL,
|
||||
MantaURL: *storageURL,
|
||||
AccountName: accountName,
|
||||
}
|
||||
|
||||
// Default to constructing an SSHAgentSigner if there are no other signers
|
||||
// passed into NewClient and there's an TRITON_KEY_ID and SSH_AUTH_SOCK
|
||||
// available in the user's environ(7).
|
||||
if len(newClient.Authorizers) == 0 {
|
||||
if err := newClient.DefaultAuth(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return newClient, nil
|
||||
}
|
||||
|
||||
var envPrefixes = []string{"TRITON", "SDC"}
|
||||
|
||||
// GetTritonEnv looks up environment variables using the preferred "TRITON"
|
||||
// prefix, but falls back to the SDC prefix. For example, looking up "USER"
|
||||
// will search for "TRITON_USER" followed by "SDC_USER". If the environment
|
||||
// variable is not set, an empty string is returned. GetTritonEnv() is used to
|
||||
// aid in the transition and deprecation of the SDC_* environment variables.
|
||||
func GetTritonEnv(name string) string {
|
||||
for _, prefix := range envPrefixes {
|
||||
if val, found := os.LookupEnv(prefix + "_" + name); found {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// initDefaultAuth provides a default key signer for a client. This should only
|
||||
// be used internally if the client has no other key signer for authenticating
|
||||
// with Triton. We first look for both `SDC_KEY_ID` and `SSH_AUTH_SOCK` in the
|
||||
// user's environ(7). If so we default to the SSH agent key signer.
|
||||
func (c *Client) DefaultAuth() error {
|
||||
tritonKeyId := GetTritonEnv("KEY_ID")
|
||||
if tritonKeyId != "" {
|
||||
input := authentication.SSHAgentSignerInput{
|
||||
KeyID: tritonKeyId,
|
||||
AccountName: c.AccountName,
|
||||
Username: c.Username,
|
||||
}
|
||||
defaultSigner, err := authentication.NewSSHAgentSigner(input)
|
||||
if err != nil {
|
||||
return pkgerrors.Wrapf(err, "unable to initialize NewSSHAgentSigner")
|
||||
}
|
||||
c.Authorizers = append(c.Authorizers, defaultSigner)
|
||||
}
|
||||
|
||||
return ErrDefaultAuth
|
||||
}
|
||||
|
||||
// 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,
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 15 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: insecureSkipTLSVerify,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func doNotFollowRedirects(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
|
||||
func (c *Client) DecodeError(resp *http.Response, requestMethod string) error {
|
||||
err := &errors.APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
}
|
||||
|
||||
if requestMethod != http.MethodHead && resp.Body != nil {
|
||||
errorDecoder := json.NewDecoder(resp.Body)
|
||||
if err := errorDecoder.Decode(err); err != nil {
|
||||
return pkgerrors.Wrapf(err, "unable to decode error response")
|
||||
}
|
||||
}
|
||||
|
||||
if err.Message == "" {
|
||||
err.Message = fmt.Sprintf("HTTP response returned status code %d", err.StatusCode)
|
||||
}
|
||||
|
||||
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.Reader
|
||||
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, pkgerrors.Wrapf(err, "unable to construct HTTP request")
|
||||
}
|
||||
|
||||
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, pkgerrors.Wrapf(err, "unable to sign HTTP request")
|
||||
}
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Accept-Version", triton.CloudAPIMajorVersion)
|
||||
req.Header.Set("User-Agent", triton.UserAgent())
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, pkgerrors.Wrapf(err, "unable to execute HTTP request")
|
||||
}
|
||||
|
||||
// We will only return a response from the API it is in the HTTP StatusCode 2xx range
|
||||
// StatusMultipleChoices is StatusCode 300
|
||||
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
return nil, c.DecodeError(resp, req.Method)
|
||||
}
|
||||
|
||||
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.Reader
|
||||
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, pkgerrors.Wrapf(err, "unable to construct HTTP request")
|
||||
}
|
||||
|
||||
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, pkgerrors.Wrapf(err, "unable to sign HTTP request")
|
||||
}
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Accept-Version", triton.CloudAPIMajorVersion)
|
||||
req.Header.Set("User-Agent", triton.UserAgent())
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, pkgerrors.Wrapf(err, "unable to execute HTTP request")
|
||||
}
|
||||
|
||||
// We will only return a response from the API it is in the HTTP StatusCode 2xx range
|
||||
// StatusMultipleChoices is StatusCode 300
|
||||
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
return nil, c.DecodeError(resp, req.Method)
|
||||
}
|
||||
|
||||
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.Reader
|
||||
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, pkgerrors.Wrapf(err, "unable to construct HTTP request")
|
||||
}
|
||||
|
||||
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, pkgerrors.Wrapf(err, "unable to sign HTTP request")
|
||||
}
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("User-Agent", triton.UserAgent())
|
||||
|
||||
if query != nil {
|
||||
req.URL.RawQuery = query.Encode()
|
||||
}
|
||||
|
||||
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, nil, pkgerrors.Wrapf(err, "unable to execute HTTP request")
|
||||
}
|
||||
|
||||
// We will only return a response from the API it is in the HTTP StatusCode 2xx range
|
||||
// StatusMultipleChoices is StatusCode 300
|
||||
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
|
||||
return resp.Body, resp.Header, nil
|
||||
}
|
||||
|
||||
return nil, nil, c.DecodeError(resp, req.Method)
|
||||
}
|
||||
|
||||
type RequestNoEncodeInput struct {
|
||||
Method string
|
||||
Path string
|
||||
Query *url.Values
|
||||
Headers *http.Header
|
||||
Body io.Reader
|
||||
}
|
||||
|
||||
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, pkgerrors.Wrapf(err, "unable to construct HTTP request")
|
||||
}
|
||||
|
||||
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, pkgerrors.Wrapf(err, "unable to sign HTTP request")
|
||||
}
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("Accept-Version", triton.CloudAPIMajorVersion)
|
||||
req.Header.Set("User-Agent", triton.UserAgent())
|
||||
|
||||
if query != nil {
|
||||
req.URL.RawQuery = query.Encode()
|
||||
}
|
||||
|
||||
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, nil, pkgerrors.Wrapf(err, "unable to execute HTTP request")
|
||||
}
|
||||
|
||||
// We will only return a response from the API it is in the HTTP StatusCode 2xx range
|
||||
// StatusMultipleChoices is StatusCode 300
|
||||
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
|
||||
return resp.Body, resp.Header, nil
|
||||
}
|
||||
|
||||
return nil, nil, c.DecodeError(resp, req.Method)
|
||||
}
|
|
@ -0,0 +1,297 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// APIError 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
|
||||
// Error codes used by the Manta API are listed at
|
||||
// https://apidocs.joyent.com/manta/api.html#errors
|
||||
type APIError struct {
|
||||
StatusCode int
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Error implements interface Error on the APIError type.
|
||||
func (e APIError) Error() string {
|
||||
return strings.Trim(fmt.Sprintf("%+q", e.Code), `"`) + ": " + strings.Trim(fmt.Sprintf("%+q", e.Message), `"`)
|
||||
}
|
||||
|
||||
// ClientError represents an error code and message returned
|
||||
// when connecting to the triton-go client
|
||||
type ClientError struct {
|
||||
StatusCode int
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Error implements interface Error on the ClientError type.
|
||||
func (e ClientError) Error() string {
|
||||
return strings.Trim(fmt.Sprintf("%+q", e.Code), `"`) + ": " + strings.Trim(fmt.Sprintf("%+q", 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")
|
||||
}
|
||||
|
||||
func IsBadRequest(err error) bool {
|
||||
return IsSpecificError(err, "BadRequest")
|
||||
}
|
||||
|
||||
func IsInUseError(err error) bool {
|
||||
return IsSpecificError(err, "InUseError")
|
||||
}
|
||||
|
||||
func IsInvalidArgument(err error) bool {
|
||||
return IsSpecificError(err, "InvalidArgument")
|
||||
}
|
||||
|
||||
func IsInvalidCredentials(err error) bool {
|
||||
return IsSpecificError(err, "InvalidCredentials")
|
||||
}
|
||||
|
||||
func IsInvalidHeader(err error) bool {
|
||||
return IsSpecificError(err, "InvalidHeader")
|
||||
}
|
||||
|
||||
func IsInvalidVersion(err error) bool {
|
||||
return IsSpecificError(err, "InvalidVersion")
|
||||
}
|
||||
|
||||
func IsMissingParameter(err error) bool {
|
||||
return IsSpecificError(err, "MissingParameter")
|
||||
}
|
||||
|
||||
func IsNotAuthorized(err error) bool {
|
||||
return IsSpecificError(err, "NotAuthorized")
|
||||
}
|
||||
|
||||
func IsRequestThrottled(err error) bool {
|
||||
return IsSpecificError(err, "RequestThrottled")
|
||||
}
|
||||
|
||||
func IsRequestTooLarge(err error) bool {
|
||||
return IsSpecificError(err, "RequestTooLarge")
|
||||
}
|
||||
|
||||
func IsRequestMoved(err error) bool {
|
||||
return IsSpecificError(err, "RequestMoved")
|
||||
}
|
||||
|
||||
func IsResourceFound(err error) bool {
|
||||
return IsSpecificError(err, "ResourceFound")
|
||||
}
|
||||
|
||||
func IsResourceNotFound(err error) bool {
|
||||
return IsSpecificError(err, "ResourceNotFound")
|
||||
}
|
||||
|
||||
func IsUnknownError(err error) bool {
|
||||
return IsSpecificError(err, "UnknownError")
|
||||
}
|
||||
|
||||
func IsEmptyResponse(err error) bool {
|
||||
return IsSpecificError(err, "EmptyResponse")
|
||||
}
|
||||
|
||||
func IsStatusNotFoundCode(err error) bool {
|
||||
return IsSpecificStatusCode(err, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func IsSpecificError(myError error, errorCode string) bool {
|
||||
switch err := errors.Cause(myError).(type) {
|
||||
case *APIError:
|
||||
if err.Code == errorCode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func IsSpecificStatusCode(myError error, statusCode int) bool {
|
||||
switch err := errors.Cause(myError).(type) {
|
||||
case *APIError:
|
||||
if err.StatusCode == statusCode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
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}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/joyent/triton-go/client"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
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 List operation.
|
||||
type ListDirectoryInput struct {
|
||||
DirectoryName string
|
||||
Limit uint64
|
||||
Marker string
|
||||
}
|
||||
|
||||
// ListDirectoryOutput contains the outputs of a List 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) {
|
||||
absPath := absFileInput(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: string(absPath),
|
||||
Query: query,
|
||||
}
|
||||
respBody, respHeader, err := s.client.ExecuteRequestStorage(ctx, reqInput)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to list directory")
|
||||
}
|
||||
defer respBody.Close()
|
||||
|
||||
var results []*DirectoryEntry
|
||||
scanner := bufio.NewScanner(respBody)
|
||||
for scanner.Scan() {
|
||||
current := &DirectoryEntry{}
|
||||
if err := json.Unmarshal(scanner.Bytes(), current); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to decode list directories response")
|
||||
}
|
||||
|
||||
results = append(results, current)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to decode list directories response")
|
||||
}
|
||||
|
||||
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 Put 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 {
|
||||
absPath := absFileInput(s.client.AccountName, input.DirectoryName)
|
||||
|
||||
headers := &http.Header{}
|
||||
headers.Set("Content-Type", "application/json; type=directory")
|
||||
|
||||
reqInput := client.RequestInput{
|
||||
Method: http.MethodPut,
|
||||
Path: string(absPath),
|
||||
Headers: headers,
|
||||
}
|
||||
respBody, _, err := s.client.ExecuteRequestStorage(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to put directory")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteDirectoryInput represents parameters to a Delete operation.
|
||||
type DeleteDirectoryInput struct {
|
||||
DirectoryName string
|
||||
ForceDelete bool //Will recursively delete all child directories and objects
|
||||
}
|
||||
|
||||
// Delete deletes a directory on the Triton Object Storage. The directory must
|
||||
// be empty.
|
||||
func (s *DirectoryClient) Delete(ctx context.Context, input *DeleteDirectoryInput) error {
|
||||
absPath := absFileInput(s.client.AccountName, input.DirectoryName)
|
||||
|
||||
if input.ForceDelete {
|
||||
err := deleteAll(*s, ctx, absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err := deleteDirectory(*s, ctx, absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteAll(c DirectoryClient, ctx context.Context, directoryPath _AbsCleanPath) error {
|
||||
objs, err := c.List(ctx, &ListDirectoryInput{
|
||||
DirectoryName: string(directoryPath),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, obj := range objs.Entries {
|
||||
newPath := absFileInput(c.client.AccountName, path.Join(string(directoryPath), obj.Name))
|
||||
if obj.Type == "directory" {
|
||||
err := deleteDirectory(c, ctx, newPath)
|
||||
if err != nil {
|
||||
return deleteAll(c, ctx, newPath)
|
||||
}
|
||||
} else {
|
||||
return deleteObject(c, ctx, newPath)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteDirectory(c DirectoryClient, ctx context.Context, directoryPath _AbsCleanPath) error {
|
||||
reqInput := client.RequestInput{
|
||||
Method: http.MethodDelete,
|
||||
Path: string(directoryPath),
|
||||
}
|
||||
respBody, _, err := c.client.ExecuteRequestStorage(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to delete directory")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteObject(c DirectoryClient, ctx context.Context, path _AbsCleanPath) error {
|
||||
objClient := &ObjectsClient{
|
||||
client: c.client,
|
||||
}
|
||||
|
||||
err := objClient.Delete(ctx, &DeleteObjectInput{
|
||||
ObjectPath: string(path),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,448 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/joyent/triton-go/client"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
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) {
|
||||
fullPath := path.Join("/", s.client.AccountName, "jobs")
|
||||
|
||||
reqInput := client.RequestInput{
|
||||
Method: http.MethodPost,
|
||||
Path: fullPath,
|
||||
Body: input,
|
||||
}
|
||||
respBody, respHeaders, err := s.client.ExecuteRequestStorage(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to create job")
|
||||
}
|
||||
|
||||
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 {
|
||||
fullPath := path.Join("/", s.client.AccountName, "jobs", input.JobID, "live", "in")
|
||||
headers := &http.Header{}
|
||||
headers.Set("Content-Type", "text/plain")
|
||||
|
||||
reader := strings.NewReader(strings.Join(input.ObjectPaths, "\n"))
|
||||
|
||||
reqInput := client.RequestNoEncodeInput{
|
||||
Method: http.MethodPost,
|
||||
Path: fullPath,
|
||||
Headers: headers,
|
||||
Body: reader,
|
||||
}
|
||||
respBody, _, err := s.client.ExecuteRequestNoEncode(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to add job inputs")
|
||||
}
|
||||
|
||||
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 {
|
||||
fullPath := path.Join("/", s.client.AccountName, "jobs", input.JobID, "live", "in", "end")
|
||||
|
||||
reqInput := client.RequestNoEncodeInput{
|
||||
Method: http.MethodPost,
|
||||
Path: fullPath,
|
||||
}
|
||||
respBody, _, err := s.client.ExecuteRequestNoEncode(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to end job inputs")
|
||||
}
|
||||
|
||||
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 {
|
||||
fullPath := path.Join("/", s.client.AccountName, "jobs", input.JobID, "live", "cancel")
|
||||
|
||||
reqInput := client.RequestNoEncodeInput{
|
||||
Method: http.MethodPost,
|
||||
Path: fullPath,
|
||||
}
|
||||
respBody, _, err := s.client.ExecuteRequestNoEncode(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to cancel job")
|
||||
}
|
||||
|
||||
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) {
|
||||
fullPath := path.Join("/", s.client.AccountName, "jobs")
|
||||
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: fullPath,
|
||||
Query: query,
|
||||
}
|
||||
respBody, respHeader, err := s.client.ExecuteRequestStorage(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to list jobs")
|
||||
}
|
||||
|
||||
var results []*JobSummary
|
||||
for {
|
||||
current := &JobSummary{}
|
||||
decoder := json.NewDecoder(respBody)
|
||||
if err = decoder.Decode(¤t); err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, errors.Wrap(err, "unable to decode list jobs response")
|
||||
}
|
||||
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) {
|
||||
fullPath := path.Join("/", s.client.AccountName, "jobs", input.JobID, "live", "status")
|
||||
|
||||
reqInput := client.RequestInput{
|
||||
Method: http.MethodGet,
|
||||
Path: fullPath,
|
||||
}
|
||||
respBody, _, err := s.client.ExecuteRequestStorage(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get job")
|
||||
}
|
||||
|
||||
job := &Job{}
|
||||
decoder := json.NewDecoder(respBody)
|
||||
if err = decoder.Decode(&job); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to decode get job response")
|
||||
}
|
||||
|
||||
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) {
|
||||
fullPath := path.Join("/", s.client.AccountName, "jobs", input.JobID, "live", "out")
|
||||
|
||||
reqInput := client.RequestInput{
|
||||
Method: http.MethodGet,
|
||||
Path: fullPath,
|
||||
}
|
||||
respBody, respHeader, err := s.client.ExecuteRequestStorage(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get job output")
|
||||
}
|
||||
|
||||
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) {
|
||||
fullPath := path.Join("/", s.client.AccountName, "jobs", input.JobID, "live", "in")
|
||||
|
||||
reqInput := client.RequestInput{
|
||||
Method: http.MethodGet,
|
||||
Path: fullPath,
|
||||
}
|
||||
respBody, respHeader, err := s.client.ExecuteRequestStorage(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get job input")
|
||||
}
|
||||
|
||||
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) {
|
||||
fullPath := path.Join("/", s.client.AccountName, "jobs", input.JobID, "live", "fail")
|
||||
|
||||
reqInput := client.RequestInput{
|
||||
Method: http.MethodGet,
|
||||
Path: fullPath,
|
||||
}
|
||||
respBody, respHeader, err := s.client.ExecuteRequestStorage(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get job failures")
|
||||
}
|
||||
|
||||
output := &GetJobFailuresOutput{
|
||||
Items: respBody,
|
||||
}
|
||||
|
||||
resultSetSize, err := strconv.ParseUint(respHeader.Get("Result-Set-Size"), 10, 64)
|
||||
if err == nil {
|
||||
output.ResultSetSize = resultSetSize
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
|
@ -0,0 +1,391 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/joyent/triton-go/client"
|
||||
tt "github.com/joyent/triton-go/errors"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ObjectsClient struct {
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
// GetObjectInput represents parameters to a GetObject operation.
|
||||
type GetInfoInput struct {
|
||||
ObjectPath string
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
// GetObjectOutput contains the outputs for a GetObject operation. It is your
|
||||
// responsibility to ensure that the io.ReadCloser ObjectReader is closed.
|
||||
type GetInfoOutput struct {
|
||||
ContentLength uint64
|
||||
ContentType string
|
||||
LastModified time.Time
|
||||
ContentMD5 string
|
||||
ETag string
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
// GetInfo sends a HEAD request to an object in the Manta service. This function
|
||||
// does not return a response body.
|
||||
func (s *ObjectsClient) GetInfo(ctx context.Context, input *GetInfoInput) (*GetInfoOutput, error) {
|
||||
absPath := absFileInput(s.client.AccountName, input.ObjectPath)
|
||||
|
||||
headers := &http.Header{}
|
||||
for key, value := range input.Headers {
|
||||
headers.Set(key, value)
|
||||
}
|
||||
|
||||
reqInput := client.RequestInput{
|
||||
Method: http.MethodHead,
|
||||
Path: string(absPath),
|
||||
Headers: headers,
|
||||
}
|
||||
_, respHeaders, err := s.client.ExecuteRequestStorage(ctx, reqInput)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get info")
|
||||
}
|
||||
|
||||
response := &GetInfoOutput{
|
||||
ContentType: respHeaders.Get("Content-Type"),
|
||||
ContentMD5: respHeaders.Get("Content-MD5"),
|
||||
ETag: respHeaders.Get("Etag"),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// IsDir is a convenience wrapper around the GetInfo function which takes an
|
||||
// ObjectPath and returns a boolean whether or not the object is a directory
|
||||
// type in Manta. Returns an error if GetInfo failed upstream for some reason.
|
||||
func (s *ObjectsClient) IsDir(ctx context.Context, objectPath string) (bool, error) {
|
||||
info, err := s.GetInfo(ctx, &GetInfoInput{
|
||||
ObjectPath: objectPath,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if info != nil {
|
||||
return strings.HasSuffix(info.ContentType, "type=directory"), nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// GetObjectInput represents parameters to a GetObject operation.
|
||||
type GetObjectInput struct {
|
||||
ObjectPath string
|
||||
Headers map[string]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
|
||||
}
|
||||
|
||||
// Get 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) {
|
||||
absPath := absFileInput(s.client.AccountName, input.ObjectPath)
|
||||
|
||||
headers := &http.Header{}
|
||||
for key, value := range input.Headers {
|
||||
headers.Set(key, value)
|
||||
}
|
||||
|
||||
reqInput := client.RequestInput{
|
||||
Method: http.MethodGet,
|
||||
Path: string(absPath),
|
||||
Headers: headers,
|
||||
}
|
||||
respBody, respHeaders, err := s.client.ExecuteRequestStorage(ctx, reqInput)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get object")
|
||||
}
|
||||
|
||||
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
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
// DeleteObject deletes an object.
|
||||
func (s *ObjectsClient) Delete(ctx context.Context, input *DeleteObjectInput) error {
|
||||
absPath := absFileInput(s.client.AccountName, input.ObjectPath)
|
||||
|
||||
headers := &http.Header{}
|
||||
for key, value := range input.Headers {
|
||||
headers.Set(key, value)
|
||||
}
|
||||
|
||||
reqInput := client.RequestInput{
|
||||
Method: http.MethodDelete,
|
||||
Path: string(absPath),
|
||||
Headers: headers,
|
||||
}
|
||||
respBody, _, err := s.client.ExecuteRequestStorage(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to delete object")
|
||||
}
|
||||
|
||||
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 {
|
||||
absPath := absFileInput(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: string(absPath),
|
||||
Query: query,
|
||||
Headers: headers,
|
||||
}
|
||||
respBody, _, err := s.client.ExecuteRequestStorage(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to put metadata")
|
||||
}
|
||||
|
||||
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.Reader
|
||||
Headers map[string]string
|
||||
ForceInsert bool //Force the creation of the directory tree
|
||||
}
|
||||
|
||||
func (s *ObjectsClient) Put(ctx context.Context, input *PutObjectInput) error {
|
||||
absPath := absFileInput(s.client.AccountName, input.ObjectPath)
|
||||
|
||||
if input.ForceInsert {
|
||||
absDirName := _AbsCleanPath(path.Dir(string(absPath)))
|
||||
exists, err := checkDirectoryTreeExists(*s, ctx, absDirName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
err := createDirectory(*s, ctx, absDirName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return putObject(*s, ctx, input, absPath)
|
||||
}
|
||||
}
|
||||
|
||||
return putObject(*s, ctx, input, absPath)
|
||||
}
|
||||
|
||||
// _AbsCleanPath is an internal type that means the input has been
|
||||
// path.Clean()'ed and is an absolute path.
|
||||
type _AbsCleanPath string
|
||||
|
||||
func absFileInput(accountName, objPath string) _AbsCleanPath {
|
||||
cleanInput := path.Clean(objPath)
|
||||
if strings.HasPrefix(cleanInput, path.Join("/", accountName, "/")) {
|
||||
return _AbsCleanPath(cleanInput)
|
||||
}
|
||||
|
||||
cleanAbs := path.Clean(path.Join("/", accountName, objPath))
|
||||
return _AbsCleanPath(cleanAbs)
|
||||
}
|
||||
|
||||
func putObject(c ObjectsClient, ctx context.Context, input *PutObjectInput, absPath _AbsCleanPath) error {
|
||||
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{}
|
||||
for key, value := range input.Headers {
|
||||
headers.Set(key, value)
|
||||
}
|
||||
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: string(absPath),
|
||||
Headers: headers,
|
||||
Body: input.ObjectReader,
|
||||
}
|
||||
respBody, _, err := c.client.ExecuteRequestNoEncode(ctx, reqInput)
|
||||
if respBody != nil {
|
||||
defer respBody.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to put object")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDirectory(c ObjectsClient, ctx context.Context, absPath _AbsCleanPath) error {
|
||||
dirClient := &DirectoryClient{
|
||||
client: c.client,
|
||||
}
|
||||
|
||||
// An abspath starts w/ a leading "/" which gets added to the slice as an
|
||||
// empty string. Start all array math at 1.
|
||||
parts := strings.Split(string(absPath), "/")
|
||||
if len(parts) < 2 {
|
||||
return errors.New("no path components to create directory")
|
||||
}
|
||||
|
||||
folderPath := parts[1]
|
||||
// Don't attempt to create a manta account as a directory
|
||||
for i := 2; i < len(parts); i++ {
|
||||
part := parts[i]
|
||||
folderPath = path.Clean(path.Join("/", folderPath, part))
|
||||
err := dirClient.Put(ctx, &PutDirectoryInput{
|
||||
DirectoryName: folderPath,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkDirectoryTreeExists(c ObjectsClient, ctx context.Context, absPath _AbsCleanPath) (bool, error) {
|
||||
exists, err := c.IsDir(ctx, string(absPath))
|
||||
if err != nil {
|
||||
if tt.IsResourceNotFoundError(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if exists {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// 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: path.Join("/", s.Client.AccountName, "keys", 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, errors.Wrapf(err, "error signing string")
|
||||
}
|
||||
|
||||
output.Signature = signature
|
||||
return output, nil
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/joyent/triton-go/client"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
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 errors.Wrapf(err, "unable to put snaplink")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
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
|
||||
Username string
|
||||
Signers []authentication.Signer
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
||||
//
|
||||
// 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/.
|
||||
//
|
||||
|
||||
package triton
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// The main version number of the current released Triton-go SDK.
|
||||
const Version = "0.9.0"
|
||||
|
||||
// A pre-release marker for the version. If this is "" (empty string)
|
||||
// then it means that it is a final release. Otherwise, this is a pre-release
|
||||
// such as "dev" (in development), "beta", "rc1", etc.
|
||||
var Prerelease = ""
|
||||
|
||||
func UserAgent() string {
|
||||
if Prerelease != "" {
|
||||
return fmt.Sprintf("triton-go/%s-%s (%s-%s; %s)", Version, Prerelease, runtime.GOARCH, runtime.GOOS, runtime.Version())
|
||||
} else {
|
||||
return fmt.Sprintf("triton-go/%s (%s-%s; %s)", Version, runtime.GOARCH, runtime.GOOS, runtime.Version())
|
||||
}
|
||||
}
|
||||
|
||||
const CloudAPIMajorVersion = "8"
|
|
@ -1224,6 +1224,36 @@
|
|||
"revision": "c2b33e8439af944379acbdd9c3a5fe0bc44bd8a5",
|
||||
"revisionTime": "2018-02-06T20:15:40Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "Lg8OHK87XRGCaipG+5+zFyN8OMw=",
|
||||
"path": "github.com/joyent/triton-go",
|
||||
"revision": "545edbe0d564f075ac576f1ad177f4ff39c9adaf",
|
||||
"revisionTime": "2018-01-16T16:57:42Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "Y03+L+I0FVZ2bMGWt1MHTDEyWM4=",
|
||||
"path": "github.com/joyent/triton-go/authentication",
|
||||
"revision": "545edbe0d564f075ac576f1ad177f4ff39c9adaf",
|
||||
"revisionTime": "2018-01-16T16:57:42Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "MuJsGBr6HlXQYxZY9cM5rBk+Lns=",
|
||||
"path": "github.com/joyent/triton-go/client",
|
||||
"revision": "545edbe0d564f075ac576f1ad177f4ff39c9adaf",
|
||||
"revisionTime": "2018-01-16T16:57:42Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "d/Py6j/uMgOAFNFGpsQrNnSsO+k=",
|
||||
"path": "github.com/joyent/triton-go/errors",
|
||||
"revision": "545edbe0d564f075ac576f1ad177f4ff39c9adaf",
|
||||
"revisionTime": "2018-01-16T16:57:42Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "5v533ELM047YOiwHsyMaVzITpR0=",
|
||||
"path": "github.com/joyent/triton-go/storage",
|
||||
"revision": "545edbe0d564f075ac576f1ad177f4ff39c9adaf",
|
||||
"revisionTime": "2018-01-16T16:57:42Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "VJk3rOWfxEV9Ilig5lgzH1qg8Ss=",
|
||||
"path": "github.com/keybase/go-crypto/brainpool",
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Manta - Storage Backends - Configuration"
|
||||
sidebar_current: "docs-configuration-storage-manta"
|
||||
description: |-
|
||||
The Manta storage backend is used to persist Vault's data in Triton's Manta Object
|
||||
Storage. The storage folder must already exist.
|
||||
---
|
||||
|
||||
# Manta Storage Backend
|
||||
|
||||
The Manta storage backend is used to persist Vault's data in [Triton's Manta Object
|
||||
Storage][manta-object-store]. The storage folder must already exist.
|
||||
|
||||
- **No High Availability** – the Manta storage backend does not support high
|
||||
availability.
|
||||
|
||||
- **Community Supported** – the Manta storage backend is supported by the
|
||||
community. While it has undergone review by HashiCorp employees, they may not
|
||||
be as knowledgeable about the technology. If you encounter problems with them,
|
||||
you may be referred to the original author.
|
||||
|
||||
```hcl
|
||||
storage "manta" {
|
||||
directory = "manta-directory"
|
||||
user = "myuser"
|
||||
key_id = "40:9d:d3:f9:0b:86:62:48:f4:2e:a5:8e:43:00:2a:9b"
|
||||
}
|
||||
```
|
||||
|
||||
## `manta` Parameters
|
||||
|
||||
- `directory` `(string: <required>)` – Specifies the name of the manta directory to use.
|
||||
This will be in the `/stor/` folder in the specific manta account
|
||||
|
||||
The following settings are used for authenticating to Manta.
|
||||
|
||||
- `user` `(string: <required>)` – Specifies the Manta user account name. This can also be provided via
|
||||
the environment variable `MANTA_USER`.
|
||||
|
||||
- `key_id` `(string: <required>)` – The fingerprint of the public key of the SSH key pair to use for authentication with the Manta API.
|
||||
It is assumed that the SSH agent has the private key corresponding to this key ID loaded. This can also be provided
|
||||
via the environment variable `MANTA_KEY_ID`.
|
||||
|
||||
- `subuser` - The name of a subuser that has been granted access to the Manta account. This can also be
|
||||
provided via the environment variable `MANTA_SUBUSER`.
|
||||
|
||||
- `url` – Specifies the Manta URL. Defaults to `https://us-east.manta.joyent.com`. This can also be provided via
|
||||
the environment variable `MANTA_URL`.
|
||||
|
||||
- `max_parallel` `(string: "128")` – Specifies The maximum number of concurrent
|
||||
requests to Manta.
|
||||
|
||||
## `manta` Examples
|
||||
|
||||
This example shows configuring the Azure storage backend with a custom number of
|
||||
maximum parallel connections.
|
||||
|
||||
```hcl
|
||||
storage "manta" {
|
||||
directory = "vault-storage-directory"
|
||||
max_parallel = 512
|
||||
}
|
||||
```
|
||||
|
||||
[manta-object-store]: https://www.joyent.com/triton/object-storage
|
|
@ -138,6 +138,9 @@
|
|||
<li<%= sidebar_current("docs-configuration-storage-in-memory")%>>
|
||||
<a href="/docs/configuration/storage/in-memory.html">In-Memory</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-configuration-storage-manta")%>>
|
||||
<a href="/docs/configuration/storage/manta.html">Manta</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-configuration-storage-mysql")%>>
|
||||
<a href="/docs/configuration/storage/mysql.html">MySQL</a>
|
||||
</li>
|
||||
|
|
Loading…
Reference in New Issue