builder/triton: Switch to joyent/triton-go library

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.
This commit is contained in:
James Nugent 2017-04-26 12:07:45 -07:00
parent 9f992b8f80
commit d9ba951929
6 changed files with 111 additions and 78 deletions

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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