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:
parent
9f992b8f80
commit
d9ba951929
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue