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:
Paul Stack 2018-02-13 01:22:41 +02:00 committed by Jeff Mitchell
parent 60732577f5
commit 3c683dba92
31 changed files with 3927 additions and 0 deletions

View File

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

265
physical/manta/manta.go Normal file
View File

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

View File

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

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

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

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

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

33
vendor/github.com/joyent/triton-go/Gopkg.lock generated vendored Normal file
View File

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

38
vendor/github.com/joyent/triton-go/Gopkg.toml generated vendored Normal file
View File

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

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

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

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

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

View File

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

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

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

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

View File

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

View File

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

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

View File

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

View File

@ -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, ":")
}

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

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

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

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

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

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

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

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

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

@ -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(&current); 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
}

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

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

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

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

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

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

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

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

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

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

30
vendor/vendor.json vendored
View File

@ -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",

View File

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

View File

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