Skip to content

Commit

Permalink
builder/triton: Switch to joyent/triton-go library
Browse files Browse the repository at this point in the history
This commit substitutes the now-deprecated gosdc library for the newer
triton-go library. This is transparent from a user perspective, except
for the fact that key material can now be ommitted and requests can be
signed with an SSH agent. This allows for both encrypted keys and ECDSA
keys to be used.

In addition, a fix is made to not pass in an empty array of networks if
none are specified in configuration, thus honouring the API default of
putting instances with no explicit networks specified on the Joyent
public and internal shared networks.
  • Loading branch information
jen20 committed Apr 26, 2017
1 parent 9f992b8 commit d9ba951
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 75 deletions.
100 changes: 58 additions & 42 deletions builder/triton/access_config.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand All @@ -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 cloudapi.New(client.NewClient(
c.Endpoint,
cloudapi.DefaultAPIVersion,
creds,
log.New(os.Stdout, "", log.Flags()),
)), nil
return signer, nil
}

func (c *AccessConfig) Comm() communicator.Config {
return communicator.Config{}
}
func (c *AccessConfig) createPrivateKeySigner() (authentication.Signer, error) {
var privateKeyMaterial []byte
var err error

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
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)
}
}

b, err := ioutil.ReadFile(keyMaterial)
// Create signer
signer, err := authentication.NewPrivateKeySigner(c.KeyID, privateKeyMaterial, c.Account)
if err != nil {
return "", fmt.Errorf("Error reading key_material from path '%s': %s",
keyMaterial, err)
return nil, fmt.Errorf("Error creating Triton request signer: %s", err)
}

return string(b), nil
// 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{}
}
6 changes: 6 additions & 0 deletions builder/triton/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
2 changes: 1 addition & 1 deletion builder/triton/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion builder/triton/driver_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
71 changes: 41 additions & 30 deletions builder/triton/driver_triton.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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,
Expand All @@ -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
}

if len(config.MachineNetworks) > 0 {
input.Networks = config.MachineNetworks
}

machine, err := d.client.CreateMachine(opts)
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
}
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion builder/triton/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit d9ba951

Please sign in to comment.