gateway/manta: Add support for RBAC (#5332)

Manta has the ability to allow users to authenticate with a 
username other than the main account. We want to expose 
this functionality to minio manta gateway.
master
Paul Stack 7 years ago committed by Nitish Tiwari
parent b85c75996d
commit a1a98617ca
  1. 22
      cmd/gateway/manta/gateway-manta.go
  2. 6
      docs/gateway/manta.md
  3. 18
      vendor/github.com/joyent/triton-go/CHANGELOG.md
  4. 87
      vendor/github.com/joyent/triton-go/README.md
  5. 31
      vendor/github.com/joyent/triton-go/authentication/private_key_signer.go
  6. 21
      vendor/github.com/joyent/triton-go/authentication/ssh_agent_signer.go
  7. 37
      vendor/github.com/joyent/triton-go/client/client.go
  8. 1
      vendor/github.com/joyent/triton-go/storage/objects.go
  9. 1
      vendor/github.com/joyent/triton-go/triton.go
  10. 24
      vendor/vendor.json

@ -32,7 +32,6 @@ import (
"github.com/joyent/triton-go/authentication" "github.com/joyent/triton-go/authentication"
tclient "github.com/joyent/triton-go/client" tclient "github.com/joyent/triton-go/client"
"github.com/joyent/triton-go/storage" "github.com/joyent/triton-go/storage"
"github.com/minio/cli" "github.com/minio/cli"
minio "github.com/minio/minio/cmd" minio "github.com/minio/minio/cmd"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
@ -68,6 +67,7 @@ ENVIRONMENT VARIABLES:
MINIO_ACCESS_KEY: The Manta account name. MINIO_ACCESS_KEY: The Manta account name.
MINIO_SECRET_KEY: A KeyID associated with the Manta account. MINIO_SECRET_KEY: A KeyID associated with the Manta account.
MANTA_KEY_MATERIAL: The path to the SSH Key associated with the Manta account if the MINIO_SECRET_KEY is not in SSH Agent. MANTA_KEY_MATERIAL: The path to the SSH Key associated with the Manta account if the MINIO_SECRET_KEY is not in SSH Agent.
MANTA_SUBUSER: The username of a user who has limited access to your account.
BROWSER: BROWSER:
MINIO_BROWSER: To disable web browser access, set this value to "off". MINIO_BROWSER: To disable web browser access, set this value to "off".
@ -140,7 +140,14 @@ func (g *Manta) NewGatewayLayer(creds auth.Credentials) (minio.GatewayLayer, err
keyMaterial := os.Getenv("MANTA_KEY_MATERIAL") keyMaterial := os.Getenv("MANTA_KEY_MATERIAL")
if keyMaterial == "" { if keyMaterial == "" {
signer, err = authentication.NewSSHAgentSigner(creds.SecretKey, creds.AccessKey) input := authentication.SSHAgentSignerInput{
KeyID: creds.SecretKey,
AccountName: creds.AccessKey,
}
if userName, ok := os.LookupEnv("MANTA_SUBUSER"); ok {
input.Username = userName
}
signer, err = authentication.NewSSHAgentSigner(input)
if err != nil { if err != nil {
return nil, errors.Trace(err) return nil, errors.Trace(err)
} }
@ -168,7 +175,16 @@ func (g *Manta) NewGatewayLayer(creds auth.Credentials) (minio.GatewayLayer, err
keyBytes = []byte(keyMaterial) keyBytes = []byte(keyMaterial)
} }
signer, err = authentication.NewPrivateKeySigner(creds.SecretKey, keyBytes, creds.AccessKey) input := authentication.PrivateKeySignerInput{
KeyID: creds.SecretKey,
PrivateKeyMaterial: keyBytes,
AccountName: creds.AccessKey,
}
if userName, ok := os.LookupEnv("MANTA_SUBUSER"); ok {
input.Username = userName
}
signer, err = authentication.NewPrivateKeySigner(input)
if err != nil { if err != nil {
return nil, errors.Trace(err) return nil, errors.Trace(err)
} }

@ -7,7 +7,8 @@ Minio Gateway adds Amazon S3 compatibility to Manta Object Storage.
docker run -p 9000:9000 --name manta-s3 \ docker run -p 9000:9000 --name manta-s3 \
-e "MINIO_ACCESS_KEY=joyentaccountname" \ -e "MINIO_ACCESS_KEY=joyentaccountname" \
-e "MINIO_SECRET_KEY=joyentkeyid" \ -e "MINIO_SECRET_KEY=joyentkeyid" \
-e "MINIO_KEY_MATERIAL=~/.ssh/id_rsa -e "MANTA_KEY_MATERIAL=~/.ssh/id_rsa" \
-e "MANTA_SUBUSER=devuser"
minio/minio gateway manta minio/minio gateway manta
``` ```
@ -15,7 +16,8 @@ docker run -p 9000:9000 --name manta-s3 \
``` ```
export MINIO_ACCESS_KEY=joyentaccountname export MINIO_ACCESS_KEY=joyentaccountname
export MINIO_SECRET_KEY=joyentkeyid export MINIO_SECRET_KEY=joyentkeyid
export MINIO_KEY_MATERIAL=~/.ssh/id_rsa export MANTA_KEY_MATERIAL=~/.ssh/id_rsa
export MANTA_SUBUSER=devuser
minio gateway manta minio gateway manta
``` ```
## Test using Minio Browser ## Test using Minio Browser

@ -1,5 +1,23 @@
## Unreleased ## Unreleased
## 0.5.2 (December 28)
- Standardise the API SSH Signers input casing and naming
## 0.5.1 (December 28)
- Include leading '/' when working with SSH Agent signers
## 0.5.0 (December 28)
- Add support for RBAC in triton-go [#82]
This is a breaking change. No longer do we pass individual parameters to the SSH Signer funcs, but we now pass an input Struct. This will guard from from additional parameter changes in the future.
We also now add support for using `SDC_*` and `TRITON_*` env vars when working with the Default agent signer
## 0.4.2 (December 22)
- Fixing a panic when the user loses network connectivity when making a GET request to instance [#81]
## 0.4.1 (December 15) ## 0.4.1 (December 15)
- Clean up the handling of directory sanitization. Use abs paths everywhere [#79] - Clean up the handling of directory sanitization. Use abs paths everywhere [#79]

@ -15,11 +15,17 @@ using a key stored with the local SSH Agent (using an [`SSHAgentSigner`][6].
To construct a Signer, use the `New*` range of methods in the `authentication` To construct a Signer, use the `New*` range of methods in the `authentication`
package. In the case of `authentication.NewSSHAgentSigner`, the parameters are package. In the case of `authentication.NewSSHAgentSigner`, the parameters are
the fingerprint of the key with which to sign, and the account name (normally the fingerprint of the key with which to sign, and the account name (normally
stored in the `SDC_ACCOUNT` environment variable). For example: stored in the `TRITON_ACCOUNT` environment variable). There is also support for
passing in a username, this will allow you to use an account other than the main
Triton account. For example:
``` ```go
const fingerprint := "a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11" input := authentication.SSHAgentSignerInput{
sshKeySigner, err := authentication.NewSSHAgentSigner(fingerprint, "AccountName") KeyID: "a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11",
AccountName: "AccountName",
Username: "Username",
}
sshKeySigner, err := authentication.NewSSHAgentSigner(input)
if err != nil { if err != nil {
log.Fatalf("NewSSHAgentSigner: %s", err) log.Fatalf("NewSSHAgentSigner: %s", err)
} }
@ -36,17 +42,18 @@ their own seperate client. In order to initialize a package client, simply pass
the global `triton.ClientConfig` struct into the client's constructor function. the global `triton.ClientConfig` struct into the client's constructor function.
```go ```go
config := &triton.ClientConfig{ config := &triton.ClientConfig{
TritonURL: os.Getenv("SDC_URL"), TritonURL: os.Getenv("TRITON_URL"),
MantaURL: os.Getenv("MANTA_URL"), MantaURL: os.Getenv("MANTA_URL"),
AccountName: accountName, AccountName: accountName,
Signers: []authentication.Signer{sshKeySigner}, Username: os.Getenv("TRITON_USER"),
} Signers: []authentication.Signer{sshKeySigner},
}
c, err := compute.NewClient(config) c, err := compute.NewClient(config)
if err != nil { if err != nil {
log.Fatalf("compute.NewClient: %s", err) log.Fatalf("compute.NewClient: %s", err)
} }
``` ```
Constructing `compute.Client` returns an interface which exposes `compute` API Constructing `compute.Client` returns an interface which exposes `compute` API
@ -57,10 +64,10 @@ The same `triton.ClientConfig` will initialize the Manta `storage` client as
well... well...
```go ```go
c, err := storage.NewClient(config) c, err := storage.NewClient(config)
if err != nil { if err != nil {
log.Fatalf("storage.NewClient: %s", err) log.Fatalf("storage.NewClient: %s", err)
} }
``` ```
## Error Handling ## Error Handling
@ -81,13 +88,14 @@ set:
- `TRITON_TEST` - must be set to any value in order to indicate desire to create - `TRITON_TEST` - must be set to any value in order to indicate desire to create
resources resources
- `SDC_URL` - the base endpoint for the Triton API - `TRITON_URL` - the base endpoint for the Triton API
- `SDC_ACCOUNT` - the account name for the Triton API - `TRITON_ACCOUNT` - the account name for the Triton API
- `SDC_KEY_ID` - the fingerprint of the SSH key identifying the key - `TRITON_KEY_ID` - the fingerprint of the SSH key identifying the key
Additionally, you may set `SDC_KEY_MATERIAL` to the contents of an unencrypted Additionally, you may set `TRITON_KEY_MATERIAL` to the contents of an unencrypted
private key. If this is set, the PrivateKeySigner (see above) will be used - if private key. If this is set, the PrivateKeySigner (see above) will be used - if
not the SSHAgentSigner will be used. not the SSHAgentSigner will be used. You can also set `TRITON_USER` to run the tests
against an account other than the main Triton account
### Example Run ### Example Run
@ -96,9 +104,9 @@ The verbose output has been removed for brevity here.
``` ```
$ HTTP_PROXY=http://localhost:8888 \ $ HTTP_PROXY=http://localhost:8888 \
TRITON_TEST=1 \ TRITON_TEST=1 \
SDC_URL=https://us-sw-1.api.joyent.com \ TRITON_URL=https://us-sw-1.api.joyent.com \
SDC_ACCOUNT=AccountName \ TRITON_ACCOUNT=AccountName \
SDC_KEY_ID=a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11 \ TRITON_KEY_ID=a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11 \
go test -v -run "TestAccKey" go test -v -run "TestAccKey"
=== RUN TestAccKey_Create === RUN TestAccKey_Create
--- PASS: TestAccKey_Create (12.46s) --- PASS: TestAccKey_Create (12.46s)
@ -118,7 +126,7 @@ referencing your SSH key file use by your active `triton` CLI profile.
```sh ```sh
$ eval "$(triton env us-sw-1)" $ eval "$(triton env us-sw-1)"
$ SDC_KEY_FILE=~/.ssh/triton-id_rsa go run examples/compute/instances.go $ TRITON_KEY_FILE=~/.ssh/triton-id_rsa go run examples/compute/instances.go
``` ```
The following is a complete example of how to initialize the `compute` package The following is a complete example of how to initialize the `compute` package
@ -144,15 +152,21 @@ import (
) )
func main() { func main() {
keyID := os.Getenv("SDC_KEY_ID") keyID := os.Getenv("TRITON_KEY_ID")
accountName := os.Getenv("SDC_ACCOUNT") accountName := os.Getenv("TRITON_ACCOUNT")
keyMaterial := os.Getenv("SDC_KEY_MATERIAL") keyMaterial := os.Getenv("TRITON_KEY_MATERIAL")
userName := os.Getenv("TRITON_USER")
var signer authentication.Signer var signer authentication.Signer
var err error var err error
if keyMaterial == "" { if keyMaterial == "" {
signer, err = authentication.NewSSHAgentSigner(keyID, accountName) input := authentication.SSHAgentSignerInput{
KeyID: keyID,
AccountName: accountName,
Username: userName,
}
signer, err = authentication.NewSSHAgentSigner(input)
if err != nil { if err != nil {
log.Fatalf("Error Creating SSH Agent Signer: {{err}}", err) log.Fatalf("Error Creating SSH Agent Signer: {{err}}", err)
} }
@ -180,15 +194,22 @@ func main() {
keyBytes = []byte(keyMaterial) keyBytes = []byte(keyMaterial)
} }
signer, err = authentication.NewPrivateKeySigner(keyID, []byte(keyMaterial), accountName) input := authentication.PrivateKeySignerInput{
KeyID: keyID,
PrivateKeyMaterial: keyBytes,
AccountName: accountName,
Username: userName,
}
signer, err = authentication.NewPrivateKeySigner(input)
if err != nil { if err != nil {
log.Fatalf("Error Creating SSH Private Key Signer: {{err}}", err) log.Fatalf("Error Creating SSH Private Key Signer: {{err}}", err)
} }
} }
config := &triton.ClientConfig{ config := &triton.ClientConfig{
TritonURL: os.Getenv("SDC_URL"), TritonURL: os.Getenv("TRITON_URL"),
AccountName: accountName, AccountName: accountName,
Username: userName,
Signers: []authentication.Signer{signer}, Signers: []authentication.Signer{signer},
} }

@ -9,6 +9,7 @@ import (
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"path"
"strings" "strings"
"github.com/hashicorp/errwrap" "github.com/hashicorp/errwrap"
@ -20,15 +21,23 @@ type PrivateKeySigner struct {
keyFingerprint string keyFingerprint string
algorithm string algorithm string
accountName string accountName string
userName string
hashFunc crypto.Hash hashFunc crypto.Hash
privateKey *rsa.PrivateKey privateKey *rsa.PrivateKey
} }
func NewPrivateKeySigner(keyFingerprint string, privateKeyMaterial []byte, accountName string) (*PrivateKeySigner, error) { type PrivateKeySignerInput struct {
keyFingerprintMD5 := strings.Replace(keyFingerprint, ":", "", -1) KeyID string
PrivateKeyMaterial []byte
AccountName string
Username string
}
func NewPrivateKeySigner(input PrivateKeySignerInput) (*PrivateKeySigner, error) {
keyFingerprintMD5 := strings.Replace(input.KeyID, ":", "", -1)
block, _ := pem.Decode(privateKeyMaterial) block, _ := pem.Decode(input.PrivateKeyMaterial)
if block == nil { if block == nil {
return nil, errors.New("Error PEM-decoding private key material: nil block received") return nil, errors.New("Error PEM-decoding private key material: nil block received")
} }
@ -51,13 +60,17 @@ func NewPrivateKeySigner(keyFingerprint string, privateKeyMaterial []byte, accou
signer := &PrivateKeySigner{ signer := &PrivateKeySigner{
formattedKeyFingerprint: displayKeyFingerprint, formattedKeyFingerprint: displayKeyFingerprint,
keyFingerprint: keyFingerprint, keyFingerprint: input.KeyID,
accountName: accountName, accountName: input.AccountName,
hashFunc: crypto.SHA1, hashFunc: crypto.SHA1,
privateKey: rsakey, privateKey: rsakey,
} }
if input.Username != "" {
signer.userName = input.Username
}
_, algorithm, err := signer.SignRaw("HelloWorld") _, algorithm, err := signer.SignRaw("HelloWorld")
if err != nil { if err != nil {
return nil, fmt.Errorf("Cannot sign using ssh agent: %s", err) return nil, fmt.Errorf("Cannot sign using ssh agent: %s", err)
@ -80,7 +93,13 @@ func (s *PrivateKeySigner) Sign(dateHeader string) (string, error) {
} }
signedBase64 := base64.StdEncoding.EncodeToString(signed) signedBase64 := base64.StdEncoding.EncodeToString(signed)
keyID := fmt.Sprintf("/%s/keys/%s", s.accountName, s.formattedKeyFingerprint) var keyID string
if s.userName != "" {
keyID = path.Join("/", s.accountName, "users", s.userName, "keys", s.formattedKeyFingerprint)
} else {
keyID = path.Join("/", s.accountName, "keys", s.formattedKeyFingerprint)
}
return fmt.Sprintf(authorizationHeaderFormat, keyID, "rsa-sha1", headerName, signedBase64), nil return fmt.Sprintf(authorizationHeaderFormat, keyID, "rsa-sha1", headerName, signedBase64), nil
} }

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"net" "net"
"os" "os"
"path"
"strings" "strings"
"github.com/hashicorp/errwrap" "github.com/hashicorp/errwrap"
@ -24,13 +25,20 @@ type SSHAgentSigner struct {
keyFingerprint string keyFingerprint string
algorithm string algorithm string
accountName string accountName string
userName string
keyIdentifier string keyIdentifier string
agent agent.Agent agent agent.Agent
key ssh.PublicKey key ssh.PublicKey
} }
func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, error) { type SSHAgentSignerInput struct {
KeyID string
AccountName string
Username string
}
func NewSSHAgentSigner(input SSHAgentSignerInput) (*SSHAgentSigner, error) {
sshAgentAddress, agentOk := os.LookupEnv("SSH_AUTH_SOCK") sshAgentAddress, agentOk := os.LookupEnv("SSH_AUTH_SOCK")
if !agentOk { if !agentOk {
return nil, ErrUnsetEnvVar return nil, ErrUnsetEnvVar
@ -44,8 +52,8 @@ func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, err
ag := agent.NewClient(conn) ag := agent.NewClient(conn)
signer := &SSHAgentSigner{ signer := &SSHAgentSigner{
keyFingerprint: keyFingerprint, keyFingerprint: input.KeyID,
accountName: accountName, accountName: input.AccountName,
agent: ag, agent: ag,
} }
@ -55,7 +63,12 @@ func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, err
} }
signer.key = matchingKey signer.key = matchingKey
signer.formattedKeyFingerprint = formatPublicKeyFingerprint(signer.key, true) signer.formattedKeyFingerprint = formatPublicKeyFingerprint(signer.key, true)
signer.keyIdentifier = fmt.Sprintf("/%s/keys/%s", signer.accountName, signer.formattedKeyFingerprint) 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") _, algorithm, err := signer.SignRaw("HelloWorld")
if err != nil { if err != nil {

@ -21,7 +21,7 @@ import (
const nilContext = "nil context" const nilContext = "nil context"
var ( var (
ErrDefaultAuth = errors.New("default SSH agent authentication requires SDC_KEY_ID and SSH_AUTH_SOCK") ErrDefaultAuth = errors.New("default SSH agent authentication requires SDC_KEY_ID / TRITON_KEY_ID and SSH_AUTH_SOCK")
ErrAccountName = errors.New("missing account name for Triton/Manta") ErrAccountName = errors.New("missing account name for Triton/Manta")
ErrMissingURL = errors.New("missing Triton and/or Manta URL") ErrMissingURL = errors.New("missing Triton and/or Manta URL")
@ -36,6 +36,7 @@ type Client struct {
TritonURL url.URL TritonURL url.URL
MantaURL url.URL MantaURL url.URL
AccountName string AccountName string
Username string
} }
// New is used to construct a Client in order to make API // New is used to construct a Client in order to make API
@ -81,7 +82,7 @@ func New(tritonURL string, mantaURL string, accountName string, signers ...authe
} }
// Default to constructing an SSHAgentSigner if there are no other signers // 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 // passed into NewClient and there's an TRITON_KEY_ID and SSH_AUTH_SOCK
// available in the user's environ(7). // available in the user's environ(7).
if len(newClient.Authorizers) == 0 { if len(newClient.Authorizers) == 0 {
if err := newClient.DefaultAuth(); err != nil { if err := newClient.DefaultAuth(); err != nil {
@ -92,21 +93,43 @@ func New(tritonURL string, mantaURL string, accountName string, signers ...authe
return newClient, nil 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 // 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 // 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 // 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. // user's environ(7). If so we default to the SSH agent key signer.
func (c *Client) DefaultAuth() error { func (c *Client) DefaultAuth() error {
if keyID, keyOk := os.LookupEnv("SDC_KEY_ID"); keyOk { tritonKeyId := GetTritonEnv("KEY_ID")
defaultSigner, err := authentication.NewSSHAgentSigner(keyID, c.AccountName) if tritonKeyId != "" {
input := authentication.SSHAgentSignerInput{
KeyID: tritonKeyId,
AccountName: c.AccountName,
Username: c.Username,
}
defaultSigner, err := authentication.NewSSHAgentSigner(input)
if err != nil { if err != nil {
return errwrap.Wrapf("problem initializing NewSSHAgentSigner: {{err}}", err) return errwrap.Wrapf("problem initializing NewSSHAgentSigner: {{err}}", err)
} }
c.Authorizers = append(c.Authorizers, defaultSigner) c.Authorizers = append(c.Authorizers, defaultSigner)
} else {
return ErrDefaultAuth
} }
return nil
return ErrDefaultAuth
} }
// InsecureSkipTLSVerify turns off TLS verification for the client connection. This // InsecureSkipTLSVerify turns off TLS verification for the client connection. This

@ -260,7 +260,6 @@ func (s *ObjectsClient) Put(ctx context.Context, input *PutObjectInput) error {
absPath := absFileInput(s.client.AccountName, input.ObjectPath) absPath := absFileInput(s.client.AccountName, input.ObjectPath)
if input.ForceInsert { if input.ForceInsert {
// IsDir() uses a path relative to the account
absDirName := _AbsCleanPath(path.Dir(string(absPath))) absDirName := _AbsCleanPath(path.Dir(string(absPath)))
exists, err := checkDirectoryTreeExists(*s, ctx, absDirName) exists, err := checkDirectoryTreeExists(*s, ctx, absDirName)
if err != nil { if err != nil {

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

24
vendor/vendor.json vendored

@ -292,28 +292,28 @@
"revisionTime": "2016-01-12T19:33:35Z" "revisionTime": "2016-01-12T19:33:35Z"
}, },
{ {
"checksumSHA1": "NYs0qvjZwsMZAXMtg2HRiED2cb4=", "checksumSHA1": "oINoQSRkPinChzwEHr3VatB9++Y=",
"path": "github.com/joyent/triton-go", "path": "github.com/joyent/triton-go",
"revision": "8365851ee7afcbb4cc1c7ba2e414b242ce0574f1", "revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
"revisionTime": "2017-12-15T19:09:06Z" "revisionTime": "2017-12-28T20:20:46Z"
}, },
{ {
"checksumSHA1": "Cth7NCLH/HaeKh9ZMRpQtudTEQQ=", "checksumSHA1": "d6pxw8DLxYehLr92fWZTLEWVws8=",
"path": "github.com/joyent/triton-go/authentication", "path": "github.com/joyent/triton-go/authentication",
"revision": "8365851ee7afcbb4cc1c7ba2e414b242ce0574f1", "revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
"revisionTime": "2017-12-15T19:09:06Z" "revisionTime": "2017-12-28T20:20:46Z"
}, },
{ {
"checksumSHA1": "3ju04DVaxotpCKBF3Q/0vCSOlec=", "checksumSHA1": "GCHfn8d1Mhswm7n7IRnT0n/w+dw=",
"path": "github.com/joyent/triton-go/client", "path": "github.com/joyent/triton-go/client",
"revision": "8365851ee7afcbb4cc1c7ba2e414b242ce0574f1", "revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
"revisionTime": "2017-12-15T19:09:06Z" "revisionTime": "2017-12-28T20:20:46Z"
}, },
{ {
"checksumSHA1": "/WtyDZMgstGbBYtQ0f+ZfKMS4v8=", "checksumSHA1": "PJe3Rs8H466xR8o5audO8oWk44Q=",
"path": "github.com/joyent/triton-go/storage", "path": "github.com/joyent/triton-go/storage",
"revision": "8365851ee7afcbb4cc1c7ba2e414b242ce0574f1", "revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
"revisionTime": "2017-12-15T19:09:06Z" "revisionTime": "2017-12-28T20:20:46Z"
}, },
{ {
"path": "github.com/klauspost/cpuid", "path": "github.com/klauspost/cpuid",

Loading…
Cancel
Save