builder/triton: Add support for Triton RBAC
Fixes: #5737 Triton allows for subusers to be granted access to your account. This PR allows a user to be specified
This commit is contained in:
parent
4e1beb009c
commit
c3467b686c
|
@ -19,6 +19,7 @@ import (
|
||||||
type AccessConfig struct {
|
type AccessConfig struct {
|
||||||
Endpoint string `mapstructure:"triton_url"`
|
Endpoint string `mapstructure:"triton_url"`
|
||||||
Account string `mapstructure:"triton_account"`
|
Account string `mapstructure:"triton_account"`
|
||||||
|
Username string `mapstructure:"triton_user"`
|
||||||
KeyID string `mapstructure:"triton_key_id"`
|
KeyID string `mapstructure:"triton_key_id"`
|
||||||
KeyMaterial string `mapstructure:"triton_key_material"`
|
KeyMaterial string `mapstructure:"triton_key_material"`
|
||||||
|
|
||||||
|
@ -65,7 +66,12 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AccessConfig) createSSHAgentSigner() (authentication.Signer, error) {
|
func (c *AccessConfig) createSSHAgentSigner() (authentication.Signer, error) {
|
||||||
signer, err := authentication.NewSSHAgentSigner(c.KeyID, c.Account)
|
input := authentication.SSHAgentSignerInput{
|
||||||
|
KeyID: c.KeyID,
|
||||||
|
AccountName: c.Account,
|
||||||
|
Username: c.Username,
|
||||||
|
}
|
||||||
|
signer, err := authentication.NewSSHAgentSigner(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error creating Triton request signer: %s", err)
|
return nil, fmt.Errorf("Error creating Triton request signer: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -94,8 +100,14 @@ func (c *AccessConfig) createPrivateKeySigner() (authentication.Signer, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create signer
|
input := authentication.PrivateKeySignerInput{
|
||||||
signer, err := authentication.NewPrivateKeySigner(c.KeyID, privateKeyMaterial, c.Account)
|
KeyID: c.KeyID,
|
||||||
|
AccountName: c.Account,
|
||||||
|
Username: c.Username,
|
||||||
|
PrivateKeyMaterial: privateKeyMaterial,
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := authentication.NewPrivateKeySigner(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error creating Triton request signer: %s", err)
|
return nil, fmt.Errorf("Error creating Triton request signer: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -114,6 +126,7 @@ func (c *AccessConfig) CreateTritonClient() (*Client, error) {
|
||||||
config := &tgo.ClientConfig{
|
config := &tgo.ClientConfig{
|
||||||
AccountName: c.Account,
|
AccountName: c.Account,
|
||||||
TritonURL: c.Endpoint,
|
TritonURL: c.Endpoint,
|
||||||
|
Username: c.Username,
|
||||||
Signers: []authentication.Signer{c.signer},
|
Signers: []authentication.Signer{c.signer},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
## 0.5.2 (December 28)
|
||||||
|
|
||||||
|
- Standardise the API SSH Signers input casing and naming
|
||||||
|
|
||||||
|
## 0.5.1 (December 28)
|
||||||
|
|
||||||
|
- Include leading '/' when working with SSH Agent signers
|
||||||
|
|
||||||
|
## 0.5.0 (December 28)
|
||||||
|
|
||||||
|
- Add support for RBAC in triton-go [#82]
|
||||||
|
This is a breaking change. No longer do we pass individual parameters to the SSH Signer funcs, but we now pass an input Struct. This will guard from from additional parameter changes in the future.
|
||||||
|
We also now add support for using `SDC_*` and `TRITON_*` env vars when working with the Default agent signer
|
||||||
|
|
||||||
|
## 0.4.2 (December 22)
|
||||||
|
|
||||||
|
- Fixing a panic when the user loses network connectivity when making a GET request to instance [#81]
|
||||||
|
|
||||||
|
## 0.4.1 (December 15)
|
||||||
|
|
||||||
|
- Clean up the handling of directory sanitization. Use abs paths everywhere [#79]
|
||||||
|
|
||||||
|
## 0.4.0 (December 15)
|
||||||
|
|
||||||
|
- Fix an issue where Manta HEAD requests do not return an error resp body [#77]
|
||||||
|
- Add support for recursively creating child directories [#78]
|
||||||
|
|
||||||
|
## 0.3.0 (December 14)
|
||||||
|
|
||||||
|
- Introduce CloudAPI's ListRulesMachines under networking
|
||||||
|
- Enable HTTP KeepAlives by default in the client. 15s idle timeout, 2x
|
||||||
|
connections per host, total of 10x connections per client.
|
||||||
|
- Expose an optional Headers attribute to clients to allow them to customize
|
||||||
|
HTTP headers when making Object requests.
|
||||||
|
- Fix a bug in Directory ListIndex [#69](https://github.com/joyent/issues/69)
|
||||||
|
- Inputs to Object inputs have been relaxed to `io.Reader` (formerly a
|
||||||
|
`io.ReadSeeker`) [#73](https://github.com/joyent/issues/73).
|
||||||
|
- Add support for ForceDelete of all children of a directory [#71](https://github.com/joyent/issues/71)
|
||||||
|
- storage: Introduce `Objects.GetInfo` and `Objects.IsDir` using HEAD requests [#74](https://github.com/joyent/triton-go/issues/74)
|
||||||
|
|
||||||
|
## 0.2.1 (November 8)
|
||||||
|
|
||||||
|
- Fixing a bug where CreateUser and UpdateUser didn't return the UserID
|
||||||
|
|
||||||
|
## 0.2.0 (November 7)
|
||||||
|
|
||||||
|
- Introduce CloudAPI's Ping under compute
|
||||||
|
- Introduce CloudAPI's RebootMachine under compute instances
|
||||||
|
- Introduce CloudAPI's ListUsers, GetUser, CreateUser, UpdateUser and DeleteUser under identity package
|
||||||
|
- Introduce CloudAPI's ListMachineSnapshots, GetMachineSnapshot, CreateSnapshot, DeleteMachineSnapshot and StartMachineFromSnapshot under compute package
|
||||||
|
- tools: Introduce unit testing and scripts for linting, etc.
|
||||||
|
- bug: Fix the `compute.ListMachineRules` endpoint
|
||||||
|
|
||||||
|
## 0.1.0 (November 2)
|
||||||
|
|
||||||
|
- Initial release of a versioned SDK
|
|
@ -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}'
|
|
@ -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
|
|
@ -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"
|
|
@ -3,6 +3,8 @@
|
||||||
`triton-go` is an idiomatic library exposing a client SDK for Go applications
|
`triton-go` is an idiomatic library exposing a client SDK for Go applications
|
||||||
using Joyent's Triton Compute and Storage (Manta) APIs.
|
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
|
## Usage
|
||||||
|
|
||||||
Triton uses [HTTP Signature][4] to sign the Date header in each HTTP request
|
Triton uses [HTTP Signature][4] to sign the Date header in each HTTP request
|
||||||
|
@ -13,11 +15,17 @@ using a key stored with the local SSH Agent (using an [`SSHAgentSigner`][6].
|
||||||
To construct a Signer, use the `New*` range of methods in the `authentication`
|
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)
|
||||||
}
|
}
|
||||||
|
@ -34,17 +42,18 @@ their own seperate client. In order to initialize a package client, simply pass
|
||||||
the global `triton.ClientConfig` struct into the client's constructor function.
|
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
|
||||||
|
@ -55,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
|
||||||
|
@ -79,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
|
||||||
|
|
||||||
|
@ -94,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)
|
||||||
|
@ -116,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
|
||||||
|
@ -142,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)
|
||||||
}
|
}
|
||||||
|
@ -178,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},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
package authentication
|
||||||
|
|
||||||
|
// DON'T USE THIS OUTSIDE TESTING ~ This key was only created to use for
|
||||||
|
// internal unit testing. It should never be used for acceptance testing either.
|
||||||
|
//
|
||||||
|
// This is just a randomly generated key pair.
|
||||||
|
var Dummy = struct {
|
||||||
|
Fingerprint string
|
||||||
|
PrivateKey []byte
|
||||||
|
PublicKey []byte
|
||||||
|
Signer Signer
|
||||||
|
}{
|
||||||
|
"9f:d6:65:fc:d6:60:dc:d0:4e:db:2d:75:f7:92:8c:31",
|
||||||
|
[]byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
-----END RSA PRIVATE KEY-----`),
|
||||||
|
[]byte(`ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC6L2U2MIlqEd4VIULodeL8I1fnzwL8vaAlTHrluc2mFUL3yFbXDOhG89cfHlpadvuURuk2E/oHRSgIENNo+C32Ed8P+g1D7odPVCkMgZ9mZcj3X+1yMZVIFvE3tZpi38ew+saNtqXC9A8KE3newxWYWQxMQNHkbfM4E4Aas6KaUJPzTAjZLleDSyl3yOMozkjWzANHTG24NacdNHU6GKf6kRAgTqCUtN9iruhDHqeYxdcE2aT5Jo8PUUDKOQX/zWKbYQdcHKA0TCtpWsNwDEraYqVTZcwmKiBAo1haWbdSRs0HpY8t7Xb4prTauYhWxKoilz9kIGtoCUJpJaVgKRT68naEf/7KnLltc2VtpOJZkr6+4js8LztPnTIs15dW41kDKfMOkEK+yOKIesH9kVF9V8Xo++JXwJX6bsZPN8DAmFV5JuWtHqe2zXorQdMPjy/nsNTRvS3lU68LtxY9KGz5p91IvdnxF4vkRYyBTXB++3JxEp6GINUaPcyzZx/zdEgA2vfzsb7UtxNBg/skm4HtK/LmP5Hr4eMjRyC7gQxQ91Z8I0YbKuXHAk20+ZRJ9qwfXFQqmqvt0L7Sw3j+B8nDvlZMBisBOQ/N7Ghg1oywYN+G6iGRhrdOfarWDeSqGxf9HQV3l3x3mpzXfupxELGJljP1/dtWRdBuFiY711rcqw== test-dummy-20171002140848`),
|
||||||
|
nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
testSigner, _ := NewTestSigner()
|
||||||
|
Dummy.Signer = testSigner
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
block, _ := pem.Decode(privateKeyMaterial)
|
func NewPrivateKeySigner(input PrivateKeySignerInput) (*PrivateKeySigner, error) {
|
||||||
|
keyFingerprintMD5 := strings.Replace(input.KeyID, ":", "", -1)
|
||||||
|
|
||||||
|
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"
|
||||||
|
@ -15,21 +16,32 @@ import (
|
||||||
"golang.org/x/crypto/ssh/agent"
|
"golang.org/x/crypto/ssh/agent"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnsetEnvVar = errors.New("SSH_AUTH_SOCK is not set")
|
||||||
|
)
|
||||||
|
|
||||||
type SSHAgentSigner struct {
|
type SSHAgentSigner struct {
|
||||||
formattedKeyFingerprint string
|
formattedKeyFingerprint string
|
||||||
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 {
|
||||||
sshAgentAddress := os.Getenv("SSH_AUTH_SOCK")
|
KeyID string
|
||||||
if sshAgentAddress == "" {
|
AccountName string
|
||||||
return nil, errors.New("SSH_AUTH_SOCK is not set")
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSSHAgentSigner(input SSHAgentSignerInput) (*SSHAgentSigner, error) {
|
||||||
|
sshAgentAddress, agentOk := os.LookupEnv("SSH_AUTH_SOCK")
|
||||||
|
if !agentOk {
|
||||||
|
return nil, ErrUnsetEnvVar
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := net.Dial("unix", sshAgentAddress)
|
conn, err := net.Dial("unix", sshAgentAddress)
|
||||||
|
@ -39,12 +51,41 @@ func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, err
|
||||||
|
|
||||||
ag := agent.NewClient(conn)
|
ag := agent.NewClient(conn)
|
||||||
|
|
||||||
keys, err := ag.List()
|
signer := &SSHAgentSigner{
|
||||||
|
keyFingerprint: input.KeyID,
|
||||||
|
accountName: input.AccountName,
|
||||||
|
agent: ag,
|
||||||
|
}
|
||||||
|
|
||||||
|
matchingKey, err := signer.MatchKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signer.key = matchingKey
|
||||||
|
signer.formattedKeyFingerprint = formatPublicKeyFingerprint(signer.key, true)
|
||||||
|
if input.Username != "" {
|
||||||
|
signer.userName = input.Username
|
||||||
|
signer.keyIdentifier = path.Join("/", signer.accountName, "users", input.Username, "keys", signer.formattedKeyFingerprint)
|
||||||
|
} else {
|
||||||
|
signer.keyIdentifier = path.Join("/", signer.accountName, "keys", signer.formattedKeyFingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, algorithm, err := signer.SignRaw("HelloWorld")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Cannot sign using ssh agent: %s", err)
|
||||||
|
}
|
||||||
|
signer.algorithm = algorithm
|
||||||
|
|
||||||
|
return signer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHAgentSigner) MatchKey() (ssh.PublicKey, error) {
|
||||||
|
keys, err := s.agent.List()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errwrap.Wrapf("Error listing keys in SSH Agent: %s", err)
|
return nil, errwrap.Wrapf("Error listing keys in SSH Agent: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
keyFingerprintStripped := strings.TrimPrefix(keyFingerprint, "MD5:")
|
keyFingerprintStripped := strings.TrimPrefix(s.keyFingerprint, "MD5:")
|
||||||
keyFingerprintStripped = strings.TrimPrefix(keyFingerprintStripped, "SHA256:")
|
keyFingerprintStripped = strings.TrimPrefix(keyFingerprintStripped, "SHA256:")
|
||||||
keyFingerprintStripped = strings.Replace(keyFingerprintStripped, ":", "", -1)
|
keyFingerprintStripped = strings.Replace(keyFingerprintStripped, ":", "", -1)
|
||||||
|
|
||||||
|
@ -64,27 +105,10 @@ func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if matchingKey == nil {
|
if matchingKey == nil {
|
||||||
return nil, fmt.Errorf("No key in the SSH Agent matches fingerprint: %s", keyFingerprint)
|
return nil, fmt.Errorf("No key in the SSH Agent matches fingerprint: %s", s.keyFingerprint)
|
||||||
}
|
}
|
||||||
|
|
||||||
formattedKeyFingerprint := formatPublicKeyFingerprint(matchingKey, true)
|
return matchingKey, nil
|
||||||
|
|
||||||
signer := &SSHAgentSigner{
|
|
||||||
formattedKeyFingerprint: formattedKeyFingerprint,
|
|
||||||
keyFingerprint: keyFingerprint,
|
|
||||||
accountName: accountName,
|
|
||||||
agent: ag,
|
|
||||||
key: matchingKey,
|
|
||||||
keyIdentifier: fmt.Sprintf("/%s/keys/%s", accountName, formattedKeyFingerprint),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, algorithm, err := signer.SignRaw("HelloWorld")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Cannot sign using ssh agent: %s", err)
|
|
||||||
}
|
|
||||||
signer.algorithm = algorithm
|
|
||||||
|
|
||||||
return signer, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSHAgentSigner) Sign(dateHeader string) (string, error) {
|
func (s *SSHAgentSigner) Sign(dateHeader string) (string, error) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -19,7 +20,14 @@ import (
|
||||||
|
|
||||||
const nilContext = "nil context"
|
const nilContext = "nil context"
|
||||||
|
|
||||||
var MissingKeyIdError = errors.New("Default SSH agent authentication requires SDC_KEY_ID")
|
var (
|
||||||
|
ErrDefaultAuth = errors.New("default SSH agent authentication requires SDC_KEY_ID / TRITON_KEY_ID and SSH_AUTH_SOCK")
|
||||||
|
ErrAccountName = errors.New("missing account name for Triton/Manta")
|
||||||
|
ErrMissingURL = errors.New("missing Triton and/or Manta URL")
|
||||||
|
|
||||||
|
BadTritonURL = "invalid format of triton URL"
|
||||||
|
BadMantaURL = "invalid format of manta URL"
|
||||||
|
)
|
||||||
|
|
||||||
// Client represents a connection to the Triton Compute or Object Storage APIs.
|
// Client represents a connection to the Triton Compute or Object Storage APIs.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
|
@ -28,7 +36,7 @@ type Client struct {
|
||||||
TritonURL url.URL
|
TritonURL url.URL
|
||||||
MantaURL url.URL
|
MantaURL url.URL
|
||||||
AccountName string
|
AccountName string
|
||||||
Endpoint 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
|
||||||
|
@ -37,61 +45,93 @@ type Client struct {
|
||||||
// At least one signer must be provided - example signers include
|
// At least one signer must be provided - example signers include
|
||||||
// authentication.PrivateKeySigner and authentication.SSHAgentSigner.
|
// authentication.PrivateKeySigner and authentication.SSHAgentSigner.
|
||||||
func New(tritonURL string, mantaURL string, accountName string, signers ...authentication.Signer) (*Client, error) {
|
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)
|
cloudURL, err := url.Parse(tritonURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errwrap.Wrapf("invalid endpoint URL: {{err}}", err)
|
return nil, errwrap.Wrapf(BadTritonURL+": {{err}}", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
storageURL, err := url.Parse(mantaURL)
|
storageURL, err := url.Parse(mantaURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errwrap.Wrapf("invalid manta URL: {{err}}", err)
|
return nil, errwrap.Wrapf(BadMantaURL+": {{err}}", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if accountName == "" {
|
authorizers := make([]authentication.Signer, 0)
|
||||||
return nil, errors.New("account name can not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Transport: httpTransport(false),
|
|
||||||
CheckRedirect: doNotFollowRedirects,
|
|
||||||
}
|
|
||||||
|
|
||||||
newClient := &Client{
|
|
||||||
HTTPClient: httpClient,
|
|
||||||
Authorizers: signers,
|
|
||||||
TritonURL: *cloudURL,
|
|
||||||
MantaURL: *storageURL,
|
|
||||||
AccountName: accountName,
|
|
||||||
// TODO(justinwr): Deprecated?
|
|
||||||
// Endpoint: tritonURL,
|
|
||||||
}
|
|
||||||
|
|
||||||
var authorizers []authentication.Signer
|
|
||||||
for _, key := range signers {
|
for _, key := range signers {
|
||||||
if key != nil {
|
if key != nil {
|
||||||
authorizers = append(authorizers, key)
|
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
|
// Default to constructing an SSHAgentSigner if there are no other signers
|
||||||
// passed into NewClient and there's an SDC_KEY_ID value available in the
|
// passed into NewClient and there's an TRITON_KEY_ID and SSH_AUTH_SOCK
|
||||||
// user environ.
|
// available in the user's environ(7).
|
||||||
if len(authorizers) == 0 {
|
if len(newClient.Authorizers) == 0 {
|
||||||
keyID := os.Getenv("SDC_KEY_ID")
|
if err := newClient.DefaultAuth(); err != nil {
|
||||||
if len(keyID) != 0 {
|
return nil, err
|
||||||
keySigner, err := authentication.NewSSHAgentSigner(keyID, accountName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errwrap.Wrapf("Problem initializing NewSSHAgentSigner: {{err}}", err)
|
|
||||||
}
|
|
||||||
newClient.Authorizers = append(authorizers, keySigner)
|
|
||||||
} else {
|
|
||||||
return nil, MissingKeyIdError
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
// be used internally if the client has no other key signer for authenticating
|
||||||
|
// with Triton. We first look for both `SDC_KEY_ID` and `SSH_AUTH_SOCK` in the
|
||||||
|
// user's environ(7). If so we default to the SSH agent key signer.
|
||||||
|
func (c *Client) DefaultAuth() error {
|
||||||
|
tritonKeyId := GetTritonEnv("KEY_ID")
|
||||||
|
if tritonKeyId != "" {
|
||||||
|
input := authentication.SSHAgentSignerInput{
|
||||||
|
KeyID: tritonKeyId,
|
||||||
|
AccountName: c.AccountName,
|
||||||
|
Username: c.Username,
|
||||||
|
}
|
||||||
|
defaultSigner, err := authentication.NewSSHAgentSigner(input)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf("problem initializing NewSSHAgentSigner: {{err}}", err)
|
||||||
|
}
|
||||||
|
c.Authorizers = append(c.Authorizers, defaultSigner)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrDefaultAuth
|
||||||
|
}
|
||||||
|
|
||||||
// InsecureSkipTLSVerify turns off TLS verification for the client connection. This
|
// InsecureSkipTLSVerify turns off TLS verification for the client connection. This
|
||||||
// allows connection to an endpoint with a certificate which was signed by a non-
|
// 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
|
// trusted CA, such as self-signed certificates. This can be useful when connecting
|
||||||
|
@ -112,8 +152,8 @@ func httpTransport(insecureSkipTLSVerify bool) *http.Transport {
|
||||||
KeepAlive: 30 * time.Second,
|
KeepAlive: 30 * time.Second,
|
||||||
}).Dial,
|
}).Dial,
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
DisableKeepAlives: true,
|
MaxIdleConns: 10,
|
||||||
MaxIdleConnsPerHost: -1,
|
IdleConnTimeout: 15 * time.Second,
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
InsecureSkipVerify: insecureSkipTLSVerify,
|
InsecureSkipVerify: insecureSkipTLSVerify,
|
||||||
},
|
},
|
||||||
|
@ -158,7 +198,7 @@ func (c *Client) ExecuteRequestURIParams(ctx context.Context, inputs RequestInpu
|
||||||
body := inputs.Body
|
body := inputs.Body
|
||||||
query := inputs.Query
|
query := inputs.Query
|
||||||
|
|
||||||
var requestBody io.ReadSeeker
|
var requestBody io.Reader
|
||||||
if body != nil {
|
if body != nil {
|
||||||
marshaled, err := json.MarshalIndent(body, "", " ")
|
marshaled, err := json.MarshalIndent(body, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -217,7 +257,7 @@ func (c *Client) ExecuteRequestRaw(ctx context.Context, inputs RequestInput) (*h
|
||||||
path := inputs.Path
|
path := inputs.Path
|
||||||
body := inputs.Body
|
body := inputs.Body
|
||||||
|
|
||||||
var requestBody io.ReadSeeker
|
var requestBody io.Reader
|
||||||
if body != nil {
|
if body != nil {
|
||||||
marshaled, err := json.MarshalIndent(body, "", " ")
|
marshaled, err := json.MarshalIndent(body, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -270,7 +310,7 @@ func (c *Client) ExecuteRequestStorage(ctx context.Context, inputs RequestInput)
|
||||||
endpoint := c.MantaURL
|
endpoint := c.MantaURL
|
||||||
endpoint.Path = path
|
endpoint.Path = path
|
||||||
|
|
||||||
var requestBody io.ReadSeeker
|
var requestBody io.Reader
|
||||||
if body != nil {
|
if body != nil {
|
||||||
marshaled, err := json.MarshalIndent(body, "", " ")
|
marshaled, err := json.MarshalIndent(body, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -323,10 +363,17 @@ func (c *Client) ExecuteRequestStorage(ctx context.Context, inputs RequestInput)
|
||||||
StatusCode: resp.StatusCode,
|
StatusCode: resp.StatusCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
errorDecoder := json.NewDecoder(resp.Body)
|
if req.Method != http.MethodHead {
|
||||||
if err := errorDecoder.Decode(mantaError); err != nil {
|
errorDecoder := json.NewDecoder(resp.Body)
|
||||||
return nil, nil, errwrap.Wrapf("Error decoding error response: {{err}}", err)
|
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
|
return nil, nil, mantaError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,7 +382,7 @@ type RequestNoEncodeInput struct {
|
||||||
Path string
|
Path string
|
||||||
Query *url.Values
|
Query *url.Values
|
||||||
Headers *http.Header
|
Headers *http.Header
|
||||||
Body io.ReadSeeker
|
Body io.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ExecuteRequestNoEncode(ctx context.Context, inputs RequestNoEncodeInput) (io.ReadCloser, http.Header, error) {
|
func (c *Client) ExecuteRequestNoEncode(ctx context.Context, inputs RequestNoEncodeInput) (io.ReadCloser, http.Header, error) {
|
||||||
|
|
|
@ -38,7 +38,7 @@ func (c *ComputeClient) Images() *ImagesClient {
|
||||||
return &ImagesClient{c.Client}
|
return &ImagesClient{c.Client}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Machines returns a Compute client used for accessing functions pertaining to
|
// Machine returns a Compute client used for accessing functions pertaining to
|
||||||
// machine functionality in the Triton API.
|
// machine functionality in the Triton API.
|
||||||
func (c *ComputeClient) Instances() *InstancesClient {
|
func (c *ComputeClient) Instances() *InstancesClient {
|
||||||
return &InstancesClient{c.Client}
|
return &InstancesClient{c.Client}
|
||||||
|
@ -55,3 +55,9 @@ func (c *ComputeClient) Packages() *PackagesClient {
|
||||||
func (c *ComputeClient) Services() *ServicesClient {
|
func (c *ComputeClient) Services() *ServicesClient {
|
||||||
return &ServicesClient{c.Client}
|
return &ServicesClient{c.Client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snapshots returns a Compute client used for accessing functions pertaining to
|
||||||
|
// Snapshots functionality in the Triton API.
|
||||||
|
func (c *ComputeClient) Snapshots() *SnapshotsClient {
|
||||||
|
return &SnapshotsClient{c.Client}
|
||||||
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ func (c *DataCentersClient) Get(ctx context.Context, input *GetDataCenterInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusFound {
|
if resp.StatusCode != http.StatusFound {
|
||||||
return nil, fmt.Errorf("Error executing Get request: expected status code 302, got %s",
|
return nil, fmt.Errorf("Error executing Get request: expected status code 302, got %d",
|
||||||
resp.StatusCode)
|
resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,12 @@ func IsUnknownError(err error) bool {
|
||||||
return isSpecificError(err, "UnknownError")
|
return isSpecificError(err, "UnknownError")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsEmptyResponse tests whether err wraps a client.TritonError with code
|
||||||
|
// EmptyResponse
|
||||||
|
func IsEmptyResponse(err error) bool {
|
||||||
|
return isSpecificError(err, "EmptyResponse")
|
||||||
|
}
|
||||||
|
|
||||||
// isSpecificError checks whether the error represented by err wraps
|
// isSpecificError checks whether the error represented by err wraps
|
||||||
// an underlying client.TritonError with code errorCode.
|
// an underlying client.TritonError with code errorCode.
|
||||||
func isSpecificError(err error, errorCode string) bool {
|
func isSpecificError(err error, errorCode string) bool {
|
||||||
|
|
|
@ -97,10 +97,13 @@ func (c *InstancesClient) Get(ctx context.Context, input *GetInstanceInput) (*In
|
||||||
Path: path,
|
Path: path,
|
||||||
}
|
}
|
||||||
response, err := c.client.ExecuteRequestRaw(ctx, reqInputs)
|
response, err := c.client.ExecuteRequestRaw(ctx, reqInputs)
|
||||||
if response != nil {
|
if response == nil {
|
||||||
|
return nil, errwrap.Wrapf("Error executing Get request: {{err}}", err)
|
||||||
|
}
|
||||||
|
if response.Body != nil {
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
}
|
}
|
||||||
if response == nil || response.StatusCode == http.StatusNotFound || response.StatusCode == http.StatusGone {
|
if response.StatusCode == http.StatusNotFound || response.StatusCode == http.StatusGone {
|
||||||
return nil, &client.TritonError{
|
return nil, &client.TritonError{
|
||||||
StatusCode: response.StatusCode,
|
StatusCode: response.StatusCode,
|
||||||
Code: "ResourceNotFound",
|
Code: "ResourceNotFound",
|
||||||
|
@ -962,6 +965,32 @@ func (c *InstancesClient) Start(ctx context.Context, input *StartInstanceInput)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RebootInstanceInput struct {
|
||||||
|
InstanceID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *InstancesClient) Reboot(ctx context.Context, input *RebootInstanceInput) error {
|
||||||
|
path := fmt.Sprintf("/%s/machines/%s", c.client.AccountName, input.InstanceID)
|
||||||
|
|
||||||
|
params := &url.Values{}
|
||||||
|
params.Set("action", "reboot")
|
||||||
|
|
||||||
|
reqInputs := client.RequestInput{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: path,
|
||||||
|
Query: params,
|
||||||
|
}
|
||||||
|
respReader, err := c.client.ExecuteRequestURIParams(ctx, reqInputs)
|
||||||
|
if respReader != nil {
|
||||||
|
defer respReader.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf("Error executing Start request: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var reservedInstanceCNSTags = map[string]struct{}{
|
var reservedInstanceCNSTags = map[string]struct{}{
|
||||||
CNSTagDisable: {},
|
CNSTagDisable: {},
|
||||||
CNSTagReversePTR: {},
|
CNSTagReversePTR: {},
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
package compute
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/hashicorp/errwrap"
|
||||||
|
"github.com/joyent/triton-go/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pingEndpoint = "/--ping"
|
||||||
|
|
||||||
|
type CloudAPI struct {
|
||||||
|
Versions []string `json:"versions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PingOutput struct {
|
||||||
|
Ping string `json:"ping"`
|
||||||
|
CloudAPI CloudAPI `json:"cloudapi"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping sends a request to the '/--ping' endpoint and returns a `pong` as well
|
||||||
|
// as a list of API version numbers your instance of CloudAPI is presenting.
|
||||||
|
func (c *ComputeClient) Ping(ctx context.Context) (*PingOutput, error) {
|
||||||
|
reqInputs := client.RequestInput{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: pingEndpoint,
|
||||||
|
}
|
||||||
|
response, err := c.Client.ExecuteRequestRaw(ctx, reqInputs)
|
||||||
|
if response == nil {
|
||||||
|
return nil, fmt.Errorf("Ping request has empty response")
|
||||||
|
}
|
||||||
|
if response.Body != nil {
|
||||||
|
defer response.Body.Close()
|
||||||
|
}
|
||||||
|
if response.StatusCode == http.StatusNotFound || response.StatusCode == http.StatusGone {
|
||||||
|
return nil, &client.TritonError{
|
||||||
|
StatusCode: response.StatusCode,
|
||||||
|
Code: "ResourceNotFound",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, errwrap.Wrapf("Error executing Get request: {{err}}",
|
||||||
|
c.Client.DecodeError(response.StatusCode, response.Body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result *PingOutput
|
||||||
|
decoder := json.NewDecoder(response.Body)
|
||||||
|
if err = decoder.Decode(&result); err != nil {
|
||||||
|
return nil, errwrap.Wrapf("Error decoding Get response: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
package compute
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/errwrap"
|
||||||
|
"github.com/joyent/triton-go/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SnapshotsClient struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type Snapshot struct {
|
||||||
|
Name string
|
||||||
|
State string
|
||||||
|
Created time.Time
|
||||||
|
Updated time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListSnapshotsInput struct {
|
||||||
|
MachineID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SnapshotsClient) List(ctx context.Context, input *ListSnapshotsInput) ([]*Snapshot, error) {
|
||||||
|
path := fmt.Sprintf("/%s/machines/%s/snapshots", c.client.AccountName, input.MachineID)
|
||||||
|
reqInputs := client.RequestInput{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
||||||
|
if respReader != nil {
|
||||||
|
defer respReader.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, errwrap.Wrapf("Error executing List request: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []*Snapshot
|
||||||
|
decoder := json.NewDecoder(respReader)
|
||||||
|
if err = decoder.Decode(&result); err != nil {
|
||||||
|
return nil, errwrap.Wrapf("Error decoding List response: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetSnapshotInput struct {
|
||||||
|
MachineID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SnapshotsClient) Get(ctx context.Context, input *GetSnapshotInput) (*Snapshot, error) {
|
||||||
|
path := fmt.Sprintf("/%s/machines/%s/snapshots/%s", c.client.AccountName, input.MachineID, input.Name)
|
||||||
|
reqInputs := client.RequestInput{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
||||||
|
if respReader != nil {
|
||||||
|
defer respReader.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, errwrap.Wrapf("Error executing Get request: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result *Snapshot
|
||||||
|
decoder := json.NewDecoder(respReader)
|
||||||
|
if err = decoder.Decode(&result); err != nil {
|
||||||
|
return nil, errwrap.Wrapf("Error decoding Get response: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteSnapshotInput struct {
|
||||||
|
MachineID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SnapshotsClient) Delete(ctx context.Context, input *DeleteSnapshotInput) error {
|
||||||
|
path := fmt.Sprintf("/%s/machines/%s/snapshots/%s", c.client.AccountName, input.MachineID, input.Name)
|
||||||
|
reqInputs := client.RequestInput{
|
||||||
|
Method: http.MethodDelete,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
||||||
|
if respReader != nil {
|
||||||
|
defer respReader.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf("Error executing Delete request: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type StartMachineFromSnapshotInput struct {
|
||||||
|
MachineID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SnapshotsClient) StartMachine(ctx context.Context, input *StartMachineFromSnapshotInput) error {
|
||||||
|
path := fmt.Sprintf("/%s/machines/%s/snapshots/%s", c.client.AccountName, input.MachineID, input.Name)
|
||||||
|
reqInputs := client.RequestInput{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
||||||
|
if respReader != nil {
|
||||||
|
defer respReader.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf("Error executing StartMachine request: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateSnapshotInput struct {
|
||||||
|
MachineID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SnapshotsClient) Create(ctx context.Context, input *CreateSnapshotInput) (*Snapshot, error) {
|
||||||
|
path := fmt.Sprintf("/%s/machines/%s/snapshots", c.client.AccountName, input.MachineID)
|
||||||
|
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
data["name"] = input.Name
|
||||||
|
|
||||||
|
reqInputs := client.RequestInput{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: path,
|
||||||
|
Body: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
||||||
|
if respReader != nil {
|
||||||
|
defer respReader.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, errwrap.Wrapf("Error executing Create request: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result *Snapshot
|
||||||
|
decoder := json.NewDecoder(respReader)
|
||||||
|
if err = decoder.Decode(&result); err != nil {
|
||||||
|
return nil, errwrap.Wrapf("Error decoding Create response: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/errwrap"
|
"github.com/hashicorp/errwrap"
|
||||||
"github.com/joyent/triton-go/client"
|
"github.com/joyent/triton-go/client"
|
||||||
)
|
)
|
||||||
|
@ -227,7 +229,7 @@ type ListMachineRulesInput struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *FirewallClient) ListMachineRules(ctx context.Context, input *ListMachineRulesInput) ([]*FirewallRule, error) {
|
func (c *FirewallClient) ListMachineRules(ctx context.Context, input *ListMachineRulesInput) ([]*FirewallRule, error) {
|
||||||
path := fmt.Sprintf("/%s/machines/%s/firewallrules", c.client.AccountName, input.MachineID)
|
path := fmt.Sprintf("/%s/machines/%s/fwrules", c.client.AccountName, input.MachineID)
|
||||||
reqInputs := client.RequestInput{
|
reqInputs := client.RequestInput{
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
Path: path,
|
Path: path,
|
||||||
|
@ -243,7 +245,56 @@ func (c *FirewallClient) ListMachineRules(ctx context.Context, input *ListMachin
|
||||||
var result []*FirewallRule
|
var result []*FirewallRule
|
||||||
decoder := json.NewDecoder(respReader)
|
decoder := json.NewDecoder(respReader)
|
||||||
if err = decoder.Decode(&result); err != nil {
|
if err = decoder.Decode(&result); err != nil {
|
||||||
return nil, errwrap.Wrapf("Error decoding ListRules response: {{err}}", err)
|
return nil, errwrap.Wrapf("Error decoding ListMachineRules response: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRuleMachinesInput struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Machine struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Brand string `json:"brand"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Memory int `json:"memory"`
|
||||||
|
Disk int `json:"disk"`
|
||||||
|
Metadata map[string]string `json:"metadata"`
|
||||||
|
Tags map[string]interface{} `json:"tags"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
Docker bool `json:"docker"`
|
||||||
|
IPs []string `json:"ips"`
|
||||||
|
Networks []string `json:"networks"`
|
||||||
|
PrimaryIP string `json:"primaryIp"`
|
||||||
|
FirewallEnabled bool `json:"firewall_enabled"`
|
||||||
|
ComputeNode string `json:"compute_node"`
|
||||||
|
Package string `json:"package"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *FirewallClient) ListRuleMachines(ctx context.Context, input *ListRuleMachinesInput) ([]*Machine, error) {
|
||||||
|
path := fmt.Sprintf("/%s/fwrules/%s/machines", c.client.AccountName, input.ID)
|
||||||
|
reqInputs := client.RequestInput{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
||||||
|
if respReader != nil {
|
||||||
|
defer respReader.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, errwrap.Wrapf("Error executing ListRuleMachines request: {{err}}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []*Machine
|
||||||
|
decoder := json.NewDecoder(respReader)
|
||||||
|
if err = decoder.Decode(&result); err != nil {
|
||||||
|
return nil, errwrap.Wrapf("Error decoding ListRuleMachines response: {{err}}", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -823,34 +823,34 @@
|
||||||
"revision": "c01cf91b011868172fdcd9f41838e80c9d716264"
|
"revision": "c01cf91b011868172fdcd9f41838e80c9d716264"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "EqvUu0Ku0Ec5Tk6yhGNOuOr8yeA=",
|
"checksumSHA1": "oINoQSRkPinChzwEHr3VatB9++Y=",
|
||||||
"path": "github.com/joyent/triton-go",
|
"path": "github.com/joyent/triton-go",
|
||||||
"revision": "5a58ad2cdec95cddd1e0a2e56f559341044b04f0",
|
"revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
|
||||||
"revisionTime": "2017-10-17T16:55:58Z"
|
"revisionTime": "2017-12-28T20:20:46Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "JKf97EAAAZFQ6Wf8qN9X7TWqNBY=",
|
"checksumSHA1": "d6pxw8DLxYehLr92fWZTLEWVws8=",
|
||||||
"path": "github.com/joyent/triton-go/authentication",
|
"path": "github.com/joyent/triton-go/authentication",
|
||||||
"revision": "5a58ad2cdec95cddd1e0a2e56f559341044b04f0",
|
"revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
|
||||||
"revisionTime": "2017-10-17T16:55:58Z"
|
"revisionTime": "2017-12-28T20:20:46Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "dlO1or0cyVMAmZzyLcBuoy+M0xU=",
|
"checksumSHA1": "GCHfn8d1Mhswm7n7IRnT0n/w+dw=",
|
||||||
"path": "github.com/joyent/triton-go/client",
|
"path": "github.com/joyent/triton-go/client",
|
||||||
"revision": "5a58ad2cdec95cddd1e0a2e56f559341044b04f0",
|
"revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
|
||||||
"revisionTime": "2017-10-17T16:55:58Z"
|
"revisionTime": "2017-12-28T20:20:46Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "O/y7BfKJFUf3A8TCRMXgo9HSb1w=",
|
"checksumSHA1": "U9D/fCNr+1uD1p/O0PW0yD/izqc=",
|
||||||
"path": "github.com/joyent/triton-go/compute",
|
"path": "github.com/joyent/triton-go/compute",
|
||||||
"revision": "5a58ad2cdec95cddd1e0a2e56f559341044b04f0",
|
"revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
|
||||||
"revisionTime": "2017-10-17T16:55:58Z"
|
"revisionTime": "2017-12-28T20:20:46Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "gyLtPyKlcumRSkrAH+SsDQo1GnY=",
|
"checksumSHA1": "9OdR/eI3qbmADruKn6yE1Dbx3LE=",
|
||||||
"path": "github.com/joyent/triton-go/network",
|
"path": "github.com/joyent/triton-go/network",
|
||||||
"revision": "5a58ad2cdec95cddd1e0a2e56f559341044b04f0",
|
"revision": "86ba9699869b6cd5ea3290faad7be659efc7d6ce",
|
||||||
"revisionTime": "2017-10-17T16:55:58Z"
|
"revisionTime": "2017-12-28T20:20:46Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "gEjGS03N1eysvpQ+FCHTxPcbxXc=",
|
"checksumSHA1": "gEjGS03N1eysvpQ+FCHTxPcbxXc=",
|
||||||
|
|
|
@ -92,6 +92,9 @@ builder.
|
||||||
of `triton_key_id` is stored. For example `/home/soandso/.ssh/id_rsa`. If
|
of `triton_key_id` is stored. For example `/home/soandso/.ssh/id_rsa`. If
|
||||||
this is not specified, the SSH agent is used to sign requests with the
|
this is not specified, the SSH agent is used to sign requests with the
|
||||||
`triton_key_id` specified.
|
`triton_key_id` specified.
|
||||||
|
|
||||||
|
- `triton_user` (string) - The username of a user who has access to your Triton
|
||||||
|
account.
|
||||||
|
|
||||||
- `source_machine_firewall_enabled` (boolean) - Whether or not the firewall of
|
- `source_machine_firewall_enabled` (boolean) - Whether or not the firewall of
|
||||||
the VM used to create an image of is enabled. The Triton firewall only
|
the VM used to create an image of is enabled. The Triton firewall only
|
||||||
|
@ -149,7 +152,7 @@ builder.
|
||||||
|
|
||||||
## Basic Example
|
## Basic Example
|
||||||
|
|
||||||
Below is a minimal example to create an joyent-brand image on the Joyent public
|
Below is a minimal example to create an image on the Joyent public
|
||||||
cloud:
|
cloud:
|
||||||
|
|
||||||
``` json
|
``` json
|
||||||
|
|
Loading…
Reference in New Issue