backend/manta: Support Triton RBAC

Triton Manta allows an account other than the main triton account to be used via RBAC.

Here we expose the SDC_USER / TRITON_USER options to the backend so that a user can be specified.
This commit is contained in:
Paul Stack 2018-01-03 22:12:46 +02:00 committed by Martin Atkins
parent 894a7c966e
commit 191cf283d5
16 changed files with 827 additions and 176 deletions

View File

@ -26,6 +26,12 @@ func New() backend.Backend {
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"TRITON_ACCOUNT", "SDC_ACCOUNT"}, ""),
},
"user": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"TRITON_USER", "SDC_USER"}, ""),
},
"url": {
Type: schema.TypeString,
Optional: true,
@ -80,6 +86,7 @@ type Backend struct {
type BackendConfig struct {
AccountId string
Username string
KeyId string
AccountUrl string
KeyMaterial string
@ -100,6 +107,10 @@ func (b *Backend) configure(ctx context.Context) error {
SkipTls: data.Get("insecure_skip_tls_verify").(bool),
}
if v, ok := data.GetOk("user"); ok {
config.Username = v.(string)
}
if v, ok := data.GetOk("key_material"); ok {
config.KeyMaterial = v.(string)
}
@ -127,7 +138,12 @@ func (b *Backend) configure(ctx context.Context) error {
var err error
if config.KeyMaterial == "" {
signer, err = authentication.NewSSHAgentSigner(config.KeyId, config.AccountId)
input := authentication.SSHAgentSignerInput{
KeyID: config.KeyId,
AccountName: config.AccountId,
Username: config.Username,
}
signer, err = authentication.NewSSHAgentSigner(input)
if err != nil {
return errwrap.Wrapf("Error Creating SSH Agent Signer: {{err}}", err)
}
@ -155,7 +171,14 @@ func (b *Backend) configure(ctx context.Context) error {
keyBytes = []byte(config.KeyMaterial)
}
signer, err = authentication.NewPrivateKeySigner(config.KeyId, keyBytes, config.AccountId)
input := authentication.PrivateKeySignerInput{
KeyID: config.KeyId,
PrivateKeyMaterial: keyBytes,
AccountName: config.AccountId,
Username: config.Username,
}
signer, err = authentication.NewPrivateKeySigner(input)
if err != nil {
return errwrap.Wrapf("Error Creating SSH Private Key Signer: {{err}}", err)
}
@ -164,6 +187,7 @@ func (b *Backend) configure(ctx context.Context) error {
clientConfig := &triton.ClientConfig{
MantaURL: config.AccountUrl,
AccountName: config.AccountId,
Username: config.Username,
Signers: []authentication.Signer{signer},
}
triton, err := storage.NewClient(clientConfig)

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

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

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

@ -0,0 +1,47 @@
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 tests"
@echo $(TEST) | \
xargs -t go test -v $(TESTARGS) -timeout=30s -parallel=1 | grep -Ev 'TRITON_TEST|TestAcc'
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}'

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

@ -0,0 +1,39 @@
# 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]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/hashicorp/errwrap"
packages = ["."]
revision = "7554cd9344cec97297fa6649b055a8c98c2a1e55"
[[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 = "bd6f299fb381e4c3393d1c4b1f0b94f5e77650c8"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "28853a8970ee33112a9e7998b18e658bed04d177537ec69db678189f0b8a9a7d"
solver-name = "gps-cdcl"
solver-version = 1

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

@ -0,0 +1,42 @@
# 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]]
name = "github.com/davecgh/go-spew"
version = "1.1.0"
[[constraint]]
branch = "master"
name = "github.com/hashicorp/errwrap"
[[constraint]]
branch = "master"
name = "github.com/sean-/seed"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"

View File

@ -3,6 +3,8 @@
`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
@ -13,11 +15,17 @@ using a key stored with the local SSH Agent (using an [`SSHAgentSigner`][6].
To construct a Signer, use the `New*` range of methods in the `authentication`
package. In the case of `authentication.NewSSHAgentSigner`, the parameters are
the fingerprint of the key with which to sign, and the account name (normally
stored in the `SDC_ACCOUNT` environment variable). For example:
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:
```
const fingerprint := "a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11"
sshKeySigner, err := authentication.NewSSHAgentSigner(fingerprint, "AccountName")
```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)
}
@ -34,17 +42,18 @@ their own seperate client. In order to initialize a package client, simply pass
the global `triton.ClientConfig` struct into the client's constructor function.
```go
config := &triton.ClientConfig{
TritonURL: os.Getenv("SDC_URL"),
MantaURL: os.Getenv("MANTA_URL"),
AccountName: accountName,
Signers: []authentication.Signer{sshKeySigner},
}
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)
}
c, err := compute.NewClient(config)
if err != nil {
log.Fatalf("compute.NewClient: %s", err)
}
```
Constructing `compute.Client` returns an interface which exposes `compute` API
@ -55,10 +64,10 @@ 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)
}
c, err := storage.NewClient(config)
if err != nil {
log.Fatalf("storage.NewClient: %s", err)
}
```
## Error Handling
@ -79,13 +88,14 @@ set:
- `TRITON_TEST` - must be set to any value in order to indicate desire to create
resources
- `SDC_URL` - the base endpoint for the Triton API
- `SDC_ACCOUNT` - the account name for the Triton API
- `SDC_KEY_ID` - the fingerprint of the SSH key identifying the key
- `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 `SDC_KEY_MATERIAL` to the contents of an unencrypted
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.
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
@ -94,9 +104,9 @@ The verbose output has been removed for brevity here.
```
$ HTTP_PROXY=http://localhost:8888 \
TRITON_TEST=1 \
SDC_URL=https://us-sw-1.api.joyent.com \
SDC_ACCOUNT=AccountName \
SDC_KEY_ID=a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11 \
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)
@ -116,7 +126,7 @@ referencing your SSH key file use by your active `triton` CLI profile.
```sh
$ eval "$(triton env us-sw-1)"
$ SDC_KEY_FILE=~/.ssh/triton-id_rsa go run examples/compute/instances.go
$ 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
@ -142,15 +152,21 @@ import (
)
func main() {
keyID := os.Getenv("SDC_KEY_ID")
accountName := os.Getenv("SDC_ACCOUNT")
keyMaterial := os.Getenv("SDC_KEY_MATERIAL")
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 == "" {
signer, err = authentication.NewSSHAgentSigner(keyID, accountName)
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)
}
@ -178,15 +194,22 @@ func main() {
keyBytes = []byte(keyMaterial)
}
signer, err = authentication.NewPrivateKeySigner(keyID, []byte(keyMaterial), accountName)
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("SDC_URL"),
TritonURL: os.Getenv("TRITON_URL"),
AccountName: accountName,
Username: userName,
Signers: []authentication.Signer{signer},
}

View File

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

@ -9,6 +9,7 @@ import (
"encoding/pem"
"errors"
"fmt"
"path"
"strings"
"github.com/hashicorp/errwrap"
@ -20,15 +21,23 @@ type PrivateKeySigner struct {
keyFingerprint string
algorithm string
accountName string
userName string
hashFunc crypto.Hash
privateKey *rsa.PrivateKey
}
func NewPrivateKeySigner(keyFingerprint string, privateKeyMaterial []byte, accountName string) (*PrivateKeySigner, error) {
keyFingerprintMD5 := strings.Replace(keyFingerprint, ":", "", -1)
type PrivateKeySignerInput struct {
KeyID string
PrivateKeyMaterial []byte
AccountName string
Username string
}
block, _ := pem.Decode(privateKeyMaterial)
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")
}
@ -51,13 +60,17 @@ func NewPrivateKeySigner(keyFingerprint string, privateKeyMaterial []byte, accou
signer := &PrivateKeySigner{
formattedKeyFingerprint: displayKeyFingerprint,
keyFingerprint: keyFingerprint,
accountName: accountName,
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)
@ -80,7 +93,13 @@ func (s *PrivateKeySigner) Sign(dateHeader string) (string, error) {
}
signedBase64 := base64.StdEncoding.EncodeToString(signed)
keyID := fmt.Sprintf("/%s/keys/%s", s.accountName, s.formattedKeyFingerprint)
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
}

View File

@ -8,6 +8,7 @@ import (
"fmt"
"net"
"os"
"path"
"strings"
"github.com/hashicorp/errwrap"
@ -15,21 +16,32 @@ import (
"golang.org/x/crypto/ssh/agent"
)
var (
ErrUnsetEnvVar = errors.New("SSH_AUTH_SOCK is not set")
)
type SSHAgentSigner struct {
formattedKeyFingerprint string
keyFingerprint string
algorithm string
accountName string
userName string
keyIdentifier string
agent agent.Agent
key ssh.PublicKey
}
func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, error) {
sshAgentAddress := os.Getenv("SSH_AUTH_SOCK")
if sshAgentAddress == "" {
return nil, errors.New("SSH_AUTH_SOCK is not set")
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)
@ -39,12 +51,41 @@ func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, err
ag := agent.NewClient(conn)
keys, err := ag.List()
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, errwrap.Wrapf("Error listing keys in SSH Agent: %s", err)
}
keyFingerprintStripped := strings.TrimPrefix(keyFingerprint, "MD5:")
keyFingerprintStripped := strings.TrimPrefix(s.keyFingerprint, "MD5:")
keyFingerprintStripped = strings.TrimPrefix(keyFingerprintStripped, "SHA256:")
keyFingerprintStripped = strings.Replace(keyFingerprintStripped, ":", "", -1)
@ -64,27 +105,10 @@ func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, err
}
if matchingKey == nil {
return nil, fmt.Errorf("No key in the SSH Agent matches fingerprint: %s", keyFingerprint)
return nil, fmt.Errorf("No key in the SSH Agent matches fingerprint: %s", s.keyFingerprint)
}
formattedKeyFingerprint := formatPublicKeyFingerprint(matchingKey, true)
signer := &SSHAgentSigner{
formattedKeyFingerprint: formattedKeyFingerprint,
keyFingerprint: keyFingerprint,
accountName: accountName,
agent: ag,
key: matchingKey,
keyIdentifier: fmt.Sprintf("/%s/keys/%s", accountName, formattedKeyFingerprint),
}
_, algorithm, err := signer.SignRaw("HelloWorld")
if err != nil {
return nil, fmt.Errorf("Cannot sign using ssh agent: %s", err)
}
signer.algorithm = algorithm
return signer, nil
return matchingKey, nil
}
func (s *SSHAgentSigner) Sign(dateHeader string) (string, error) {

View File

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

@ -6,6 +6,7 @@ import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
@ -19,7 +20,14 @@ import (
const nilContext = "nil context"
var MissingKeyIdError = errors.New("Default SSH agent authentication requires SDC_KEY_ID")
var (
ErrDefaultAuth = errors.New("default SSH agent authentication requires SDC_KEY_ID / TRITON_KEY_ID and SSH_AUTH_SOCK")
ErrAccountName = errors.New("missing account name for Triton/Manta")
ErrMissingURL = errors.New("missing Triton and/or Manta URL")
BadTritonURL = "invalid format of triton URL"
BadMantaURL = "invalid format of manta URL"
)
// Client represents a connection to the Triton Compute or Object Storage APIs.
type Client struct {
@ -28,7 +36,7 @@ type Client struct {
TritonURL url.URL
MantaURL url.URL
AccountName string
Endpoint string
Username string
}
// New is used to construct a Client in order to make API
@ -37,61 +45,93 @@ type Client struct {
// 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, errwrap.Wrapf("invalid endpoint URL: {{err}}", err)
return nil, errwrap.Wrapf(BadTritonURL+": {{err}}", err)
}
storageURL, err := url.Parse(mantaURL)
if err != nil {
return nil, errwrap.Wrapf("invalid manta URL: {{err}}", err)
return nil, errwrap.Wrapf(BadMantaURL+": {{err}}", err)
}
if accountName == "" {
return nil, errors.New("account name can not be empty")
}
httpClient := &http.Client{
Transport: httpTransport(false),
CheckRedirect: doNotFollowRedirects,
}
newClient := &Client{
HTTPClient: httpClient,
Authorizers: signers,
TritonURL: *cloudURL,
MantaURL: *storageURL,
AccountName: accountName,
// TODO(justinwr): Deprecated?
// Endpoint: tritonURL,
}
var authorizers []authentication.Signer
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 SDC_KEY_ID value available in the
// user environ.
if len(authorizers) == 0 {
keyID := os.Getenv("SDC_KEY_ID")
if len(keyID) != 0 {
keySigner, err := authentication.NewSSHAgentSigner(keyID, accountName)
if err != nil {
return nil, errwrap.Wrapf("Problem initializing NewSSHAgentSigner: {{err}}", err)
}
newClient.Authorizers = append(authorizers, keySigner)
} else {
return nil, MissingKeyIdError
// 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 errwrap.Wrapf("problem initializing NewSSHAgentSigner: {{err}}", err)
}
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
@ -112,8 +152,8 @@ func httpTransport(insecureSkipTLSVerify bool) *http.Transport {
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
DisableKeepAlives: true,
MaxIdleConnsPerHost: -1,
MaxIdleConns: 10,
IdleConnTimeout: 15 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecureSkipTLSVerify,
},
@ -158,7 +198,7 @@ func (c *Client) ExecuteRequestURIParams(ctx context.Context, inputs RequestInpu
body := inputs.Body
query := inputs.Query
var requestBody io.ReadSeeker
var requestBody io.Reader
if body != nil {
marshaled, err := json.MarshalIndent(body, "", " ")
if err != nil {
@ -217,7 +257,7 @@ func (c *Client) ExecuteRequestRaw(ctx context.Context, inputs RequestInput) (*h
path := inputs.Path
body := inputs.Body
var requestBody io.ReadSeeker
var requestBody io.Reader
if body != nil {
marshaled, err := json.MarshalIndent(body, "", " ")
if err != nil {
@ -270,7 +310,7 @@ func (c *Client) ExecuteRequestStorage(ctx context.Context, inputs RequestInput)
endpoint := c.MantaURL
endpoint.Path = path
var requestBody io.ReadSeeker
var requestBody io.Reader
if body != nil {
marshaled, err := json.MarshalIndent(body, "", " ")
if err != nil {
@ -323,10 +363,17 @@ func (c *Client) ExecuteRequestStorage(ctx context.Context, inputs RequestInput)
StatusCode: resp.StatusCode,
}
errorDecoder := json.NewDecoder(resp.Body)
if err := errorDecoder.Decode(mantaError); err != nil {
return nil, nil, errwrap.Wrapf("Error decoding error response: {{err}}", err)
if req.Method != http.MethodHead {
errorDecoder := json.NewDecoder(resp.Body)
if err := errorDecoder.Decode(mantaError); err != nil {
return nil, nil, errwrap.Wrapf("Error decoding error response: {{err}}", err)
}
}
if mantaError.Message == "" {
mantaError.Message = fmt.Sprintf("HTTP response returned status code %d", resp.StatusCode)
}
return nil, nil, mantaError
}
@ -335,7 +382,7 @@ type RequestNoEncodeInput struct {
Path string
Query *url.Values
Headers *http.Header
Body io.ReadSeeker
Body io.Reader
}
func (c *Client) ExecuteRequestNoEncode(ctx context.Context, inputs RequestNoEncodeInput) (io.ReadCloser, http.Header, error) {

View File

@ -1,12 +1,12 @@
package storage
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strconv"
"time"
@ -27,14 +27,14 @@ type DirectoryEntry struct {
Type string `json:"type"`
}
// ListDirectoryInput represents parameters to a ListDirectory operation.
// ListDirectoryInput represents parameters to a List operation.
type ListDirectoryInput struct {
DirectoryName string
Limit uint64
Marker string
}
// ListDirectoryOutput contains the outputs of a ListDirectory operation.
// ListDirectoryOutput contains the outputs of a List operation.
type ListDirectoryOutput struct {
Entries []*DirectoryEntry
ResultSetSize uint64
@ -42,7 +42,7 @@ type ListDirectoryOutput struct {
// List lists the contents of a directory on the Triton Object Store service.
func (s *DirectoryClient) List(ctx context.Context, input *ListDirectoryInput) (*ListDirectoryOutput, error) {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.DirectoryName)
absPath := absFileInput(s.client.AccountName, input.DirectoryName)
query := &url.Values{}
if input.Limit != 0 {
query.Set("limit", strconv.FormatUint(input.Limit, 10))
@ -53,30 +53,30 @@ func (s *DirectoryClient) List(ctx context.Context, input *ListDirectoryInput) (
reqInput := client.RequestInput{
Method: http.MethodGet,
Path: path,
Path: string(absPath),
Query: query,
}
respBody, respHeader, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return nil, errwrap.Wrapf("Error executing ListDirectory request: {{err}}", err)
return nil, errwrap.Wrapf("Error executing List request: {{err}}", err)
}
defer respBody.Close()
var results []*DirectoryEntry
for {
scanner := bufio.NewScanner(respBody)
for scanner.Scan() {
current := &DirectoryEntry{}
decoder := json.NewDecoder(respBody)
if err = decoder.Decode(&current); err != nil {
if err == io.EOF {
break
}
return nil, errwrap.Wrapf("Error decoding ListDirectory response: {{err}}", err)
if err := json.Unmarshal(scanner.Bytes(), current); err != nil {
return nil, errwrap.Wrapf("error decoding list response: {{err}}", err)
}
results = append(results, current)
}
if err := scanner.Err(); err != nil {
return nil, errwrap.Wrapf("error decoding list responses: {{err}}", err)
}
output := &ListDirectoryOutput{
Entries: results,
}
@ -89,7 +89,7 @@ func (s *DirectoryClient) List(ctx context.Context, input *ListDirectoryInput) (
return output, nil
}
// PutDirectoryInput represents parameters to a PutDirectory operation.
// PutDirectoryInput represents parameters to a Put operation.
type PutDirectoryInput struct {
DirectoryName string
}
@ -98,13 +98,14 @@ type PutDirectoryInput struct {
// create-or-update operation. Your private namespace starts at /:login, and you
// can create any nested set of directories or objects within it.
func (s *DirectoryClient) Put(ctx context.Context, input *PutDirectoryInput) error {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.DirectoryName)
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: path,
Path: string(absPath),
Headers: headers,
}
respBody, _, err := s.client.ExecuteRequestStorage(ctx, reqInput)
@ -112,27 +113,66 @@ func (s *DirectoryClient) Put(ctx context.Context, input *PutDirectoryInput) err
defer respBody.Close()
}
if err != nil {
return errwrap.Wrapf("Error executing PutDirectory request: {{err}}", err)
return errwrap.Wrapf("Error executing Put request: {{err}}", err)
}
return nil
}
// DeleteDirectoryInput represents parameters to a DeleteDirectory operation.
// 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 {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.DirectoryName)
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: path,
Path: string(directoryPath),
}
respBody, _, err := s.client.ExecuteRequestStorage(ctx, reqInput)
respBody, _, err := c.client.ExecuteRequestStorage(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
@ -142,3 +182,18 @@ func (s *DirectoryClient) Delete(ctx context.Context, input *DeleteDirectoryInpu
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
}

View File

@ -3,10 +3,10 @@ package storage
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
@ -19,9 +19,90 @@ 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, errwrap.Wrapf("Error executing get info request: {{err}}", err)
}
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
@ -36,19 +117,25 @@ type GetObjectOutput struct {
ObjectReader io.ReadCloser
}
// GetObject retrieves an object from the Manta service. If error is nil (i.e.
// the call returns successfully), it is your responsibility to close the io.ReadCloser
// named ObjectReader in the operation output.
// 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) {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.ObjectPath)
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: path,
Method: http.MethodGet,
Path: string(absPath),
Headers: headers,
}
respBody, respHeaders, err := s.client.ExecuteRequestStorage(ctx, reqInput)
if err != nil {
return nil, errwrap.Wrapf("Error executing GetDirectory request: {{err}}", err)
return nil, errwrap.Wrapf("Error executing Get request: {{err}}", err)
}
response := &GetObjectOutput{
@ -82,22 +169,29 @@ func (s *ObjectsClient) Get(ctx context.Context, input *GetObjectInput) (*GetObj
// 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 {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.ObjectPath)
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: path,
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 errwrap.Wrapf("Error executing DeleteObject request: {{err}}", err)
return errwrap.Wrapf("Error executing Delete request: {{err}}", err)
}
return nil
@ -120,7 +214,7 @@ type PutObjectMetadataInput struct {
// - Content-MD5
// - Durability-Level
func (s *ObjectsClient) PutMetadata(ctx context.Context, input *PutObjectMetadataInput) error {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.ObjectPath)
absPath := absFileInput(s.client.AccountName, input.ObjectPath)
query := &url.Values{}
query.Set("metadata", "true")
@ -132,7 +226,7 @@ func (s *ObjectsClient) PutMetadata(ctx context.Context, input *PutObjectMetadat
reqInput := client.RequestInput{
Method: http.MethodPut,
Path: path,
Path: string(absPath),
Query: query,
Headers: headers,
}
@ -141,7 +235,7 @@ func (s *ObjectsClient) PutMetadata(ctx context.Context, input *PutObjectMetadat
defer respBody.Close()
}
if err != nil {
return errwrap.Wrapf("Error executing PutObjectMetadata request: {{err}}", err)
return errwrap.Wrapf("Error executing PutMetadata request: {{err}}", err)
}
return nil
@ -157,17 +251,55 @@ type PutObjectInput struct {
IfModifiedSince *time.Time
ContentLength uint64
MaxContentLength uint64
ObjectReader io.ReadSeeker
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 {
path := fmt.Sprintf("/%s%s", s.client.AccountName, input.ObjectPath)
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))
}
@ -192,17 +324,64 @@ func (s *ObjectsClient) Put(ctx context.Context, input *PutObjectInput) error {
reqInput := client.RequestNoEncodeInput{
Method: http.MethodPut,
Path: path,
Path: string(absPath),
Headers: headers,
Body: input.ObjectReader,
}
respBody, _, err := s.client.ExecuteRequestNoEncode(ctx, reqInput)
respBody, _, err := c.client.ExecuteRequestNoEncode(ctx, reqInput)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
return errwrap.Wrapf("Error executing PutObjectMetadata request: {{err}}", err)
return errwrap.Wrapf("Error executing Put request: {{err}}", err)
}
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 {
errType := &client.MantaError{}
if errwrap.ContainsType(err, errType) {
mantaErr := errwrap.GetType(err, errType).(*client.MantaError)
if mantaErr.StatusCode == http.StatusNotFound {
return false, nil
}
}
return false, err
}
if exists {
return true, nil
}
return false, nil
}

View File

@ -14,5 +14,6 @@ type ClientConfig struct {
TritonURL string
MantaURL string
AccountName string
Username string
Signers []authentication.Signer
}

24
vendor/vendor.json vendored
View File

@ -1802,28 +1802,28 @@
"revisionTime": "2016-06-16T18:50:15Z"
},
{
"checksumSHA1": "EqvUu0Ku0Ec5Tk6yhGNOuOr8yeA=",
"checksumSHA1": "oINoQSRkPinChzwEHr3VatB9++Y=",
"path": "github.com/joyent/triton-go",
"revision": "5a58ad2cdec95cddd1e0a2e56f559341044b04f0",
"revisionTime": "2017-10-17T16:55:58Z"
"revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
"revisionTime": "2017-12-28T20:20:46Z"
},
{
"checksumSHA1": "JKf97EAAAZFQ6Wf8qN9X7TWqNBY=",
"checksumSHA1": "d6pxw8DLxYehLr92fWZTLEWVws8=",
"path": "github.com/joyent/triton-go/authentication",
"revision": "5a58ad2cdec95cddd1e0a2e56f559341044b04f0",
"revisionTime": "2017-10-17T16:55:58Z"
"revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
"revisionTime": "2017-12-28T20:20:46Z"
},
{
"checksumSHA1": "dlO1or0cyVMAmZzyLcBuoy+M0xU=",
"checksumSHA1": "GCHfn8d1Mhswm7n7IRnT0n/w+dw=",
"path": "github.com/joyent/triton-go/client",
"revision": "5a58ad2cdec95cddd1e0a2e56f559341044b04f0",
"revisionTime": "2017-10-17T16:55:58Z"
"revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
"revisionTime": "2017-12-28T20:20:46Z"
},
{
"checksumSHA1": "9VONvM4aQL088cLPgg+Z0K0dshc=",
"checksumSHA1": "PJe3Rs8H466xR8o5audO8oWk44Q=",
"path": "github.com/joyent/triton-go/storage",
"revision": "5a58ad2cdec95cddd1e0a2e56f559341044b04f0",
"revisionTime": "2017-10-17T16:55:58Z"
"revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
"revisionTime": "2017-12-28T20:20:46Z"
},
{
"checksumSHA1": "g+afVQQVopBLiLB5pFZp/8s6aBs=",

View File

@ -43,16 +43,10 @@ data "terraform_remote_state" "foo" {
The following configuration options are supported:
* `account` - (Required) This is the name of the Manta account. It can also be provided via the `SDC_ACCOUNT` or `TRITON_ACCOUNT` environment variables.
* `user` - (Optional) The username of the Triton account used to authenticate with the Triton API. It can also be provided via the `SDC_USER` or `TRITON_USER` environment variables.
* `url` - (Optional) The Manta API Endpoint. It can also be provided via the `MANTA_URL` environment variable. Defaults to `https://us-east.manta.joyent.com`.
* `key_material` - (Optional) This is the private key of an SSH key associated with the Triton account to be used. If this is not set, the private key corresponding to the fingerprint in key_id must be available via an SSH Agent. Can be set via the `SDC_KEY_MATERIAL` or `TRITON_KEY_MATERIAL` environment variables.
* `key_id` - (Required) This is the fingerprint of the public key matching the key specified in key_path. It can be obtained via the command ssh-keygen -l -E md5 -f /path/to/key. Can be set via the `SDC_KEY_ID` or `TRITON_KEY_ID` environment variables.
* `insecure_skip_tls_verify` - (Optional) This allows skipping TLS verification of the Triton endpoint. It is useful when connecting to a temporary Triton installation such as Cloud-On-A-Laptop which does not generally use a certificate signed by a trusted root CA. Defaults to `false`.
* `path` - (Required) The path relative to your private storage directory (`/$MANTA_USER/stor`) where the state file will be stored. **Please Note:** If this path does not exist, then the backend will create this folder location as part of backend creation.
* `objectName` - (Optional) The name of the state file (defaults to `terraform.tfstate`)
The following [Manta environment variables](https://apidocs.joyent.com/manta/#setting-up-your-environment) are supported:
* `MANTA_URL` - (Required) The API endpoint
* `MANTA_USER` - (Required) The Manta user
* `MANTA_KEY_ID` - (Required) The MD5 fingerprint of your SSH key
* `MANTA_KEY_MATERIAL` - (Required) The path to the private key for accessing Manta (must align with the `MANTA_KEY_ID`). This key must *not* be protected by passphrase.
* `objectName` - (Optional) The name of the state file (defaults to `terraform.tfstate`)