Merge pull request #4838 from jen20/triton-new-lib
builder/triton: Replace deprecated joyent/gosdc library with joyent/triton-go
This commit is contained in:
commit
a0271a18c1
|
@ -1,16 +1,15 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/packer/helper/communicator"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gosdc/cloudapi"
|
||||
"github.com/joyent/gosign/auth"
|
||||
"github.com/joyent/triton-go"
|
||||
"github.com/joyent/triton-go/authentication"
|
||||
)
|
||||
|
||||
// AccessConfig is for common configuration related to Triton access
|
||||
|
@ -19,29 +18,40 @@ type AccessConfig struct {
|
|||
Account string `mapstructure:"triton_account"`
|
||||
KeyID string `mapstructure:"triton_key_id"`
|
||||
KeyMaterial string `mapstructure:"triton_key_material"`
|
||||
|
||||
signer authentication.Signer
|
||||
}
|
||||
|
||||
// Prepare performs basic validation on the AccessConfig
|
||||
// Prepare performs basic validation on the AccessConfig and ensures we can sign
|
||||
// a request.
|
||||
func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
var errs []error
|
||||
|
||||
if c.Endpoint == "" {
|
||||
// Use Joyent public cloud as the default endpoint if none is in environment
|
||||
// Use Joyent public cloud as the default endpoint if none is specified
|
||||
c.Endpoint = "https://us-east-1.api.joyent.com"
|
||||
}
|
||||
|
||||
if c.Account == "" {
|
||||
errs = append(errs, fmt.Errorf("triton_account is required to use the triton builder"))
|
||||
errs = append(errs, errors.New("triton_account is required to use the triton builder"))
|
||||
}
|
||||
|
||||
if c.KeyID == "" {
|
||||
errs = append(errs, fmt.Errorf("triton_key_id is required to use the triton builder"))
|
||||
errs = append(errs, errors.New("triton_key_id is required to use the triton builder"))
|
||||
}
|
||||
|
||||
var err error
|
||||
c.KeyMaterial, err = processKeyMaterial(c.KeyMaterial)
|
||||
if c.KeyMaterial == "" || err != nil {
|
||||
errs = append(errs, fmt.Errorf("valid triton_key_material is required to use the triton builder"))
|
||||
if c.KeyMaterial == "" {
|
||||
signer, err := c.createSSHAgentSigner()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
c.signer = signer
|
||||
} else {
|
||||
signer, err := c.createPrivateKeySigner()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
c.signer = signer
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
|
@ -51,49 +61,55 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// CreateTritonClient returns an SDC client configured with the appropriate client credentials
|
||||
// or an error if creating the client fails.
|
||||
func (c *AccessConfig) CreateTritonClient() (*cloudapi.Client, error) {
|
||||
keyData, err := processKeyMaterial(c.KeyMaterial)
|
||||
func (c *AccessConfig) createSSHAgentSigner() (authentication.Signer, error) {
|
||||
signer, err := authentication.NewSSHAgentSigner(c.KeyID, c.Account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Error creating Triton request signer: %s", err)
|
||||
}
|
||||
|
||||
userauth, err := auth.NewAuth(c.Account, keyData, "rsa-sha256")
|
||||
// Ensure we can sign a request
|
||||
_, err = signer.Sign("Wed, 26 Apr 2017 16:01:11 UTC")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Error signing test request: %s", err)
|
||||
}
|
||||
|
||||
creds := &auth.Credentials{
|
||||
UserAuthentication: userauth,
|
||||
SdcKeyId: c.KeyID,
|
||||
SdcEndpoint: auth.Endpoint{URL: c.Endpoint},
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
func (c *AccessConfig) createPrivateKeySigner() (authentication.Signer, error) {
|
||||
var privateKeyMaterial []byte
|
||||
var err error
|
||||
|
||||
// Check for keyMaterial being a file path
|
||||
if _, err = os.Stat(c.KeyMaterial); err != nil {
|
||||
privateKeyMaterial = []byte(c.KeyMaterial)
|
||||
} else {
|
||||
privateKeyMaterial, err = ioutil.ReadFile(c.KeyMaterial)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error reading key material from path '%s': %s",
|
||||
c.KeyMaterial, err)
|
||||
}
|
||||
}
|
||||
|
||||
return cloudapi.New(client.NewClient(
|
||||
c.Endpoint,
|
||||
cloudapi.DefaultAPIVersion,
|
||||
creds,
|
||||
log.New(os.Stdout, "", log.Flags()),
|
||||
)), nil
|
||||
// Create signer
|
||||
signer, err := authentication.NewPrivateKeySigner(c.KeyID, privateKeyMaterial, c.Account)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error creating Triton request signer: %s", err)
|
||||
}
|
||||
|
||||
// Ensure we can sign a request
|
||||
_, err = signer.Sign("Wed, 26 Apr 2017 16:01:11 UTC")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error signing test request: %s", err)
|
||||
}
|
||||
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
func (c *AccessConfig) CreateTritonClient() (*triton.Client, error) {
|
||||
return triton.NewClient(c.Endpoint, c.Account, c.signer)
|
||||
}
|
||||
|
||||
func (c *AccessConfig) Comm() communicator.Config {
|
||||
return communicator.Config{}
|
||||
}
|
||||
|
||||
func processKeyMaterial(keyMaterial string) (string, error) {
|
||||
// Check for keyMaterial being a file path
|
||||
if _, err := os.Stat(keyMaterial); err != nil {
|
||||
// Not a valid file. Assume that keyMaterial is the key data
|
||||
return keyMaterial, nil
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(keyMaterial)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error reading key_material from path '%s': %s",
|
||||
keyMaterial, err)
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
|
|
@ -5,39 +5,60 @@ import (
|
|||
)
|
||||
|
||||
func TestAccessConfig_Prepare(t *testing.T) {
|
||||
ac := testAccessConfig(t)
|
||||
ac := testAccessConfig()
|
||||
errs := ac.Prepare(nil)
|
||||
if errs != nil {
|
||||
t.Fatal("should not error")
|
||||
}
|
||||
|
||||
ac = testAccessConfig(t)
|
||||
ac = testAccessConfig()
|
||||
ac.Account = ""
|
||||
errs = ac.Prepare(nil)
|
||||
if errs == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
|
||||
ac = testAccessConfig(t)
|
||||
ac = testAccessConfig()
|
||||
ac.KeyID = ""
|
||||
errs = ac.Prepare(nil)
|
||||
if errs == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
|
||||
ac = testAccessConfig(t)
|
||||
ac.KeyMaterial = ""
|
||||
errs = ac.Prepare(nil)
|
||||
if errs == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
}
|
||||
|
||||
func testAccessConfig(t *testing.T) AccessConfig {
|
||||
func testAccessConfig() AccessConfig {
|
||||
return AccessConfig{
|
||||
Endpoint: "test-endpoint",
|
||||
Account: "test-account",
|
||||
KeyID: "test-id",
|
||||
KeyMaterial: "test-private-key",
|
||||
KeyID: "c5:9d:37:d2:28:d3:ef:39:1b:0a:0e:37:d5:b4:7c:59",
|
||||
KeyMaterial: testKeyMaterial,
|
||||
}
|
||||
}
|
||||
|
||||
const testKeyMaterial = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpgIBAAKCAQEAuujYxDCBu9W6es650mLcy9RaLGqjHT2KXPs4fHVr1sfBxPDk
|
||||
ChpekrVEfE69wpf7/oduQwLmBTIBBNtr/aH5e8gt2uCe1kD6swjnAG+nWZB63/BW
|
||||
XF9zFFE/Vs/dOyHIkqoLhVHurYYFBGqDXH4w1N02vfyQaH/VQDumF9ZiH6b9u28/
|
||||
WtrHSLPeGidgrt3csh0Q4Bydm2xSCx4Kfeouv0rM25mFoiq/QaTXkfWS0sIzrhhU
|
||||
rXL4N6B8tBRojHghpjh8LjG4ufJ7Q0QMWfeBfTqQ9llaEtiMIBznyq+oF7vwv0pc
|
||||
Cw2eXcURfg/9e5M8S3gSthkqGN9NjQUSeNsgCQIDAQABAoIBAQCcy6zcmFyc+GTh
|
||||
lP5psanMDC5BSIvhgbjK26y9K7v1h8nTrsl+eDSSGiKDrYKe9eTd1zr2WD4iaZpV
|
||||
OsVTFkg2QO3Gydw1nHkzK+qtgP0As6WAqxunjiL6DlZ2OxY5/tNFxgS4KM1zIBSh
|
||||
acEdHHdWeuTraC60m1iH9AIXyS6zoW+YvKr3Cu+gjQgDxg90Uzx7gB7/tAT9uTCG
|
||||
NHXRCJFrjLlKwWap5QpbbrEMZXjwwb4FEC6KOWaTHDGtB6V2NHBYfpAucuLXx19H
|
||||
jKUnogZHxTFbYwf7oZSVCR6tUm/Dytq0DmZv+wkCtUSqP0hljqO71yOOMiWA7fVq
|
||||
4cyD8TGJAoGBAPVVebrIEm8gMTt7s37YFwZXXcC2GT/jVi5bOhWhL5zOA0VxJMH7
|
||||
hUmrRNffNbFciSUM7CeSqh7Zq9v2l+qCjy9iZfUN6NY1s7d5q7NVkVX/GBuZ8ULp
|
||||
d81L4ifnr9KsEIzWz8X3Y/efO/20YqoEqLJm6qUyZYHWJbv9Z8Cteef7AoGBAMMJ
|
||||
HkzRop/VAi5RFzuJyNEzeOyveGpngtsnAY/UcEeWoxkUfHy/GAZAH8aAz9FqLaBv
|
||||
xGL++g3l8nHtov+gkrnJpK/f0QEWY+PbRWxSRHLW0rBdQJRB8nisNrWJwj4ysNhj
|
||||
ejYgBfSSmwkLBnvjNce6RwtZ5d+VRFGRl63CfMTLAoGBAK7Vaxqg2gI3ft5VGWWb
|
||||
uUzblgRvwS62ZARFHu+rHrMwXURvjTJwfFwzoav1dd4fg9zTiLfq3TF/DeqDoV+O
|
||||
C1xJUz9/2h5NxvVJ0ALNR/VxBU0mN7jniGjVWyX1BmesF19G9mquEp+06puyoV1o
|
||||
VJBOp4lykMQmSF3gCMBW4DlhAoGBAINdauk28iBRqqxjthBGF9rAnpxc+/A/VCYk
|
||||
OasU3aN6VNSZtdeYJqhfHIfpTxCwQZckcNR1BRvDW+9crkMbdnho1uIXEIF5AUMB
|
||||
99qj9rKa+0ILLWoumRCqfhb8eLbIEdFN/4zhOOGotX/7yxw6x4iFcUC2Blz3/xIp
|
||||
zE4fB0bNAoGBAK/ms0TixeqdNCY8WVHjT6H1W34Y1gvMlwSDodlUk0v8rUHAK4bO
|
||||
TKvdCYbxCQ+kqAFlicY1ZwxoeJzW6k3K+kJ+qWBn0yH4W61M8uKOIvciu1w1CXxG
|
||||
XZHg281yLxOfJj9YnPG73+sZFucyhtNPiq/1pR4tpm6YLMk8KSTy7XU5
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
|
|
@ -36,6 +36,12 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
errs = multierror.Append(errs, b.config.Comm.Prepare(&b.config.ctx)...)
|
||||
errs = multierror.Append(errs, b.config.TargetImageConfig.Prepare(&b.config.ctx)...)
|
||||
|
||||
// If we are using an SSH agent to sign requests, and no private key has been
|
||||
// specified for SSH, use the agent for connecting for provisioning.
|
||||
if b.config.AccessConfig.KeyMaterial == "" && b.config.Comm.SSHPrivateKey == "" {
|
||||
b.config.Comm.SSHAgentAuth = true
|
||||
}
|
||||
|
||||
return nil, errs.ErrorOrNil()
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
|
||||
func testConfig(t *testing.T) Config {
|
||||
return Config{
|
||||
AccessConfig: testAccessConfig(t),
|
||||
AccessConfig: testAccessConfig(),
|
||||
SourceMachineConfig: testSourceMachineConfig(t),
|
||||
TargetImageConfig: testTargetImageConfig(t),
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ type Driver interface {
|
|||
CreateMachine(config Config) (string, error)
|
||||
DeleteImage(imageId string) error
|
||||
DeleteMachine(machineId string) error
|
||||
GetMachine(machineId string) (string, error)
|
||||
GetMachineIP(machineId string) (string, error)
|
||||
StopMachine(machineId string) error
|
||||
WaitForImageCreation(imageId string, timeout time.Duration) error
|
||||
WaitForMachineDeletion(machineId string, timeout time.Duration) error
|
||||
|
|
|
@ -69,7 +69,7 @@ func (d *DriverMock) DeleteMachine(machineId string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (d *DriverMock) GetMachine(machineId string) (string, error) {
|
||||
func (d *DriverMock) GetMachineIP(machineId string) (string, error) {
|
||||
if d.GetMachineErr != nil {
|
||||
return "", d.GetMachineErr
|
||||
}
|
||||
|
|
|
@ -2,15 +2,14 @@ package triton
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/joyent/gosdc/cloudapi"
|
||||
"github.com/joyent/triton-go"
|
||||
)
|
||||
|
||||
type driverTriton struct {
|
||||
client *cloudapi.Client
|
||||
client *triton.Client
|
||||
ui packer.Ui
|
||||
}
|
||||
|
||||
|
@ -27,30 +26,27 @@ func NewDriverTriton(ui packer.Ui, config Config) (Driver, error) {
|
|||
}
|
||||
|
||||
func (d *driverTriton) CreateImageFromMachine(machineId string, config Config) (string, error) {
|
||||
opts := cloudapi.CreateImageFromMachineOpts{
|
||||
Machine: machineId,
|
||||
image, err := d.client.Images().CreateImageFromMachine(&triton.CreateImageFromMachineInput{
|
||||
MachineID: machineId,
|
||||
Name: config.ImageName,
|
||||
Version: config.ImageVersion,
|
||||
Description: config.ImageDescription,
|
||||
Homepage: config.ImageHomepage,
|
||||
HomePage: config.ImageHomepage,
|
||||
EULA: config.ImageEULA,
|
||||
ACL: config.ImageACL,
|
||||
Tags: config.ImageTags,
|
||||
}
|
||||
|
||||
image, err := d.client.CreateImageFromMachine(opts)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return image.Id, err
|
||||
return image.ID, err
|
||||
}
|
||||
|
||||
func (d *driverTriton) CreateMachine(config Config) (string, error) {
|
||||
opts := cloudapi.CreateMachineOpts{
|
||||
input := &triton.CreateMachineInput{
|
||||
Package: config.MachinePackage,
|
||||
Image: config.MachineImage,
|
||||
Networks: config.MachineNetworks,
|
||||
Metadata: config.MachineMetadata,
|
||||
Tags: config.MachineTags,
|
||||
FirewallEnabled: config.MachineFirewallEnabled,
|
||||
|
@ -59,29 +55,39 @@ func (d *driverTriton) CreateMachine(config Config) (string, error) {
|
|||
if config.MachineName == "" {
|
||||
// If not supplied generate a name for the source VM: "packer-builder-[image_name]".
|
||||
// The version is not used because it can contain characters invalid for a VM name.
|
||||
opts.Name = "packer-builder-" + config.ImageName
|
||||
input.Name = "packer-builder-" + config.ImageName
|
||||
} else {
|
||||
opts.Name = config.MachineName
|
||||
input.Name = config.MachineName
|
||||
}
|
||||
|
||||
machine, err := d.client.CreateMachine(opts)
|
||||
if len(config.MachineNetworks) > 0 {
|
||||
input.Networks = config.MachineNetworks
|
||||
}
|
||||
|
||||
machine, err := d.client.Machines().CreateMachine(input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return machine.Id, nil
|
||||
return machine.ID, nil
|
||||
}
|
||||
|
||||
func (d *driverTriton) DeleteImage(imageId string) error {
|
||||
return d.client.DeleteImage(imageId)
|
||||
return d.client.Images().DeleteImage(&triton.DeleteImageInput{
|
||||
ImageID: imageId,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *driverTriton) DeleteMachine(machineId string) error {
|
||||
return d.client.DeleteMachine(machineId)
|
||||
return d.client.Machines().DeleteMachine(&triton.DeleteMachineInput{
|
||||
ID: machineId,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *driverTriton) GetMachine(machineId string) (string, error) {
|
||||
machine, err := d.client.GetMachine(machineId)
|
||||
func (d *driverTriton) GetMachineIP(machineId string) (string, error) {
|
||||
machine, err := d.client.Machines().GetMachine(&triton.GetMachineInput{
|
||||
ID: machineId,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -90,7 +96,9 @@ func (d *driverTriton) GetMachine(machineId string) (string, error) {
|
|||
}
|
||||
|
||||
func (d *driverTriton) StopMachine(machineId string) error {
|
||||
return d.client.StopMachine(machineId)
|
||||
return d.client.Machines().StopMachine(&triton.StopMachineInput{
|
||||
MachineID: machineId,
|
||||
})
|
||||
}
|
||||
|
||||
// waitForMachineState uses the supplied client to wait for the state of
|
||||
|
@ -101,7 +109,9 @@ func (d *driverTriton) StopMachine(machineId string) error {
|
|||
func (d *driverTriton) WaitForMachineState(machineId string, state string, timeout time.Duration) error {
|
||||
return waitFor(
|
||||
func() (bool, error) {
|
||||
machine, err := d.client.GetMachine(machineId)
|
||||
machine, err := d.client.Machines().GetMachine(&triton.GetMachineInput{
|
||||
ID: machineId,
|
||||
})
|
||||
if machine == nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -118,16 +128,15 @@ func (d *driverTriton) WaitForMachineState(machineId string, state string, timeo
|
|||
func (d *driverTriton) WaitForMachineDeletion(machineId string, timeout time.Duration) error {
|
||||
return waitFor(
|
||||
func() (bool, error) {
|
||||
machine, err := d.client.GetMachine(machineId)
|
||||
if err != nil {
|
||||
//TODO(jen20): is there a better way here than searching strings?
|
||||
if strings.Contains(err.Error(), "410") || strings.Contains(err.Error(), "404") {
|
||||
return true, nil
|
||||
}
|
||||
machine, err := d.client.Machines().GetMachine(&triton.GetMachineInput{
|
||||
ID: machineId,
|
||||
})
|
||||
if err != nil && triton.IsResourceNotFound(err) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if machine != nil {
|
||||
return false, nil
|
||||
return machine.State == "deleted", nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
|
@ -140,7 +149,9 @@ func (d *driverTriton) WaitForMachineDeletion(machineId string, timeout time.Dur
|
|||
func (d *driverTriton) WaitForImageCreation(imageId string, timeout time.Duration) error {
|
||||
return waitFor(
|
||||
func() (bool, error) {
|
||||
image, err := d.client.GetImage(imageId)
|
||||
image, err := d.client.Images().GetImage(&triton.GetImageInput{
|
||||
ImageID: imageId,
|
||||
})
|
||||
if image == nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ func commHost(state multistep.StateBag) (string, error) {
|
|||
driver := state.Get("driver").(Driver)
|
||||
machineID := state.Get("machine").(string)
|
||||
|
||||
machine, err := driver.GetMachine(machineID)
|
||||
machine, err := driver.GetMachineIP(machineID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,363 @@
|
|||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
1.1. "Contributor"
|
||||
|
||||
means each individual or legal entity that creates, contributes to the
|
||||
creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
|
||||
means the combination of the Contributions of others (if any) used by a
|
||||
Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
|
||||
means Source Code Form to which the initial Contributor has attached the
|
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
||||
Modifications of such Source Code Form, in each case including portions
|
||||
thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
a. that the initial Contributor has attached the notice described in
|
||||
Exhibit B to the Covered Software; or
|
||||
|
||||
b. that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the terms of
|
||||
a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
|
||||
means a work that combines Covered Software with other material, in a
|
||||
separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
|
||||
means having the right to grant, to the maximum extent possible, whether
|
||||
at the time of the initial grant or subsequently, any and all of the
|
||||
rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
|
||||
means any of the following:
|
||||
|
||||
a. any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered Software; or
|
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the License,
|
||||
by the making, using, selling, offering for sale, having made, import,
|
||||
or transfer of either its Contributions or its Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||
General Public License, Version 2.1, the GNU Affero General Public
|
||||
License, Version 3.0, or any later versions of those licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that controls, is
|
||||
controlled by, or is under common control with You. For purposes of this
|
||||
definition, "control" means (a) the power, direct or indirect, to cause
|
||||
the direction or management of such entity, whether by contract or
|
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||
outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
a. under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
||||
sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or
|
||||
|
||||
b. for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights to
|
||||
grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||
Section 2.1.
|
||||
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
a. such Covered Software must also be made available in Source Code Form,
|
||||
as described in Section 3.1, and You must inform recipients of the
|
||||
Executable Form how they can obtain a copy of such Source Code Form by
|
||||
reasonable means in a timely manner, at a charge no more than the cost
|
||||
of distribution to the recipient; and
|
||||
|
||||
b. You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter the
|
||||
recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty, or
|
||||
limitations of liability) contained within the Source Code Form of the
|
||||
Covered Software, except that You may alter any license notices to the
|
||||
extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this License
|
||||
with respect to some or all of the Covered Software due to statute,
|
||||
judicial order, or regulation then You must: (a) comply with the terms of
|
||||
this License to the maximum extent possible; and (b) describe the
|
||||
limitations and the code they affect. Such description must be placed in a
|
||||
text file included with all distributions of the Covered Software under
|
||||
this License. Except to the extent prohibited by statute or regulation,
|
||||
such description must be sufficiently detailed for a recipient of ordinary
|
||||
skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You
|
||||
fail to comply with any of its terms. However, if You become compliant,
|
||||
then the rights granted under this License from a particular Contributor
|
||||
are reinstated (a) provisionally, unless and until such Contributor
|
||||
explicitly and finally terminates Your grants, and (b) on an ongoing
|
||||
basis, if such Contributor fails to notify You of the non-compliance by
|
||||
some reasonable means prior to 60 days after You have come back into
|
||||
compliance. Moreover, Your grants from a particular Contributor are
|
||||
reinstated on an ongoing basis if such Contributor notifies You of the
|
||||
non-compliance by some reasonable means, this is the first time You have
|
||||
received notice of non-compliance with this License from such
|
||||
Contributor, and You become compliant prior to 30 days after Your receipt
|
||||
of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
||||
license agreements (excluding distributors and resellers) which have been
|
||||
validly granted by You or Your distributors under this License prior to
|
||||
termination shall survive termination.
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
Covered Software is provided under this License on an "as is" basis,
|
||||
without warranty of any kind, either expressed, implied, or statutory,
|
||||
including, without limitation, warranties that the Covered Software is free
|
||||
of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
The entire risk as to the quality and performance of the Covered Software
|
||||
is with You. Should any Covered Software prove defective in any respect,
|
||||
You (not any Contributor) assume the cost of any necessary servicing,
|
||||
repair, or correction. This disclaimer of warranty constitutes an essential
|
||||
part of this License. No use of any Covered Software is authorized under
|
||||
this License except under this disclaimer.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including
|
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||
distributes Covered Software as permitted above, be liable to You for any
|
||||
direct, indirect, special, incidental, or consequential damages of any
|
||||
character including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses, even if such party shall have been
|
||||
informed of the possibility of such damages. This limitation of liability
|
||||
shall not apply to liability for death or personal injury resulting from
|
||||
such party's negligence to the extent applicable law prohibits such
|
||||
limitation. Some jurisdictions do not allow the exclusion or limitation of
|
||||
incidental or consequential damages, so this exclusion and limitation may
|
||||
not apply to You.
|
||||
|
||||
8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the courts
|
||||
of a jurisdiction where the defendant maintains its principal place of
|
||||
business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing
|
||||
in this Section shall prevent a party's ability to bring cross-claims or
|
||||
counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides that
|
||||
the language of a contract shall be construed against the drafter shall not
|
||||
be used to construe this License against a Contributor.
|
||||
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses If You choose to distribute Source Code Form that is
|
||||
Incompatible With Secondary Licenses under the terms of this version of
|
||||
the License, the notice described in Exhibit B of this License must be
|
||||
attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular file,
|
||||
then You may include the notice in a location (such as a LICENSE file in a
|
||||
relevant directory) where a recipient would be likely to look for such a
|
||||
notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
This Source Code Form is "Incompatible
|
||||
With Secondary Licenses", as defined by
|
||||
the Mozilla Public License, v. 2.0.
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
default: test
|
||||
|
||||
test:
|
||||
go vet ./...
|
||||
go test -race ./...
|
||||
|
||||
updatedeps:
|
||||
go get -f -t -u ./...
|
||||
go get -f -u ./...
|
||||
|
||||
.PHONY: default test updatedeps
|
|
@ -0,0 +1,43 @@
|
|||
go-retryablehttp
|
||||
================
|
||||
|
||||
[![Build Status](http://img.shields.io/travis/hashicorp/go-retryablehttp.svg?style=flat-square)][travis]
|
||||
[![Go Documentation](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)][godocs]
|
||||
|
||||
[travis]: http://travis-ci.org/hashicorp/go-retryablehttp
|
||||
[godocs]: http://godoc.org/github.com/hashicorp/go-retryablehttp
|
||||
|
||||
The `retryablehttp` package provides a familiar HTTP client interface with
|
||||
automatic retries and exponential backoff. It is a thin wrapper over the
|
||||
standard `net/http` client library and exposes nearly the same public API. This
|
||||
makes `retryablehttp` very easy to drop into existing programs.
|
||||
|
||||
`retryablehttp` performs automatic retries under certain conditions. Mainly, if
|
||||
an error is returned by the client (connection errors, etc.), or if a 500-range
|
||||
response code is received, then a retry is invoked after a wait period.
|
||||
Otherwise, the response is returned and left to the caller to interpret.
|
||||
|
||||
The main difference from `net/http` is that requests which take a request body
|
||||
(POST/PUT et. al) require an `io.ReadSeeker` to be provided. This enables the
|
||||
request body to be "rewound" if the initial request fails so that the full
|
||||
request can be attempted again.
|
||||
|
||||
Example Use
|
||||
===========
|
||||
|
||||
Using this library should look almost identical to what you would do with
|
||||
`net/http`. The most simple example of a GET request is shown below:
|
||||
|
||||
```go
|
||||
resp, err := retryablehttp.Get("/foo")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
The returned response object is an `*http.Response`, the same thing you would
|
||||
usually get from `net/http`. Had the request failed one or more times, the above
|
||||
call would block and retry with exponential backoff.
|
||||
|
||||
For more usage and examples see the
|
||||
[godoc](http://godoc.org/github.com/hashicorp/go-retryablehttp).
|
|
@ -0,0 +1,302 @@
|
|||
// The retryablehttp package provides a familiar HTTP client interface with
|
||||
// automatic retries and exponential backoff. It is a thin wrapper over the
|
||||
// standard net/http client library and exposes nearly the same public API.
|
||||
// This makes retryablehttp very easy to drop into existing programs.
|
||||
//
|
||||
// retryablehttp performs automatic retries under certain conditions. Mainly, if
|
||||
// an error is returned by the client (connection errors etc), or if a 500-range
|
||||
// response is received, then a retry is invoked. Otherwise, the response is
|
||||
// returned and left to the caller to interpret.
|
||||
//
|
||||
// The main difference from net/http is that requests which take a request body
|
||||
// (POST/PUT et. al) require an io.ReadSeeker to be provided. This enables the
|
||||
// request body to be "rewound" if the initial request fails so that the full
|
||||
// request can be attempted again.
|
||||
package retryablehttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-cleanhttp"
|
||||
)
|
||||
|
||||
var (
|
||||
// Default retry configuration
|
||||
defaultRetryWaitMin = 1 * time.Second
|
||||
defaultRetryWaitMax = 30 * time.Second
|
||||
defaultRetryMax = 4
|
||||
|
||||
// defaultClient is used for performing requests without explicitly making
|
||||
// a new client. It is purposely private to avoid modifications.
|
||||
defaultClient = NewClient()
|
||||
|
||||
// We need to consume response bodies to maintain http connections, but
|
||||
// limit the size we consume to respReadLimit.
|
||||
respReadLimit = int64(4096)
|
||||
)
|
||||
|
||||
// LenReader is an interface implemented by many in-memory io.Reader's. Used
|
||||
// for automatically sending the right Content-Length header when possible.
|
||||
type LenReader interface {
|
||||
Len() int
|
||||
}
|
||||
|
||||
// Request wraps the metadata needed to create HTTP requests.
|
||||
type Request struct {
|
||||
// body is a seekable reader over the request body payload. This is
|
||||
// used to rewind the request data in between retries.
|
||||
body io.ReadSeeker
|
||||
|
||||
// Embed an HTTP request directly. This makes a *Request act exactly
|
||||
// like an *http.Request so that all meta methods are supported.
|
||||
*http.Request
|
||||
}
|
||||
|
||||
// NewRequest creates a new wrapped request.
|
||||
func NewRequest(method, url string, body io.ReadSeeker) (*Request, error) {
|
||||
// Wrap the body in a noop ReadCloser if non-nil. This prevents the
|
||||
// reader from being closed by the HTTP client.
|
||||
var rcBody io.ReadCloser
|
||||
if body != nil {
|
||||
rcBody = ioutil.NopCloser(body)
|
||||
}
|
||||
|
||||
// Make the request with the noop-closer for the body.
|
||||
httpReq, err := http.NewRequest(method, url, rcBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if we can set the Content-Length automatically.
|
||||
if lr, ok := body.(LenReader); ok {
|
||||
httpReq.ContentLength = int64(lr.Len())
|
||||
}
|
||||
|
||||
return &Request{body, httpReq}, nil
|
||||
}
|
||||
|
||||
// RequestLogHook allows a function to run before each retry. The HTTP
|
||||
// request which will be made, and the retry number (0 for the initial
|
||||
// request) are available to users. The internal logger is exposed to
|
||||
// consumers.
|
||||
type RequestLogHook func(*log.Logger, *http.Request, int)
|
||||
|
||||
// ResponseLogHook is like RequestLogHook, but allows running a function
|
||||
// on each HTTP response. This function will be invoked at the end of
|
||||
// every HTTP request executed, regardless of whether a subsequent retry
|
||||
// needs to be performed or not. If the response body is read or closed
|
||||
// from this method, this will affect the response returned from Do().
|
||||
type ResponseLogHook func(*log.Logger, *http.Response)
|
||||
|
||||
// CheckRetry specifies a policy for handling retries. It is called
|
||||
// following each request with the response and error values returned by
|
||||
// the http.Client. If CheckRetry returns false, the Client stops retrying
|
||||
// and returns the response to the caller. If CheckRetry returns an error,
|
||||
// that error value is returned in lieu of the error from the request. The
|
||||
// Client will close any response body when retrying, but if the retry is
|
||||
// aborted it is up to the CheckResponse callback to properly close any
|
||||
// response body before returning.
|
||||
type CheckRetry func(resp *http.Response, err error) (bool, error)
|
||||
|
||||
// Client is used to make HTTP requests. It adds additional functionality
|
||||
// like automatic retries to tolerate minor outages.
|
||||
type Client struct {
|
||||
HTTPClient *http.Client // Internal HTTP client.
|
||||
Logger *log.Logger // Customer logger instance.
|
||||
|
||||
RetryWaitMin time.Duration // Minimum time to wait
|
||||
RetryWaitMax time.Duration // Maximum time to wait
|
||||
RetryMax int // Maximum number of retries
|
||||
|
||||
// RequestLogHook allows a user-supplied function to be called
|
||||
// before each retry.
|
||||
RequestLogHook RequestLogHook
|
||||
|
||||
// ResponseLogHook allows a user-supplied function to be called
|
||||
// with the response from each HTTP request executed.
|
||||
ResponseLogHook ResponseLogHook
|
||||
|
||||
// CheckRetry specifies the policy for handling retries, and is called
|
||||
// after each request. The default policy is DefaultRetryPolicy.
|
||||
CheckRetry CheckRetry
|
||||
}
|
||||
|
||||
// NewClient creates a new Client with default settings.
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
HTTPClient: cleanhttp.DefaultClient(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
RetryWaitMin: defaultRetryWaitMin,
|
||||
RetryWaitMax: defaultRetryWaitMax,
|
||||
RetryMax: defaultRetryMax,
|
||||
CheckRetry: DefaultRetryPolicy,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultRetryPolicy provides a default callback for Client.CheckRetry, which
|
||||
// will retry on connection errors and server errors.
|
||||
func DefaultRetryPolicy(resp *http.Response, err error) (bool, error) {
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
// Check the response code. We retry on 500-range responses to allow
|
||||
// the server time to recover, as 500's are typically not permanent
|
||||
// errors and may relate to outages on the server side. This will catch
|
||||
// invalid response codes as well, like 0 and 999.
|
||||
if resp.StatusCode == 0 || resp.StatusCode >= 500 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Do wraps calling an HTTP method with retries.
|
||||
func (c *Client) Do(req *Request) (*http.Response, error) {
|
||||
c.Logger.Printf("[DEBUG] %s %s", req.Method, req.URL)
|
||||
|
||||
for i := 0; ; i++ {
|
||||
var code int // HTTP response code
|
||||
|
||||
// Always rewind the request body when non-nil.
|
||||
if req.body != nil {
|
||||
if _, err := req.body.Seek(0, 0); err != nil {
|
||||
return nil, fmt.Errorf("failed to seek body: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.RequestLogHook != nil {
|
||||
c.RequestLogHook(c.Logger, req.Request, i)
|
||||
}
|
||||
|
||||
// Attempt the request
|
||||
resp, err := c.HTTPClient.Do(req.Request)
|
||||
|
||||
// Check if we should continue with retries.
|
||||
checkOK, checkErr := c.CheckRetry(resp, err)
|
||||
|
||||
if err != nil {
|
||||
c.Logger.Printf("[ERR] %s %s request failed: %v", req.Method, req.URL, err)
|
||||
} else {
|
||||
// Call this here to maintain the behavior of logging all requests,
|
||||
// even if CheckRetry signals to stop.
|
||||
if c.ResponseLogHook != nil {
|
||||
// Call the response logger function if provided.
|
||||
c.ResponseLogHook(c.Logger, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// Now decide if we should continue.
|
||||
if !checkOK {
|
||||
if checkErr != nil {
|
||||
err = checkErr
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// We're going to retry, consume any response to reuse the connection.
|
||||
if err == nil {
|
||||
c.drainBody(resp.Body)
|
||||
}
|
||||
|
||||
remain := c.RetryMax - i
|
||||
if remain == 0 {
|
||||
break
|
||||
}
|
||||
wait := backoff(c.RetryWaitMin, c.RetryWaitMax, i)
|
||||
desc := fmt.Sprintf("%s %s", req.Method, req.URL)
|
||||
if code > 0 {
|
||||
desc = fmt.Sprintf("%s (status: %d)", desc, code)
|
||||
}
|
||||
c.Logger.Printf("[DEBUG] %s: retrying in %s (%d left)", desc, wait, remain)
|
||||
time.Sleep(wait)
|
||||
}
|
||||
|
||||
// Return an error if we fall out of the retry loop
|
||||
return nil, fmt.Errorf("%s %s giving up after %d attempts",
|
||||
req.Method, req.URL, c.RetryMax+1)
|
||||
}
|
||||
|
||||
// Try to read the response body so we can reuse this connection.
|
||||
func (c *Client) drainBody(body io.ReadCloser) {
|
||||
defer body.Close()
|
||||
_, err := io.Copy(ioutil.Discard, io.LimitReader(body, respReadLimit))
|
||||
if err != nil {
|
||||
c.Logger.Printf("[ERR] error reading response body: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get is a shortcut for doing a GET request without making a new client.
|
||||
func Get(url string) (*http.Response, error) {
|
||||
return defaultClient.Get(url)
|
||||
}
|
||||
|
||||
// Get is a convenience helper for doing simple GET requests.
|
||||
func (c *Client) Get(url string) (*http.Response, error) {
|
||||
req, err := NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
// Head is a shortcut for doing a HEAD request without making a new client.
|
||||
func Head(url string) (*http.Response, error) {
|
||||
return defaultClient.Head(url)
|
||||
}
|
||||
|
||||
// Head is a convenience method for doing simple HEAD requests.
|
||||
func (c *Client) Head(url string) (*http.Response, error) {
|
||||
req, err := NewRequest("HEAD", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
// Post is a shortcut for doing a POST request without making a new client.
|
||||
func Post(url, bodyType string, body io.ReadSeeker) (*http.Response, error) {
|
||||
return defaultClient.Post(url, bodyType, body)
|
||||
}
|
||||
|
||||
// Post is a convenience method for doing simple POST requests.
|
||||
func (c *Client) Post(url, bodyType string, body io.ReadSeeker) (*http.Response, error) {
|
||||
req, err := NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", bodyType)
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
// PostForm is a shortcut to perform a POST with form data without creating
|
||||
// a new client.
|
||||
func PostForm(url string, data url.Values) (*http.Response, error) {
|
||||
return defaultClient.PostForm(url, data)
|
||||
}
|
||||
|
||||
// PostForm is a convenience method for doing simple POST operations using
|
||||
// pre-filled url.Values form data.
|
||||
func (c *Client) PostForm(url string, data url.Values) (*http.Response, error) {
|
||||
return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
||||
}
|
||||
|
||||
// backoff is used to calculate how long to sleep before retrying
|
||||
// after observing failures. It takes the minimum/maximum wait time and
|
||||
// iteration, and returns the duration to wait.
|
||||
func backoff(min, max time.Duration, iter int) time.Duration {
|
||||
mult := math.Pow(2, float64(iter)) * float64(min)
|
||||
sleep := time.Duration(mult)
|
||||
if float64(sleep) != mult || sleep > max {
|
||||
sleep = max
|
||||
}
|
||||
return sleep
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
gocommon
|
||||
========
|
||||
|
||||
Common Go library for Joyent's Triton and Manta.
|
||||
|
||||
[![wercker status](https://app.wercker.com/status/2f63bf7f68bfdd46b979abad19c0bee0/s/master "wercker status")](https://app.wercker.com/project/byKey/2f63bf7f68bfdd46b979abad19c0bee0)
|
||||
|
||||
## Installation
|
||||
|
||||
Use `go-get` to install gocommon.
|
||||
```
|
||||
go get github.com/joyent/gocommon
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Auto-generated documentation can be found on godoc.
|
||||
|
||||
- [github.com/joyent/gocommon](http://godoc.org/github.com/joyent/gocommon)
|
||||
- [github.com/joyent/gocommon/client](http://godoc.org/github.com/joyent/client)
|
||||
- [github.com/joyent/gocommon/errors](http://godoc.org/github.com/joyent/gocommon/errors)
|
||||
- [github.com/joyent/gocommon/http](http://godoc.org/github.com/joyent/gocommon/http)
|
||||
- [github.com/joyent/gocommon/jpc](http://godoc.org/github.com/joyent/gocommon/jpc)
|
||||
- [github.com/joyent/gocommon/testing](http://godoc.org/github.com/joyent/gocommon/testing)
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Report bugs and request features using [GitHub Issues](https://github.com/joyent/gocommon/issues), or contribute code via a [GitHub Pull Request](https://github.com/joyent/gocommon/pulls). Changes will be code reviewed before merging. In the near future, automated tests will be run, but in the meantime please `go fmt`, `go lint`, and test all contributions.
|
||||
|
||||
|
||||
## Developing
|
||||
|
||||
This library assumes a Go development environment setup based on [How to Write Go Code](https://golang.org/doc/code.html). Your GOPATH environment variable should be pointed at your workspace directory.
|
||||
|
||||
You can now use `go get github.com/joyent/gocommon` to install the repository to the correct location, but if you are intending on contributing back a change you may want to consider cloning the repository via git yourself. This way you can have a single source tree for all Joyent Go projects with each repo having two remotes -- your own fork on GitHub and the upstream origin.
|
||||
|
||||
For example if your GOPATH is `~/src/joyent/go` and you're working on multiple repos then that directory tree might look like:
|
||||
|
||||
```
|
||||
~/src/joyent/go/
|
||||
|_ pkg/
|
||||
|_ src/
|
||||
|_ github.com
|
||||
|_ joyent
|
||||
|_ gocommon
|
||||
|_ gomanta
|
||||
|_ gosdc
|
||||
|_ gosign
|
||||
```
|
||||
|
||||
### Recommended Setup
|
||||
|
||||
```
|
||||
$ mkdir -p ${GOPATH}/src/github.com/joyent
|
||||
$ cd ${GOPATH}/src/github.com/joyent
|
||||
$ git clone git@github.com:<yourname>/gocommon.git
|
||||
|
||||
# fetch dependencies
|
||||
$ git clone git@github.com:<yourname>/gosign.git
|
||||
$ go get -v -t ./...
|
||||
|
||||
# add upstream remote
|
||||
$ cd gocommon
|
||||
$ git remote add upstream git@github.com:joyent/gocommon.git
|
||||
$ git remote -v
|
||||
origin git@github.com:<yourname>/gocommon.git (fetch)
|
||||
origin git@github.com:<yourname>/gocommon.git (push)
|
||||
upstream git@github.com:joyent/gocommon.git (fetch)
|
||||
upstream git@github.com:joyent/gocommon.git (push)
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
The library needs values for the `SDC_URL`, `MANTA_URL`, `MANTA_KEY_ID` and `SDC_KEY_ID` environment variables even though the tests are run locally. You can generate a temporary key and use its fingerprint for tests without adding the key to your Triton Cloud account.
|
||||
|
||||
```
|
||||
# create a temporary key
|
||||
ssh-keygen -b 2048 -C "Testing Key" -f /tmp/id_rsa -t rsa -P ""
|
||||
|
||||
# set up environment
|
||||
# note: leave the -E md5 argument off on older ssh-keygen
|
||||
export KEY_ID=$(ssh-keygen -E md5 -lf /tmp/id_rsa | awk -F' ' '{print $2}' | cut -d':' -f2-)
|
||||
export SDC_KEY_ID=${KEY_ID}
|
||||
export MANTA_KEY_ID=${KEY_ID}
|
||||
export SDC_URL=https://us-east-1.api.joyent.com
|
||||
export MANTA_URL=https://us-east.manta.joyent.com
|
||||
|
||||
cd ${GOPATH}/src/github.com/joyent/gocommon
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### Build the Library
|
||||
|
||||
```
|
||||
cd ${GOPATH}/src/github.com/joyent/gocommon
|
||||
go build ./...
|
||||
```
|
|
@ -1,110 +0,0 @@
|
|||
//
|
||||
// gocommon - Go library to interact with the JoyentCloud
|
||||
//
|
||||
//
|
||||
// Copyright (c) 2013 Joyent Inc.
|
||||
//
|
||||
// Written by Daniele Stroppa <daniele.stroppa@joyent.com>
|
||||
//
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
joyenthttp "github.com/joyent/gocommon/http"
|
||||
"github.com/joyent/gosign/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
// The HTTP request methods.
|
||||
GET = "GET"
|
||||
POST = "POST"
|
||||
PUT = "PUT"
|
||||
DELETE = "DELETE"
|
||||
HEAD = "HEAD"
|
||||
COPY = "COPY"
|
||||
)
|
||||
|
||||
// Client implementations sends service requests to the JoyentCloud.
|
||||
type Client interface {
|
||||
SendRequest(method, apiCall, rfc1123Date string, request *joyenthttp.RequestData, response *joyenthttp.ResponseData) (err error)
|
||||
// MakeServiceURL prepares a full URL to a service endpoint, with optional
|
||||
// URL parts. It uses the first endpoint it can find for the given service type.
|
||||
MakeServiceURL(parts []string) string
|
||||
SignURL(path string, expires time.Time) (string, error)
|
||||
}
|
||||
|
||||
// This client sends requests without authenticating.
|
||||
type client struct {
|
||||
mu sync.Mutex
|
||||
logger *log.Logger
|
||||
baseURL string
|
||||
creds *auth.Credentials
|
||||
httpClient *joyenthttp.Client
|
||||
}
|
||||
|
||||
var _ Client = (*client)(nil)
|
||||
|
||||
func newClient(baseURL string, credentials *auth.Credentials, httpClient *joyenthttp.Client, logger *log.Logger) Client {
|
||||
client := client{baseURL: baseURL, logger: logger, creds: credentials, httpClient: httpClient}
|
||||
return &client
|
||||
}
|
||||
|
||||
func NewClient(baseURL, apiVersion string, credentials *auth.Credentials, logger *log.Logger) Client {
|
||||
sharedHttpClient := joyenthttp.New(credentials, apiVersion, logger)
|
||||
return newClient(baseURL, credentials, sharedHttpClient, logger)
|
||||
}
|
||||
|
||||
func (c *client) sendRequest(method, url, rfc1123Date string, request *joyenthttp.RequestData, response *joyenthttp.ResponseData) (err error) {
|
||||
if request.ReqValue != nil || response.RespValue != nil {
|
||||
err = c.httpClient.JsonRequest(method, url, rfc1123Date, request, response)
|
||||
} else {
|
||||
err = c.httpClient.BinaryRequest(method, url, rfc1123Date, request, response)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *client) SendRequest(method, apiCall, rfc1123Date string, request *joyenthttp.RequestData, response *joyenthttp.ResponseData) (err error) {
|
||||
url := c.MakeServiceURL([]string{c.creds.UserAuthentication.User, apiCall})
|
||||
err = c.sendRequest(method, url, rfc1123Date, request, response)
|
||||
return
|
||||
}
|
||||
|
||||
func makeURL(base string, parts []string) string {
|
||||
if !strings.HasSuffix(base, "/") && len(parts) > 0 {
|
||||
base += "/"
|
||||
}
|
||||
if parts[1] == "" {
|
||||
return base + parts[0]
|
||||
}
|
||||
return base + strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
func (c *client) MakeServiceURL(parts []string) string {
|
||||
return makeURL(c.baseURL, parts)
|
||||
}
|
||||
|
||||
func (c *client) SignURL(path string, expires time.Time) (string, error) {
|
||||
parsedURL, err := url.Parse(c.baseURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("bad Manta endpoint URL %q: %v", c.baseURL, err)
|
||||
}
|
||||
userAuthentication := c.creds.UserAuthentication
|
||||
userAuthentication.Algorithm = "RSA-SHA1"
|
||||
keyId := url.QueryEscape(fmt.Sprintf("/%s/keys/%s", userAuthentication.User, c.creds.MantaKeyId))
|
||||
params := fmt.Sprintf("algorithm=%s&expires=%d&keyId=%s", userAuthentication.Algorithm, expires.Unix(), keyId)
|
||||
signingLine := fmt.Sprintf("GET\n%s\n%s\n%s", parsedURL.Host, path, params)
|
||||
|
||||
signature, err := auth.GetSignature(userAuthentication, signingLine)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot generate URL signature: %v", err)
|
||||
}
|
||||
signedURL := fmt.Sprintf("%s%s?%s&signature=%s", c.baseURL, path, params, url.QueryEscape(signature))
|
||||
return signedURL, nil
|
||||
}
|
|
@ -1,292 +0,0 @@
|
|||
//
|
||||
// gocommon - Go library to interact with the JoyentCloud
|
||||
// This package provides an Error implementation which knows about types of error, and which has support
|
||||
// for error causes.
|
||||
//
|
||||
// Copyright (c) 2013 Joyent Inc.
|
||||
//
|
||||
// Written by Daniele Stroppa <daniele.stroppa@joyent.com>
|
||||
//
|
||||
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Code string
|
||||
|
||||
const (
|
||||
// Public available error types.
|
||||
// These errors are provided because they are specifically required by business logic in the callers.
|
||||
BadRequestError = Code("BadRequest")
|
||||
InternalErrorError = Code("InternalError")
|
||||
InvalidArgumentError = Code("InvalidArgument")
|
||||
InvalidCredentialsError = Code("InvalidCredentials")
|
||||
InvalidHeaderError = Code("InvalidHeader")
|
||||
InvalidVersionError = Code("InvalidVersion")
|
||||
MissingParameterError = Code("MissinParameter")
|
||||
NotAuthorizedError = Code("NotAuthorized")
|
||||
RequestThrottledError = Code("RequestThrottled")
|
||||
RequestTooLargeError = Code("RequestTooLarge")
|
||||
RequestMovedError = Code("RequestMoved")
|
||||
ResourceNotFoundError = Code("ResourceNotFound")
|
||||
UnknownErrorError = Code("UnkownError")
|
||||
)
|
||||
|
||||
// Error instances store an optional error cause.
|
||||
type Error interface {
|
||||
error
|
||||
Cause() error
|
||||
}
|
||||
|
||||
type gojoyentError struct {
|
||||
error
|
||||
errcode Code
|
||||
cause error
|
||||
}
|
||||
|
||||
// Type checks.
|
||||
var _ Error = (*gojoyentError)(nil)
|
||||
|
||||
// Code returns the error code.
|
||||
func (err *gojoyentError) code() Code {
|
||||
if err.errcode != UnknownErrorError {
|
||||
return err.errcode
|
||||
}
|
||||
if e, ok := err.cause.(*gojoyentError); ok {
|
||||
return e.code()
|
||||
}
|
||||
return UnknownErrorError
|
||||
}
|
||||
|
||||
// Cause returns the error cause.
|
||||
func (err *gojoyentError) Cause() error {
|
||||
return err.cause
|
||||
}
|
||||
|
||||
// CausedBy returns true if this error or its cause are of the specified error code.
|
||||
func (err *gojoyentError) causedBy(code Code) bool {
|
||||
if err.code() == code {
|
||||
return true
|
||||
}
|
||||
if cause, ok := err.cause.(*gojoyentError); ok {
|
||||
return cause.code() == code
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Error fulfills the error interface, taking account of any caused by error.
|
||||
func (err *gojoyentError) Error() string {
|
||||
if err.cause != nil {
|
||||
return fmt.Sprintf("%v\ncaused by: %v", err.error, err.cause)
|
||||
}
|
||||
return err.error.Error()
|
||||
}
|
||||
|
||||
func IsBadRequest(err error) bool {
|
||||
if e, ok := err.(*gojoyentError); ok {
|
||||
return e.causedBy(BadRequestError)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsInternalError(err error) bool {
|
||||
if e, ok := err.(*gojoyentError); ok {
|
||||
return e.causedBy(InternalErrorError)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsInvalidArgument(err error) bool {
|
||||
if e, ok := err.(*gojoyentError); ok {
|
||||
return e.causedBy(InvalidArgumentError)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsInvalidCredentials(err error) bool {
|
||||
if e, ok := err.(*gojoyentError); ok {
|
||||
return e.causedBy(InvalidCredentialsError)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsInvalidHeader(err error) bool {
|
||||
if e, ok := err.(*gojoyentError); ok {
|
||||
return e.causedBy(InvalidHeaderError)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsInvalidVersion(err error) bool {
|
||||
if e, ok := err.(*gojoyentError); ok {
|
||||
return e.causedBy(InvalidVersionError)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsMissingParameter(err error) bool {
|
||||
if e, ok := err.(*gojoyentError); ok {
|
||||
return e.causedBy(MissingParameterError)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsNotAuthorized(err error) bool {
|
||||
if e, ok := err.(*gojoyentError); ok {
|
||||
return e.causedBy(NotAuthorizedError)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsRequestThrottled(err error) bool {
|
||||
if e, ok := err.(*gojoyentError); ok {
|
||||
return e.causedBy(RequestThrottledError)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsRequestTooLarge(err error) bool {
|
||||
if e, ok := err.(*gojoyentError); ok {
|
||||
return e.causedBy(RequestTooLargeError)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsRequestMoved(err error) bool {
|
||||
if e, ok := err.(*gojoyentError); ok {
|
||||
return e.causedBy(RequestMovedError)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsResourceNotFound(err error) bool {
|
||||
if e, ok := err.(*gojoyentError); ok {
|
||||
return e.causedBy(ResourceNotFoundError)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsUnknownError(err error) bool {
|
||||
if e, ok := err.(*gojoyentError); ok {
|
||||
return e.causedBy(UnknownErrorError)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// New creates a new Error instance with the specified cause.
|
||||
func makeErrorf(code Code, cause error, format string, args ...interface{}) Error {
|
||||
return &gojoyentError{
|
||||
errcode: code,
|
||||
error: fmt.Errorf(format, args...),
|
||||
cause: cause,
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new UnknownError Error instance with the specified cause.
|
||||
func Newf(cause error, format string, args ...interface{}) Error {
|
||||
return makeErrorf(UnknownErrorError, cause, format, args...)
|
||||
}
|
||||
|
||||
// New creates a new BadRequest Error instance with the specified cause.
|
||||
func NewBadRequestf(cause error, context interface{}, format string, args ...interface{}) Error {
|
||||
if format == "" {
|
||||
format = fmt.Sprintf("Bad Request: %s", context)
|
||||
}
|
||||
return makeErrorf(BadRequestError, cause, format, args...)
|
||||
}
|
||||
|
||||
// New creates a new InternalError Error instance with the specified cause.
|
||||
func NewInternalErrorf(cause error, context interface{}, format string, args ...interface{}) Error {
|
||||
if format == "" {
|
||||
format = fmt.Sprintf("Internal Error: %s", context)
|
||||
}
|
||||
return makeErrorf(InternalErrorError, cause, format, args...)
|
||||
}
|
||||
|
||||
// New creates a new InvalidArgument Error instance with the specified cause.
|
||||
func NewInvalidArgumentf(cause error, context interface{}, format string, args ...interface{}) Error {
|
||||
if format == "" {
|
||||
format = fmt.Sprintf("Invalid Argument: %s", context)
|
||||
}
|
||||
return makeErrorf(InvalidArgumentError, cause, format, args...)
|
||||
}
|
||||
|
||||
// New creates a new InvalidCredentials Error instance with the specified cause.
|
||||
func NewInvalidCredentialsf(cause error, context interface{}, format string, args ...interface{}) Error {
|
||||
if format == "" {
|
||||
format = fmt.Sprintf("Invalid Credentials: %s", context)
|
||||
}
|
||||
return makeErrorf(InvalidCredentialsError, cause, format, args...)
|
||||
}
|
||||
|
||||
// New creates a new InvalidHeader Error instance with the specified cause.
|
||||
func NewInvalidHeaderf(cause error, context interface{}, format string, args ...interface{}) Error {
|
||||
if format == "" {
|
||||
format = fmt.Sprintf("Invalid Header: %s", context)
|
||||
}
|
||||
return makeErrorf(InvalidHeaderError, cause, format, args...)
|
||||
}
|
||||
|
||||
// New creates a new InvalidVersion Error instance with the specified cause.
|
||||
func NewInvalidVersionf(cause error, context interface{}, format string, args ...interface{}) Error {
|
||||
if format == "" {
|
||||
format = fmt.Sprintf("Invalid Version: %s", context)
|
||||
}
|
||||
return makeErrorf(InvalidVersionError, cause, format, args...)
|
||||
}
|
||||
|
||||
// New creates a new MissingParameter Error instance with the specified cause.
|
||||
func NewMissingParameterf(cause error, context interface{}, format string, args ...interface{}) Error {
|
||||
if format == "" {
|
||||
format = fmt.Sprintf("Missing Parameter: %s", context)
|
||||
}
|
||||
return makeErrorf(MissingParameterError, cause, format, args...)
|
||||
}
|
||||
|
||||
// New creates a new NotAuthorized Error instance with the specified cause.
|
||||
func NewNotAuthorizedf(cause error, context interface{}, format string, args ...interface{}) Error {
|
||||
if format == "" {
|
||||
format = fmt.Sprintf("Not Authorized: %s", context)
|
||||
}
|
||||
return makeErrorf(NotAuthorizedError, cause, format, args...)
|
||||
}
|
||||
|
||||
// New creates a new RequestThrottled Error instance with the specified cause.
|
||||
func NewRequestThrottledf(cause error, context interface{}, format string, args ...interface{}) Error {
|
||||
if format == "" {
|
||||
format = fmt.Sprintf("Request Throttled: %s", context)
|
||||
}
|
||||
return makeErrorf(RequestThrottledError, cause, format, args...)
|
||||
}
|
||||
|
||||
// New creates a new RequestTooLarge Error instance with the specified cause.
|
||||
func NewRequestTooLargef(cause error, context interface{}, format string, args ...interface{}) Error {
|
||||
if format == "" {
|
||||
format = fmt.Sprintf("Request Too Large: %s", context)
|
||||
}
|
||||
return makeErrorf(RequestTooLargeError, cause, format, args...)
|
||||
}
|
||||
|
||||
// New creates a new RequestMoved Error instance with the specified cause.
|
||||
func NewRequestMovedf(cause error, context interface{}, format string, args ...interface{}) Error {
|
||||
if format == "" {
|
||||
format = fmt.Sprintf("Request Moved: %s", context)
|
||||
}
|
||||
return makeErrorf(RequestMovedError, cause, format, args...)
|
||||
}
|
||||
|
||||
// New creates a new ResourceNotFound Error instance with the specified cause.
|
||||
func NewResourceNotFoundf(cause error, context interface{}, format string, args ...interface{}) Error {
|
||||
if format == "" {
|
||||
format = fmt.Sprintf("Resource Not Found: %s", context)
|
||||
}
|
||||
return makeErrorf(ResourceNotFoundError, cause, format, args...)
|
||||
}
|
||||
|
||||
// New creates a new UnknownError Error instance with the specified cause.
|
||||
func NewUnknownErrorf(cause error, context interface{}, format string, args ...interface{}) Error {
|
||||
if format == "" {
|
||||
format = fmt.Sprintf("Unknown Error: %s", context)
|
||||
}
|
||||
return makeErrorf(UnknownErrorError, cause, format, args...)
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* The gocommon package collects common packages to interact with the Joyent Public Cloud and Joyent Manta services.
|
||||
*
|
||||
* The gocommon package is structured as follow:
|
||||
*
|
||||
* - gocommon/client. Client for sending requests.
|
||||
* - gocommon/errors. Joyent specific errors.
|
||||
* - gocommon/http. HTTP client for sending requests.
|
||||
* - gocommon/jpc. This package provides common structures and functions across packages.
|
||||
* - gocommon/testing. Testing Suite for local testing.
|
||||
*
|
||||
* Copyright (c) 2016 Joyent Inc.
|
||||
* Written by Daniele Stroppa <daniele.stroppa@joyent.com>
|
||||
*
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package gocommon
|
|
@ -1,427 +0,0 @@
|
|||
//
|
||||
// gocommon - Go library to interact with the JoyentCloud
|
||||
// An HTTP Client which sends json and binary requests, handling data marshalling and response processing.
|
||||
//
|
||||
// Copyright (c) 2013 Joyent Inc.
|
||||
//
|
||||
// Written by Daniele Stroppa <daniele.stroppa@joyent.com>
|
||||
//
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/joyent/gocommon"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
"github.com/joyent/gocommon/jpc"
|
||||
"github.com/joyent/gosign/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
contentTypeJSON = "application/json"
|
||||
contentTypeOctetStream = "application/octet-stream"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
http.Client
|
||||
maxSendAttempts int
|
||||
credentials *auth.Credentials
|
||||
apiVersion string
|
||||
logger *log.Logger
|
||||
trace bool
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
func (e *ErrorResponse) Error() string {
|
||||
return fmt.Sprintf("Failed: %d: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
type ErrorWrapper struct {
|
||||
Error ErrorResponse `json:"error"`
|
||||
}
|
||||
|
||||
type RequestData struct {
|
||||
ReqHeaders http.Header
|
||||
Params *url.Values
|
||||
ReqValue interface{}
|
||||
ReqReader io.Reader
|
||||
ReqLength int
|
||||
}
|
||||
|
||||
type ResponseData struct {
|
||||
ExpectedStatus []int
|
||||
RespHeaders *http.Header
|
||||
RespValue interface{}
|
||||
RespReader io.ReadCloser
|
||||
}
|
||||
|
||||
const (
|
||||
// The maximum number of times to try sending a request before we give up
|
||||
// (assuming any unsuccessful attempts can be sensibly tried again).
|
||||
MaxSendAttempts = 3
|
||||
)
|
||||
|
||||
// New returns a new http *Client using the default net/http client.
|
||||
func New(credentials *auth.Credentials, apiVersion string, logger *log.Logger) *Client {
|
||||
return &Client{*http.DefaultClient, MaxSendAttempts, credentials, apiVersion, logger, false}
|
||||
}
|
||||
|
||||
// SetTrace allows control over whether requests will write their
|
||||
// contents to the logger supplied during construction. Note that this
|
||||
// is not safe to call from multiple go-routines.
|
||||
func (client *Client) SetTrace(traceEnabled bool) {
|
||||
client.trace = traceEnabled
|
||||
}
|
||||
|
||||
func gojoyentAgent() string {
|
||||
return fmt.Sprintf("gocommon (%s)", gocommon.Version)
|
||||
}
|
||||
|
||||
func createHeaders(extraHeaders http.Header, credentials *auth.Credentials, contentType, rfc1123Date,
|
||||
apiVersion string, isMantaRequest bool) (http.Header, error) {
|
||||
|
||||
headers := make(http.Header)
|
||||
if extraHeaders != nil {
|
||||
for header, values := range extraHeaders {
|
||||
for _, value := range values {
|
||||
headers.Add(header, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if extraHeaders.Get("Content-Type") == "" {
|
||||
headers.Add("Content-Type", contentType)
|
||||
}
|
||||
if extraHeaders.Get("Accept") == "" {
|
||||
headers.Add("Accept", contentType)
|
||||
}
|
||||
if rfc1123Date != "" {
|
||||
headers.Set("Date", rfc1123Date)
|
||||
} else {
|
||||
headers.Set("Date", getDateForRegion(credentials, isMantaRequest))
|
||||
}
|
||||
authHeaders, err := auth.CreateAuthorizationHeader(headers, credentials, isMantaRequest)
|
||||
if err != nil {
|
||||
return http.Header{}, err
|
||||
}
|
||||
headers.Set("Authorization", authHeaders)
|
||||
if apiVersion != "" {
|
||||
headers.Set("X-Api-Version", apiVersion)
|
||||
}
|
||||
headers.Add("User-Agent", gojoyentAgent())
|
||||
return headers, nil
|
||||
}
|
||||
|
||||
func getDateForRegion(credentials *auth.Credentials, isManta bool) string {
|
||||
if isManta {
|
||||
location, _ := time.LoadLocation(jpc.Locations["us-east-1"])
|
||||
return time.Now().In(location).Format(time.RFC1123)
|
||||
} else {
|
||||
location, _ := time.LoadLocation(jpc.Locations[credentials.Region()])
|
||||
return time.Now().In(location).Format(time.RFC1123)
|
||||
}
|
||||
}
|
||||
|
||||
// JsonRequest JSON encodes and sends the object in reqData.ReqValue (if any) to the specified URL.
|
||||
// Optional method arguments are passed using the RequestData object.
|
||||
// Relevant RequestData fields:
|
||||
// ReqHeaders: additional HTTP header values to add to the request.
|
||||
// ExpectedStatus: the allowed HTTP response status values, else an error is returned.
|
||||
// ReqValue: the data object to send.
|
||||
// RespValue: the data object to decode the result into.
|
||||
func (c *Client) JsonRequest(method, url, rfc1123Date string, request *RequestData, response *ResponseData) (err error) {
|
||||
err = nil
|
||||
var body []byte
|
||||
if request.Params != nil {
|
||||
url += "?" + request.Params.Encode()
|
||||
}
|
||||
if request.ReqValue != nil {
|
||||
body, err = json.Marshal(request.ReqValue)
|
||||
if err != nil {
|
||||
err = errors.Newf(err, "failed marshalling the request body")
|
||||
return
|
||||
}
|
||||
}
|
||||
headers, err := createHeaders(request.ReqHeaders, c.credentials, contentTypeJSON, rfc1123Date, c.apiVersion,
|
||||
isMantaRequest(url, c.credentials.UserAuthentication.User))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
respBody, respHeader, err := c.sendRequest(
|
||||
method, url, bytes.NewReader(body), len(body), headers, response.ExpectedStatus, c.logger)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer respBody.Close()
|
||||
respData, err := ioutil.ReadAll(respBody)
|
||||
if err != nil {
|
||||
err = errors.Newf(err, "failed reading the response body")
|
||||
return
|
||||
}
|
||||
|
||||
if len(respData) > 0 {
|
||||
if response.RespValue != nil {
|
||||
if dest, ok := response.RespValue.(*[]byte); ok {
|
||||
*dest = respData
|
||||
//err = decodeJSON(bytes.NewReader(respData), false, response.RespValue)
|
||||
//if err != nil {
|
||||
// err = errors.Newf(err, "failed unmarshaling/decoding the response body: %s", respData)
|
||||
//}
|
||||
} else {
|
||||
err = json.Unmarshal(respData, response.RespValue)
|
||||
if err != nil {
|
||||
err = decodeJSON(bytes.NewReader(respData), true, response.RespValue)
|
||||
if err != nil {
|
||||
err = errors.Newf(err, "failed unmarshaling/decoding the response body: %s", respData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if respHeader != nil {
|
||||
response.RespHeaders = respHeader
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func decodeJSON(r io.Reader, multiple bool, into interface{}) error {
|
||||
d := json.NewDecoder(r)
|
||||
if multiple {
|
||||
return decodeStream(d, into)
|
||||
}
|
||||
return d.Decode(into)
|
||||
}
|
||||
|
||||
func decodeStream(d *json.Decoder, into interface{}) error {
|
||||
t := reflect.TypeOf(into)
|
||||
if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Slice {
|
||||
return fmt.Errorf("unexpected type %s", t)
|
||||
}
|
||||
elemType := t.Elem().Elem()
|
||||
slice := reflect.ValueOf(into).Elem()
|
||||
for {
|
||||
val := reflect.New(elemType)
|
||||
if err := d.Decode(val.Interface()); err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
slice.Set(reflect.Append(slice, val.Elem()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sends the byte array in reqData.ReqValue (if any) to the specified URL.
|
||||
// Optional method arguments are passed using the RequestData object.
|
||||
// Relevant RequestData fields:
|
||||
// ReqHeaders: additional HTTP header values to add to the request.
|
||||
// ExpectedStatus: the allowed HTTP response status values, else an error is returned.
|
||||
// ReqReader: an io.Reader providing the bytes to send.
|
||||
// RespReader: assigned an io.ReadCloser instance used to read the returned data..
|
||||
func (c *Client) BinaryRequest(method, url, rfc1123Date string, request *RequestData, response *ResponseData) (err error) {
|
||||
err = nil
|
||||
|
||||
if request.Params != nil {
|
||||
url += "?" + request.Params.Encode()
|
||||
}
|
||||
headers, err := createHeaders(request.ReqHeaders, c.credentials, contentTypeOctetStream, rfc1123Date,
|
||||
c.apiVersion, isMantaRequest(url, c.credentials.UserAuthentication.User))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
respBody, respHeader, err := c.sendRequest(
|
||||
method, url, request.ReqReader, request.ReqLength, headers, response.ExpectedStatus, c.logger)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if response.RespReader != nil {
|
||||
response.RespReader = respBody
|
||||
}
|
||||
if respHeader != nil {
|
||||
response.RespHeaders = respHeader
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Sends the specified request to URL and checks that the HTTP response status is as expected.
|
||||
// reqReader: a reader returning the data to send.
|
||||
// length: the number of bytes to send.
|
||||
// headers: HTTP headers to include with the request.
|
||||
// expectedStatus: a slice of allowed response status codes.
|
||||
func (c *Client) sendRequest(method, URL string, reqReader io.Reader, length int, headers http.Header,
|
||||
expectedStatus []int, logger *log.Logger) (rc io.ReadCloser, respHeader *http.Header, err error) {
|
||||
reqData := make([]byte, length)
|
||||
if reqReader != nil {
|
||||
nrRead, err := io.ReadFull(reqReader, reqData)
|
||||
if err != nil {
|
||||
err = errors.Newf(err, "failed reading the request data, read %v of %v bytes", nrRead, length)
|
||||
return rc, respHeader, err
|
||||
}
|
||||
}
|
||||
rawResp, err := c.sendRateLimitedRequest(method, URL, headers, reqData, logger)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if logger != nil && c.trace {
|
||||
logger.Printf("Request: %s %s\n", method, URL)
|
||||
logger.Printf("Request header: %s\n", headers)
|
||||
logger.Printf("Request body: %s\n", reqData)
|
||||
logger.Printf("Response: %s\n", rawResp.Status)
|
||||
logger.Printf("Response header: %s\n", rawResp.Header)
|
||||
logger.Printf("Response body: %s\n", rawResp.Body)
|
||||
logger.Printf("Response error: %s\n", err)
|
||||
}
|
||||
|
||||
foundStatus := false
|
||||
if len(expectedStatus) == 0 {
|
||||
expectedStatus = []int{http.StatusOK}
|
||||
}
|
||||
for _, status := range expectedStatus {
|
||||
if rawResp.StatusCode == status {
|
||||
foundStatus = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundStatus && len(expectedStatus) > 0 {
|
||||
err = handleError(URL, rawResp)
|
||||
rawResp.Body.Close()
|
||||
return
|
||||
}
|
||||
return rawResp.Body, &rawResp.Header, err
|
||||
}
|
||||
|
||||
func (c *Client) sendRateLimitedRequest(method, URL string, headers http.Header, reqData []byte,
|
||||
logger *log.Logger) (resp *http.Response, err error) {
|
||||
for i := 0; i < c.maxSendAttempts; i++ {
|
||||
var reqReader io.Reader
|
||||
if reqData != nil {
|
||||
reqReader = bytes.NewReader(reqData)
|
||||
}
|
||||
req, err := http.NewRequest(method, URL, reqReader)
|
||||
if err != nil {
|
||||
err = errors.Newf(err, "failed creating the request %s", URL)
|
||||
return nil, err
|
||||
}
|
||||
// Setting req.Close to true to avoid malformed HTTP version "nullHTTP/1.1" error
|
||||
// See http://stackoverflow.com/questions/17714494/golang-http-request-results-in-eof-errors-when-making-multiple-requests-successi
|
||||
req.Close = true
|
||||
for header, values := range headers {
|
||||
for _, value := range values {
|
||||
req.Header.Add(header, value)
|
||||
}
|
||||
}
|
||||
req.ContentLength = int64(len(reqData))
|
||||
resp, err = c.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Newf(err, "failed executing the request %s", URL)
|
||||
}
|
||||
if resp.StatusCode != http.StatusRequestEntityTooLarge || resp.Header.Get("Retry-After") == "" {
|
||||
return resp, nil
|
||||
}
|
||||
resp.Body.Close()
|
||||
retryAfter, err := strconv.ParseFloat(resp.Header.Get("Retry-After"), 64)
|
||||
if err != nil {
|
||||
return nil, errors.Newf(err, "Invalid Retry-After header %s", URL)
|
||||
}
|
||||
if retryAfter == 0 {
|
||||
return nil, errors.Newf(err, "Resource limit exeeded at URL %s", URL)
|
||||
}
|
||||
if logger != nil {
|
||||
logger.Println("Too many requests, retrying in %dms.", int(retryAfter*1000))
|
||||
}
|
||||
time.Sleep(time.Duration(retryAfter) * time.Second)
|
||||
}
|
||||
return nil, errors.Newf(err, "Maximum number of attempts (%d) reached sending request to %s", c.maxSendAttempts, URL)
|
||||
}
|
||||
|
||||
type HttpError struct {
|
||||
StatusCode int
|
||||
Data map[string][]string
|
||||
Url string
|
||||
ResponseMessage string
|
||||
}
|
||||
|
||||
func (e *HttpError) Error() string {
|
||||
return fmt.Sprintf("request %q returned unexpected status %d with body %q",
|
||||
e.Url,
|
||||
e.StatusCode,
|
||||
e.ResponseMessage,
|
||||
)
|
||||
}
|
||||
|
||||
// The HTTP response status code was not one of those expected, so we construct an error.
|
||||
// NotFound (404) codes have their own NotFound error type.
|
||||
// We also make a guess at duplicate value errors.
|
||||
func handleError(URL string, resp *http.Response) error {
|
||||
errBytes, _ := ioutil.ReadAll(resp.Body)
|
||||
errInfo := string(errBytes)
|
||||
// Check if we have a JSON representation of the failure, if so decode it.
|
||||
if resp.Header.Get("Content-Type") == contentTypeJSON {
|
||||
var errResponse ErrorResponse
|
||||
if err := json.Unmarshal(errBytes, &errResponse); err == nil {
|
||||
errInfo = errResponse.Message
|
||||
}
|
||||
}
|
||||
httpError := &HttpError{
|
||||
resp.StatusCode, map[string][]string(resp.Header), URL, errInfo,
|
||||
}
|
||||
switch resp.StatusCode {
|
||||
case http.StatusBadRequest:
|
||||
return errors.NewBadRequestf(httpError, "", "Bad request %s", URL)
|
||||
case http.StatusUnauthorized:
|
||||
return errors.NewNotAuthorizedf(httpError, "", "Unauthorised URL %s", URL)
|
||||
//return errors.NewInvalidCredentialsf(httpError, "", "Unauthorised URL %s", URL)
|
||||
case http.StatusForbidden:
|
||||
//return errors.
|
||||
case http.StatusNotFound:
|
||||
return errors.NewResourceNotFoundf(httpError, "", "Resource not found %s", URL)
|
||||
case http.StatusMethodNotAllowed:
|
||||
//return errors.
|
||||
case http.StatusNotAcceptable:
|
||||
return errors.NewInvalidHeaderf(httpError, "", "Invalid Header %s", URL)
|
||||
case http.StatusConflict:
|
||||
return errors.NewMissingParameterf(httpError, "", "Missing parameters %s", URL)
|
||||
//return errors.NewInvalidArgumentf(httpError, "", "Invalid parameter %s", URL)
|
||||
case http.StatusRequestEntityTooLarge:
|
||||
return errors.NewRequestTooLargef(httpError, "", "Request too large %s", URL)
|
||||
case http.StatusUnsupportedMediaType:
|
||||
//return errors.
|
||||
case http.StatusServiceUnavailable:
|
||||
return errors.NewInternalErrorf(httpError, "", "Internal error %s", URL)
|
||||
case 420:
|
||||
// SlowDown
|
||||
return errors.NewRequestThrottledf(httpError, "", "Request throttled %s", URL)
|
||||
case 422:
|
||||
// Unprocessable Entity
|
||||
return errors.NewInvalidArgumentf(httpError, "", "Invalid parameters %s", URL)
|
||||
case 449:
|
||||
// RetryWith
|
||||
return errors.NewInvalidVersionf(httpError, "", "Invalid version %s", URL)
|
||||
//RequestMovedError -> ?
|
||||
}
|
||||
|
||||
return errors.NewUnknownErrorf(httpError, "", "Unknown error %s", URL)
|
||||
}
|
||||
|
||||
func isMantaRequest(url, user string) bool {
|
||||
return strings.Contains(url, "/"+user+"/stor") || strings.Contains(url, "/"+user+"/jobs") || strings.Contains(url, "/"+user+"/public")
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
*
|
||||
* gocommon - Go library to interact with the JoyentCloud
|
||||
*
|
||||
*
|
||||
* Copyright (c) 2016 Joyent Inc.
|
||||
*
|
||||
* Written by Daniele Stroppa <daniele.stroppa@joyent.com>
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package jpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"github.com/joyent/gosign/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
// Environment variables
|
||||
SdcAccount = "SDC_ACCOUNT"
|
||||
SdcKeyId = "SDC_KEY_ID"
|
||||
SdcUrl = "SDC_URL"
|
||||
MantaUser = "MANTA_USER"
|
||||
MantaKeyId = "MANTA_KEY_ID"
|
||||
MantaUrl = "MANTA_URL"
|
||||
)
|
||||
|
||||
var Locations = map[string]string{
|
||||
"us-east-1": "America/New_York",
|
||||
"us-west-1": "America/Los_Angeles",
|
||||
"us-sw-1": "America/Los_Angeles",
|
||||
"eu-ams-1": "Europe/Amsterdam",
|
||||
}
|
||||
|
||||
// getConfig returns the value of the first available environment
|
||||
// variable, among the given ones.
|
||||
func getConfig(envVars ...string) (value string) {
|
||||
value = ""
|
||||
for _, v := range envVars {
|
||||
value = os.Getenv(v)
|
||||
if value != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// getUserHome returns the value of HOME environment
|
||||
// variable for the user environment.
|
||||
func getUserHome() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return os.Getenv("APPDATA")
|
||||
} else {
|
||||
return os.Getenv("HOME")
|
||||
}
|
||||
}
|
||||
|
||||
// credentialsFromEnv creates and initializes the credentials from the
|
||||
// environment variables.
|
||||
func credentialsFromEnv(key string) (*auth.Credentials, error) {
|
||||
var keyName string
|
||||
if key == "" {
|
||||
keyName = getUserHome() + "/.ssh/id_rsa"
|
||||
} else {
|
||||
keyName = key
|
||||
}
|
||||
privateKey, err := ioutil.ReadFile(keyName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authentication, err := auth.NewAuth(getConfig(SdcAccount, MantaUser), string(privateKey), "rsa-sha256")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &auth.Credentials{
|
||||
UserAuthentication: authentication,
|
||||
SdcKeyId: getConfig(SdcKeyId),
|
||||
SdcEndpoint: auth.Endpoint{URL: getConfig(SdcUrl)},
|
||||
MantaKeyId: getConfig(MantaKeyId),
|
||||
MantaEndpoint: auth.Endpoint{URL: getConfig(MantaUrl)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CompleteCredentialsFromEnv gets and verifies all the required
|
||||
// authentication parameters have values in the environment.
|
||||
func CompleteCredentialsFromEnv(keyName string) (cred *auth.Credentials, err error) {
|
||||
cred, err = credentialsFromEnv(keyName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := reflect.ValueOf(cred).Elem()
|
||||
t := v.Type()
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
f := v.Field(i)
|
||||
if f.String() == "" {
|
||||
return nil, fmt.Errorf("Required environment variable not set for credentials attribute: %s", t.Field(i).Name)
|
||||
}
|
||||
}
|
||||
return cred, nil
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
*
|
||||
* gocommon - Go library to interact with the JoyentCloud
|
||||
*
|
||||
*
|
||||
* Copyright (c) 2016 Joyent Inc.
|
||||
*
|
||||
* Written by Daniele Stroppa <daniele.stroppa@joyent.com>
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package gocommon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type VersionNum struct {
|
||||
Major int
|
||||
Minor int
|
||||
Micro int
|
||||
}
|
||||
|
||||
func (v *VersionNum) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Micro)
|
||||
}
|
||||
|
||||
var VersionNumber = VersionNum{
|
||||
Major: 0,
|
||||
Minor: 1,
|
||||
Micro: 0,
|
||||
}
|
||||
|
||||
var Version = VersionNumber.String()
|
|
@ -1,40 +0,0 @@
|
|||
box: golang
|
||||
|
||||
build:
|
||||
steps:
|
||||
# Sets the go workspace and places you package
|
||||
# at the right place in the workspace tree
|
||||
- setup-go-workspace:
|
||||
package-dir: github.com/joyent/gocommon
|
||||
|
||||
# Gets the dependencies
|
||||
- script:
|
||||
name: go get
|
||||
code: |
|
||||
go get -v -t ./...
|
||||
|
||||
# Build the project
|
||||
- script:
|
||||
name: go build
|
||||
code: |
|
||||
go build ./...
|
||||
|
||||
- script:
|
||||
name: make a new key for testing
|
||||
code: |
|
||||
ssh-keygen -b 2048 \
|
||||
-C "Testing Key" \
|
||||
-f /root/.ssh/id_rsa \
|
||||
-t rsa \
|
||||
-P ""
|
||||
|
||||
# Test the project
|
||||
- script:
|
||||
name: go test
|
||||
code: |
|
||||
export KEY_ID=$(ssh-keygen -lf /root/.ssh/id_rsa | awk -F' ' '{print $2}' | cut -d':' -f2-)
|
||||
export SDC_KEY_ID=${KEY_ID}
|
||||
export MANTA_KEY_ID=${KEY_ID}
|
||||
export SDC_URL=https://us-east-1.api.joyent.com
|
||||
export MANTA_URL=https://us-east.manta.joyent.com
|
||||
go test ./...
|
|
@ -1,373 +0,0 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
|
@ -1,163 +0,0 @@
|
|||
# gosdc
|
||||
|
||||
[![wercker status](https://app.wercker.com/status/349ee60ed0afffd99d2b2b354ada5938/s/master "wercker status")](https://app.wercker.com/project/bykey/349ee60ed0afffd99d2b2b354ada5938)
|
||||
|
||||
`gosdc` is a Go client for Joyent's SmartDataCenter
|
||||
|
||||
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-generate-toc again -->
|
||||
**Table of Contents**
|
||||
|
||||
- [gosdc](#gosdc)
|
||||
- [Usage](#usage)
|
||||
- [Examples](#examples)
|
||||
- [Resources](#resources)
|
||||
- [License](#license)
|
||||
|
||||
<!-- markdown-toc end -->
|
||||
|
||||
## Usage
|
||||
|
||||
To create a client
|
||||
([`*cloudapi.Client`](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client)),
|
||||
you'll need a few things:
|
||||
|
||||
1. your account ID
|
||||
2. the ID of the key associated with your account
|
||||
3. your private key material
|
||||
4. the cloud endpoint you want to use (for example
|
||||
`https://us-east-1.api.joyentcloud.com`)
|
||||
|
||||
Given these four pieces of information, you can initialize a client with the
|
||||
following:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gosdc/cloudapi"
|
||||
"github.com/joyent/gosign/auth"
|
||||
)
|
||||
|
||||
func client(key, keyId, account, endpoint string) (*cloudapi.Client, error) {
|
||||
keyData, err := ioutil.ReadFile(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userAuth, err := auth.NewAuth(account, string(keyData), "rsa-sha256")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
creds := &auth.Credentials{
|
||||
UserAuthentication: auth,
|
||||
SdcKeyId: keyId,
|
||||
SdcEndpoint: auth.Endpoint{URL: endpoint},
|
||||
}
|
||||
|
||||
return cloudapi.New(client.NewClient(
|
||||
creds.SdcEndpoint.URL,
|
||||
cloudapi.DefaultAPIVersion,
|
||||
creds,
|
||||
log.New(os.Stderr, "", log.LstdFlags),
|
||||
)), nil
|
||||
}
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
Projects using the gosdc API:
|
||||
|
||||
- [triton-terraform](https://github.com/joyent/triton-terraform)
|
||||
|
||||
## Resources
|
||||
|
||||
After creating a client, you can manipulate resources in the following ways:
|
||||
|
||||
| Resource | Create | Read | Update | Delete | Extra |
|
||||
|----------|--------|------|--------|--------|-------|
|
||||
| Datacenters | | [GetDatacenter](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetDatacenter), [ListDatacenters](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ListDatacenters) | | | |
|
||||
| Firewall Rules | [CreateFirewallRule](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.CreateFirewallRule) | [GetFirewallRule](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetFirewallRule), [ListFirewallRules](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ListFirewallRules), [ListmachineFirewallRules](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ListMachineFirewallRules) | [UpdateFirewallRule](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.UpdateFirewallRule), [EnableFirewallRule](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.EnableFirewallRule), [DisableFirewallRule](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.DisableFirewallRule) | [DeleteFirewallRule](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.DeleteFirewallRule) | |
|
||||
| Instrumentations | [CreateInstrumentation](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.CreateInstrumentation) | [GetInstrumentation](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetInstrumentation), [ListInstrumentations](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ListInstrumentations), [GetInstrumentationHeatmap](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetInstrumentationHeatmap), [GetInstrumentationHeatmapDetails](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetInstrumentationHeatmapDetails), [GetInstrumentationValue](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetInstrumentationValue) | | [DeleteInstrumentation](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.DeleteInstrumentation) | [DescribeAnalytics](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.DescribeAnalytics) |
|
||||
| Keys | [CreateKey](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.CreateKey) | [GetKey](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetKey), [ListKeys](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ListKeys) | | [DeleteKey](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.DeleteKey) | |
|
||||
| Machines | [CreateMachine](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.CreateMachine) | [GetMachine](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetMachine), [ListMachines](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ListMachines), [ListFirewallRuleMachines](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ListFirewallRuleMachines) | [RenameMachine](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.RenameMachine), [ResizeMachine](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ResizeMachine) | [DeleteMachine](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.DeleteMachine) | [CountMachines](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.CountMachines), [MachineAudit](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.MachineAudit), [StartMachine](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.StartMachine), [StartMachineFromSnapshot](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.StartMachineFromSnapshot), [StopMachine](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.StopMachine), [RebootMachine](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.RebootMachine) |
|
||||
| Machine (Images) | [CreateImageFromMachine](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.CreateImageFromMachine) | [GetImage](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetImage), [ListImages](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ListImages) | | [DeleteImage](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.DeleteImage) | [ExportImage](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ExportImage) |
|
||||
| Machine (Metadata) | | [GetMachineMetadata](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetMachineMetadata) | [UpdateMachineMetadata](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.UpdateMachineMetadata) | [DeleteMachineMetadata](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.DeleteMachineMetadata), [DeleteAllMachineMetadata](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.DeleteAllMachineMetadata) | |
|
||||
| Machine (Snapshots) | [CreateMachineSnapshot](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.CreateMachineSnapshot) | [GetMachineSnapshot](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetMachineSnapshot), [ListMachineSnapshots](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ListMachineSnapshots) | | [DeleteMachineSnapshot](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.DeleteMachineSnapshot) | |
|
||||
| Machine (Tags) | | [GetMachineTag](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetMachineTag), [ListMachineTags](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ListMachineTags) | [AddMachineTags](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.AddMachineTags), [ReplaceMachineTags](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ReplaceMachineTags) | [DeleteMachineTag](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.DeleteMachineTag), [DeleteMachineTags](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.DeleteMachineTags) | [EnableFirewallMachine](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.EnableFirewallMachine), [DisableFirewallMachine](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.DisableFirewallMachine) |
|
||||
| Networks | | [GetNetwork](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetNetwork), [ListNetworks](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ListNetworks) | | | |
|
||||
| Packages | | [GetPackage](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.GetPackage), [ListPackages](https://godoc.org/github.com/joyent/gosdc/cloudapi#Client.ListPackages) | | | |
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Report bugs and request features using [GitHub Issues](https://github.com/joyent/gosdc/issues), or contribute code via a [GitHub Pull Request](https://github.com/joyent/gosdc/pulls). Changes will be code reviewed before merging. In the near future, automated tests will be run, but in the meantime please `go fmt`, `go lint`, and test all contributions.
|
||||
|
||||
|
||||
## Developing
|
||||
|
||||
This library assumes a Go development environment setup based on [How to Write Go Code](https://golang.org/doc/code.html). Your GOPATH environment variable should be pointed at your workspace directory.
|
||||
|
||||
You can now use `go get github.com/joyent/gosdc` to install the repository to the correct location, but if you are intending on contributing back a change you may want to consider cloning the repository via git yourself. This way you can have a single source tree for all Joyent Go projects with each repo having two remotes -- your own fork on GitHub and the upstream origin.
|
||||
|
||||
For example if your GOPATH is `~/src/joyent/go` and you're working on multiple repos then that directory tree might look like:
|
||||
|
||||
```
|
||||
~/src/joyent/go/
|
||||
|_ pkg/
|
||||
|_ src/
|
||||
|_ github.com
|
||||
|_ joyent
|
||||
|_ gocommon
|
||||
|_ gomanta
|
||||
|_ gosdc
|
||||
|_ gosign
|
||||
```
|
||||
|
||||
### Recommended Setup
|
||||
|
||||
```
|
||||
$ mkdir -p ${GOPATH}/src/github.com/joyent
|
||||
$ cd ${GOPATH}/src/github.com/joyent
|
||||
$ git clone git@github.com:<yourname>/gosdc.git
|
||||
|
||||
# fetch dependencies
|
||||
$ git clone git@github.com:<yourname>/gocommon.git
|
||||
$ git clone git@github.com:<yourname>/gosign.git
|
||||
$ go get -v -t ./...
|
||||
|
||||
# add upstream remote
|
||||
$ cd gosdc
|
||||
$ git remote add upstream git@github.com:joyent/gosdc.git
|
||||
$ git remote -v
|
||||
origin git@github.com:<yourname>/gosdc.git (fetch)
|
||||
origin git@github.com:<yourname>/gosdc.git (push)
|
||||
upstream git@github.com:joyent/gosdc.git (fetch)
|
||||
upstream git@github.com:joyent/gosdc.git (push)
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
You can run the tests either locally or against live Triton. If you want to run the tests locally you'll want to generate an SSH key and pass the appropriate flags to the test harness as shown below.
|
||||
|
||||
```
|
||||
cd ${GOPATH}/src/github.com/joyent/gosdc
|
||||
ssh-keygen -b 2048 -C "Testing Key" -f test_key.id_rsa -t rsa -P ""
|
||||
env KEY_NAME=`pwd`/test_key.id_rsa LIVE=false go test ./...
|
||||
```
|
||||
|
||||
### Build the Library
|
||||
|
||||
```
|
||||
cd ${GOPATH}/src/github.com/joyent/gosdc
|
||||
go build ./...
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
gosdc is licensed under the Mozilla Public License Version 2.0, a copy of which
|
||||
is available at [LICENSE](LICENSE)
|
|
@ -1,127 +0,0 @@
|
|||
/*
|
||||
Package cloudapi interacts with the Cloud API (http://apidocs.joyent.com/cloudapi/).
|
||||
|
||||
Licensed under the Mozilla Public License version 2.0
|
||||
|
||||
Copyright (c) Joyent Inc.
|
||||
*/
|
||||
package cloudapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
jh "github.com/joyent/gocommon/http"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultAPIVersion defines the default version of the Cloud API to use
|
||||
DefaultAPIVersion = "~7.3"
|
||||
|
||||
// CloudAPI URL parts
|
||||
apiKeys = "keys"
|
||||
apiPackages = "packages"
|
||||
apiImages = "images"
|
||||
apiDatacenters = "datacenters"
|
||||
apiMachines = "machines"
|
||||
apiMetadata = "metadata"
|
||||
apiSnapshots = "snapshots"
|
||||
apiTags = "tags"
|
||||
apiAnalytics = "analytics"
|
||||
apiInstrumentations = "instrumentations"
|
||||
apiInstrumentationsValue = "value"
|
||||
apiInstrumentationsRaw = "raw"
|
||||
apiInstrumentationsHeatmap = "heatmap"
|
||||
apiInstrumentationsImage = "image"
|
||||
apiInstrumentationsDetails = "details"
|
||||
apiUsage = "usage"
|
||||
apiAudit = "audit"
|
||||
apiFirewallRules = "fwrules"
|
||||
apiFirewallRulesEnable = "enable"
|
||||
apiFirewallRulesDisable = "disable"
|
||||
apiNetworks = "networks"
|
||||
apiFabricVLANs = "fabrics/default/vlans"
|
||||
apiFabricNetworks = "networks"
|
||||
apiNICs = "nics"
|
||||
apiServices = "services"
|
||||
|
||||
// CloudAPI actions
|
||||
actionExport = "export"
|
||||
actionStop = "stop"
|
||||
actionStart = "start"
|
||||
actionReboot = "reboot"
|
||||
actionResize = "resize"
|
||||
actionRename = "rename"
|
||||
actionEnableFw = "enable_firewall"
|
||||
actionDisableFw = "disable_firewall"
|
||||
)
|
||||
|
||||
// Client provides a means to access the Joyent CloudAPI
|
||||
type Client struct {
|
||||
client client.Client
|
||||
}
|
||||
|
||||
// New creates a new Client.
|
||||
func New(client client.Client) *Client {
|
||||
return &Client{client}
|
||||
}
|
||||
|
||||
// Filter represents a filter that can be applied to an API request.
|
||||
type Filter struct {
|
||||
v url.Values
|
||||
}
|
||||
|
||||
// NewFilter creates a new Filter.
|
||||
func NewFilter() *Filter {
|
||||
return &Filter{make(url.Values)}
|
||||
}
|
||||
|
||||
// Set a value for the specified filter.
|
||||
func (f *Filter) Set(filter, value string) {
|
||||
f.v.Set(filter, value)
|
||||
}
|
||||
|
||||
// Add a value for the specified filter.
|
||||
func (f *Filter) Add(filter, value string) {
|
||||
f.v.Add(filter, value)
|
||||
}
|
||||
|
||||
// request represents an API request
|
||||
type request struct {
|
||||
method string
|
||||
url string
|
||||
filter *Filter
|
||||
reqValue interface{}
|
||||
reqHeader http.Header
|
||||
resp interface{}
|
||||
respHeader *http.Header
|
||||
expectedStatus int
|
||||
}
|
||||
|
||||
// Helper method to send an API request
|
||||
func (c *Client) sendRequest(req request) (*jh.ResponseData, error) {
|
||||
request := jh.RequestData{
|
||||
ReqValue: req.reqValue,
|
||||
ReqHeaders: req.reqHeader,
|
||||
}
|
||||
if req.filter != nil {
|
||||
request.Params = &req.filter.v
|
||||
}
|
||||
if req.expectedStatus == 0 {
|
||||
req.expectedStatus = http.StatusOK
|
||||
}
|
||||
respData := jh.ResponseData{
|
||||
RespValue: req.resp,
|
||||
RespHeaders: req.respHeader,
|
||||
ExpectedStatus: []int{req.expectedStatus},
|
||||
}
|
||||
err := c.client.SendRequest(req.method, req.url, "", &request, &respData)
|
||||
return &respData, err
|
||||
}
|
||||
|
||||
// Helper method to create the API URL
|
||||
func makeURL(parts ...string) string {
|
||||
return path.Join(parts...)
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// ListDatacenters provides a list of all datacenters this cloud is aware of.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListDatacenters
|
||||
func (c *Client) ListDatacenters() (map[string]interface{}, error) {
|
||||
var resp map[string]interface{}
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: apiDatacenters,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of datcenters")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetDatacenter gets an individual datacenter by name. Returns an HTTP redirect
|
||||
// to your client, the datacenter URL is in the Location header.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetDatacenter
|
||||
func (c *Client) GetDatacenter(datacenterName string) (string, error) {
|
||||
var respHeader http.Header
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiDatacenters, datacenterName),
|
||||
respHeader: &respHeader,
|
||||
expectedStatus: http.StatusFound,
|
||||
}
|
||||
respData, err := c.sendRequest(req)
|
||||
if err != nil {
|
||||
return "", errors.Newf(err, "failed to get datacenter with name: %s", datacenterName)
|
||||
}
|
||||
return respData.RespHeaders.Get("Location"), nil
|
||||
}
|
|
@ -1,182 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
type FabricVLAN struct {
|
||||
Id int16 `json:"vlan_id"` // Number between 0-4095 indicating VLAN Id
|
||||
Name string `json:"name"` // Unique name to identify VLAN
|
||||
Description string `json:"description,omitempty"` // Optional description of the VLAN
|
||||
}
|
||||
|
||||
type FabricNetwork struct {
|
||||
Id string `json:"id"` // Unique identifier for network
|
||||
Name string `json:"name"` // Network name
|
||||
Public bool `json:"public"` // Whether or not this is an RFC1918 network
|
||||
Fabric bool `json:"fabric"` // Whether this network is on a fabric
|
||||
Description string `json:"description"` // Optional description of network
|
||||
Subnet string `json:"subnet"` // CIDR formatted string describing network
|
||||
ProvisionStartIp string `json:"provision_start_ip"` // First IP on the network that can be assigned
|
||||
ProvisionEndIp string `json:"provision_end_ip"` // Last assignable IP on the network
|
||||
Gateway string `json:"gateway"` // Optional Gateway IP
|
||||
Resolvers []string `json:"resolvers,omitempty"` // Array of IP addresses for resolvers
|
||||
Routes map[string]string `json:"routes,omitempty"` // Map of CIDR block to Gateway IP Address
|
||||
InternetNAT bool `json:"internet_nat"` // If a NAT zone is provisioned at Gateway IP Address
|
||||
VLANId int16 `json:"vlan_id"` // VLAN network is on
|
||||
}
|
||||
|
||||
type CreateFabricNetworkOpts struct {
|
||||
Name string `json:"name"` // Network name
|
||||
Description string `json:"description,omitempty"` // Optional description of network
|
||||
Subnet string `json:"subnet"` // CIDR formatted string describing network
|
||||
ProvisionStartIp string `json:"provision_start_ip"` // First IP on the network that can be assigned
|
||||
ProvisionEndIp string `json:"provision_end_ip"` // Last assignable IP on the network
|
||||
Gateway string `json:"gateway,omitempty"` // Optional Gateway IP
|
||||
Resolvers []string `json:"resolvers,omitempty"` // Array of IP addresses for resolvers
|
||||
Routes map[string]string `json:"routes,omitempty"` // Map of CIDR block to Gateway IP Address
|
||||
InternetNAT bool `json:"internet_nat"` // If a NAT zone is provisioned at Gateway IP Address
|
||||
}
|
||||
|
||||
// ListFabricVLANs lists VLANs
|
||||
// See API docs: https://apidocs.joyent.com/cloudapi/#ListFabricVLANs
|
||||
func (c *Client) ListFabricVLANs() ([]FabricVLAN, error) {
|
||||
var resp []FabricVLAN
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: apiFabricVLANs,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of fabric VLANs")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetFabricLAN retrieves a single VLAN by ID
|
||||
// See API docs: https://apidocs.joyent.com/cloudapi/#GetFabricVLAN
|
||||
func (c *Client) GetFabricVLAN(vlanID int16) (*FabricVLAN, error) {
|
||||
var resp FabricVLAN
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiFabricVLANs, strconv.Itoa(int(vlanID))),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get fabric VLAN with id %d", vlanID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// CreateFabricVLAN creates a new VLAN with the specified options
|
||||
// See API docs: https://apidocs.joyent.com/cloudapi/#CreateFabricVLAN
|
||||
func (c *Client) CreateFabricVLAN(vlan FabricVLAN) (*FabricVLAN, error) {
|
||||
var resp FabricVLAN
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: apiFabricVLANs,
|
||||
reqValue: vlan,
|
||||
resp: &resp,
|
||||
expectedStatus: http.StatusCreated,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to create fabric VLAN: %d - %s", vlan.Id, vlan.Name)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// UpdateFabricVLAN updates a given VLAN with new fields
|
||||
// See API docs: https://apidocs.joyent.com/cloudapi/#UpdateFabricVLAN
|
||||
func (c *Client) UpdateFabricVLAN(vlan FabricVLAN) (*FabricVLAN, error) {
|
||||
var resp FabricVLAN
|
||||
req := request{
|
||||
method: client.PUT,
|
||||
url: makeURL(apiFabricVLANs, strconv.Itoa(int(vlan.Id))),
|
||||
reqValue: vlan,
|
||||
resp: &resp,
|
||||
expectedStatus: http.StatusAccepted,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to update fabric VLAN with id %d to %s - %s", vlan.Id, vlan.Name, vlan.Description)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteFabricVLAN delets a given VLAN as specified by ID
|
||||
// See API docs: https://apidocs.joyent.com/cloudapi/#DeleteFabricVLAN
|
||||
func (c *Client) DeleteFabricVLAN(vlanID int16) error {
|
||||
req := request{
|
||||
method: client.DELETE,
|
||||
url: makeURL(apiFabricVLANs, strconv.Itoa(int(vlanID))),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to delete fabric VLAN with id %d", vlanID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListFabricNetworks lists the networks inside the given VLAN
|
||||
// See API docs: https://apidocs.joyent.com/cloudapi/#ListFabricNetworks
|
||||
func (c *Client) ListFabricNetworks(vlanID int16) ([]FabricNetwork, error) {
|
||||
var resp []FabricNetwork
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiFabricVLANs, strconv.Itoa(int(vlanID)), apiFabricNetworks),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of networks on fabric %d", vlanID)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetFabricNetwork gets a single network by VLAN and Network IDs
|
||||
// See API docs: https://apidocs.joyent.com/cloudapi/#GetFabricNetwork
|
||||
func (c *Client) GetFabricNetwork(vlanID int16, networkID string) (*FabricNetwork, error) {
|
||||
var resp FabricNetwork
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiFabricVLANs, strconv.Itoa(int(vlanID)), apiFabricNetworks, networkID),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get fabric network %s on vlan %d", networkID, vlanID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// CreateFabricNetwork creates a new fabric network
|
||||
// See API docs: https://apidocs.joyent.com/cloudapi/#CreateFabricNetwork
|
||||
func (c *Client) CreateFabricNetwork(vlanID int16, opts CreateFabricNetworkOpts) (*FabricNetwork, error) {
|
||||
var resp FabricNetwork
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: makeURL(apiFabricVLANs, strconv.Itoa(int(vlanID)), apiFabricNetworks),
|
||||
reqValue: opts,
|
||||
resp: &resp,
|
||||
expectedStatus: http.StatusCreated,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to create fabric network %s on vlan %d", opts.Name, vlanID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteFabricNetwork deletes an existing fabric network
|
||||
// See API docs: https://apidocs.joyent.com/cloudapi/#DeleteFabricNetwork
|
||||
func (c *Client) DeleteFabricNetwork(vlanID int16, networkID string) error {
|
||||
req := request{
|
||||
method: client.DELETE,
|
||||
url: makeURL(apiFabricVLANs, strconv.Itoa(int(vlanID)), apiFabricNetworks, networkID),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to delete fabric network %s on vlan %d", networkID, vlanID)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// FirewallRule represent a firewall rule that can be specifed for a machine.
|
||||
type FirewallRule struct {
|
||||
Id string // Unique identifier for the rule
|
||||
Enabled bool // Whether the rule is enabled or not
|
||||
Rule string // Firewall rule in the form 'FROM <target a> TO <target b> <action> <protocol> <port>'
|
||||
}
|
||||
|
||||
// CreateFwRuleOpts represent the option that can be specified
|
||||
// when creating a new firewall rule.
|
||||
type CreateFwRuleOpts struct {
|
||||
Enabled bool `json:"enabled"` // Whether to enable the rule or not
|
||||
Rule string `json:"rule"` // Firewall rule in the form 'FROM <target a> TO <target b> <action> <protocol> <port>'
|
||||
}
|
||||
|
||||
// ListFirewallRules lists all the firewall rules on record for a specified account.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListFirewallRules
|
||||
func (c *Client) ListFirewallRules() ([]FirewallRule, error) {
|
||||
var resp []FirewallRule
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: apiFirewallRules,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of firewall rules")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetFirewallRule returns the specified firewall rule.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetFirewallRule
|
||||
func (c *Client) GetFirewallRule(fwRuleID string) (*FirewallRule, error) {
|
||||
var resp FirewallRule
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiFirewallRules, fwRuleID),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get firewall rule with id %s", fwRuleID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// CreateFirewallRule creates the firewall rule with the specified options.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#CreateFirewallRule
|
||||
func (c *Client) CreateFirewallRule(opts CreateFwRuleOpts) (*FirewallRule, error) {
|
||||
var resp FirewallRule
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: apiFirewallRules,
|
||||
reqValue: opts,
|
||||
resp: &resp,
|
||||
expectedStatus: http.StatusCreated,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to create firewall rule: %s", opts.Rule)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// UpdateFirewallRule updates the specified firewall rule.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#UpdateFirewallRule
|
||||
func (c *Client) UpdateFirewallRule(fwRuleID string, opts CreateFwRuleOpts) (*FirewallRule, error) {
|
||||
var resp FirewallRule
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: makeURL(apiFirewallRules, fwRuleID),
|
||||
reqValue: opts,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to update firewall rule with id %s to %s", fwRuleID, opts.Rule)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// EnableFirewallRule enables the given firewall rule record if it is disabled.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#EnableFirewallRule
|
||||
func (c *Client) EnableFirewallRule(fwRuleID string) (*FirewallRule, error) {
|
||||
var resp FirewallRule
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: makeURL(apiFirewallRules, fwRuleID, apiFirewallRulesEnable),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to enable firewall rule with id %s", fwRuleID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DisableFirewallRule disables the given firewall rule record if it is enabled.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#DisableFirewallRule
|
||||
func (c *Client) DisableFirewallRule(fwRuleID string) (*FirewallRule, error) {
|
||||
var resp FirewallRule
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: makeURL(apiFirewallRules, fwRuleID, apiFirewallRulesDisable),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to disable firewall rule with id %s", fwRuleID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteFirewallRule removes the given firewall rule record from all the required account machines.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#DeleteFirewallRule
|
||||
func (c *Client) DeleteFirewallRule(fwRuleID string) error {
|
||||
req := request{
|
||||
method: client.DELETE,
|
||||
url: makeURL(apiFirewallRules, fwRuleID),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to delete firewall rule with id %s", fwRuleID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListFirewallRuleMachines return the list of machines affected by the given firewall rule.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListFirewallRuleMachines
|
||||
func (c *Client) ListFirewallRuleMachines(fwRuleID string) ([]Machine, error) {
|
||||
var resp []Machine
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiFirewallRules, fwRuleID, apiMachines),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of machines affected by firewall rule wit id %s", fwRuleID)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// Image represent the software packages that will be available on newly provisioned machines
|
||||
type Image struct {
|
||||
Id string // Unique identifier for the image
|
||||
Name string // Image friendly name
|
||||
OS string // Underlying operating system
|
||||
Version string // Image version
|
||||
Type string // Image type, one of 'smartmachine' or 'virtualmachine'
|
||||
Description string // Image description
|
||||
Requirements map[string]interface{} // Minimum requirements for provisioning a machine with this image, e.g. 'password' indicates that a password must be provided
|
||||
Homepage string // URL for a web page including detailed information for this image (new in API version 7.0)
|
||||
PublishedAt string `json:"published_at"` // Time this image has been made publicly available (new in API version 7.0)
|
||||
Public bool // Indicates if the image is publicly available (new in API version 7.1)
|
||||
State string // Current image state. One of 'active', 'unactivated', 'disabled', 'creating', 'failed' (new in API version 7.1)
|
||||
Tags map[string]string // A map of key/value pairs that allows clients to categorize images by any given criteria (new in API version 7.1)
|
||||
EULA string // URL of the End User License Agreement (EULA) for the image (new in API version 7.1)
|
||||
ACL []string // An array of account UUIDs given access to a private image. The field is only relevant to private images (new in API version 7.1)
|
||||
Owner string // The UUID of the user owning the image
|
||||
}
|
||||
|
||||
// ExportImageOpts represent the option that can be specified
|
||||
// when exporting an image.
|
||||
type ExportImageOpts struct {
|
||||
MantaPath string `json:"manta_path"` // The Manta path prefix to use when exporting the image
|
||||
}
|
||||
|
||||
// MantaLocation represent the properties that allow a user
|
||||
// to retrieve the image file and manifest from Manta
|
||||
type MantaLocation struct {
|
||||
MantaURL string `json:"manta_url"` // Manta datacenter URL
|
||||
ImagePath string `json:"image_path"` // Path to the image
|
||||
ManifestPath string `json:"manifest_path"` // Path to the image manifest
|
||||
}
|
||||
|
||||
// CreateImageFromMachineOpts represent the option that can be specified
|
||||
// when creating a new image from an existing machine.
|
||||
type CreateImageFromMachineOpts struct {
|
||||
Machine string `json:"machine"` // The machine UUID from which the image is to be created
|
||||
Name string `json:"name"` // Image name
|
||||
Version string `json:"version"` // Image version
|
||||
Description string `json:"description,omitempty"` // Image description
|
||||
Homepage string `json:"homepage,omitempty"` // URL for a web page including detailed information for this image
|
||||
EULA string `json:"eula,omitempty"` // URL of the End User License Agreement (EULA) for the image
|
||||
ACL []string `json:"acl,omitempty"` // An array of account UUIDs given access to a private image. The field is only relevant to private images
|
||||
Tags map[string]string `json:"tags,omitempty"` // A map of key/value pairs that allows clients to categorize images by any given criteria
|
||||
}
|
||||
|
||||
// ListImages provides a list of images available in the datacenter.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListImages
|
||||
func (c *Client) ListImages(filter *Filter) ([]Image, error) {
|
||||
var resp []Image
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: apiImages,
|
||||
filter: filter,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of images")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetImage returns the image specified by imageId.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetImage
|
||||
func (c *Client) GetImage(imageID string) (*Image, error) {
|
||||
var resp Image
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiImages, imageID),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get image with id: %s", imageID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteImage (Beta) Delete the image specified by imageId. Must be image owner to do so.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#DeleteImage
|
||||
func (c *Client) DeleteImage(imageID string) error {
|
||||
req := request{
|
||||
method: client.DELETE,
|
||||
url: makeURL(apiImages, imageID),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to delete image with id: %s", imageID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExportImage (Beta) Exports an image to the specified Manta path.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListImages
|
||||
func (c *Client) ExportImage(imageID string, opts ExportImageOpts) (*MantaLocation, error) {
|
||||
var resp MantaLocation
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: fmt.Sprintf("%s/%s?action=%s", apiImages, imageID, actionExport),
|
||||
reqValue: opts,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to export image %s to %s", imageID, opts.MantaPath)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// CreateImageFromMachine (Beta) Create a new custom image from a machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListImages
|
||||
func (c *Client) CreateImageFromMachine(opts CreateImageFromMachineOpts) (*Image, error) {
|
||||
var resp Image
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: apiImages,
|
||||
reqValue: opts,
|
||||
resp: &resp,
|
||||
expectedStatus: http.StatusCreated,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to create image from machine %s", opts.Machine)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
|
@ -1,216 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// Analytics represents the available analytics
|
||||
type Analytics struct {
|
||||
Modules map[string]interface{} // Namespace to organize metrics
|
||||
Fields map[string]interface{} // Fields represent metadata by which data points can be filtered or decomposed
|
||||
Types map[string]interface{} // Types are used with both metrics and fields for two purposes: to hint to clients at how to best label values, and to distinguish between numeric and discrete quantities.
|
||||
Metrics map[string]interface{} // Metrics describe quantities which can be measured by the system
|
||||
Transformations map[string]interface{} // Transformations are post-processing functions that can be applied to data when it's retrieved.
|
||||
}
|
||||
|
||||
// Instrumentation specify which metric to collect, how frequently to aggregate data (e.g., every second, every hour, etc.)
|
||||
// how much data to keep (e.g., 10 minutes' worth, 6 months' worth, etc.) and other configuration options
|
||||
type Instrumentation struct {
|
||||
Module string `json:"module"`
|
||||
Stat string `json:"stat"`
|
||||
Predicate string `json:"predicate"`
|
||||
Decomposition []string `json:"decomposition"`
|
||||
ValueDimension int `json:"value-dimenstion"`
|
||||
ValueArity string `json:"value-arity"`
|
||||
RetentionTime int `json:"retention-time"`
|
||||
Granularity int `json:"granularitiy"`
|
||||
IdleMax int `json:"idle-max"`
|
||||
Transformations []string `json:"transformations"`
|
||||
PersistData bool `json:"persist-data"`
|
||||
Crtime int `json:"crtime"`
|
||||
ValueScope string `json:"value-scope"`
|
||||
Id string `json:"id"`
|
||||
Uris []Uri `json:"uris"`
|
||||
}
|
||||
|
||||
// Uri represents a Universal Resource Identifier
|
||||
type Uri struct {
|
||||
Uri string // Resource identifier
|
||||
Name string // URI name
|
||||
}
|
||||
|
||||
// InstrumentationValue represents the data associated to an instrumentation for a point in time
|
||||
type InstrumentationValue struct {
|
||||
Value interface{}
|
||||
Transformations map[string]interface{}
|
||||
StartTime int
|
||||
Duration int
|
||||
}
|
||||
|
||||
// HeatmapOpts represent the option that can be specified
|
||||
// when retrieving an instrumentation.'s heatmap
|
||||
type HeatmapOpts struct {
|
||||
Height int `json:"height"` // Height of the image in pixels
|
||||
Width int `json:"width"` // Width of the image in pixels
|
||||
Ymin int `json:"ymin"` // Y-Axis value for the bottom of the image (default: 0)
|
||||
Ymax int `json:"ymax"` // Y-Axis value for the top of the image (default: auto)
|
||||
Nbuckets int `json:"nbuckets"` // Number of buckets in the vertical dimension
|
||||
Selected []string `json:"selected"` // Array of field values to highlight, isolate or exclude
|
||||
Isolate bool `json:"isolate"` // If true, only draw selected values
|
||||
Exclude bool `json:"exclude"` // If true, don't draw selected values at all
|
||||
Hues []string `json:"hues"` // Array of colors for highlighting selected field values
|
||||
DecomposeAll bool `json:"decompose_all"` // Highlight all field values
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
// Heatmap represents an instrumentation's heatmap
|
||||
type Heatmap struct {
|
||||
BucketTime int `json:"bucket_time"` // Time corresponding to the bucket (Unix seconds)
|
||||
BucketYmin int `json:"bucket_ymin"` // Minimum y-axis value for the bucket
|
||||
BucketYmax int `json:"bucket_ymax"` // Maximum y-axis value for the bucket
|
||||
Present map[string]interface{} `json:"present"` // If the instrumentation defines a discrete decomposition, this property's value is an object whose keys are values of that field and whose values are the number of data points in that bucket for that key
|
||||
Total int `json:"total"` // The total number of data points in the bucket
|
||||
}
|
||||
|
||||
// CreateInstrumentationOpts represent the option that can be specified
|
||||
// when creating a new instrumentation.
|
||||
type CreateInstrumentationOpts struct {
|
||||
Clone int `json:"clone"` // An existing instrumentation ID to be cloned
|
||||
Module string `json:"module"` // Analytics module
|
||||
Stat string `json:"stat"` // Analytics stat
|
||||
Predicate string `json:"predicate"` // Instrumentation predicate, must be JSON string
|
||||
Decomposition string `json:"decomposition"`
|
||||
Granularity int `json:"granularity"` // Number of seconds between data points (default is 1)
|
||||
RetentionTime int `json:"retention-time"` // How long to keep this instrumentation data for
|
||||
PersistData bool `json:"persist-data"` // Whether or not to store this for historical analysis
|
||||
IdleMax int `json:"idle-max"` // Number of seconds after which if the instrumentation or its data has not been accessed via the API the service may delete the instrumentation and its data
|
||||
}
|
||||
|
||||
// DescribeAnalytics retrieves the "schema" for instrumentations that can be created.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#DescribeAnalytics
|
||||
func (c *Client) DescribeAnalytics() (*Analytics, error) {
|
||||
var resp Analytics
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: apiAnalytics,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get analytics")
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// ListInstrumentations retrieves all currently created instrumentations.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListInstrumentations
|
||||
func (c *Client) ListInstrumentations() ([]Instrumentation, error) {
|
||||
var resp []Instrumentation
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiAnalytics, apiInstrumentations),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get instrumentations")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetInstrumentation retrieves the configuration for the specified instrumentation.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetInstrumentation
|
||||
func (c *Client) GetInstrumentation(instrumentationID string) (*Instrumentation, error) {
|
||||
var resp Instrumentation
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiAnalytics, apiInstrumentations, instrumentationID),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get instrumentation with id %s", instrumentationID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GetInstrumentationValue retrieves the data associated to an instrumentation
|
||||
// for a point in time.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetInstrumentationValue
|
||||
func (c *Client) GetInstrumentationValue(instrumentationID string) (*InstrumentationValue, error) {
|
||||
var resp InstrumentationValue
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiAnalytics, apiInstrumentations, instrumentationID, apiInstrumentationsValue, apiInstrumentationsRaw),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get value for instrumentation with id %s", instrumentationID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GetInstrumentationHeatmap retrieves the specified instrumentation's heatmap.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetInstrumentationHeatmap
|
||||
func (c *Client) GetInstrumentationHeatmap(instrumentationID string) (*Heatmap, error) {
|
||||
var resp Heatmap
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiAnalytics, apiInstrumentations, instrumentationID, apiInstrumentationsValue, apiInstrumentationsHeatmap, apiInstrumentationsImage),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get heatmap image for instrumentation with id %s", instrumentationID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GetInstrumentationHeatmapDetails allows you to retrieve the bucket details
|
||||
// for a heatmap.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetInstrumentationHeatmapDetails
|
||||
func (c *Client) GetInstrumentationHeatmapDetails(instrumentationID string) (*Heatmap, error) {
|
||||
var resp Heatmap
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiAnalytics, apiInstrumentations, instrumentationID, apiInstrumentationsValue, apiInstrumentationsHeatmap, apiInstrumentationsDetails),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get heatmap details for instrumentation with id %s", instrumentationID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// CreateInstrumentation Creates an instrumentation. You can clone an existing
|
||||
// instrumentation by passing in the parameter clone, which should be a numeric id
|
||||
// of an existing instrumentation.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#CreateInstrumentation
|
||||
func (c *Client) CreateInstrumentation(opts CreateInstrumentationOpts) (*Instrumentation, error) {
|
||||
var resp Instrumentation
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: makeURL(apiAnalytics, apiInstrumentations),
|
||||
reqValue: opts,
|
||||
resp: &resp,
|
||||
expectedStatus: http.StatusCreated,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to create instrumentation")
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteInstrumentation destroys an instrumentation.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#DeleteInstrumentation
|
||||
func (c *Client) DeleteInstrumentation(instrumentationID string) error {
|
||||
req := request{
|
||||
method: client.DELETE,
|
||||
url: makeURL(apiAnalytics, apiInstrumentations, instrumentationID),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to delete instrumentation with id %s", instrumentationID)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// Key represent a public key
|
||||
type Key struct {
|
||||
Name string // Name for the key
|
||||
Fingerprint string // Key Fingerprint
|
||||
Key string // OpenSSH formatted public key
|
||||
}
|
||||
|
||||
/*func (k Key) Equals(other Key) bool {
|
||||
if k.Name == other.Name && k.Fingerprint == other.Fingerprint && k.Key == other.Key {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}*/
|
||||
|
||||
// CreateKeyOpts represent the option that can be specified
|
||||
// when creating a new key.
|
||||
type CreateKeyOpts struct {
|
||||
Name string `json:"name"` // Name for the key, optional
|
||||
Key string `json:"key"` // OpenSSH formatted public key
|
||||
}
|
||||
|
||||
// ListKeys returns a list of public keys registered with a specific account.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListKeys
|
||||
func (c *Client) ListKeys() ([]Key, error) {
|
||||
var resp []Key
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: apiKeys,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of keys")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetKey returns the key identified by keyName.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetKey
|
||||
func (c *Client) GetKey(keyName string) (*Key, error) {
|
||||
var resp Key
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiKeys, keyName),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get key with name: %s", keyName)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// CreateKey creates a new key with the specified options.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#CreateKey
|
||||
func (c *Client) CreateKey(opts CreateKeyOpts) (*Key, error) {
|
||||
var resp Key
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: apiKeys,
|
||||
reqValue: opts,
|
||||
resp: &resp,
|
||||
expectedStatus: http.StatusCreated,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to create key with name: %s", opts.Name)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteKey deletes the key identified by keyName.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#DeleteKey
|
||||
func (c *Client) DeleteKey(keyName string) error {
|
||||
req := request{
|
||||
method: client.DELETE,
|
||||
url: makeURL(apiKeys, keyName),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to delete key with name: %s", keyName)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// ListMachineFirewallRules lists all the firewall rules for the specified machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListMachineFirewallRules
|
||||
func (c *Client) ListMachineFirewallRules(machineID string) ([]FirewallRule, error) {
|
||||
var resp []FirewallRule
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiMachines, machineID, apiFirewallRules),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of firewall rules for machine with id %s", machineID)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// EnableFirewallMachine enables the firewall for the specified machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#EnableMachineFirewall
|
||||
func (c *Client) EnableFirewallMachine(machineID string) error {
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: fmt.Sprintf("%s/%s?action=%s", apiMachines, machineID, actionEnableFw),
|
||||
expectedStatus: http.StatusAccepted,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to enable firewall on machine with id: %s", machineID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableFirewallMachine disables the firewall for the specified machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#DisableMachineFirewall
|
||||
func (c *Client) DisableFirewallMachine(machineID string) error {
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: fmt.Sprintf("%s/%s?action=%s", apiMachines, machineID, actionDisableFw),
|
||||
expectedStatus: http.StatusAccepted,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to disable firewall on machine with id: %s", machineID)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// UpdateMachineMetadata updates the metadata for a given machine.
|
||||
// Any metadata keys passed in here are created if they do not exist, and
|
||||
// overwritten if they do.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#UpdateMachineMetadata
|
||||
func (c *Client) UpdateMachineMetadata(machineID string, metadata map[string]string) (map[string]interface{}, error) {
|
||||
var resp map[string]interface{}
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: makeURL(apiMachines, machineID, apiMetadata),
|
||||
reqValue: metadata,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to update metadata for machine with id %s", machineID)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetMachineMetadata returns the complete set of metadata associated with the
|
||||
// specified machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetMachineMetadata
|
||||
func (c *Client) GetMachineMetadata(machineID string) (map[string]interface{}, error) {
|
||||
var resp map[string]interface{}
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiMachines, machineID, apiMetadata),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of metadata for machine with id %s", machineID)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DeleteMachineMetadata deletes a single metadata key from the specified machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#DeleteMachineMetadata
|
||||
func (c *Client) DeleteMachineMetadata(machineID, metadataKey string) error {
|
||||
req := request{
|
||||
method: client.DELETE,
|
||||
url: makeURL(apiMachines, machineID, apiMetadata, metadataKey),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to delete metadata with key %s for machine with id %s", metadataKey, machineID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAllMachineMetadata deletes all metadata keys from the specified machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#DeleteAllMachineMetadata
|
||||
func (c *Client) DeleteAllMachineMetadata(machineID string) error {
|
||||
req := request{
|
||||
method: client.DELETE,
|
||||
url: makeURL(apiMachines, machineID, apiMetadata),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to delete metadata for machine with id %s", machineID)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// NICState represents the state of a NIC
|
||||
type NICState string
|
||||
|
||||
var (
|
||||
NICStateProvisioning NICState = "provisioning"
|
||||
NICStateRunning NICState = "running"
|
||||
NICStateStopped NICState = "stopped"
|
||||
)
|
||||
|
||||
// NIC represents a NIC on a machine
|
||||
type NIC struct {
|
||||
IP string `json:"ip"` // NIC's IPv4 Address
|
||||
MAC string `json:"mac"` // NIC's MAC address
|
||||
Primary bool `json:"primary"` // Whether this is the machine's primary NIC
|
||||
Netmask string `json:"netmask"` // IPv4 netmask
|
||||
Gateway string `json:"gateway"` // IPv4 gateway
|
||||
State NICState `json:"state"` // Describes the state of the NIC (e.g. provisioning, running, or stopped)
|
||||
Network string `json:"network"` // Network ID this NIC is attached to
|
||||
}
|
||||
|
||||
type addNICOptions struct {
|
||||
Network string `json:"network"` // UUID of network this NIC should attach to
|
||||
}
|
||||
|
||||
// ListNICs lists all the NICs on a machine belonging to a given account
|
||||
// See API docs: https://apidocs.joyent.com/cloudapi/#ListNics
|
||||
func (c *Client) ListNICs(machineID string) ([]NIC, error) {
|
||||
var resp []NIC
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiMachines, machineID, apiNICs),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to list NICs")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetNIC gets a specific NIC on a machine belonging to a given account
|
||||
// See API docs: https://apidocs.joyent.com/cloudapi/#GetNic
|
||||
func (c *Client) GetNIC(machineID, MAC string) (*NIC, error) {
|
||||
resp := new(NIC)
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiMachines, machineID, apiNICs, MAC),
|
||||
resp: resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get NIC with MAC: %s", MAC)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddNIC creates a new NIC on a machine belonging to a given account.
|
||||
// *WARNING*: this causes the machine to reboot while adding the NIC.
|
||||
// See API docs: https://apidocs.joyent.com/cloudapi/#AddNic
|
||||
func (c *Client) AddNIC(machineID, networkID string) (*NIC, error) {
|
||||
resp := new(NIC)
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: makeURL(apiMachines, machineID, apiNICs),
|
||||
reqValue: addNICOptions{networkID},
|
||||
resp: resp,
|
||||
expectedStatus: http.StatusCreated,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to add NIC to machine %s on network: %s", machineID, networkID)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// RemoveNIC removes a NIC on a machine belonging to a given account.
|
||||
// *WARNING*: this causes the machine to reboot while removing the NIC.
|
||||
// See API docs: https://apidocs.joyent.com/cloudapi/#RemoveNic
|
||||
func (c *Client) RemoveNIC(machineID, MAC string) error {
|
||||
req := request{
|
||||
method: client.DELETE,
|
||||
url: makeURL(apiMachines, machineID, apiNICs, MAC),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to remove NIC: %s", MAC)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// Snapshot represent a point in time state of a machine.
|
||||
type Snapshot struct {
|
||||
Name string // Snapshot name
|
||||
State string // Snapshot state
|
||||
}
|
||||
|
||||
// SnapshotOpts represent the option that can be specified
|
||||
// when creating a new machine snapshot.
|
||||
type SnapshotOpts struct {
|
||||
Name string `json:"name"` // Snapshot name
|
||||
}
|
||||
|
||||
// CreateMachineSnapshot creates a new snapshot for the machine with the options specified.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#CreateMachineSnapshot
|
||||
func (c *Client) CreateMachineSnapshot(machineID string, opts SnapshotOpts) (*Snapshot, error) {
|
||||
var resp Snapshot
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: makeURL(apiMachines, machineID, apiSnapshots),
|
||||
reqValue: opts,
|
||||
resp: &resp,
|
||||
expectedStatus: http.StatusCreated,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to create snapshot %s from machine with id %s", opts.Name, machineID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// StartMachineFromSnapshot starts the machine from the specified snapshot.
|
||||
// Machine must be in 'stopped' state.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#StartMachineFromSnapshot
|
||||
func (c *Client) StartMachineFromSnapshot(machineID, snapshotName string) error {
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: makeURL(apiMachines, machineID, apiSnapshots, snapshotName),
|
||||
expectedStatus: http.StatusAccepted,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to start machine with id %s from snapshot %s", machineID, snapshotName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListMachineSnapshots lists all snapshots for the specified machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListMachineSnapshots
|
||||
func (c *Client) ListMachineSnapshots(machineID string) ([]Snapshot, error) {
|
||||
var resp []Snapshot
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiMachines, machineID, apiSnapshots),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of snapshots for machine with id %s", machineID)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetMachineSnapshot returns the state of the specified snapshot.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetMachineSnapshot
|
||||
func (c *Client) GetMachineSnapshot(machineID, snapshotName string) (*Snapshot, error) {
|
||||
var resp Snapshot
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiMachines, machineID, apiSnapshots, snapshotName),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get snapshot %s for machine with id %s", snapshotName, machineID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteMachineSnapshot deletes the specified snapshot.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#DeleteMachineSnapshot
|
||||
func (c *Client) DeleteMachineSnapshot(machineID, snapshotName string) error {
|
||||
req := request{
|
||||
method: client.DELETE,
|
||||
url: makeURL(apiMachines, machineID, apiSnapshots, snapshotName),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to delete snapshot %s for machine with id %s", snapshotName, machineID)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// AddMachineTags adds additional tags to the specified machine.
|
||||
// This API lets you append new tags, not overwrite existing tags.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#AddMachineTags
|
||||
func (c *Client) AddMachineTags(machineID string, tags map[string]string) (map[string]string, error) {
|
||||
var resp map[string]string
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: makeURL(apiMachines, machineID, apiTags),
|
||||
reqValue: tags,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to add tags for machine with id %s", machineID)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ReplaceMachineTags replaces existing tags for the specified machine.
|
||||
// This API lets you overwrite existing tags, not append to existing tags.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ReplaceMachineTags
|
||||
func (c *Client) ReplaceMachineTags(machineID string, tags map[string]string) (map[string]string, error) {
|
||||
var resp map[string]string
|
||||
req := request{
|
||||
method: client.PUT,
|
||||
url: makeURL(apiMachines, machineID, apiTags),
|
||||
reqValue: tags,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to replace tags for machine with id %s", machineID)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListMachineTags returns the complete set of tags associated with the specified machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListMachineTags
|
||||
func (c *Client) ListMachineTags(machineID string) (map[string]string, error) {
|
||||
var resp map[string]string
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiMachines, machineID, apiTags),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of tags for machine with id %s", machineID)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetMachineTag returns the value for a single tag on the specified machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetMachineTag
|
||||
func (c *Client) GetMachineTag(machineID, tagKey string) (string, error) {
|
||||
var resp []byte
|
||||
requestHeaders := make(http.Header)
|
||||
requestHeaders.Set("Accept", "text/plain")
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiMachines, machineID, apiTags, tagKey),
|
||||
resp: &resp,
|
||||
reqHeader: requestHeaders,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return "", errors.Newf(err, "failed to get tag %s for machine with id %s", tagKey, machineID)
|
||||
}
|
||||
return string(resp), nil
|
||||
}
|
||||
|
||||
// DeleteMachineTag deletes a single tag from the specified machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#DeleteMachineTag
|
||||
func (c *Client) DeleteMachineTag(machineID, tagKey string) error {
|
||||
req := request{
|
||||
method: client.DELETE,
|
||||
url: makeURL(apiMachines, machineID, apiTags, tagKey),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to delete tag with key %s for machine with id %s", tagKey, machineID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteMachineTags deletes all tags from the specified machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#DeleteMachineTags
|
||||
func (c *Client) DeleteMachineTags(machineID string) error {
|
||||
req := request{
|
||||
method: client.DELETE,
|
||||
url: makeURL(apiMachines, machineID, apiTags),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to delete tags for machine with id %s", machineID)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,306 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// Machine represent a provisioned virtual machines
|
||||
type Machine struct {
|
||||
Id string // Unique identifier for the image
|
||||
Name string // Machine friendly name
|
||||
Type string // Machine type, one of 'smartmachine' or 'virtualmachine'
|
||||
State string // Current state of the machine
|
||||
Dataset string // The dataset URN the machine was provisioned with. For new images/datasets this value will be the dataset id, i.e, same value than the image attribute
|
||||
Memory int // The amount of memory the machine has (in Mb)
|
||||
Disk int // The amount of disk the machine has (in Gb)
|
||||
IPs []string // The IP addresses the machine has
|
||||
Metadata map[string]string // Map of the machine metadata, e.g. authorized-keys
|
||||
Tags map[string]string // Map of the machine tags
|
||||
Created string // When the machine was created
|
||||
Updated string // When the machine was updated
|
||||
Package string // The name of the package used to create the machine
|
||||
Image string // The image id the machine was provisioned with
|
||||
PrimaryIP string // The primary (public) IP address for the machine
|
||||
Networks []string // The network IDs for the machine
|
||||
FirewallEnabled bool `json:"firewall_enabled"` // whether or not the firewall is enabled
|
||||
}
|
||||
|
||||
// Equals compares two machines. Ignores state and timestamps.
|
||||
func (m Machine) Equals(other Machine) bool {
|
||||
if m.Id == other.Id && m.Name == other.Name && m.Type == other.Type && m.Dataset == other.Dataset &&
|
||||
m.Memory == other.Memory && m.Disk == other.Disk && m.Package == other.Package && m.Image == other.Image &&
|
||||
m.compareIPs(other) && m.compareMetadata(other) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Helper method to compare two machines IPs
|
||||
func (m Machine) compareIPs(other Machine) bool {
|
||||
if len(m.IPs) != len(other.IPs) {
|
||||
return false
|
||||
}
|
||||
for i, v := range m.IPs {
|
||||
if v != other.IPs[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Helper method to compare two machines metadata
|
||||
func (m Machine) compareMetadata(other Machine) bool {
|
||||
if len(m.Metadata) != len(other.Metadata) {
|
||||
return false
|
||||
}
|
||||
for k, v := range m.Metadata {
|
||||
if v != other.Metadata[k] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// CreateMachineOpts represent the option that can be specified
|
||||
// when creating a new machine.
|
||||
type CreateMachineOpts struct {
|
||||
Name string `json:"name"` // Machine friendly name, default is a randomly generated name
|
||||
Package string `json:"package"` // Name of the package to use on provisioning
|
||||
Image string `json:"image"` // The image UUID
|
||||
Networks []string `json:"networks"` // Desired networks IDs
|
||||
Metadata map[string]string `json:"-"` // An arbitrary set of metadata key/value pairs can be set at provision time
|
||||
Tags map[string]string `json:"-"` // An arbitrary set of tags can be set at provision time
|
||||
FirewallEnabled bool `json:"firewall_enabled"` // Completely enable or disable firewall for this machine (new in API version 7.0)
|
||||
}
|
||||
|
||||
// AuditAction represents an action/event accomplished by a machine.
|
||||
type AuditAction struct {
|
||||
Action string // Action name
|
||||
Parameters map[string]interface{} // Original set of parameters sent when the action was requested
|
||||
Time string // When the action finished
|
||||
Success string // Either 'yes' or 'no', depending on the action successfulness
|
||||
Caller Caller // Account requesting the action
|
||||
}
|
||||
|
||||
// Caller represents an account requesting an action.
|
||||
type Caller struct {
|
||||
Type string // Authentication type for the action request. One of 'basic', 'operator', 'signature' or 'token'
|
||||
User string // When the authentication type is 'basic', this member will be present and include user login
|
||||
IP string // The IP addresses this from which the action was requested. Not present if type is 'operator'
|
||||
KeyId string // When authentication type is either 'signature' or 'token', SSH key identifier
|
||||
}
|
||||
|
||||
// appendJSON marshals the given attribute value and appends it as an encoded value to the given json data.
|
||||
// The newly encode (attr, value) is inserted just before the closing "}" in the json data.
|
||||
func appendJSON(data []byte, attr string, value interface{}) ([]byte, error) {
|
||||
newData, err := json.Marshal(&value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
strData := string(data)
|
||||
result := fmt.Sprintf(`%s, "%s":%s}`, strData[:len(strData)-1], attr, string(newData))
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
type jsonOpts CreateMachineOpts
|
||||
|
||||
// MarshalJSON turns the given CreateMachineOpts into JSON
|
||||
func (opts CreateMachineOpts) MarshalJSON() ([]byte, error) {
|
||||
jo := jsonOpts(opts)
|
||||
data, err := json.Marshal(&jo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range opts.Tags {
|
||||
if !strings.HasPrefix(k, "tag.") {
|
||||
k = "tag." + k
|
||||
}
|
||||
data, err = appendJSON(data, k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for k, v := range opts.Metadata {
|
||||
if !strings.HasPrefix(k, "metadata.") {
|
||||
k = "metadata." + k
|
||||
}
|
||||
data, err = appendJSON(data, k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ListMachines lists all machines on record for an account.
|
||||
// You can paginate this API by passing in offset, and limit
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListMachines
|
||||
func (c *Client) ListMachines(filter *Filter) ([]Machine, error) {
|
||||
var resp []Machine
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: apiMachines,
|
||||
filter: filter,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of machines")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CountMachines returns the number of machines on record for an account.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListMachines
|
||||
func (c *Client) CountMachines() (int, error) {
|
||||
var resp int
|
||||
req := request{
|
||||
method: client.HEAD,
|
||||
url: apiMachines,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return -1, errors.Newf(err, "failed to get count of machines")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetMachine returns the machine specified by machineId.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetMachine
|
||||
func (c *Client) GetMachine(machineID string) (*Machine, error) {
|
||||
var resp Machine
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiMachines, machineID),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get machine with id: %s", machineID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// CreateMachine creates a new machine with the options specified.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#CreateMachine
|
||||
func (c *Client) CreateMachine(opts CreateMachineOpts) (*Machine, error) {
|
||||
var resp Machine
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: apiMachines,
|
||||
reqValue: opts,
|
||||
resp: &resp,
|
||||
expectedStatus: http.StatusCreated,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to create machine with name: %s", opts.Name)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// StopMachine stops a running machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#StopMachine
|
||||
func (c *Client) StopMachine(machineID string) error {
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: fmt.Sprintf("%s/%s?action=%s", apiMachines, machineID, actionStop),
|
||||
expectedStatus: http.StatusAccepted,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to stop machine with id: %s", machineID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartMachine starts a stopped machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#StartMachine
|
||||
func (c *Client) StartMachine(machineID string) error {
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: fmt.Sprintf("%s/%s?action=%s", apiMachines, machineID, actionStart),
|
||||
expectedStatus: http.StatusAccepted,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to start machine with id: %s", machineID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RebootMachine reboots (stop followed by a start) a machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#RebootMachine
|
||||
func (c *Client) RebootMachine(machineID string) error {
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: fmt.Sprintf("%s/%s?action=%s", apiMachines, machineID, actionReboot),
|
||||
expectedStatus: http.StatusAccepted,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to reboot machine with id: %s", machineID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResizeMachine allows you to resize a SmartMachine. Virtual machines can also
|
||||
// be resized, but only resizing virtual machines to a higher capacity package
|
||||
// is supported.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ResizeMachine
|
||||
func (c *Client) ResizeMachine(machineID, packageName string) error {
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: fmt.Sprintf("%s/%s?action=%s&package=%s", apiMachines, machineID, actionResize, packageName),
|
||||
expectedStatus: http.StatusAccepted,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to resize machine with id: %s", machineID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenameMachine renames an existing machine.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#RenameMachine
|
||||
func (c *Client) RenameMachine(machineID, machineName string) error {
|
||||
req := request{
|
||||
method: client.POST,
|
||||
url: fmt.Sprintf("%s/%s?action=%s&name=%s", apiMachines, machineID, actionRename, machineName),
|
||||
expectedStatus: http.StatusAccepted,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to rename machine with id: %s", machineID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteMachine allows you to completely destroy a machine. Machine must be in the 'stopped' state.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#DeleteMachine
|
||||
func (c *Client) DeleteMachine(machineID string) error {
|
||||
req := request{
|
||||
method: client.DELETE,
|
||||
url: makeURL(apiMachines, machineID),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return errors.Newf(err, "failed to delete machine with id %s", machineID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MachineAudit provides a list of machine's accomplished actions, (sorted from
|
||||
// latest to older one).
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#MachineAudit
|
||||
func (c *Client) MachineAudit(machineID string) ([]AuditAction, error) {
|
||||
var resp []AuditAction
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiMachines, machineID, apiAudit),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get actions for machine with id %s", machineID)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// Network represents a network available to a given account
|
||||
type Network struct {
|
||||
Id string // Unique identifier for the network
|
||||
Name string // Network name
|
||||
Public bool // Whether this a public or private (rfc1918) network
|
||||
Description string // Optional description for this network, when name is not enough
|
||||
}
|
||||
|
||||
// ListNetworks lists all the networks which can be used by the given account.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListNetworks
|
||||
func (c *Client) ListNetworks() ([]Network, error) {
|
||||
var resp []Network
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: apiNetworks,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of networks")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetNetwork retrieves an individual network record.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetNetwork
|
||||
func (c *Client) GetNetwork(networkID string) (*Network, error) {
|
||||
var resp Network
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiNetworks, networkID),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get network with id %s", networkID)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// Package represents a named collections of resources that are used to describe the 'sizes'
|
||||
// of either a smart machine or a virtual machine.
|
||||
type Package struct {
|
||||
Name string // Name for the package
|
||||
Memory int // Memory available (in Mb)
|
||||
Disk int // Disk space available (in Gb)
|
||||
Swap int // Swap memory available (in Mb)
|
||||
VCPUs int // Number of VCPUs for the package
|
||||
Default bool // Indicates whether this is the default package in the datacenter
|
||||
Id string // Unique identifier for the package
|
||||
Version string // Version for the package
|
||||
Group string // Group this package belongs to
|
||||
Description string // Human friendly description for the package
|
||||
}
|
||||
|
||||
// ListPackages provides a list of packages available in the datacenter.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#ListPackages
|
||||
func (c *Client) ListPackages(filter *Filter) ([]Package, error) {
|
||||
var resp []Package
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: apiPackages,
|
||||
filter: filter,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of packages")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetPackage returns the package specified by packageName. NOTE: packageName can
|
||||
// specify either the package name or package ID.
|
||||
// See API docs: http://apidocs.joyent.com/cloudapi/#GetPackage
|
||||
func (c *Client) GetPackage(packageName string) (*Package, error) {
|
||||
var resp Package
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: makeURL(apiPackages, packageName),
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get package with name: %s", packageName)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package cloudapi
|
||||
|
||||
import (
|
||||
"github.com/joyent/gocommon/client"
|
||||
"github.com/joyent/gocommon/errors"
|
||||
)
|
||||
|
||||
// list available services
|
||||
func (c *Client) ListServices() (map[string]string, error) {
|
||||
var resp map[string]string
|
||||
req := request{
|
||||
method: client.GET,
|
||||
url: apiServices,
|
||||
resp: &resp,
|
||||
}
|
||||
if _, err := c.sendRequest(req); err != nil {
|
||||
return nil, errors.Newf(err, "failed to get list of services")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
Package gosdc enables Go programs to interact with the Joyent CloudAPI.
|
||||
|
||||
The gosdc package is structured as follow:
|
||||
|
||||
- gosdc/cloudapi. This package interacts with the Cloud API (http://apidocs.joyent.com/cloudapi/).
|
||||
- gosdc/localservices. This package provides local services to be used for testing.
|
||||
|
||||
Licensed under the Mozilla Public License version 2.0
|
||||
|
||||
Copyright (c) Joyent Inc.
|
||||
|
||||
*/
|
||||
package gosdc
|
|
@ -1,46 +0,0 @@
|
|||
box: golang
|
||||
|
||||
# services:
|
||||
# - postgres
|
||||
# http://devcenter.wercker.com/docs/services/postgresql.html
|
||||
|
||||
# - mongodb
|
||||
# http://devcenter.wercker.com/docs/services/mongodb.html
|
||||
|
||||
build:
|
||||
# The steps that will be executed on build
|
||||
# Steps make up the actions in your pipeline
|
||||
# Read more about steps on our dev center:
|
||||
# http://devcenter.wercker.com/docs/steps/index.html
|
||||
steps:
|
||||
# Sets the go workspace and places you package
|
||||
# at the right place in the workspace tree
|
||||
- setup-go-workspace:
|
||||
package-dir: github.com/joyent/gosdc
|
||||
|
||||
# Gets the dependencies
|
||||
- script:
|
||||
name: go get
|
||||
code: |
|
||||
go get -v -t ./...
|
||||
|
||||
- script:
|
||||
name: make a new key for testing
|
||||
code: |
|
||||
ssh-keygen -b 2048 \
|
||||
-C "Testing Key" \
|
||||
-f $WERCKER_SOURCE_DIR/test_key.id_rsa \
|
||||
-t rsa \
|
||||
-P ""
|
||||
|
||||
# Build the project
|
||||
- script:
|
||||
name: go build
|
||||
code: |
|
||||
go build ./...
|
||||
|
||||
# Test the project
|
||||
- script:
|
||||
name: go test
|
||||
code: |
|
||||
env KEY_NAME=$WERCKER_SOURCE_DIR/test_key.id_rsa LIVE=false go test ./...
|
|
@ -1,373 +0,0 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
|
@ -1,73 +0,0 @@
|
|||
gosign
|
||||
======
|
||||
|
||||
Go HTTP signing library for Joyent's Triton and Manta.
|
||||
|
||||
## Installation
|
||||
|
||||
Use `go-get` to install gosign
|
||||
```
|
||||
go get github.com/joyent/gosign
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Documentation can be found on godoc.
|
||||
|
||||
- [github.com/joyent/gosign](http://godoc.org/github.com/joyent/gosign)
|
||||
- [github.com/joyent/gosign/auth](http://godoc.org/github.com/joyent/gosign/auth)
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Report bugs and request features using [GitHub Issues](https://github.com/joyent/gosign/issues), or contribute code via a [GitHub Pull Request](https://github.com/joyent/gosign/pulls). Changes will be code reviewed before merging. In the near future, automated tests will be run, but in the meantime please `go fmt`, `go lint`, and test all contributions.
|
||||
|
||||
|
||||
## Developing
|
||||
|
||||
This library assumes a Go development environment setup based on [How to Write Go Code](https://golang.org/doc/code.html). Your GOPATH environment variable should be pointed at your workspace directory.
|
||||
|
||||
You can now use `go get github.com/joyent/gosign` to install the repository to the correct location, but if you are intending on contributing back a change you may want to consider cloning the repository via git yourself. This way you can have a single source tree for all Joyent Go projects with each repo having two remotes -- your own fork on GitHub and the upstream origin.
|
||||
|
||||
For example if your GOPATH is `~/src/joyent/go` and you're working on multiple repos then that directory tree might look like:
|
||||
|
||||
```
|
||||
~/src/joyent/go/
|
||||
|_ pkg/
|
||||
|_ src/
|
||||
|_ github.com
|
||||
|_ joyent
|
||||
|_ gocommon
|
||||
|_ gomanta
|
||||
|_ gosdc
|
||||
|_ gosign
|
||||
```
|
||||
|
||||
### Recommended Setup
|
||||
|
||||
```
|
||||
$ mkdir -p ${GOPATH}/src/github.com/joyent
|
||||
$ cd ${GOPATH}/src/github.com/joyent
|
||||
$ git clone git@github.com:<yourname>/gosign.git
|
||||
$ cd gosign
|
||||
$ git remote add upstream git@github.com:joyent/gosign.git
|
||||
$ git remote -v
|
||||
origin git@github.com:<yourname>/gosign.git (fetch)
|
||||
origin git@github.com:<yourname>/gosign.git (push)
|
||||
upstream git@github.com:joyent/gosign.git (fetch)
|
||||
upstream git@github.com:joyent/gosign.git (push)
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
```
|
||||
cd ${GOPATH}/src/github.com/joyent/gosign
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### Build the Library
|
||||
|
||||
```
|
||||
cd ${GOPATH}/src/github.com/joyent/gosign
|
||||
go build ./...
|
||||
```
|
|
@ -1,135 +0,0 @@
|
|||
//
|
||||
// gosign - Go HTTP signing library for the Joyent Public Cloud and Joyent Manta
|
||||
//
|
||||
//
|
||||
// Copyright (c) 2013 Joyent Inc.
|
||||
//
|
||||
// Written by Daniele Stroppa <daniele.stroppa@joyent.com>
|
||||
//
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// Authorization Headers
|
||||
SdcSignature = "Signature keyId=\"/%s/keys/%s\",algorithm=\"%s\" %s"
|
||||
MantaSignature = "Signature keyId=\"/%s/keys/%s\",algorithm=\"%s\",signature=\"%s\""
|
||||
)
|
||||
|
||||
type Endpoint struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
User string
|
||||
PrivateKey PrivateKey
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
type Credentials struct {
|
||||
UserAuthentication *Auth
|
||||
SdcKeyId string
|
||||
SdcEndpoint Endpoint
|
||||
MantaKeyId string
|
||||
MantaEndpoint Endpoint
|
||||
}
|
||||
|
||||
type PrivateKey struct {
|
||||
key *rsa.PrivateKey
|
||||
}
|
||||
|
||||
// NewAuth creates a new Auth.
|
||||
func NewAuth(user, privateKey, algorithm string) (*Auth, error) {
|
||||
block, _ := pem.Decode([]byte(privateKey))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("invalid private key data: %s", privateKey)
|
||||
}
|
||||
rsakey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("An error occurred while parsing the key: %s", err)
|
||||
}
|
||||
return &Auth{user, PrivateKey{rsakey}, algorithm}, nil
|
||||
}
|
||||
|
||||
// The CreateAuthorizationHeader returns the Authorization header for the give request.
|
||||
func CreateAuthorizationHeader(headers http.Header, credentials *Credentials, isMantaRequest bool) (string, error) {
|
||||
if isMantaRequest {
|
||||
signature, err := GetSignature(credentials.UserAuthentication, "date: "+headers.Get("Date"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf(MantaSignature, credentials.UserAuthentication.User, credentials.MantaKeyId,
|
||||
credentials.UserAuthentication.Algorithm, signature), nil
|
||||
}
|
||||
signature, err := GetSignature(credentials.UserAuthentication, headers.Get("Date"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf(SdcSignature, credentials.UserAuthentication.User, credentials.SdcKeyId,
|
||||
credentials.UserAuthentication.Algorithm, signature), nil
|
||||
}
|
||||
|
||||
// The GetSignature method signs the specified key according to http://apidocs.joyent.com/cloudapi/#issuing-requests
|
||||
// and http://apidocs.joyent.com/manta/api.html#authentication.
|
||||
func GetSignature(auth *Auth, signing string) (string, error) {
|
||||
hashFunc := getHashFunction(auth.Algorithm)
|
||||
hash := hashFunc.New()
|
||||
hash.Write([]byte(signing))
|
||||
|
||||
digest := hash.Sum(nil)
|
||||
|
||||
signed, err := rsa.SignPKCS1v15(rand.Reader, auth.PrivateKey.key, hashFunc, digest)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("An error occurred while signing the key: %s", err)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signed), nil
|
||||
}
|
||||
|
||||
// Helper method to get the Hash function based on the algorithm
|
||||
func getHashFunction(algorithm string) (hashFunc crypto.Hash) {
|
||||
switch strings.ToLower(algorithm) {
|
||||
case "rsa-sha1":
|
||||
hashFunc = crypto.SHA1
|
||||
case "rsa-sha224", "rsa-sha256":
|
||||
hashFunc = crypto.SHA256
|
||||
case "rsa-sha384", "rsa-sha512":
|
||||
hashFunc = crypto.SHA512
|
||||
default:
|
||||
hashFunc = crypto.SHA256
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cred *Credentials) Region() string {
|
||||
sdcUrl := cred.SdcEndpoint.URL
|
||||
|
||||
if isLocalhost(sdcUrl) {
|
||||
return "some-region"
|
||||
}
|
||||
return sdcUrl[strings.LastIndex(sdcUrl, "/")+1 : strings.Index(sdcUrl, ".")]
|
||||
}
|
||||
|
||||
func isLocalhost(u string) bool {
|
||||
parsedUrl, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(parsedUrl.Host, "localhost") || strings.HasPrefix(parsedUrl.Host, "127.0.0.1") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
* The sign package enables Go programs to create signed requests for
|
||||
* the Joyent Public Cloud and Joyent Manta services.
|
||||
*
|
||||
* The sign package is structured as follow:
|
||||
*
|
||||
* - gosign/auth. This package deals with the authorization and signature of requests.
|
||||
*
|
||||
* Copyright (c) 2016 Joyent Inc.
|
||||
* Written by Daniele Stroppa <daniele.stroppa@joyent.com>
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package gosign
|
|
@ -0,0 +1,216 @@
|
|||
# triton-go
|
||||
|
||||
`go-triton` is an idiomatic library exposing a client SDK for Go applications using the Joyent Triton API.
|
||||
|
||||
## Usage
|
||||
|
||||
Triton uses [HTTP Signature][4] to sign the Date header in each HTTP request made to the Triton API. Currently, requests can be signed using either a private key file loaded from disk (using an [`authentication.PrivateKeySigner`][5]), or using a key stored with the local SSH Agent (using an [`SSHAgentSigner`][6].
|
||||
|
||||
To construct a Signer, use the `New*` range of methods in the `authentication` package. In the case of `authentication.NewSSHAgentSigner`, the parameters are the fingerprint of the key with which to sign, and the account name (normally stored in the `SDC_ACCOUNT` environment variable). For example:
|
||||
|
||||
```
|
||||
const fingerprint := "a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11"
|
||||
sshKeySigner, err := authentication.NewSSHAgentSigner(fingerprint, "AccountName")
|
||||
if err != nil {
|
||||
log.Fatalf("NewSSHAgentSigner: %s", err)
|
||||
}
|
||||
```
|
||||
|
||||
An appropriate key fingerprint can be generated using `ssh-keygen`:
|
||||
|
||||
```
|
||||
ssh-keygen -Emd5 -lf ~/.ssh/id_rsa.pub | cut -d " " -f 2 | sed 's/MD5://'
|
||||
```
|
||||
|
||||
To construct a Client, use the `NewClient` function, passing in the endpoint, account name and constructed signer:
|
||||
|
||||
```go
|
||||
client, err := triton.NewClient("https://us-sw-1.api.joyent.com/", "AccountName", sshKeySigner)
|
||||
if err != nil {
|
||||
log.Fatalf("NewClient: %s", err)
|
||||
}
|
||||
```
|
||||
|
||||
Having constructed a `triton.Client`, use the methods available to access functionality by functional grouping. For example, for access to operations on SSH keys, use the `Keys()` method to obtain a client which has access to the `CreateKey`, `ListKeys` and `DeleteKey` operations. For access to operations on Machines, use the `Machines()` method to obtain a client which has access to the `RenameMachine`, `GetMachineMetadata`, `GetMachineTag`, and other operations.
|
||||
|
||||
Operation methods take their formal parameters via a struct named `OperationInput` - for example when creating an SSH key, the `CreateKeyInput` struct is used with the `func CreateKey(*CreateKeyInput) (*Key, error)` method. This allows specification of named parameters:
|
||||
|
||||
```
|
||||
client := state.Client().Keys()
|
||||
|
||||
key, err := client.CreateKey(&CreateKeyInput{
|
||||
Name: "tempKey",
|
||||
Key: "ssh-rsa .....",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Key contains the return value.
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
If an error is returned by the HTTP API, the `error` returned from the function will contain an instance of `triton.TritonError` in the chain. Error wrapping is performed using the [errwrap][7] library from HashiCorp.
|
||||
|
||||
## Completeness
|
||||
|
||||
The following list is updated as new functionality is added. The complete list of operations is taken from the [CloudAPI documentation](https://apidocs.joyent.com/cloudapi).
|
||||
|
||||
- Accounts
|
||||
- [x] GetAccount
|
||||
- [x] UpdateAccount
|
||||
- Keys
|
||||
- [x] ListKeys
|
||||
- [x] GetKey
|
||||
- [x] CreateKey
|
||||
- [x] DeleteKey
|
||||
- Users
|
||||
- [ ] ListUsers
|
||||
- [ ] GetUser
|
||||
- [ ] CreateUser
|
||||
- [ ] UpdateUser
|
||||
- [ ] ChangeUserPassword
|
||||
- [ ] DeleteUser
|
||||
- Roles
|
||||
- [x] ListRoles
|
||||
- [x] GetRole
|
||||
- [x] CreateRole
|
||||
- [x] UpdateRole
|
||||
- [x] DeleteRole
|
||||
- Role Tags
|
||||
- [ ] SetRoleTags
|
||||
- Policies
|
||||
- [ ] ListPolicies
|
||||
- [ ] GetPolicy
|
||||
- [ ] CreatePolicy
|
||||
- [ ] UpdatePolicy
|
||||
- [ ] DeletePolicy
|
||||
- User SSH Keys
|
||||
- [x] ListUserKeys
|
||||
- [x] GetUserKey
|
||||
- [x] CreateUserKey
|
||||
- [x] DeleteUserKey
|
||||
- Config
|
||||
- [x] GetConfig
|
||||
- [x] UpdateConfig
|
||||
- Datacenters
|
||||
- [x] ListDatacenters
|
||||
- [x] GetDatacenter
|
||||
- Services
|
||||
- [x] ListServices
|
||||
- Images
|
||||
- [x] ListImages
|
||||
- [x] GetImage
|
||||
- [x] DeleteImage
|
||||
- [x] ExportImage
|
||||
- [x] CreateImageFromMachine
|
||||
- [x] UpdateImage
|
||||
- Packages
|
||||
- [x] ListPackages
|
||||
- [x] GetPackage
|
||||
- Instances
|
||||
- [ ] ListMachines
|
||||
- [x] GetMachine
|
||||
- [x] CreateMachine
|
||||
- [ ] StopMachine
|
||||
- [ ] StartMachine
|
||||
- [ ] RebootMachine
|
||||
- [x] ResizeMachine
|
||||
- [x] RenameMachine
|
||||
- [x] EnableMachineFirewall
|
||||
- [x] DisableMachineFirewall
|
||||
- [ ] CreateMachineSnapshot
|
||||
- [ ] StartMachineFromSnapshot
|
||||
- [ ] ListMachineSnapshots
|
||||
- [ ] GetMachineSnapshot
|
||||
- [ ] DeleteMachineSnapshot
|
||||
- [x] UpdateMachineMetadata
|
||||
- [ ] ListMachineMetadata
|
||||
- [ ] GetMachineMetadata
|
||||
- [ ] DeleteMachineMetadata
|
||||
- [ ] DeleteAllMachineMetadata
|
||||
- [x] AddMachineTags
|
||||
- [x] ReplaceMachineTags
|
||||
- [x] ListMachineTags
|
||||
- [x] GetMachineTag
|
||||
- [x] DeleteMachineTag
|
||||
- [x] DeleteMachineTags
|
||||
- [x] DeleteMachine
|
||||
- [ ] MachineAudit
|
||||
- Analytics
|
||||
- [ ] DescribeAnalytics
|
||||
- [ ] ListInstrumentations
|
||||
- [ ] GetInstrumentation
|
||||
- [ ] GetInstrumentationValue
|
||||
- [ ] GetInstrumentationHeatmap
|
||||
- [ ] GetInstrumentationHeatmapDetails
|
||||
- [ ] CreateInstrumentation
|
||||
- [ ] DeleteInstrumentation
|
||||
- Firewall Rules
|
||||
- [x] ListFirewallRules
|
||||
- [x] GetFirewallRule
|
||||
- [x] CreateFirewallRule
|
||||
- [x] UpdateFirewallRule
|
||||
- [x] EnableFirewallRule
|
||||
- [x] DisableFirewallRule
|
||||
- [x] DeleteFirewallRule
|
||||
- [ ] ListMachineFirewallRules
|
||||
- [x] ListFirewallRuleMachines
|
||||
- Fabrics
|
||||
- [x] ListFabricVLANs
|
||||
- [x] CreateFabricVLAN
|
||||
- [x] GetFabricVLAN
|
||||
- [x] UpdateFabricVLAN
|
||||
- [x] DeleteFabricVLAN
|
||||
- [x] ListFabricNetworks
|
||||
- [x] CreateFabricNetwork
|
||||
- [x] GetFabricNetwork
|
||||
- [x] DeleteFabricNetwork
|
||||
- Networks
|
||||
- [x] ListNetworks
|
||||
- [x] GetNetwork
|
||||
- Nics
|
||||
- [ ] ListNics
|
||||
- [ ] GetNic
|
||||
- [x] AddNic
|
||||
- [x] RemoveNic
|
||||
|
||||
## Running Acceptance Tests
|
||||
|
||||
Acceptance Tests run directly against the Triton API, so you will need either a local installation or Triton or an account with Joyent in order to run them. The tests create real resources (and thus cost real money!)
|
||||
|
||||
In order to run acceptance tests, the following environment variables must be set:
|
||||
|
||||
- `TRITON_TEST` - must be set to any value in order to indicate desire to create resources
|
||||
- `SDC_URL` - the base endpoint for the Triton API
|
||||
- `SDC_ACCOUNT` - the account name for the Triton API
|
||||
- `SDC_KEY_ID` - the fingerprint of the SSH key identifying the key
|
||||
|
||||
Additionally, you may set `SDC_KEY_MATERIAL` to the contents of an unencrypted private key. If this is set, the PrivateKeySigner (see above) will be used - if not the SSHAgentSigner will be used.
|
||||
|
||||
### Example Run
|
||||
|
||||
The verbose output has been removed for brevity here.
|
||||
|
||||
```
|
||||
$ HTTP_PROXY=http://localhost:8888 \
|
||||
TRITON_TEST=1 \
|
||||
SDC_URL=https://us-sw-1.api.joyent.com \
|
||||
SDC_ACCOUNT=AccountName \
|
||||
SDC_KEY_ID=a4:c6:f3:75:80:27:e0:03:a9:98:79:ef:c5:0a:06:11 \
|
||||
go test -v -run "TestAccKey"
|
||||
=== RUN TestAccKey_Create
|
||||
--- PASS: TestAccKey_Create (12.46s)
|
||||
=== RUN TestAccKey_Get
|
||||
--- PASS: TestAccKey_Get (4.30s)
|
||||
=== RUN TestAccKey_Delete
|
||||
--- PASS: TestAccKey_Delete (15.08s)
|
||||
PASS
|
||||
ok github.com/jen20/triton-go 31.861s
|
||||
```
|
||||
|
||||
[4]: https://github.com/joyent/node-http-signature/blob/master/http_signing.md
|
||||
[5]: https://godoc.org/github.com/joyent/go-triton/authentication
|
||||
[6]: https://godoc.org/github.com/joyent/go-triton/authentication
|
||||
[7]: https://github.com/hashicorp/go-errwrap
|
|
@ -0,0 +1,94 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
)
|
||||
|
||||
type AccountsClient struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// Accounts returns a c used for accessing functions pertaining
|
||||
// to Account functionality in the Triton API.
|
||||
func (c *Client) Accounts() *AccountsClient {
|
||||
return &AccountsClient{c}
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
ID string `json:"id"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
CompanyName string `json:"companyName"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Address string `json:"address"`
|
||||
PostalCode string `json:"postalCode"`
|
||||
City string `json:"city"`
|
||||
State string `json:"state"`
|
||||
Country string `json:"country"`
|
||||
Phone string `json:"phone"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
TritonCNSEnabled bool `json:"triton_cns_enabled"`
|
||||
}
|
||||
|
||||
type GetAccountInput struct{}
|
||||
|
||||
func (client *AccountsClient) GetAccount(input *GetAccountInput) (*Account, error) {
|
||||
path := fmt.Sprintf("/%s", client.accountName)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetAccount request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Account
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding GetAccount response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type UpdateAccountInput struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
CompanyName string `json:"companyName,omitempty"`
|
||||
FirstName string `json:"firstName,omitempty"`
|
||||
LastName string `json:"lastName,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
PostalCode string `json:"postalCode,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
TritonCNSEnabled bool `json:"triton_cns_enabled,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateAccount updates your account details with the given parameters.
|
||||
// TODO(jen20) Work out a safe way to test this
|
||||
func (client *AccountsClient) UpdateAccount(input *UpdateAccountInput) (*Account, error) {
|
||||
respReader, err := client.executeRequest(http.MethodPost, fmt.Sprintf("/%s", client.accountName), input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing UpdateAccount request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Account
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding UpdateAccount response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
66
vendor/github.com/joyent/triton-go/authentication/ecdsa_signature.go
generated
vendored
Normal file
66
vendor/github.com/joyent/triton-go/authentication/ecdsa_signature.go
generated
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type ecdsaSignature struct {
|
||||
hashAlgorithm string
|
||||
R *big.Int
|
||||
S *big.Int
|
||||
}
|
||||
|
||||
func (s *ecdsaSignature) SignatureType() string {
|
||||
return fmt.Sprintf("ecdsa-%s", s.hashAlgorithm)
|
||||
}
|
||||
|
||||
func (s *ecdsaSignature) String() string {
|
||||
toEncode := struct {
|
||||
R *big.Int
|
||||
S *big.Int
|
||||
}{
|
||||
R: s.R,
|
||||
S: s.S,
|
||||
}
|
||||
|
||||
signatureBytes, err := asn1.Marshal(toEncode)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error marshaling signature: %s", err))
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signatureBytes)
|
||||
}
|
||||
|
||||
func newECDSASignature(signatureBlob []byte) (*ecdsaSignature, error) {
|
||||
var ecSig struct {
|
||||
R *big.Int
|
||||
S *big.Int
|
||||
}
|
||||
|
||||
if err := ssh.Unmarshal(signatureBlob, &ecSig); err != nil {
|
||||
return nil, errwrap.Wrapf("Error unmarshaling signature: {{err}}", err)
|
||||
}
|
||||
|
||||
rValue := ecSig.R.Bytes()
|
||||
var hashAlgorithm string
|
||||
switch len(rValue) {
|
||||
case 31, 32:
|
||||
hashAlgorithm = "sha256"
|
||||
case 65, 66:
|
||||
hashAlgorithm = "sha512"
|
||||
default:
|
||||
return nil, fmt.Errorf("Unsupported key length: %d", len(rValue))
|
||||
}
|
||||
|
||||
return &ecdsaSignature{
|
||||
hashAlgorithm: hashAlgorithm,
|
||||
R: ecSig.R,
|
||||
S: ecSig.S,
|
||||
}, nil
|
||||
}
|
76
vendor/github.com/joyent/triton-go/authentication/private_key_signer.go
generated
vendored
Normal file
76
vendor/github.com/joyent/triton-go/authentication/private_key_signer.go
generated
vendored
Normal file
|
@ -0,0 +1,76 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type PrivateKeySigner struct {
|
||||
formattedKeyFingerprint string
|
||||
keyFingerprint string
|
||||
accountName string
|
||||
hashFunc crypto.Hash
|
||||
|
||||
privateKey *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func NewPrivateKeySigner(keyFingerprint string, privateKeyMaterial []byte, accountName string) (*PrivateKeySigner, error) {
|
||||
keyFingerprintMD5 := strings.Replace(keyFingerprint, ":", "", -1)
|
||||
|
||||
block, _ := pem.Decode(privateKeyMaterial)
|
||||
if block == nil {
|
||||
return nil, errors.New("Error PEM-decoding private key material: nil block received")
|
||||
}
|
||||
|
||||
rsakey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error parsing private key: {{err}}", err)
|
||||
}
|
||||
|
||||
sshPublicKey, err := ssh.NewPublicKey(rsakey.Public())
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error parsing SSH key from private key: {{err}}", err)
|
||||
}
|
||||
|
||||
matchKeyFingerprint := formatPublicKeyFingerprint(sshPublicKey, false)
|
||||
displayKeyFingerprint := formatPublicKeyFingerprint(sshPublicKey, true)
|
||||
if matchKeyFingerprint != keyFingerprintMD5 {
|
||||
return nil, errors.New("Private key file does not match public key fingerprint")
|
||||
}
|
||||
|
||||
return &PrivateKeySigner{
|
||||
formattedKeyFingerprint: displayKeyFingerprint,
|
||||
keyFingerprint: keyFingerprint,
|
||||
accountName: accountName,
|
||||
|
||||
hashFunc: crypto.SHA1,
|
||||
privateKey: rsakey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrivateKeySigner) Sign(dateHeader string) (string, error) {
|
||||
const headerName = "date"
|
||||
|
||||
hash := s.hashFunc.New()
|
||||
hash.Write([]byte(fmt.Sprintf("%s: %s", headerName, dateHeader)))
|
||||
digest := hash.Sum(nil)
|
||||
|
||||
signed, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, s.hashFunc, digest)
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("Error signing date header: {{err}}", err)
|
||||
}
|
||||
signedBase64 := base64.StdEncoding.EncodeToString(signed)
|
||||
|
||||
keyID := fmt.Sprintf("/%s/keys/%s", s.accountName, s.formattedKeyFingerprint)
|
||||
return fmt.Sprintf(authorizationHeaderFormat, keyID, "rsa-sha1", headerName, signedBase64), nil
|
||||
}
|
25
vendor/github.com/joyent/triton-go/authentication/rsa_signature.go
generated
vendored
Normal file
25
vendor/github.com/joyent/triton-go/authentication/rsa_signature.go
generated
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
type rsaSignature struct {
|
||||
hashAlgorithm string
|
||||
signature []byte
|
||||
}
|
||||
|
||||
func (s *rsaSignature) SignatureType() string {
|
||||
return s.hashAlgorithm
|
||||
}
|
||||
|
||||
func (s *rsaSignature) String() string {
|
||||
return base64.StdEncoding.EncodeToString(s.signature)
|
||||
}
|
||||
|
||||
func newRSASignature(signatureBlob []byte) (*rsaSignature, error) {
|
||||
return &rsaSignature{
|
||||
hashAlgorithm: "rsa-sha1",
|
||||
signature: signatureBlob,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type httpAuthSignature interface {
|
||||
SignatureType() string
|
||||
String() string
|
||||
}
|
||||
|
||||
func keyFormatToKeyType(keyFormat string) (string, error) {
|
||||
if keyFormat == "ssh-rsa" {
|
||||
return "rsa", nil
|
||||
}
|
||||
|
||||
if keyFormat == "ssh-ed25519" {
|
||||
return "ed25519", nil
|
||||
}
|
||||
|
||||
if regexp.MustCompile("^ecdsa-sha2-*").Match([]byte(keyFormat)) {
|
||||
return "ecdsa", nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Unknown key format: %s", keyFormat)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package authentication
|
||||
|
||||
const authorizationHeaderFormat = `Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"`
|
||||
|
||||
type Signer interface {
|
||||
Sign(dateHeader string) (string, error)
|
||||
}
|
104
vendor/github.com/joyent/triton-go/authentication/ssh_agent_signer.go
generated
vendored
Normal file
104
vendor/github.com/joyent/triton-go/authentication/ssh_agent_signer.go
generated
vendored
Normal file
|
@ -0,0 +1,104 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
)
|
||||
|
||||
type SSHAgentSigner struct {
|
||||
formattedKeyFingerprint string
|
||||
keyFingerprint string
|
||||
accountName string
|
||||
keyIdentifier string
|
||||
|
||||
agent agent.Agent
|
||||
key ssh.PublicKey
|
||||
}
|
||||
|
||||
func NewSSHAgentSigner(keyFingerprint, accountName string) (*SSHAgentSigner, error) {
|
||||
sshAgentAddress := os.Getenv("SSH_AUTH_SOCK")
|
||||
if sshAgentAddress == "" {
|
||||
return nil, errors.New("SSH_AUTH_SOCK is not set")
|
||||
}
|
||||
|
||||
conn, err := net.Dial("unix", sshAgentAddress)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error dialing SSH agent: {{err}}", err)
|
||||
}
|
||||
|
||||
ag := agent.NewClient(conn)
|
||||
|
||||
keys, err := ag.List()
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error listing keys in SSH Agent: %s", err)
|
||||
}
|
||||
|
||||
keyFingerprintMD5 := strings.Replace(keyFingerprint, ":", "", -1)
|
||||
|
||||
var matchingKey ssh.PublicKey
|
||||
for _, key := range keys {
|
||||
h := md5.New()
|
||||
h.Write(key.Marshal())
|
||||
fp := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
if fp == keyFingerprintMD5 {
|
||||
matchingKey = key
|
||||
}
|
||||
}
|
||||
|
||||
if matchingKey == nil {
|
||||
return nil, fmt.Errorf("No key in the SSH Agent matches fingerprint: %s", keyFingerprint)
|
||||
}
|
||||
|
||||
formattedKeyFingerprint := formatPublicKeyFingerprint(matchingKey, true)
|
||||
|
||||
return &SSHAgentSigner{
|
||||
formattedKeyFingerprint: formattedKeyFingerprint,
|
||||
keyFingerprint: keyFingerprint,
|
||||
accountName: accountName,
|
||||
agent: ag,
|
||||
key: matchingKey,
|
||||
keyIdentifier: fmt.Sprintf("/%s/keys/%s", accountName, formattedKeyFingerprint),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SSHAgentSigner) Sign(dateHeader string) (string, error) {
|
||||
const headerName = "date"
|
||||
|
||||
signature, err := s.agent.Sign(s.key, []byte(fmt.Sprintf("%s: %s", headerName, dateHeader)))
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("Error signing date header: {{err}}", err)
|
||||
}
|
||||
|
||||
keyFormat, err := keyFormatToKeyType(signature.Format)
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("Error reading signature: {{err}}", err)
|
||||
}
|
||||
|
||||
var authSignature httpAuthSignature
|
||||
switch keyFormat {
|
||||
case "rsa":
|
||||
authSignature, err = newRSASignature(signature.Blob)
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("Error reading signature: {{err}}", err)
|
||||
}
|
||||
case "ecdsa":
|
||||
authSignature, err = newECDSASignature(signature.Blob)
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("Error reading signature: {{err}}", err)
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("Unsupported algorithm from SSH agent: %s", signature.Format)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(authorizationHeaderFormat, s.keyIdentifier,
|
||||
authSignature.SignatureType(), headerName, authSignature.String()), nil
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// formatPublicKeyFingerprint produces the MD5 fingerprint of the given SSH
|
||||
// public key. If display is true, the fingerprint is formatted with colons
|
||||
// between each byte, as per the output of OpenSSL.
|
||||
func formatPublicKeyFingerprint(key ssh.PublicKey, display bool) string {
|
||||
publicKeyFingerprint := md5.New()
|
||||
publicKeyFingerprint.Write(key.Marshal())
|
||||
publicKeyFingerprintString := fmt.Sprintf("%x", publicKeyFingerprint.Sum(nil))
|
||||
|
||||
if !display {
|
||||
return publicKeyFingerprintString
|
||||
}
|
||||
|
||||
formatted := ""
|
||||
for i := 0; i < len(publicKeyFingerprintString); i = i + 2 {
|
||||
formatted = fmt.Sprintf("%s%s:", formatted, publicKeyFingerprintString[i:i+2])
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(formatted, ":")
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/joyent/triton-go/authentication"
|
||||
)
|
||||
|
||||
// Client represents a connection to the Triton API.
|
||||
type Client struct {
|
||||
client *retryablehttp.Client
|
||||
authorizer []authentication.Signer
|
||||
apiURL url.URL
|
||||
accountName string
|
||||
}
|
||||
|
||||
// NewClient is used to construct a Client in order to make API
|
||||
// requests to the Triton API.
|
||||
//
|
||||
// At least one signer must be provided - example signers include
|
||||
// authentication.PrivateKeySigner and authentication.SSHAgentSigner.
|
||||
func NewClient(endpoint string, accountName string, signers ...authentication.Signer) (*Client, error) {
|
||||
defaultRetryWaitMin := 1 * time.Second
|
||||
defaultRetryWaitMax := 5 * time.Minute
|
||||
defaultRetryMax := 32
|
||||
|
||||
apiURL, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("invalid endpoint: {{err}}", err)
|
||||
}
|
||||
|
||||
if accountName == "" {
|
||||
return nil, fmt.Errorf("account name can not be empty")
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
DisableKeepAlives: true,
|
||||
MaxIdleConnsPerHost: -1,
|
||||
},
|
||||
CheckRedirect: doNotFollowRedirects,
|
||||
}
|
||||
|
||||
retryableClient := &retryablehttp.Client{
|
||||
HTTPClient: httpClient,
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
RetryWaitMin: defaultRetryWaitMin,
|
||||
RetryWaitMax: defaultRetryWaitMax,
|
||||
RetryMax: defaultRetryMax,
|
||||
CheckRetry: retryablehttp.DefaultRetryPolicy,
|
||||
}
|
||||
|
||||
return &Client{
|
||||
client: retryableClient,
|
||||
authorizer: signers,
|
||||
apiURL: *apiURL,
|
||||
accountName: accountName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func doNotFollowRedirects(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
|
||||
func (c *Client) executeRequestURIParams(method, path string, body interface{}, query *url.Values) (io.ReadCloser, error) {
|
||||
var requestBody io.ReadSeeker
|
||||
if body != nil {
|
||||
marshaled, err := json.MarshalIndent(body, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requestBody = bytes.NewReader(marshaled)
|
||||
}
|
||||
|
||||
endpoint := c.apiURL
|
||||
endpoint.Path = path
|
||||
if query != nil {
|
||||
endpoint.RawQuery = query.Encode()
|
||||
}
|
||||
|
||||
req, err := retryablehttp.NewRequest(method, endpoint.String(), requestBody)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error constructing HTTP request: {{err}}", err)
|
||||
}
|
||||
|
||||
dateHeader := time.Now().UTC().Format(time.RFC1123)
|
||||
req.Header.Set("date", dateHeader)
|
||||
|
||||
authHeader, err := c.authorizer[0].Sign(dateHeader)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error signing HTTP request: {{err}}", err)
|
||||
}
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Accept-Version", "8")
|
||||
req.Header.Set("User-Agent", "triton-go Client API")
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing HTTP request: {{err}}", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
return nil, c.decodeError(resp.StatusCode, resp.Body)
|
||||
}
|
||||
|
||||
func (c *Client) decodeError(statusCode int, body io.Reader) error {
|
||||
tritonError := &TritonError{
|
||||
StatusCode: statusCode,
|
||||
}
|
||||
|
||||
errorDecoder := json.NewDecoder(body)
|
||||
if err := errorDecoder.Decode(tritonError); err != nil {
|
||||
return errwrap.Wrapf("Error decoding error response: {{err}}", err)
|
||||
}
|
||||
|
||||
return tritonError
|
||||
}
|
||||
|
||||
func (c *Client) executeRequest(method, path string, body interface{}) (io.ReadCloser, error) {
|
||||
return c.executeRequestURIParams(method, path, body, nil)
|
||||
}
|
||||
|
||||
func (c *Client) executeRequestRaw(method, path string, body interface{}) (*http.Response, error) {
|
||||
var requestBody io.ReadSeeker
|
||||
if body != nil {
|
||||
marshaled, err := json.MarshalIndent(body, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requestBody = bytes.NewReader(marshaled)
|
||||
}
|
||||
|
||||
endpoint := c.apiURL
|
||||
endpoint.Path = path
|
||||
|
||||
req, err := retryablehttp.NewRequest(method, endpoint.String(), requestBody)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error constructing HTTP request: {{err}}", err)
|
||||
}
|
||||
|
||||
dateHeader := time.Now().UTC().Format(time.RFC1123)
|
||||
req.Header.Set("date", dateHeader)
|
||||
|
||||
authHeader, err := c.authorizer[0].Sign(dateHeader)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error signing HTTP request: {{err}}", err)
|
||||
}
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Accept-Version", "8")
|
||||
req.Header.Set("User-Agent", "triton-go c API")
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing HTTP request: {{err}}", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
)
|
||||
|
||||
type ConfigClient struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// Config returns a c used for accessing functions pertaining
|
||||
// to Config functionality in the Triton API.
|
||||
func (c *Client) Config() *ConfigClient {
|
||||
return &ConfigClient{c}
|
||||
}
|
||||
|
||||
// Config represents configuration for your account.
|
||||
type Config struct {
|
||||
// DefaultNetwork is the network that docker containers are provisioned on.
|
||||
DefaultNetwork string `json:"default_network"`
|
||||
}
|
||||
|
||||
type GetConfigInput struct{}
|
||||
|
||||
// GetConfig outputs configuration for your account.
|
||||
func (client *ConfigClient) GetConfig(input *GetConfigInput) (*Config, error) {
|
||||
respReader, err := client.executeRequest(http.MethodGet, fmt.Sprintf("/%s/config", client.accountName), nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetConfig request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Config
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding GetConfig response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type UpdateConfigInput struct {
|
||||
// DefaultNetwork is the network that docker containers are provisioned on.
|
||||
DefaultNetwork string `json:"default_network"`
|
||||
}
|
||||
|
||||
// UpdateConfig updates configuration values for your account.
|
||||
// TODO(jen20) Work out a safe way to test this (after networks c implemented)
|
||||
func (client *ConfigClient) UpdateConfig(input *UpdateConfigInput) (*Config, error) {
|
||||
respReader, err := client.executeRequest(http.MethodPut, fmt.Sprintf("/%s/config", client.accountName), input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing UpdateConfig request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Config
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding UpdateConfig response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
)
|
||||
|
||||
type DataCentersClient struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// DataCenters returns a c used for accessing functions pertaining
|
||||
// to Datacenter functionality in the Triton API.
|
||||
func (c *Client) Datacenters() *DataCentersClient {
|
||||
return &DataCentersClient{c}
|
||||
}
|
||||
|
||||
type DataCenter struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type ListDataCentersInput struct{}
|
||||
|
||||
func (client *DataCentersClient) ListDataCenters(*ListDataCentersInput) ([]*DataCenter, error) {
|
||||
path := fmt.Sprintf("/%s/datacenters", client.accountName)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing ListDatacenters request: {{err}}", err)
|
||||
}
|
||||
|
||||
var intermediate map[string]string
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&intermediate); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding ListDatacenters response: {{err}}", err)
|
||||
}
|
||||
|
||||
keys := make([]string, len(intermediate))
|
||||
i := 0
|
||||
for k := range intermediate {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
result := make([]*DataCenter, len(intermediate))
|
||||
i = 0
|
||||
for _, key := range keys {
|
||||
result[i] = &DataCenter{
|
||||
Name: key,
|
||||
URL: intermediate[key],
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type GetDataCenterInput struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (client *DataCentersClient) GetDataCenter(input *GetDataCenterInput) (*DataCenter, error) {
|
||||
path := fmt.Sprintf("/%s/datacenters/%s", client.accountName, input.Name)
|
||||
resp, err := client.executeRequestRaw(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetDatacenter request: {{err}}", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusFound {
|
||||
return nil, fmt.Errorf("Error executing GetDatacenter request: expected status code 302, got %s",
|
||||
resp.StatusCode)
|
||||
}
|
||||
|
||||
location := resp.Header.Get("Location")
|
||||
if location == "" {
|
||||
return nil, errors.New("Error decoding GetDatacenter response: no Location header")
|
||||
}
|
||||
|
||||
return &DataCenter{
|
||||
Name: input.Name,
|
||||
URL: location,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
)
|
||||
|
||||
// TritonError represents an error code and message along with
|
||||
// the status code of the HTTP request which resulted in the error
|
||||
// message. Error codes used by the Triton API are listed at
|
||||
// https://apidocs.joyent.com/cloudapi/#cloudapi-http-responses
|
||||
type TritonError struct {
|
||||
StatusCode int
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Error implements interface Error on the TritonError type.
|
||||
func (e TritonError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// IsBadRequest tests whether err wraps a TritonError with
|
||||
// code BadRequest
|
||||
func IsBadRequest(err error) bool {
|
||||
return isSpecificError(err, "BadRequest")
|
||||
}
|
||||
|
||||
// IsInternalError tests whether err wraps a TritonError with
|
||||
// code InternalError
|
||||
func IsInternalError(err error) bool {
|
||||
return isSpecificError(err, "InternalError")
|
||||
}
|
||||
|
||||
// IsInUseError tests whether err wraps a TritonError with
|
||||
// code InUseError
|
||||
func IsInUseError(err error) bool {
|
||||
return isSpecificError(err, "InUseError")
|
||||
}
|
||||
|
||||
// IsInvalidArgument tests whether err wraps a TritonError with
|
||||
// code InvalidArgument
|
||||
func IsInvalidArgument(err error) bool {
|
||||
return isSpecificError(err, "InvalidArgument")
|
||||
}
|
||||
|
||||
// IsInvalidCredentials tests whether err wraps a TritonError with
|
||||
// code InvalidCredentials
|
||||
func IsInvalidCredentials(err error) bool {
|
||||
return isSpecificError(err, "InvalidCredentials")
|
||||
}
|
||||
|
||||
// IsInvalidHeader tests whether err wraps a TritonError with
|
||||
// code InvalidHeader
|
||||
func IsInvalidHeader(err error) bool {
|
||||
return isSpecificError(err, "InvalidHeader")
|
||||
}
|
||||
|
||||
// IsInvalidVersion tests whether err wraps a TritonError with
|
||||
// code InvalidVersion
|
||||
func IsInvalidVersion(err error) bool {
|
||||
return isSpecificError(err, "InvalidVersion")
|
||||
}
|
||||
|
||||
// IsMissingParameter tests whether err wraps a TritonError with
|
||||
// code MissingParameter
|
||||
func IsMissingParameter(err error) bool {
|
||||
return isSpecificError(err, "MissingParameter")
|
||||
}
|
||||
|
||||
// IsNotAuthorized tests whether err wraps a TritonError with
|
||||
// code NotAuthorized
|
||||
func IsNotAuthorized(err error) bool {
|
||||
return isSpecificError(err, "NotAuthorized")
|
||||
}
|
||||
|
||||
// IsRequestThrottled tests whether err wraps a TritonError with
|
||||
// code RequestThrottled
|
||||
func IsRequestThrottled(err error) bool {
|
||||
return isSpecificError(err, "RequestThrottled")
|
||||
}
|
||||
|
||||
// IsRequestTooLarge tests whether err wraps a TritonError with
|
||||
// code RequestTooLarge
|
||||
func IsRequestTooLarge(err error) bool {
|
||||
return isSpecificError(err, "RequestTooLarge")
|
||||
}
|
||||
|
||||
// IsRequestMoved tests whether err wraps a TritonError with
|
||||
// code RequestMoved
|
||||
func IsRequestMoved(err error) bool {
|
||||
return isSpecificError(err, "RequestMoved")
|
||||
}
|
||||
|
||||
// IsResourceNotFound tests whether err wraps a TritonError with
|
||||
// code ResourceNotFound
|
||||
func IsResourceNotFound(err error) bool {
|
||||
return isSpecificError(err, "ResourceNotFound")
|
||||
}
|
||||
|
||||
// IsUnknownError tests whether err wraps a TritonError with
|
||||
// code UnknownError
|
||||
func IsUnknownError(err error) bool {
|
||||
return isSpecificError(err, "UnknownError")
|
||||
}
|
||||
|
||||
// isSpecificError checks whether the error represented by err wraps
|
||||
// an underlying TritonError with code errorCode.
|
||||
func isSpecificError(err error, errorCode string) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
tritonErrorInterface := errwrap.GetType(err.(error), &TritonError{})
|
||||
if tritonErrorInterface == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
tritonErr := tritonErrorInterface.(*TritonError)
|
||||
if tritonErr.Code == errorCode {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
)
|
||||
|
||||
type FabricsClient struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// Fabrics returns a client used for accessing functions pertaining to
|
||||
// Fabric functionality in the Triton API.
|
||||
func (c *Client) Fabrics() *FabricsClient {
|
||||
return &FabricsClient{c}
|
||||
}
|
||||
|
||||
type FabricVLAN struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"vlan_id"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ListFabricVLANsInput struct{}
|
||||
|
||||
func (client *FabricsClient) ListFabricVLANs(*ListFabricVLANsInput) ([]*FabricVLAN, error) {
|
||||
path := fmt.Sprintf("/%s/fabrics/default/vlans", client.accountName)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing ListFabricVLANs request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result []*FabricVLAN
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding ListFabricVLANs response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type CreateFabricVLANInput struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"vlan_id"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (client *FabricsClient) CreateFabricVLAN(input *CreateFabricVLANInput) (*FabricVLAN, error) {
|
||||
path := fmt.Sprintf("/%s/fabrics/default/vlans", client.accountName)
|
||||
respReader, err := client.executeRequest(http.MethodPost, path, input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing CreateFabricVLAN request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *FabricVLAN
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding CreateFabricVLAN response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type UpdateFabricVLANInput struct {
|
||||
ID int `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (client *FabricsClient) UpdateFabricVLAN(input *UpdateFabricVLANInput) (*FabricVLAN, error) {
|
||||
path := fmt.Sprintf("/%s/fabrics/default/vlans/%d", client.accountName, input.ID)
|
||||
respReader, err := client.executeRequest(http.MethodPut, path, input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing UpdateFabricVLAN request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *FabricVLAN
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding UpdateFabricVLAN response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type GetFabricVLANInput struct {
|
||||
ID int `json:"-"`
|
||||
}
|
||||
|
||||
func (client *FabricsClient) GetFabricVLAN(input *GetFabricVLANInput) (*FabricVLAN, error) {
|
||||
path := fmt.Sprintf("/%s/fabrics/default/vlans/%d", client.accountName, input.ID)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetFabricVLAN request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *FabricVLAN
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding GetFabricVLAN response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type DeleteFabricVLANInput struct {
|
||||
ID int `json:"-"`
|
||||
}
|
||||
|
||||
func (client *FabricsClient) DeleteFabricVLAN(input *DeleteFabricVLANInput) error {
|
||||
path := fmt.Sprintf("/%s/fabrics/default/vlans/%d", client.accountName, input.ID)
|
||||
respReader, err := client.executeRequest(http.MethodDelete, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing DeleteFabricVLAN request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ListFabricNetworksInput struct {
|
||||
FabricVLANID int `json:"-"`
|
||||
}
|
||||
|
||||
func (client *FabricsClient) ListFabricNetworks(input *ListFabricNetworksInput) ([]*Network, error) {
|
||||
path := fmt.Sprintf("/%s/fabrics/default/vlans/%d/networks", client.accountName, input.FabricVLANID)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing ListFabricNetworks request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result []*Network
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding ListFabricNetworks response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type CreateFabricNetworkInput struct {
|
||||
FabricVLANID int `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Subnet string `json:"subnet"`
|
||||
ProvisionStartIP string `json:"provision_start_ip"`
|
||||
ProvisionEndIP string `json:"provision_end_ip"`
|
||||
Gateway string `json:"gateway"`
|
||||
Resolvers []string `json:"resolvers"`
|
||||
Routes map[string]string `json:"routes"`
|
||||
InternetNAT bool `json:"internet_nat"`
|
||||
}
|
||||
|
||||
func (client *FabricsClient) CreateFabricNetwork(input *CreateFabricNetworkInput) (*Network, error) {
|
||||
path := fmt.Sprintf("/%s/fabrics/default/vlans/%d/networks", client.accountName, input.FabricVLANID)
|
||||
respReader, err := client.executeRequest(http.MethodPost, path, input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing CreateFabricNetwork request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Network
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding CreateFabricNetwork response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type GetFabricNetworkInput struct {
|
||||
FabricVLANID int `json:"-"`
|
||||
NetworkID string `json:"-"`
|
||||
}
|
||||
|
||||
func (client *FabricsClient) GetFabricNetwork(input *GetFabricNetworkInput) (*Network, error) {
|
||||
path := fmt.Sprintf("/%s/fabrics/default/vlans/%d/networks/%s", client.accountName, input.FabricVLANID, input.NetworkID)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetFabricNetwork request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Network
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding GetFabricNetwork response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type DeleteFabricNetworkInput struct {
|
||||
FabricVLANID int `json:"-"`
|
||||
NetworkID string `json:"-"`
|
||||
}
|
||||
|
||||
func (client *FabricsClient) DeleteFabricNetwork(input *DeleteFabricNetworkInput) error {
|
||||
path := fmt.Sprintf("/%s/fabrics/default/vlans/%d/networks/%s", client.accountName, input.FabricVLANID, input.NetworkID)
|
||||
respReader, err := client.executeRequest(http.MethodDelete, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing DeleteFabricNetwork request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
)
|
||||
|
||||
type FirewallClient struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// Firewall returns a client used for accessing functions pertaining to
|
||||
// firewall functionality in the Triton API.
|
||||
func (c *Client) Firewall() *FirewallClient {
|
||||
return &FirewallClient{c}
|
||||
}
|
||||
|
||||
// FirewallRule represents a firewall rule
|
||||
type FirewallRule struct {
|
||||
// ID is a unique identifier for this rule
|
||||
ID string `json:"id"`
|
||||
|
||||
// Enabled indicates if the rule is enabled
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Rule is the firewall rule text
|
||||
Rule string `json:"rule"`
|
||||
|
||||
// Global indicates if the rule is global. Optional.
|
||||
Global bool `json:"global"`
|
||||
|
||||
// Description is a human-readable description for the rule. Optional
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ListFirewallRulesInput struct{}
|
||||
|
||||
func (client *FirewallClient) ListFirewallRules(*ListFirewallRulesInput) ([]*FirewallRule, error) {
|
||||
path := fmt.Sprintf("/%s/fwrules", client.accountName)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing ListFirewallRules request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result []*FirewallRule
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding ListFirewallRules response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type GetFirewallRuleInput struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
func (client *FirewallClient) GetFirewallRule(input *GetFirewallRuleInput) (*FirewallRule, error) {
|
||||
path := fmt.Sprintf("/%s/fwrules/%s", client.accountName, input.ID)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetFirewallRule request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *FirewallRule
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding GetFirewallRule response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type CreateFirewallRuleInput struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Rule string `json:"rule"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (client *FirewallClient) CreateFirewallRule(input *CreateFirewallRuleInput) (*FirewallRule, error) {
|
||||
respReader, err := client.executeRequest(http.MethodPost, fmt.Sprintf("/%s/fwrules", client.accountName), input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing CreateFirewallRule request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *FirewallRule
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding CreateFirewallRule response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type UpdateFirewallRuleInput struct {
|
||||
ID string `json:"-"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Rule string `json:"rule"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (client *FirewallClient) UpdateFirewallRule(input *UpdateFirewallRuleInput) (*FirewallRule, error) {
|
||||
respReader, err := client.executeRequest(http.MethodPost, fmt.Sprintf("/%s/fwrules/%s", client.accountName, input.ID), input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing UpdateFirewallRule request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *FirewallRule
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding UpdateFirewallRule response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type EnableFirewallRuleInput struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
func (client *FirewallClient) EnableFirewallRule(input *EnableFirewallRuleInput) (*FirewallRule, error) {
|
||||
respReader, err := client.executeRequest(http.MethodPost, fmt.Sprintf("/%s/fwrules/%s/enable", client.accountName, input.ID), input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing EnableFirewallRule request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *FirewallRule
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding EnableFirewallRule response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type DisableFirewallRuleInput struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
func (client *FirewallClient) DisableFirewallRule(input *DisableFirewallRuleInput) (*FirewallRule, error) {
|
||||
respReader, err := client.executeRequest(http.MethodPost, fmt.Sprintf("/%s/fwrules/%s/disable", client.accountName, input.ID), input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing DisableFirewallRule request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *FirewallRule
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding DisableFirewallRule response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type DeleteFirewallRuleInput struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
func (client *FirewallClient) DeleteFirewallRule(input *DeleteFirewallRuleInput) error {
|
||||
path := fmt.Sprintf("/%s/fwrules/%s", client.accountName, input.ID)
|
||||
respReader, err := client.executeRequest(http.MethodDelete, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing DeleteFirewallRule request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ListMachineFirewallRulesInput struct {
|
||||
MachineID string
|
||||
}
|
||||
|
||||
func (client *FirewallClient) ListMachineFirewallRules(input *ListMachineFirewallRulesInput) ([]*FirewallRule, error) {
|
||||
path := fmt.Sprintf("/%s/machines/%s/firewallrules", client.accountName, input.MachineID)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing ListMachineFirewallRules request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result []*FirewallRule
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding ListFirewallRules response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
)
|
||||
|
||||
type ImagesClient struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// Images returns a c used for accessing functions pertaining to
|
||||
// Images functionality in the Triton API.
|
||||
func (c *Client) Images() *ImagesClient {
|
||||
return &ImagesClient{c}
|
||||
}
|
||||
|
||||
type ImageFile struct {
|
||||
Compression string `json:"compression"`
|
||||
SHA1 string `json:"sha1"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OS string `json:"os"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
Requirements map[string]interface{} `json:"requirements"`
|
||||
Homepage string `json:"homepage"`
|
||||
Files []*ImageFile `json:"files"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
Owner string `json:"owner"`
|
||||
Public bool `json:"public"`
|
||||
State string `json:"state"`
|
||||
Tags map[string]string `json:"tags"`
|
||||
EULA string `json:"eula"`
|
||||
ACL []string `json:"acl"`
|
||||
Error TritonError `json:"error"`
|
||||
}
|
||||
|
||||
type ListImagesInput struct{}
|
||||
|
||||
func (client *ImagesClient) ListImages(*ListImagesInput) ([]*Image, error) {
|
||||
path := fmt.Sprintf("/%s/images", client.accountName)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing ListImages request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result []*Image
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding ListImages response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type GetImageInput struct {
|
||||
ImageID string
|
||||
}
|
||||
|
||||
func (client *ImagesClient) GetImage(input *GetImageInput) (*Image, error) {
|
||||
path := fmt.Sprintf("/%s/images/%s", client.accountName, input.ImageID)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetImage request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Image
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding GetImage response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type DeleteImageInput struct {
|
||||
ImageID string
|
||||
}
|
||||
|
||||
func (client *ImagesClient) DeleteImage(input *DeleteImageInput) error {
|
||||
path := fmt.Sprintf("/%s/images/%s", client.accountName, input.ImageID)
|
||||
respReader, err := client.executeRequest(http.MethodDelete, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing DeleteKey request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ExportImageInput struct {
|
||||
ImageID string
|
||||
MantaPath string
|
||||
}
|
||||
|
||||
type MantaLocation struct {
|
||||
MantaURL string `json:"manta_url"`
|
||||
ImagePath string `json:"image_path"`
|
||||
ManifestPath string `json:"manifest_path"`
|
||||
}
|
||||
|
||||
func (client *ImagesClient) ExportImage(input *ExportImageInput) (*MantaLocation, error) {
|
||||
path := fmt.Sprintf("/%s/images/%s", client.accountName, input.ImageID)
|
||||
query := &url.Values{}
|
||||
query.Set("action", "export")
|
||||
query.Set("manta_path", input.MantaPath)
|
||||
|
||||
respReader, err := client.executeRequestURIParams(http.MethodGet, path, nil, query)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetImage request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *MantaLocation
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding GetImage response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type CreateImageFromMachineInput struct {
|
||||
MachineID string `json:"machine"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
HomePage string `json:"homepage,omitempty"`
|
||||
EULA string `json:"eula,omitempty"`
|
||||
ACL []string `json:"acl,omitempty"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
func (client *ImagesClient) CreateImageFromMachine(input *CreateImageFromMachineInput) (*Image, error) {
|
||||
path := fmt.Sprintf("/%s/images", client.accountName)
|
||||
respReader, err := client.executeRequest(http.MethodPost, path, input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing CreateImageFromMachine request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Image
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding CreateImageFromMachine response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type UpdateImageInput struct {
|
||||
ImageID string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
HomePage string `json:"homepage,omitempty"`
|
||||
EULA string `json:"eula,omitempty"`
|
||||
ACL []string `json:"acl,omitempty"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
func (client *ImagesClient) UpdateImage(input *UpdateImageInput) (*Image, error) {
|
||||
path := fmt.Sprintf("/%s/images/%s", client.accountName, input.ImageID)
|
||||
query := &url.Values{}
|
||||
query.Set("action", "update")
|
||||
|
||||
respReader, err := client.executeRequestURIParams(http.MethodPost, path, input, query)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing UpdateImage request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Image
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding UpdateImage response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
)
|
||||
|
||||
type KeysClient struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// Keys returns a c used for accessing functions pertaining to
|
||||
// SSH key functionality in the Triton API.
|
||||
func (c *Client) Keys() *KeysClient {
|
||||
return &KeysClient{c}
|
||||
}
|
||||
|
||||
// Key represents a public key
|
||||
type Key struct {
|
||||
// Name of the key
|
||||
Name string `json:"name"`
|
||||
|
||||
// Key fingerprint
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
|
||||
// OpenSSH-formatted public key
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type ListKeysInput struct{}
|
||||
|
||||
// ListKeys lists all public keys we have on record for the specified
|
||||
// account.
|
||||
func (client *KeysClient) ListKeys(*ListKeysInput) ([]*Key, error) {
|
||||
path := fmt.Sprintf("/%s/keys")
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing ListKeys request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result []*Key
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding ListKeys response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type GetKeyInput struct {
|
||||
KeyName string
|
||||
}
|
||||
|
||||
func (client *KeysClient) GetKey(input *GetKeyInput) (*Key, error) {
|
||||
path := fmt.Sprintf("/%s/keys/%s", client.accountName, input.KeyName)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetKey request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Key
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding GetKey response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type DeleteKeyInput struct {
|
||||
KeyName string
|
||||
}
|
||||
|
||||
func (client *KeysClient) DeleteKey(input *DeleteKeyInput) error {
|
||||
path := fmt.Sprintf("/%s/keys/%s", client.accountName, input.KeyName)
|
||||
respReader, err := client.executeRequest(http.MethodDelete, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing DeleteKey request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateKeyInput represents the option that can be specified
|
||||
// when creating a new key.
|
||||
type CreateKeyInput struct {
|
||||
// Name of the key. Optional.
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// OpenSSH-formatted public key.
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// CreateKey uploads a new OpenSSH key to Triton for use in HTTP signing and SSH.
|
||||
func (client *KeysClient) CreateKey(input *CreateKeyInput) (*Key, error) {
|
||||
respReader, err := client.executeRequest(http.MethodPost, fmt.Sprintf("/%s/keys", client.accountName), input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing CreateKey request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Key
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding CreateKey response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,663 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
)
|
||||
|
||||
type MachinesClient struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// Machines returns a client used for accessing functions pertaining to
|
||||
// machine functionality in the Triton API.
|
||||
func (c *Client) Machines() *MachinesClient {
|
||||
return &MachinesClient{c}
|
||||
}
|
||||
|
||||
const (
|
||||
machineCNSTagDisable = "triton.cns.disable"
|
||||
machineCNSTagReversePTR = "triton.cns.reverse_ptr"
|
||||
machineCNSTagServices = "triton.cns.services"
|
||||
)
|
||||
|
||||
// MachineCNS is a container for the CNS-specific attributes. In the API these
|
||||
// values are embedded within a Machine's Tags attribute, however they are
|
||||
// exposed to the caller as their native types.
|
||||
type MachineCNS struct {
|
||||
Disable *bool
|
||||
ReversePTR *string
|
||||
Services []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]string `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"`
|
||||
DomainNames []string `json:"dns_names"`
|
||||
CNS MachineCNS
|
||||
}
|
||||
|
||||
// _Machine is a private facade over Machine that handles the necessary API
|
||||
// overrides from vmapi's machine endpoint(s).
|
||||
type _Machine struct {
|
||||
Machine
|
||||
Tags map[string]interface{} `json:"tags"`
|
||||
}
|
||||
|
||||
type NIC struct {
|
||||
IP string `json:"ip"`
|
||||
MAC string `json:"mac"`
|
||||
Primary bool `json:"primary"`
|
||||
Netmask string `json:"netmask"`
|
||||
Gateway string `json:"gateway"`
|
||||
State string `json:"state"`
|
||||
Network string `json:"network"`
|
||||
}
|
||||
|
||||
type GetMachineInput struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
func (gmi *GetMachineInput) Validate() error {
|
||||
if gmi.ID == "" {
|
||||
return fmt.Errorf("machine ID can not be empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *MachinesClient) GetMachine(input *GetMachineInput) (*Machine, error) {
|
||||
if err := input.Validate(); err != nil {
|
||||
return nil, errwrap.Wrapf("unable to get machine: {{err}}", err)
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/%s/machines/%s", client.accountName, input.ID)
|
||||
response, err := client.executeRequestRaw(http.MethodGet, path, nil)
|
||||
if response != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
if response.StatusCode == http.StatusNotFound || response.StatusCode == http.StatusGone {
|
||||
return nil, &TritonError{
|
||||
Code: "ResourceNotFound",
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetMachine request: {{err}}",
|
||||
client.decodeError(response.StatusCode, response.Body))
|
||||
}
|
||||
|
||||
var result *_Machine
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding GetMachine response: {{err}}", err)
|
||||
}
|
||||
|
||||
native, err := result.toNative()
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("unable to convert API response for machines to native type: {{err}}", err)
|
||||
}
|
||||
|
||||
return native, nil
|
||||
}
|
||||
|
||||
func (client *MachinesClient) GetMachines() ([]*Machine, error) {
|
||||
path := fmt.Sprintf("/%s/machines", client.accountName)
|
||||
response, err := client.executeRequestRaw(http.MethodGet, path, nil)
|
||||
if response != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
if response.StatusCode == http.StatusNotFound {
|
||||
return nil, &TritonError{
|
||||
Code: "ResourceNotFound",
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetMachines request: {{err}}",
|
||||
client.decodeError(response.StatusCode, response.Body))
|
||||
}
|
||||
|
||||
var results []*_Machine
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err = decoder.Decode(&results); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding GetMachines response: {{err}}", err)
|
||||
}
|
||||
|
||||
machines := make([]*Machine, 0, len(results))
|
||||
for _, machineAPI := range results {
|
||||
native, err := machineAPI.toNative()
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("unable to convert API response for machines to native type: {{err}}", err)
|
||||
}
|
||||
machines = append(machines, native)
|
||||
}
|
||||
return machines, nil
|
||||
}
|
||||
|
||||
type CreateMachineInput struct {
|
||||
Name string
|
||||
Package string
|
||||
Image string
|
||||
Networks []string
|
||||
LocalityStrict bool
|
||||
LocalityNear []string
|
||||
LocalityFar []string
|
||||
Metadata map[string]string
|
||||
Tags map[string]string
|
||||
FirewallEnabled bool
|
||||
CNS MachineCNS
|
||||
}
|
||||
|
||||
func (input *CreateMachineInput) toAPI() map[string]interface{} {
|
||||
const numExtraParams = 8
|
||||
result := make(map[string]interface{}, numExtraParams+len(input.Metadata)+len(input.Tags))
|
||||
|
||||
result["firewall_enabled"] = input.FirewallEnabled
|
||||
|
||||
if input.Name != "" {
|
||||
result["name"] = input.Name
|
||||
}
|
||||
|
||||
if input.Package != "" {
|
||||
result["package"] = input.Package
|
||||
}
|
||||
|
||||
if input.Image != "" {
|
||||
result["image"] = input.Image
|
||||
}
|
||||
|
||||
if len(input.Networks) > 0 {
|
||||
result["networks"] = input.Networks
|
||||
}
|
||||
|
||||
locality := struct {
|
||||
Strict bool `json:"strict"`
|
||||
Near []string `json:"near,omitempty"`
|
||||
Far []string `json:"far,omitempty"`
|
||||
}{
|
||||
Strict: input.LocalityStrict,
|
||||
Near: input.LocalityNear,
|
||||
Far: input.LocalityFar,
|
||||
}
|
||||
result["locality"] = locality
|
||||
for key, value := range input.Tags {
|
||||
result[fmt.Sprintf("tag.%s", key)] = value
|
||||
}
|
||||
|
||||
// Deliberately clobber any user-specified Tags with the attributes from the
|
||||
// CNS struct.
|
||||
input.CNS.toTags(result)
|
||||
|
||||
for key, value := range input.Metadata {
|
||||
result[fmt.Sprintf("metadata.%s", key)] = value
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (client *MachinesClient) CreateMachine(input *CreateMachineInput) (*Machine, error) {
|
||||
path := fmt.Sprintf("/%s/machines", client.accountName)
|
||||
respReader, err := client.executeRequest(http.MethodPost, path, input.toAPI())
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing CreateMachine request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Machine
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding CreateMachine response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type DeleteMachineInput struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) DeleteMachine(input *DeleteMachineInput) error {
|
||||
path := fmt.Sprintf("/%s/machines/%s", client.accountName, input.ID)
|
||||
response, err := client.executeRequestRaw(http.MethodDelete, path, nil)
|
||||
if response.Body != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
if response.StatusCode == http.StatusNotFound {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing DeleteMachine request: {{err}}",
|
||||
client.decodeError(response.StatusCode, response.Body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteMachineTagsInput struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) DeleteMachineTags(input *DeleteMachineTagsInput) error {
|
||||
path := fmt.Sprintf("/%s/machines/%s/tags", client.accountName, input.ID)
|
||||
response, err := client.executeRequestRaw(http.MethodDelete, path, nil)
|
||||
if response.Body != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
if response.StatusCode == http.StatusNotFound {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing DeleteMachineTags request: {{err}}",
|
||||
client.decodeError(response.StatusCode, response.Body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteMachineTagInput struct {
|
||||
ID string
|
||||
Key string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) DeleteMachineTag(input *DeleteMachineTagInput) error {
|
||||
path := fmt.Sprintf("/%s/machines/%s/tags/%s", client.accountName, input.ID, input.Key)
|
||||
response, err := client.executeRequestRaw(http.MethodDelete, path, nil)
|
||||
if response.Body != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
if response.StatusCode == http.StatusNotFound {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing DeleteMachineTag request: {{err}}",
|
||||
client.decodeError(response.StatusCode, response.Body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type RenameMachineInput struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) RenameMachine(input *RenameMachineInput) error {
|
||||
path := fmt.Sprintf("/%s/machines/%s", client.accountName, input.ID)
|
||||
|
||||
params := &url.Values{}
|
||||
params.Set("action", "rename")
|
||||
params.Set("name", input.Name)
|
||||
|
||||
respReader, err := client.executeRequestURIParams(http.MethodPost, path, nil, params)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing RenameMachine request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReplaceMachineTagsInput struct {
|
||||
ID string
|
||||
Tags map[string]string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) ReplaceMachineTags(input *ReplaceMachineTagsInput) error {
|
||||
path := fmt.Sprintf("/%s/machines/%s/tags", client.accountName, input.ID)
|
||||
respReader, err := client.executeRequest(http.MethodPut, path, input.Tags)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing ReplaceMachineTags request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AddMachineTagsInput struct {
|
||||
ID string
|
||||
Tags map[string]string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) AddMachineTags(input *AddMachineTagsInput) error {
|
||||
path := fmt.Sprintf("/%s/machines/%s/tags", client.accountName, input.ID)
|
||||
respReader, err := client.executeRequest(http.MethodPost, path, input.Tags)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing AddMachineTags request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetMachineTagInput struct {
|
||||
ID string
|
||||
Key string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) GetMachineTag(input *GetMachineTagInput) (string, error) {
|
||||
path := fmt.Sprintf("/%s/machines/%s/tags/%s", client.accountName, input.ID, input.Key)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("Error executing GetMachineTag request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result string
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return "", errwrap.Wrapf("Error decoding GetMachineTag response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type ListMachineTagsInput struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) ListMachineTags(input *ListMachineTagsInput) (map[string]string, error) {
|
||||
path := fmt.Sprintf("/%s/machines/%s/tags", client.accountName, input.ID)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing ListMachineTags request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding ListMachineTags response: {{err}}", err)
|
||||
}
|
||||
|
||||
_, tags := machineTagsExtractMeta(result)
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
type UpdateMachineMetadataInput struct {
|
||||
ID string
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) UpdateMachineMetadata(input *UpdateMachineMetadataInput) (map[string]string, error) {
|
||||
path := fmt.Sprintf("/%s/machines/%s/tags", client.accountName, input.ID)
|
||||
respReader, err := client.executeRequest(http.MethodPost, path, input.Metadata)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing UpdateMachineMetadata request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result map[string]string
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding UpdateMachineMetadata response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type ResizeMachineInput struct {
|
||||
ID string
|
||||
Package string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) ResizeMachine(input *ResizeMachineInput) error {
|
||||
path := fmt.Sprintf("/%s/machines/%s", client.accountName, input.ID)
|
||||
|
||||
params := &url.Values{}
|
||||
params.Set("action", "resize")
|
||||
params.Set("package", input.Package)
|
||||
|
||||
respReader, err := client.executeRequestURIParams(http.MethodPost, path, nil, params)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing ResizeMachine request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type EnableMachineFirewallInput struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) EnableMachineFirewall(input *EnableMachineFirewallInput) error {
|
||||
path := fmt.Sprintf("/%s/machines/%s", client.accountName, input.ID)
|
||||
|
||||
params := &url.Values{}
|
||||
params.Set("action", "enable_firewall")
|
||||
|
||||
respReader, err := client.executeRequestURIParams(http.MethodPost, path, nil, params)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing EnableMachineFirewall request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DisableMachineFirewallInput struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) DisableMachineFirewall(input *DisableMachineFirewallInput) error {
|
||||
path := fmt.Sprintf("/%s/machines/%s", client.accountName, input.ID)
|
||||
|
||||
params := &url.Values{}
|
||||
params.Set("action", "disable_firewall")
|
||||
|
||||
respReader, err := client.executeRequestURIParams(http.MethodPost, path, nil, params)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing DisableMachineFirewall request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ListNICsInput struct {
|
||||
MachineID string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) ListNICs(input *ListNICsInput) ([]*NIC, error) {
|
||||
path := fmt.Sprintf("/%s/machines/%s/nics", client.accountName, input.MachineID)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing ListNICs request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result []*NIC
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding ListNICs response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type AddNICInput struct {
|
||||
MachineID string `json:"-"`
|
||||
Network string `json:"network"`
|
||||
}
|
||||
|
||||
func (client *MachinesClient) AddNIC(input *AddNICInput) (*NIC, error) {
|
||||
path := fmt.Sprintf("/%s/machines/%s/nics", client.accountName, input.MachineID)
|
||||
respReader, err := client.executeRequest(http.MethodPost, path, input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing AddNIC request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *NIC
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding AddNIC response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type RemoveNICInput struct {
|
||||
MachineID string
|
||||
MAC string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) RemoveNIC(input *RemoveNICInput) error {
|
||||
path := fmt.Sprintf("/%s/machines/%s/nics/%s", client.accountName, input.MachineID, input.MAC)
|
||||
respReader, err := client.executeRequest(http.MethodDelete, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing RemoveNIC request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type StopMachineInput struct {
|
||||
MachineID string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) StopMachine(input *StopMachineInput) error {
|
||||
path := fmt.Sprintf("/%s/machines/%s", client.accountName, input.MachineID)
|
||||
|
||||
params := &url.Values{}
|
||||
params.Set("action", "stop")
|
||||
|
||||
respReader, err := client.executeRequestURIParams(http.MethodPost, path, nil, params)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing StopMachine request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type StartMachineInput struct {
|
||||
MachineID string
|
||||
}
|
||||
|
||||
func (client *MachinesClient) StartMachine(input *StartMachineInput) error {
|
||||
path := fmt.Sprintf("/%s/machines/%s", client.accountName, input.MachineID)
|
||||
|
||||
params := &url.Values{}
|
||||
params.Set("action", "start")
|
||||
|
||||
respReader, err := client.executeRequestURIParams(http.MethodPost, path, nil, params)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing StartMachine request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var reservedMachineCNSTags = map[string]struct{}{
|
||||
machineCNSTagDisable: {},
|
||||
machineCNSTagReversePTR: {},
|
||||
machineCNSTagServices: {},
|
||||
}
|
||||
|
||||
// machineTagsExtractMeta() extracts all of the misc parameters from Tags and
|
||||
// returns a clean CNS and Tags struct.
|
||||
func machineTagsExtractMeta(tags map[string]interface{}) (MachineCNS, map[string]string) {
|
||||
nativeCNS := MachineCNS{}
|
||||
nativeTags := make(map[string]string, len(tags))
|
||||
for k, raw := range tags {
|
||||
if _, found := reservedMachineCNSTags[k]; found {
|
||||
switch k {
|
||||
case machineCNSTagDisable:
|
||||
b := raw.(bool)
|
||||
nativeCNS.Disable = &b
|
||||
case machineCNSTagReversePTR:
|
||||
s := raw.(string)
|
||||
nativeCNS.ReversePTR = &s
|
||||
case machineCNSTagServices:
|
||||
nativeCNS.Services = strings.Split(raw.(string), ",")
|
||||
default:
|
||||
// TODO(seanc@): should assert, logic fail
|
||||
}
|
||||
} else {
|
||||
nativeTags[k] = raw.(string)
|
||||
}
|
||||
}
|
||||
|
||||
return nativeCNS, nativeTags
|
||||
}
|
||||
|
||||
// toNative() exports a given _Machine (API representation) to its native object
|
||||
// format.
|
||||
func (api *_Machine) toNative() (*Machine, error) {
|
||||
m := Machine(api.Machine)
|
||||
m.CNS, m.Tags = machineTagsExtractMeta(api.Tags)
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// toTags() injects its state information into a Tags map suitable for use to
|
||||
// submit an API call to the vmapi machine endpoint
|
||||
func (mcns *MachineCNS) toTags(m map[string]interface{}) {
|
||||
if mcns.Disable != nil {
|
||||
s := fmt.Sprintf("%t", mcns.Disable)
|
||||
m[machineCNSTagDisable] = &s
|
||||
}
|
||||
|
||||
if mcns.ReversePTR != nil {
|
||||
m[machineCNSTagReversePTR] = &mcns.ReversePTR
|
||||
}
|
||||
|
||||
if len(mcns.Services) > 0 {
|
||||
m[machineCNSTagServices] = strings.Join(mcns.Services, ",")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"fmt"
|
||||
"github.com/hashicorp/errwrap"
|
||||
)
|
||||
|
||||
type NetworksClient struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// Networks returns a c used for accessing functions pertaining to
|
||||
// Network functionality in the Triton API.
|
||||
func (c *Client) Networks() *NetworksClient {
|
||||
return &NetworksClient{c}
|
||||
}
|
||||
|
||||
type Network struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Public bool `json:"public"`
|
||||
Fabric bool `json:"fabric"`
|
||||
Description string `json:"description"`
|
||||
Subnet string `json:"subnet"`
|
||||
ProvisioningStartIP string `json:"provision_start_ip"`
|
||||
ProvisioningEndIP string `json:"provision_end_ip"`
|
||||
Gateway string `json:"gateway"`
|
||||
Resolvers []string `json:"resolvers"`
|
||||
Routes map[string]string `json:"routes"`
|
||||
InternetNAT bool `json:"internet_nat"`
|
||||
}
|
||||
|
||||
type ListNetworksInput struct{}
|
||||
|
||||
func (client *NetworksClient) ListNetworks(*ListNetworksInput) ([]*Network, error) {
|
||||
path := fmt.Sprintf("/%s/networks", client.accountName)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing ListNetworks request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result []*Network
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding ListNetworks response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type GetNetworkInput struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
func (client *NetworksClient) GetNetwork(input *GetNetworkInput) (*Network, error) {
|
||||
path := fmt.Sprintf("/%s/networks/%s", client.accountName, input.ID)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetNetwork request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Network
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding GetNetwork response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
)
|
||||
|
||||
type PackagesClient struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// Packages returns a c used for accessing functions pertaining
|
||||
// to Packages functionality in the Triton API.
|
||||
func (c *Client) Packages() *PackagesClient {
|
||||
return &PackagesClient{c}
|
||||
}
|
||||
|
||||
type Package struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Memory int64 `json:"memory"`
|
||||
Disk int64 `json:"disk"`
|
||||
Swap int64 `json:"swap"`
|
||||
LWPs int64 `json:"lwps"`
|
||||
VCPUs int64 `json:"vcpus"`
|
||||
Version string `json:"version"`
|
||||
Group string `json:"group"`
|
||||
Description string `json:"description"`
|
||||
Default bool `json:"default"`
|
||||
}
|
||||
|
||||
type ListPackagesInput struct {
|
||||
Name string `json:"name"`
|
||||
Memory int64 `json:"memory"`
|
||||
Disk int64 `json:"disk"`
|
||||
Swap int64 `json:"swap"`
|
||||
LWPs int64 `json:"lwps"`
|
||||
VCPUs int64 `json:"vcpus"`
|
||||
Version string `json:"version"`
|
||||
Group string `json:"group"`
|
||||
}
|
||||
|
||||
func (client *PackagesClient) ListPackages(input *ListPackagesInput) ([]*Package, error) {
|
||||
respReader, err := client.executeRequest(http.MethodGet, fmt.Sprintf("/%s/packages", client.accountName), input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing ListPackages request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result []*Package
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding ListPackages response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type GetPackageInput struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
func (client *PackagesClient) GetPackage(input *GetPackageInput) (*Package, error) {
|
||||
path := fmt.Sprintf("/%s/packages/%s", client.accountName, input.ID)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetPackage request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Package
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding GetPackage response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hashicorp/errwrap"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type RolesClient struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// Roles returns a c used for accessing functions pertaining
|
||||
// to Role functionality in the Triton API.
|
||||
func (c *Client) Roles() *RolesClient {
|
||||
return &RolesClient{c}
|
||||
}
|
||||
|
||||
type Role struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Policies []string `json:"policies"`
|
||||
Members []string `json:"policies"`
|
||||
DefaultMembers []string `json:"default_members"`
|
||||
}
|
||||
|
||||
type ListRolesInput struct{}
|
||||
|
||||
func (client *RolesClient) ListRoles(*ListRolesInput) ([]*Role, error) {
|
||||
respReader, err := client.executeRequest(http.MethodGet, fmt.Sprintf("/%s/roles", client.accountName), nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing ListRoles request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result []*Role
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding ListRoles response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type GetRoleInput struct{
|
||||
RoleID string
|
||||
}
|
||||
|
||||
func (client *RolesClient) GetRole(input *GetRoleInput) (*Role, error) {
|
||||
path := fmt.Sprintf("/%s/roles/%s", client.accountName, input.RoleID)
|
||||
respReader, err := client.executeRequest(http.MethodGet, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing GetRole request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Role
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding GetRole response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateRoleInput represents the options that can be specified
|
||||
// when creating a new role.
|
||||
type CreateRoleInput struct {
|
||||
// Name of the role. Required.
|
||||
Name string `json:"name"`
|
||||
|
||||
// This account's policies to be given to this role. Optional.
|
||||
Policies []string `json:"policies,omitempty"`
|
||||
|
||||
// This account's user logins to be added to this role. Optional.
|
||||
Members []string `json:"members,omitempty"`
|
||||
|
||||
// This account's user logins to be added to this role and have
|
||||
// it enabled by default. Optional.
|
||||
DefaultMembers []string `json:"default_members,omitempty"`
|
||||
}
|
||||
|
||||
func (client *RolesClient) CreateRole(input *CreateRoleInput) (*Role, error) {
|
||||
respReader, err := client.executeRequest(http.MethodPost, fmt.Sprintf("/%s/roles", client.accountName), input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing CreateRole request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Role
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding CreateRole response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UpdateRoleInput represents the options that can be specified
|
||||
// when updating a role. Anything but ID can be modified.
|
||||
type UpdateRoleInput struct {
|
||||
// ID of the role to modify. Required.
|
||||
RoleID string `json:"id"`
|
||||
|
||||
// Name of the role. Required.
|
||||
Name string `json:"name"`
|
||||
|
||||
// This account's policies to be given to this role. Optional.
|
||||
Policies []string `json:"policies,omitempty"`
|
||||
|
||||
// This account's user logins to be added to this role. Optional.
|
||||
Members []string `json:"members,omitempty"`
|
||||
|
||||
// This account's user logins to be added to this role and have
|
||||
// it enabled by default. Optional.
|
||||
DefaultMembers []string `json:"default_members,omitempty"`
|
||||
}
|
||||
|
||||
func (client *RolesClient) UpdateRole(input *UpdateRoleInput) (*Role, error) {
|
||||
respReader, err := client.executeRequest(http.MethodPost, fmt.Sprintf("/%s/roles/%s", client.accountName, input.RoleID), input)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing UpdateRole request: {{err}}", err)
|
||||
}
|
||||
|
||||
var result *Role
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding UpdateRole response: {{err}}", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type DeleteRoleInput struct {
|
||||
RoleID string
|
||||
}
|
||||
|
||||
func (client *RolesClient) DeleteRoles(input *DeleteRoleInput) error {
|
||||
path := fmt.Sprintf("/%s/roles/%s", client.accountName, input.RoleID)
|
||||
respReader, err := client.executeRequest(http.MethodDelete, path, nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("Error executing DeleteRole request: {{err}}", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package triton
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
)
|
||||
|
||||
type ServicesClient struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// Services returns a c used for accessing functions pertaining
|
||||
// to Services functionality in the Triton API.
|
||||
func (c *Client) Services() *ServicesClient {
|
||||
return &ServicesClient{c}
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
Name string
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
type ListServicesInput struct{}
|
||||
|
||||
func (client *ServicesClient) ListServices(*ListServicesInput) ([]*Service, error) {
|
||||
respReader, err := client.executeRequest(http.MethodGet, fmt.Sprintf("/%s/services", client.accountName), nil)
|
||||
if respReader != nil {
|
||||
defer respReader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("Error executing ListServices request: {{err}}", err)
|
||||
}
|
||||
|
||||
var intermediate map[string]string
|
||||
decoder := json.NewDecoder(respReader)
|
||||
if err = decoder.Decode(&intermediate); err != nil {
|
||||
return nil, errwrap.Wrapf("Error decoding ListServices response: {{err}}", err)
|
||||
}
|
||||
|
||||
keys := make([]string, len(intermediate))
|
||||
i := 0
|
||||
for k := range intermediate {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
result := make([]*Service, len(intermediate))
|
||||
i = 0
|
||||
for _, key := range keys {
|
||||
result[i] = &Service{
|
||||
Name: key,
|
||||
Endpoint: intermediate[key],
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -487,6 +487,12 @@
|
|||
"path": "github.com/hashicorp/go-multierror",
|
||||
"revision": "d30f09973e19c1dfcd120b2d9c4f168e68d6b5d5"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "ErJHGU6AVPZM9yoY/xV11TwSjQs=",
|
||||
"path": "github.com/hashicorp/go-retryablehttp",
|
||||
"revision": "2d5f5dbd904dbad432492c3ca2c12c72c9e3045a",
|
||||
"revisionTime": "2017-04-21T23:29:22Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "A1PcINvF3UiwHRKn8UcgARgvGRs=",
|
||||
"path": "github.com/hashicorp/go-rootcerts",
|
||||
|
@ -514,19 +520,16 @@
|
|||
"revision": "c01cf91b011868172fdcd9f41838e80c9d716264"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "NOwNdnb70M6s9LvhaPFabBVwlBs=",
|
||||
"path": "github.com/joyent/gocommon",
|
||||
"revision": "b78708995d1c2ebdb64a3061b0bca5d8ccdf0fc2"
|
||||
"checksumSHA1": "TFdaSXpgmpa6vJzfjtUoHLrp1AM=",
|
||||
"path": "github.com/joyent/triton-go",
|
||||
"revision": "db2461b5a5a55ca698b4062d95118c03d4aafb88",
|
||||
"revisionTime": "2017-04-26T18:53:01Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "zg/V3yqR59RlBEhTqjW7AhEp16o=",
|
||||
"path": "github.com/joyent/gosdc",
|
||||
"revision": "ec8b3503a75edca0df26581b83807677b0240716"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "xVAU7r1t8lxJ1AHvgNNXxwU+jIo=",
|
||||
"path": "github.com/joyent/gosign",
|
||||
"revision": "9abcee278795b82b36858cdfc857c8a0e7de797c"
|
||||
"checksumSHA1": "QzUqkCSn/ZHyIK346xb9V6EBw9U=",
|
||||
"path": "github.com/joyent/triton-go/authentication",
|
||||
"revision": "db2461b5a5a55ca698b4062d95118c03d4aafb88",
|
||||
"revisionTime": "2017-04-26T18:53:01Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "6nmAJBw2phU9MUmkUnqFvbO5urg=",
|
||||
|
|
|
@ -25,7 +25,7 @@ Docker images on Triton you should use the Packer Docker builder.
|
|||
The builder creates and launches a temporary VM based on a specified source
|
||||
image, runs any provisioning necessary, uses the Triton "VM to image"
|
||||
functionality to create a reusable image and finally destroys the temporary VM.
|
||||
This reusable image can then be used to launch new VM's.
|
||||
This reusable image can then be used to launch new machines.
|
||||
|
||||
The builder does *not* manage images. Once it creates an image, it is up to you
|
||||
to use it or delete it.
|
||||
|
@ -44,9 +44,9 @@ builder.
|
|||
- `triton_account` (string) - The username of the Triton account to use when
|
||||
using the Triton Cloud API.
|
||||
- `triton_key_id` (string) - The fingerprint of the public key of the SSH key
|
||||
pair to use for authentication with the Triton Cloud API.
|
||||
- `triton_key_material` (string) - Path to the file in which the private key
|
||||
of `triton_key_id` is stored. For example `~/.ssh/id_rsa`.
|
||||
pair to use for authentication with the Triton Cloud API. If
|
||||
`triton_key_material` is not set, it is assumed that the SSH agent has the
|
||||
private key corresponding to this key ID loaded.
|
||||
|
||||
- `source_machine_image` (string) - The UUID of the image to base the new
|
||||
image on. Triton supports multiple types of images, called 'brands' in
|
||||
|
@ -80,6 +80,11 @@ builder.
|
|||
own private Triton installation you will have to supply the URL of the cloud
|
||||
API of your own Triton installation.
|
||||
|
||||
- `triton_key_material` (string) - Path to the file in which the private key
|
||||
of `triton_key_id` is stored. For example `~/.ssh/id_rsa`. If this is not
|
||||
specified, the SSH agent is used to sign requests with the `triton_key_id`
|
||||
specified.
|
||||
|
||||
- `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
|
||||
filters inbound traffic to the VM. All outbound traffic is always allowed.
|
||||
|
@ -104,7 +109,9 @@ builder.
|
|||
- `source_machine_networks` (array of strings) - The UUID's of Triton networks
|
||||
added to the source machine used for creating the image. For example if any
|
||||
of the provisioners which are run need Internet access you will need to add
|
||||
the UUID's of the appropriate networks here.
|
||||
the UUID's of the appropriate networks here. If this is not specified,
|
||||
instances will be placed into the default Triton public and internal
|
||||
networks.
|
||||
- `source_machine_tags` (object of key/value strings) - Tags applied to the VM
|
||||
used to create the image.
|
||||
|
||||
|
@ -129,18 +136,21 @@ cloud:
|
|||
"builders": [
|
||||
{
|
||||
"type": "triton",
|
||||
|
||||
"triton_account": "triton_username",
|
||||
"triton_key_id": "6b:95:03:3d:d3:6e:52:69:01:96:1a:46:4a:8d:c1:7e",
|
||||
"triton_key_material": "~/.ssh/id_rsa",
|
||||
|
||||
"source_machine_name": "image-builder",
|
||||
"source_machine_package": "g3-standard-0.5-smartos",
|
||||
"source_machine_image": "70e3ae72-96b6-11e6-9056-9737fd4d0764",
|
||||
|
||||
"ssh_username": "root",
|
||||
"ssh_private_key_file": "~/.ssh/id_rsa",
|
||||
|
||||
"image_name": "my_new_image",
|
||||
"image_version": "1.0.0",
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
Loading…
Reference in New Issue