backend/manta: Update triton-go dependency to 1.1.1

This will support the features of RBAC in Manta Backends
This commit is contained in:
stack72 2018-03-13 12:10:41 +02:00
parent 9cef4674c5
commit 78525fd65a
14 changed files with 222 additions and 92 deletions

View File

@ -1,39 +1,62 @@
## Unreleased
- Add support for managing columes in Triton [#100]
## 1.1.1 (March 13 2018)
- client: Adding the rbac user support to the SSHAgentSigner [BUG!]
## 1.1.0 (March 13 2018)
- client: Add support for Manta RBAC http signatures
## 1.0.0 (February 28 2018)
- client: Add support for querystring in client/ExecuteRequestRaw [#121]
- client: Introduce SetHeader for overriding API request header [#125]
- compute/instances: Add support for passing a list of tags to filter List instances [#116]
- compute/instances: Add support for getting a count of current instances from the CloudAPI [#119]
- compute/instances: Add ability to support name-prefix [#129]
- compute/instances: Add support for Instance Deletion Protection [#131]
- identity/user: Add support for ChangeUserPassword [#111]
- expose GetTritonEnv as a root level func [#126]
## 0.9.0 (January 23 2018)
**Please Note:** This is a precursor release to marking triton-go as 1.0.0. We are going to wait and fix any bugs that occur from this large set of changes that has happened since 0.5.2
- Add support for managing volumes in Triton [#100]
- identity/policies: Add support for managing policies in Triton [#86]
- addition of triton-go errors package to expose unwraping of internal errors
- addition of triton-go errors package to expose unwrapping of internal errors
- Migration from hashicorp/errwrap to pkg/errors
- Using path.Join() for URL structures rather than fmt.Sprintf()
## 0.5.2 (December 28)
## 0.5.2 (December 28 2017)
- Standardise the API SSH Signers input casing and naming
## 0.5.1 (December 28)
## 0.5.1 (December 28 2017)
- Include leading '/' when working with SSH Agent signers
## 0.5.0 (December 28)
## 0.5.0 (December 28 2017)
- Add support for RBAC in triton-go [#82]
This is a breaking change. No longer do we pass individual parameters to the SSH Signer funcs, but we now pass an input Struct. This will guard from from additional parameter changes in the future.
We also now add support for using `SDC_*` and `TRITON_*` env vars when working with the Default agent signer
## 0.4.2 (December 22)
## 0.4.2 (December 22 2017)
- Fixing a panic when the user loses network connectivity when making a GET request to instance [#81]
## 0.4.1 (December 15)
## 0.4.1 (December 15 2017)
- Clean up the handling of directory sanitization. Use abs paths everywhere [#79]
## 0.4.0 (December 15)
## 0.4.0 (December 15 2017)
- 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)
## 0.3.0 (December 14 2017)
- Introduce CloudAPI's ListRulesMachines under networking
- Enable HTTP KeepAlives by default in the client. 15s idle timeout, 2x
@ -46,11 +69,11 @@ We also now add support for using `SDC_*` and `TRITON_*` env vars when working w
- 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)
## 0.2.1 (November 8 2017)
- Fixing a bug where CreateUser and UpdateUser didn't return the UserID
## 0.2.0 (November 7)
## 0.2.0 (November 7 2017)
- Introduce CloudAPI's Ping under compute
- Introduce CloudAPI's RebootMachine under compute instances
@ -59,6 +82,6 @@ We also now add support for using `SDC_*` and `TRITON_*` env vars when working w
- tools: Introduce unit testing and scripts for linting, etc.
- bug: Fix the `compute.ListMachineRules` endpoint
## 0.1.0 (November 2)
## 0.1.0 (November 2 2017)
- Initial release of a versioned SDK

View File

@ -1,7 +1,7 @@
TEST?=$$(go list ./... |grep -Ev 'vendor|examples|testutils')
GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor)
default: vet errcheck test
default: vet fmtcheck errcheck test
tools:: ## Download and install all dev/code tools
@echo "==> Installing dev tools"
@ -29,16 +29,16 @@ vet:: ## Check for unwanted code constructs
fi
lint:: ## Lint and vet code by common Go standards
@bash $(CURDIR)/scripts/lint.sh
@./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
@./scripts/gofmtcheck.sh
errcheck:: ## Check for unhandled errors
@bash $(CURDIR)/scripts/errcheck.sh
@./scripts/errcheck.sh
.PHONY: help
help:: ## Display this help message

View File

@ -5,6 +5,15 @@ 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)
The Triton Go SDK is used in the following open source projects.
- [Packer](http://github.com/hashicorp/packer)
- [Vault](http://github.com/hashicorp/vault)
- [Terraform](http://github.com/hashicorp/terraform)
- [Terraform Triton Provider](https://github.com/terraform-providers/terraform-provider-triton)
- [Docker Machine](https://github.com/joyent/docker-machine-driver-triton)
- [Triton Kubernetes](https://github.com/joyent/triton-kubernetes)
## Usage
Triton uses [HTTP Signature][4] to sign the Date header in each HTTP request
@ -38,7 +47,7 @@ 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
their own separate client. In order to initialize a package client, simply pass
the global `triton.ClientConfig` struct into the client's constructor function.
```go
@ -73,8 +82,8 @@ if err != nil {
## 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.
will contain an instance of `errors.APIError` in the chain. Error wrapping
is performed using the [pkg/errors][7] library.
## Acceptance Tests
@ -235,4 +244,4 @@ func main() {
[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
[7]: https://github.com/pkg/errors

View File

@ -0,0 +1,25 @@
package authentication
import "path"
type KeyID struct {
UserName string
AccountName string
Fingerprint string
IsManta bool
}
func (input *KeyID) generate() string {
var keyID string
if input.UserName != "" {
if input.IsManta {
keyID = path.Join("/", input.AccountName, input.UserName, "keys", input.Fingerprint)
} else {
keyID = path.Join("/", input.AccountName, "users", input.UserName, "keys", input.Fingerprint)
}
} else {
keyID = path.Join("/", input.AccountName, "keys", input.Fingerprint)
}
return keyID
}

View File

@ -16,7 +16,6 @@ import (
"encoding/base64"
"encoding/pem"
"fmt"
"path"
"strings"
"github.com/pkg/errors"
@ -87,7 +86,7 @@ func NewPrivateKeySigner(input PrivateKeySignerInput) (*PrivateKeySigner, error)
return signer, nil
}
func (s *PrivateKeySigner) Sign(dateHeader string) (string, error) {
func (s *PrivateKeySigner) Sign(dateHeader string, isManta bool) (string, error) {
const headerName = "date"
hash := s.hashFunc.New()
@ -100,14 +99,14 @@ func (s *PrivateKeySigner) Sign(dateHeader string) (string, error) {
}
signedBase64 := base64.StdEncoding.EncodeToString(signed)
var keyID string
if s.userName != "" {
keyID = path.Join("/", s.accountName, "users", s.userName, "keys", s.formattedKeyFingerprint)
} else {
keyID = path.Join("/", s.accountName, "keys", s.formattedKeyFingerprint)
key := &KeyID{
UserName: s.userName,
AccountName: s.accountName,
Fingerprint: s.formattedKeyFingerprint,
IsManta: isManta,
}
return fmt.Sprintf(authorizationHeaderFormat, keyID, "rsa-sha1", headerName, signedBase64), nil
return fmt.Sprintf(authorizationHeaderFormat, key.generate(), "rsa-sha1", headerName, signedBase64), nil
}
func (s *PrivateKeySigner) SignRaw(toSign string) (string, string, error) {

View File

@ -13,6 +13,6 @@ const authorizationHeaderFormat = `Signature keyId="%s",algorithm="%s",headers="
type Signer interface {
DefaultAlgorithm() string
KeyFingerprint() string
Sign(dateHeader string) (string, error)
Sign(dateHeader string, isManta bool) (string, error)
SignRaw(toSign string) (string, string, error)
}

View File

@ -15,7 +15,6 @@ import (
"fmt"
"net"
"os"
"path"
"strings"
pkgerrors "github.com/pkg/errors"
@ -64,18 +63,16 @@ func NewSSHAgentSigner(input SSHAgentSignerInput) (*SSHAgentSigner, error) {
agent: ag,
}
if input.Username != "" {
signer.userName = input.Username
}
matchingKey, err := signer.MatchKey()
if err != nil {
return nil, err
}
signer.key = matchingKey
signer.formattedKeyFingerprint = formatPublicKeyFingerprint(signer.key, true)
if input.Username != "" {
signer.userName = input.Username
signer.keyIdentifier = path.Join("/", signer.accountName, "users", input.Username, "keys", signer.formattedKeyFingerprint)
} else {
signer.keyIdentifier = path.Join("/", signer.accountName, "keys", signer.formattedKeyFingerprint)
}
_, algorithm, err := signer.SignRaw("HelloWorld")
if err != nil {
@ -118,7 +115,7 @@ func (s *SSHAgentSigner) MatchKey() (ssh.PublicKey, error) {
return matchingKey, nil
}
func (s *SSHAgentSigner) Sign(dateHeader string) (string, error) {
func (s *SSHAgentSigner) Sign(dateHeader string, isManta bool) (string, error) {
const headerName = "date"
signature, err := s.agent.Sign(s.key, []byte(fmt.Sprintf("%s: %s", headerName, dateHeader)))
@ -131,6 +128,13 @@ func (s *SSHAgentSigner) Sign(dateHeader string) (string, error) {
return "", pkgerrors.Wrap(err, "unable to format signature")
}
key := &KeyID{
UserName: s.userName,
AccountName: s.accountName,
Fingerprint: s.formattedKeyFingerprint,
IsManta: isManta,
}
var authSignature httpAuthSignature
switch keyFormat {
case "rsa":
@ -147,7 +151,7 @@ func (s *SSHAgentSigner) Sign(dateHeader string) (string, error) {
return "", fmt.Errorf("Unsupported algorithm from SSH agent: %s", signature.Format)
}
return fmt.Sprintf(authorizationHeaderFormat, s.keyIdentifier,
return fmt.Sprintf(authorizationHeaderFormat, key.generate(),
authSignature.SignatureType(), headerName, authSignature.String()), nil
}

View File

@ -26,7 +26,7 @@ func (s *TestSigner) KeyFingerprint() string {
return ""
}
func (s *TestSigner) Sign(dateHeader string) (string, error) {
func (s *TestSigner) Sign(dateHeader string, isManta bool) (string, error) {
return "", nil
}

View File

@ -18,7 +18,6 @@ import (
"net"
"net/http"
"net/url"
"os"
"time"
"github.com/joyent/triton-go"
@ -40,12 +39,13 @@ var (
// Client represents a connection to the Triton Compute or Object Storage APIs.
type Client struct {
HTTPClient *http.Client
Authorizers []authentication.Signer
TritonURL url.URL
MantaURL url.URL
AccountName string
Username string
HTTPClient *http.Client
RequestHeader *http.Header
Authorizers []authentication.Signer
TritonURL url.URL
MantaURL url.URL
AccountName string
Username string
}
// New is used to construct a Client in order to make API
@ -102,29 +102,12 @@ func New(tritonURL string, mantaURL string, accountName string, signers ...authe
return newClient, nil
}
var envPrefixes = []string{"TRITON", "SDC"}
// GetTritonEnv looks up environment variables using the preferred "TRITON"
// prefix, but falls back to the SDC prefix. For example, looking up "USER"
// will search for "TRITON_USER" followed by "SDC_USER". If the environment
// variable is not set, an empty string is returned. GetTritonEnv() is used to
// aid in the transition and deprecation of the SDC_* environment variables.
func GetTritonEnv(name string) string {
for _, prefix := range envPrefixes {
if val, found := os.LookupEnv(prefix + "_" + name); found {
return val
}
}
return ""
}
// initDefaultAuth provides a default key signer for a client. This should only
// be used internally if the client has no other key signer for authenticating
// with Triton. We first look for both `SDC_KEY_ID` and `SSH_AUTH_SOCK` in the
// user's environ(7). If so we default to the SSH agent key signer.
func (c *Client) DefaultAuth() error {
tritonKeyId := GetTritonEnv("KEY_ID")
tritonKeyId := triton.GetEnv("KEY_ID")
if tritonKeyId != "" {
input := authentication.SSHAgentSignerInput{
KeyID: tritonKeyId,
@ -153,6 +136,8 @@ func (c *Client) InsecureSkipTLSVerify() {
c.HTTPClient.Transport = httpTransport(true)
}
// httpTransport is responsible for setting up our HTTP client's transport
// settings
func httpTransport(insecureSkipTLSVerify bool) *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
@ -173,6 +158,7 @@ func doNotFollowRedirects(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}
// DecodeError decodes a backend Triton error into a more usable Go error type
func (c *Client) DecodeError(resp *http.Response, requestMethod string) error {
err := &errors.APIError{
StatusCode: resp.StatusCode,
@ -192,6 +178,23 @@ func (c *Client) DecodeError(resp *http.Response, requestMethod string) error {
return err
}
// overrideHeader overrides the header of the passed in HTTP request
func (c *Client) overrideHeader(req *http.Request) {
if c.RequestHeader != nil {
for k, vs := range *c.RequestHeader {
for _, v := range vs {
req.Header.Add(k, v)
}
}
}
}
// resetHeader will reset the struct field that stores custom header
// information
func (c *Client) resetHeader() {
c.RequestHeader = nil
}
// -----------------------------------------------------------------------------
type RequestInput struct {
@ -203,6 +206,8 @@ type RequestInput struct {
}
func (c *Client) ExecuteRequestURIParams(ctx context.Context, inputs RequestInput) (io.ReadCloser, error) {
defer c.resetHeader()
method := inputs.Method
path := inputs.Path
body := inputs.Body
@ -233,7 +238,7 @@ func (c *Client) ExecuteRequestURIParams(ctx context.Context, inputs RequestInpu
// NewClient ensures there's always an authorizer (unless this is called
// outside that constructor).
authHeader, err := c.Authorizers[0].Sign(dateHeader)
authHeader, err := c.Authorizers[0].Sign(dateHeader, false)
if err != nil {
return nil, pkgerrors.Wrapf(err, "unable to sign HTTP request")
}
@ -246,6 +251,8 @@ func (c *Client) ExecuteRequestURIParams(ctx context.Context, inputs RequestInpu
req.Header.Set("Content-Type", "application/json")
}
c.overrideHeader(req)
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, pkgerrors.Wrapf(err, "unable to execute HTTP request")
@ -265,9 +272,12 @@ func (c *Client) ExecuteRequest(ctx context.Context, inputs RequestInput) (io.Re
}
func (c *Client) ExecuteRequestRaw(ctx context.Context, inputs RequestInput) (*http.Response, error) {
defer c.resetHeader()
method := inputs.Method
path := inputs.Path
body := inputs.Body
query := inputs.Query
var requestBody io.Reader
if body != nil {
@ -280,6 +290,9 @@ func (c *Client) ExecuteRequestRaw(ctx context.Context, inputs RequestInput) (*h
endpoint := c.TritonURL
endpoint.Path = path
if query != nil {
endpoint.RawQuery = query.Encode()
}
req, err := http.NewRequest(method, endpoint.String(), requestBody)
if err != nil {
@ -291,7 +304,7 @@ func (c *Client) ExecuteRequestRaw(ctx context.Context, inputs RequestInput) (*h
// NewClient ensures there's always an authorizer (unless this is called
// outside that constructor).
authHeader, err := c.Authorizers[0].Sign(dateHeader)
authHeader, err := c.Authorizers[0].Sign(dateHeader, false)
if err != nil {
return nil, pkgerrors.Wrapf(err, "unable to sign HTTP request")
}
@ -304,6 +317,8 @@ func (c *Client) ExecuteRequestRaw(ctx context.Context, inputs RequestInput) (*h
req.Header.Set("Content-Type", "application/json")
}
c.overrideHeader(req)
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, pkgerrors.Wrapf(err, "unable to execute HTTP request")
@ -319,6 +334,8 @@ func (c *Client) ExecuteRequestRaw(ctx context.Context, inputs RequestInput) (*h
}
func (c *Client) ExecuteRequestStorage(ctx context.Context, inputs RequestInput) (io.ReadCloser, http.Header, error) {
defer c.resetHeader()
method := inputs.Method
path := inputs.Path
query := inputs.Query
@ -356,7 +373,7 @@ func (c *Client) ExecuteRequestStorage(ctx context.Context, inputs RequestInput)
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
authHeader, err := c.Authorizers[0].Sign(dateHeader)
authHeader, err := c.Authorizers[0].Sign(dateHeader, true)
if err != nil {
return nil, nil, pkgerrors.Wrapf(err, "unable to sign HTTP request")
}
@ -368,6 +385,8 @@ func (c *Client) ExecuteRequestStorage(ctx context.Context, inputs RequestInput)
req.URL.RawQuery = query.Encode()
}
c.overrideHeader(req)
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, nil, pkgerrors.Wrapf(err, "unable to execute HTTP request")
@ -391,6 +410,8 @@ type RequestNoEncodeInput struct {
}
func (c *Client) ExecuteRequestNoEncode(ctx context.Context, inputs RequestNoEncodeInput) (io.ReadCloser, http.Header, error) {
defer c.resetHeader()
method := inputs.Method
path := inputs.Path
query := inputs.Query
@ -416,7 +437,7 @@ func (c *Client) ExecuteRequestNoEncode(ctx context.Context, inputs RequestNoEnc
dateHeader := time.Now().UTC().Format(time.RFC1123)
req.Header.Set("date", dateHeader)
authHeader, err := c.Authorizers[0].Sign(dateHeader)
authHeader, err := c.Authorizers[0].Sign(dateHeader, true)
if err != nil {
return nil, nil, pkgerrors.Wrapf(err, "unable to sign HTTP request")
}
@ -429,6 +450,8 @@ func (c *Client) ExecuteRequestNoEncode(ctx context.Context, inputs RequestNoEnc
req.URL.RawQuery = query.Encode()
}
c.overrideHeader(req)
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, nil, pkgerrors.Wrapf(err, "unable to execute HTTP request")

View File

@ -9,6 +9,8 @@
package storage
import (
"net/http"
triton "github.com/joyent/triton-go"
"github.com/joyent/triton-go/client"
)
@ -34,6 +36,12 @@ func NewClient(config *triton.ClientConfig) (*StorageClient, error) {
return newStorageClient(client), nil
}
// SetHeader allows a consumer of the current client to set a custom header for
// the next backend HTTP request sent to CloudAPI.
func (c *StorageClient) SetHeader(header *http.Header) {
c.Client.RequestHeader = header
}
// Dir returns a DirectoryClient used for accessing functions pertaining to
// Directories functionality of the Manta API.
func (c *StorageClient) Dir() *DirectoryClient {

View File

@ -102,7 +102,7 @@ type PutDirectoryInput struct {
DirectoryName string
}
// Put puts a directoy into the Triton Object Storage service is an idempotent
// Put puts a director 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 {

View File

@ -9,6 +9,8 @@
package triton
import (
"os"
"github.com/joyent/triton-go/authentication"
)
@ -25,3 +27,20 @@ type ClientConfig struct {
Username string
Signers []authentication.Signer
}
var envPrefixes = []string{"TRITON", "SDC"}
// GetEnv looks up environment variables using the preferred "TRITON" prefix,
// but falls back to the retired "SDC" prefix. For example, looking up "USER"
// will search for "TRITON_USER" followed by "SDC_USER". If the environment
// variable is not set, an empty string is returned. GetEnv() is used to aid in
// the transition and deprecation of the "SDC_*" environment variables.
func GetEnv(name string) string {
for _, prefix := range envPrefixes {
if val, found := os.LookupEnv(prefix + "_" + name); found {
return val
}
}
return ""
}

View File

@ -13,20 +13,30 @@ import (
"runtime"
)
// The main version number of the current released Triton-go SDK.
const Version = "0.9.0"
// Version represents main version number of the current release
// of the Triton-go SDK.
const Version = "1.1.1"
// A pre-release marker for the version. If this is "" (empty string)
// then it means that it is a final release. Otherwise, this is a pre-release
// such as "dev" (in development), "beta", "rc1", etc.
// Prerelease adds a pre-release marker to the version.
//
// If this is "" (empty string) then it means that it is a final release.
// Otherwise, this is a pre-release such as "dev" (in development), "beta",
// "rc1", etc.
var Prerelease = ""
// UserAgent returns a Triton-go characteristic string that allows the
// network protocol peers to identify the version, release and runtime
// of the Triton-go client from which the requests originate.
func UserAgent() string {
if Prerelease != "" {
return fmt.Sprintf("triton-go/%s-%s (%s-%s; %s)", Version, Prerelease, runtime.GOARCH, runtime.GOOS, runtime.Version())
} else {
return fmt.Sprintf("triton-go/%s (%s-%s; %s)", Version, runtime.GOARCH, runtime.GOOS, runtime.Version())
return fmt.Sprintf("triton-go/%s-%s (%s-%s; %s)", Version, Prerelease,
runtime.GOARCH, runtime.GOOS, runtime.Version())
}
return fmt.Sprintf("triton-go/%s (%s-%s; %s)", Version, runtime.GOARCH,
runtime.GOOS, runtime.Version())
}
// CloudAPIMajorVersion specifies the CloudAPI version compatibility
// for current release of the Triton-go SDK.
const CloudAPIMajorVersion = "8"

38
vendor/vendor.json vendored
View File

@ -1860,34 +1860,44 @@
"revisionTime": "2016-08-03T19:07:31Z"
},
{
"checksumSHA1": "Lg8OHK87XRGCaipG+5+zFyN8OMw=",
"checksumSHA1": "hnUvydIu2VnSjYP/ROo1SLNlLv4=",
"path": "github.com/joyent/triton-go",
"revision": "545edbe0d564f075ac576f1ad177f4ff39c9adaf",
"revisionTime": "2018-01-16T16:57:42Z"
"revision": "d8f9c031492688b4b62b7fd29b1edd9897400c4e",
"revisionTime": "2018-03-13T10:08:02Z",
"version": "1.1.1",
"versionExact": "1.1.1"
},
{
"checksumSHA1": "Y03+L+I0FVZ2bMGWt1MHTDEyWM4=",
"checksumSHA1": "yNrArK8kjkVkU0bunKlemd6dFkE=",
"path": "github.com/joyent/triton-go/authentication",
"revision": "545edbe0d564f075ac576f1ad177f4ff39c9adaf",
"revisionTime": "2018-01-16T16:57:42Z"
"revision": "d8f9c031492688b4b62b7fd29b1edd9897400c4e",
"revisionTime": "2018-03-13T10:08:02Z",
"version": "1.1.1",
"versionExact": "1.1.1"
},
{
"checksumSHA1": "MuJsGBr6HlXQYxZY9cM5rBk+Lns=",
"checksumSHA1": "WBCMeFCM9qif0JwcGQL72SG7yJM=",
"path": "github.com/joyent/triton-go/client",
"revision": "545edbe0d564f075ac576f1ad177f4ff39c9adaf",
"revisionTime": "2018-01-16T16:57:42Z"
"revision": "d8f9c031492688b4b62b7fd29b1edd9897400c4e",
"revisionTime": "2018-03-13T10:08:02Z",
"version": "1.1.1",
"versionExact": "1.1.1"
},
{
"checksumSHA1": "d/Py6j/uMgOAFNFGpsQrNnSsO+k=",
"path": "github.com/joyent/triton-go/errors",
"revision": "545edbe0d564f075ac576f1ad177f4ff39c9adaf",
"revisionTime": "2018-01-16T16:57:42Z"
"revision": "d8f9c031492688b4b62b7fd29b1edd9897400c4e",
"revisionTime": "2018-03-13T10:08:02Z",
"version": "1.1.1",
"versionExact": "1.1.1"
},
{
"checksumSHA1": "5v533ELM047YOiwHsyMaVzITpR0=",
"checksumSHA1": "MAjG3M0OLUJ3hdNZLHHpXdl1THQ=",
"path": "github.com/joyent/triton-go/storage",
"revision": "545edbe0d564f075ac576f1ad177f4ff39c9adaf",
"revisionTime": "2018-01-16T16:57:42Z"
"revision": "d8f9c031492688b4b62b7fd29b1edd9897400c4e",
"revisionTime": "2018-03-13T10:08:02Z",
"version": "1.1.1",
"versionExact": "1.1.1"
},
{
"checksumSHA1": "g+afVQQVopBLiLB5pFZp/8s6aBs=",