Add Support for Manta Object Storage as a Gateway (#5025)

Manta is an Object Storage by [Joyent](https://www.joyent.com/)

This PR adds initial support for Manta. It is intended as non-production 
ready so that feedback can be obtained.
This commit is contained in:
Paul Stack
2017-12-20 10:07:56 +02:00
committed by Nitish Tiwari
parent 1f77708a30
commit 7d75d61621
69 changed files with 20974 additions and 0 deletions

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

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

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.

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,170 @@
package authentication
import (
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"net"
"os"
"strings"
"github.com/hashicorp/errwrap"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
var (
ErrUnsetEnvVar = errors.New("SSH_AUTH_SOCK is not set")
)
type SSHAgentSigner struct {
formattedKeyFingerprint string
keyFingerprint string
algorithm string
accountName string
keyIdentifier string
agent agent.Agent
key ssh.PublicKey
}
func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, error) {
sshAgentAddress, agentOk := os.LookupEnv("SSH_AUTH_SOCK")
if !agentOk {
return nil, ErrUnsetEnvVar
}
conn, err := net.Dial("unix", sshAgentAddress)
if err != nil {
return nil, errwrap.Wrapf("Error dialing SSH agent: {{err}}", err)
}
ag := agent.NewClient(conn)
signer := &SSHAgentSigner{
keyFingerprint: keyFingerprint,
accountName: accountName,
agent: ag,
}
matchingKey, err := signer.MatchKey()
if err != nil {
return nil, err
}
signer.key = matchingKey
signer.formattedKeyFingerprint = formatPublicKeyFingerprint(signer.key, true)
signer.keyIdentifier = fmt.Sprintf("/%s/keys/%s", signer.accountName, 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(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 "", errwrap.Wrapf("Error signing date header: {{err}}", err)
}
keyFormat, err := keyFormatToKeyType(signature.Format)
if err != nil {
return "", errwrap.Wrapf("Error reading signature: {{err}}", err)
}
var authSignature httpAuthSignature
switch keyFormat {
case "rsa":
authSignature, err = newRSASignature(signature.Blob)
if err != nil {
return "", errwrap.Wrapf("Error reading signature: {{err}}", err)
}
case "ecdsa":
authSignature, err = newECDSASignature(signature.Blob)
if err != nil {
return "", errwrap.Wrapf("Error reading signature: {{err}}", err)
}
default:
return "", fmt.Errorf("Unsupported algorithm from SSH agent: %s", signature.Format)
}
return fmt.Sprintf(authorizationHeaderFormat, s.keyIdentifier,
authSignature.SignatureType(), headerName, authSignature.String()), nil
}
func (s *SSHAgentSigner) SignRaw(toSign string) (string, string, error) {
signature, err := s.agent.Sign(s.key, []byte(toSign))
if err != nil {
return "", "", errwrap.Wrapf("Error signing string: {{err}}", err)
}
keyFormat, err := keyFormatToKeyType(signature.Format)
if err != nil {
return "", "", errwrap.Wrapf("Error reading signature: {{err}}", err)
}
var authSignature httpAuthSignature
switch keyFormat {
case "rsa":
authSignature, err = newRSASignature(signature.Blob)
if err != nil {
return "", "", errwrap.Wrapf("Error reading signature: {{err}}", err)
}
case "ecdsa":
authSignature, err = newECDSASignature(signature.Blob)
if err != nil {
return "", "", errwrap.Wrapf("Error reading signature: {{err}}", err)
}
default:
return "", "", fmt.Errorf("Unsupported algorithm from SSH agent: %s", signature.Format)
}
return authSignature.String(), authSignature.SignatureType(), nil
}
func (s *SSHAgentSigner) KeyFingerprint() string {
return s.formattedKeyFingerprint
}
func (s *SSHAgentSigner) DefaultAlgorithm() string {
return s.algorithm
}

View File

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

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

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

@@ -0,0 +1,421 @@
package client
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"time"
"github.com/hashicorp/errwrap"
"github.com/joyent/triton-go/authentication"
)
const nilContext = "nil context"
var (
ErrDefaultAuth = errors.New("default SSH agent authentication requires SDC_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 {
HTTPClient *http.Client
Authorizers []authentication.Signer
TritonURL url.URL
MantaURL url.URL
AccountName 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, errwrap.Wrapf(BadTritonURL+": {{err}}", err)
}
storageURL, err := url.Parse(mantaURL)
if err != nil {
return nil, errwrap.Wrapf(BadMantaURL+": {{err}}", err)
}
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 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
}
// 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 {
if keyID, keyOk := os.LookupEnv("SDC_KEY_ID"); keyOk {
defaultSigner, err := authentication.NewSSHAgentSigner(keyID, c.AccountName)
if err != nil {
return errwrap.Wrapf("problem initializing NewSSHAgentSigner: {{err}}", err)
}
c.Authorizers = append(c.Authorizers, defaultSigner)
} else {
return ErrDefaultAuth
}
return nil
}
// InsecureSkipTLSVerify turns off TLS verification for the client connection. This
// allows connection to an endpoint with a certificate which was signed by a non-
// trusted CA, such as self-signed certificates. This can be useful when connecting
// to temporary Triton installations such as Triton Cloud-On-A-Laptop.
func (c *Client) InsecureSkipTLSVerify() {
if c.HTTPClient == nil {
return
}
c.HTTPClient.Transport = httpTransport(true)
}
func httpTransport(insecureSkipTLSVerify bool) *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
MaxIdleConns: 10,
IdleConnTimeout: 15 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecureSkipTLSVerify,
},
}
}
func doNotFollowRedirects(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}
// TODO(justinwr): Deprecated?
// func (c *Client) FormatURL(path string) string {
// return fmt.Sprintf("%s%s", c.Endpoint, path)
// }
func (c *Client) DecodeError(statusCode int, body io.Reader) error {
err := &TritonError{
StatusCode: statusCode,
}
errorDecoder := json.NewDecoder(body)
if err := errorDecoder.Decode(err); err != nil {
return errwrap.Wrapf("Error decoding error response: {{err}}", err)
}
return err
}
// -----------------------------------------------------------------------------
type RequestInput struct {
Method string
Path string
Query *url.Values
Headers *http.Header
Body interface{}
}
func (c *Client) ExecuteRequestURIParams(ctx context.Context, inputs RequestInput) (io.ReadCloser, error) {
method := inputs.Method
path := inputs.Path
body := inputs.Body
query := inputs.Query
var requestBody io.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, errwrap.Wrapf("Error constructing HTTP request: {{err}}", err)
}
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
// NewClient ensures there's always an authorizer (unless this is called
// outside that constructor).
authHeader, err := c.Authorizers[0].Sign(dateHeader)
if err != nil {
return nil, errwrap.Wrapf("Error signing HTTP request: {{err}}", err)
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "application/json")
req.Header.Set("Accept-Version", "8")
req.Header.Set("User-Agent", "triton-go Client API")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, errwrap.Wrapf("Error executing HTTP request: {{err}}", err)
}
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
return resp.Body, nil
}
return nil, c.DecodeError(resp.StatusCode, resp.Body)
}
func (c *Client) ExecuteRequest(ctx context.Context, inputs RequestInput) (io.ReadCloser, error) {
return c.ExecuteRequestURIParams(ctx, inputs)
}
func (c *Client) ExecuteRequestRaw(ctx context.Context, inputs RequestInput) (*http.Response, error) {
method := inputs.Method
path := inputs.Path
body := inputs.Body
var requestBody io.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, errwrap.Wrapf("Error constructing HTTP request: {{err}}", err)
}
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
// NewClient ensures there's always an authorizer (unless this is called
// outside that constructor).
authHeader, err := c.Authorizers[0].Sign(dateHeader)
if err != nil {
return nil, errwrap.Wrapf("Error signing HTTP request: {{err}}", err)
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "application/json")
req.Header.Set("Accept-Version", "8")
req.Header.Set("User-Agent", "triton-go c API")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, errwrap.Wrapf("Error executing HTTP request: {{err}}", err)
}
return resp, nil
}
func (c *Client) ExecuteRequestStorage(ctx context.Context, inputs RequestInput) (io.ReadCloser, http.Header, error) {
method := inputs.Method
path := inputs.Path
query := inputs.Query
headers := inputs.Headers
body := inputs.Body
endpoint := c.MantaURL
endpoint.Path = path
var requestBody io.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, errwrap.Wrapf("Error constructing HTTP request: {{err}}", err)
}
if body != nil && (headers == nil || headers.Get("Content-Type") == "") {
req.Header.Set("Content-Type", "application/json")
}
if headers != nil {
for key, values := range *headers {
for _, value := range values {
req.Header.Set(key, value)
}
}
}
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
authHeader, err := c.Authorizers[0].Sign(dateHeader)
if err != nil {
return nil, nil, errwrap.Wrapf("Error signing HTTP request: {{err}}", err)
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "*/*")
req.Header.Set("User-Agent", "manta-go client API")
if query != nil {
req.URL.RawQuery = query.Encode()
}
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, nil, errwrap.Wrapf("Error executing HTTP request: {{err}}", err)
}
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
return resp.Body, resp.Header, nil
}
mantaError := &MantaError{
StatusCode: resp.StatusCode,
}
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
}
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, errwrap.Wrapf("Error constructing HTTP request: {{err}}", err)
}
if headers != nil {
for key, values := range *headers {
for _, value := range values {
req.Header.Set(key, value)
}
}
}
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
authHeader, err := c.Authorizers[0].Sign(dateHeader)
if err != nil {
return nil, nil, errwrap.Wrapf("Error signing HTTP request: {{err}}", err)
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "*/*")
req.Header.Set("User-Agent", "manta-go client API")
if query != nil {
req.URL.RawQuery = query.Encode()
}
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, nil, errwrap.Wrapf("Error executing HTTP request: {{err}}", err)
}
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
return resp.Body, resp.Header, nil
}
mantaError := &MantaError{
StatusCode: resp.StatusCode,
}
errorDecoder := json.NewDecoder(resp.Body)
if err := errorDecoder.Decode(mantaError); err != nil {
return nil, nil, errwrap.Wrapf("Error decoding error response: {{err}}", err)
}
return nil, nil, mantaError
}

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

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

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

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

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

@@ -0,0 +1,199 @@
package storage
import (
"bufio"
"context"
"encoding/json"
"net/http"
"net/url"
"path"
"strconv"
"time"
"github.com/hashicorp/errwrap"
"github.com/joyent/triton-go/client"
)
type DirectoryClient struct {
client *client.Client
}
// DirectoryEntry represents an object or directory in Manta.
type DirectoryEntry struct {
ETag string `json:"etag"`
ModifiedTime time.Time `json:"mtime"`
Name string `json:"name"`
Size uint64 `json:"size"`
Type string `json:"type"`
}
// ListDirectoryInput represents parameters to a 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, errwrap.Wrapf("Error executing List request: {{err}}", err)
}
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, 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,
}
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 errwrap.Wrapf("Error executing Put request: {{err}}", err)
}
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 errwrap.Wrapf("Error executing DeleteDirectory request: {{err}}", err)
}
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
}

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

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

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

@@ -0,0 +1,388 @@
package storage
import (
"context"
"errors"
"io"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/hashicorp/errwrap"
"github.com/joyent/triton-go/client"
)
type ObjectsClient struct {
client *client.Client
}
// GetObjectInput represents parameters to a GetObject operation.
type 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
// 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, errwrap.Wrapf("Error executing Get request: {{err}}", err)
}
response := &GetObjectOutput{
ContentType: respHeaders.Get("Content-Type"),
ContentMD5: respHeaders.Get("Content-MD5"),
ETag: respHeaders.Get("Etag"),
ObjectReader: respBody,
}
lastModified, err := time.Parse(time.RFC1123, respHeaders.Get("Last-Modified"))
if err == nil {
response.LastModified = lastModified
}
contentLength, err := strconv.ParseUint(respHeaders.Get("Content-Length"), 10, 64)
if err == nil {
response.ContentLength = contentLength
}
metadata := map[string]string{}
for key, values := range respHeaders {
if strings.HasPrefix(key, "m-") {
metadata[key] = strings.Join(values, ", ")
}
}
response.Metadata = metadata
return response, nil
}
// DeleteObjectInput represents parameters to a DeleteObject operation.
type DeleteObjectInput struct {
ObjectPath string
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 errwrap.Wrapf("Error executing Delete request: {{err}}", err)
}
return nil
}
// PutObjectMetadataInput represents parameters to a PutObjectMetadata operation.
type PutObjectMetadataInput struct {
ObjectPath string
ContentType string
Metadata map[string]string
}
// PutObjectMetadata allows you to overwrite the HTTP headers for an already
// existing object, without changing the data. Note this is an idempotent "replace"
// operation, so you must specify the complete set of HTTP headers you want
// stored on each request.
//
// You cannot change "critical" headers:
// - Content-Length
// - Content-MD5
// - Durability-Level
func (s *ObjectsClient) PutMetadata(ctx context.Context, input *PutObjectMetadataInput) error {
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 errwrap.Wrapf("Error executing PutMetadata request: {{err}}", err)
}
return nil
}
// PutObjectInput represents parameters to a PutObject operation.
type PutObjectInput struct {
ObjectPath string
DurabilityLevel uint64
ContentType string
ContentMD5 string
IfMatch string
IfModifiedSince *time.Time
ContentLength uint64
MaxContentLength uint64
ObjectReader io.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 {
// IsDir() uses a path relative to the account
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 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
}

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

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

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

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

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

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