diff --git a/builder/triton/driver.go b/builder/triton/driver.go index 2afa11d3c..5da1f687b 100644 --- a/builder/triton/driver.go +++ b/builder/triton/driver.go @@ -5,6 +5,7 @@ import ( ) type Driver interface { + GetImage(config Config) (string, error) CreateImageFromMachine(machineId string, config Config) (string, error) CreateMachine(config Config) (string, error) DeleteImage(imageId string) error diff --git a/builder/triton/driver_mock.go b/builder/triton/driver_mock.go index 831af8ada..f348c1d32 100644 --- a/builder/triton/driver_mock.go +++ b/builder/triton/driver_mock.go @@ -17,6 +17,9 @@ type DriverMock struct { DeleteMachineId string DeleteMachineErr error + GetImageId string + GetImageErr error + GetMachineErr error StopMachineId string @@ -29,6 +32,14 @@ type DriverMock struct { WaitForMachineStateErr error } +func (d *DriverMock) GetImage(config Config) (string, error) { + if d.GetImageErr != nil { + return "", d.GetImageErr + } + + return config.MachineImage, nil +} + func (d *DriverMock) CreateImageFromMachine(machineId string, config Config) (string, error) { if d.CreateImageFromMachineErr != nil { return "", d.CreateImageFromMachineErr diff --git a/builder/triton/driver_triton.go b/builder/triton/driver_triton.go index a6bc5c153..19e1e1902 100644 --- a/builder/triton/driver_triton.go +++ b/builder/triton/driver_triton.go @@ -6,6 +6,8 @@ import ( "net/http" "time" + "sort" + "github.com/hashicorp/packer/packer" "github.com/joyent/triton-go/client" "github.com/joyent/triton-go/compute" @@ -28,6 +30,36 @@ func NewDriverTriton(ui packer.Ui, config Config) (Driver, error) { }, nil } +func (d *driverTriton) GetImage(config Config) (string, error) { + computeClient, _ := d.client.Compute() + images, err := computeClient.Images().List(context.Background(), &compute.ListImagesInput{ + Name: config.MachineImageFilters.Name, + OS: config.MachineImageFilters.OS, + Version: config.MachineImageFilters.Version, + Public: config.MachineImageFilters.Public, + Type: config.MachineImageFilters.Type, + State: config.MachineImageFilters.State, + Owner: config.MachineImageFilters.Owner, + }) + if err != nil { + return "", err + } + + if len(images) == 0 { + return "", errors.New("No images found in your search. Please refine your search criteria") + } + + if len(images) > 1 { + if !config.MachineImageFilters.MostRecent { + return "", errors.New("More than 1 machine image was found in your search. Please refine your search criteria") + } else { + return mostRecentImages(images).ID, nil + } + } else { + return images[0].ID, nil + } +} + func (d *driverTriton) CreateImageFromMachine(machineId string, config Config) (string, error) { computeClient, _ := d.client.Compute() image, err := computeClient.Images().CreateFromMachine(context.Background(), &compute.CreateImageFromMachineInput{ @@ -193,3 +225,29 @@ func waitFor(f func() (bool, error), every, timeout time.Duration) error { return errors.New("Timed out while waiting for resource change") } + +func mostRecentImages(images []*compute.Image) *compute.Image { + return sortImages(images)[0] +} + +type imageSort []*compute.Image + +func sortImages(images []*compute.Image) []*compute.Image { + sortedImages := images + sort.Sort(sort.Reverse(imageSort(sortedImages))) + return sortedImages +} + +func (a imageSort) Len() int { + return len(a) +} + +func (a imageSort) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a imageSort) Less(i, j int) bool { + itime := a[i].PublishedAt + jtime := a[j].PublishedAt + return itime.Unix() < jtime.Unix() +} diff --git a/builder/triton/source_machine_config.go b/builder/triton/source_machine_config.go index 549a2a114..50c61da2d 100644 --- a/builder/triton/source_machine_config.go +++ b/builder/triton/source_machine_config.go @@ -9,13 +9,29 @@ import ( // SourceMachineConfig represents the configuration to run a machine using // the SDC API in order for provisioning to take place. type SourceMachineConfig struct { - MachineName string `mapstructure:"source_machine_name"` - MachinePackage string `mapstructure:"source_machine_package"` - MachineImage string `mapstructure:"source_machine_image"` - MachineNetworks []string `mapstructure:"source_machine_networks"` - MachineMetadata map[string]string `mapstructure:"source_machine_metadata"` - MachineTags map[string]string `mapstructure:"source_machine_tags"` - MachineFirewallEnabled bool `mapstructure:"source_machine_firewall_enabled"` + MachineName string `mapstructure:"source_machine_name"` + MachinePackage string `mapstructure:"source_machine_package"` + MachineImage string `mapstructure:"source_machine_image"` + MachineNetworks []string `mapstructure:"source_machine_networks"` + MachineMetadata map[string]string `mapstructure:"source_machine_metadata"` + MachineTags map[string]string `mapstructure:"source_machine_tags"` + MachineFirewallEnabled bool `mapstructure:"source_machine_firewall_enabled"` + MachineImageFilters MachineImageFilter `mapstructure:"source_machine_image_filter"` +} + +type MachineImageFilter struct { + MostRecent bool `mapstructure:"most_recent"` + Name string + OS string + Version string + Public bool + State string + Owner string + Type string +} + +func (m *MachineImageFilter) Empty() bool { + return m.Name == "" && m.OS == "" && m.Version == "" && m.State == "" && m.Owner == "" && m.Type == "" } // Prepare performs basic validation on a SourceMachineConfig struct. @@ -26,8 +42,8 @@ func (c *SourceMachineConfig) Prepare(ctx *interpolate.Context) []error { errs = append(errs, fmt.Errorf("A source_machine_package must be specified")) } - if c.MachineImage == "" { - errs = append(errs, fmt.Errorf("A source_machine_image must be specified")) + if c.MachineImage != "" && c.MachineImageFilters.Name != "" { + errs = append(errs, fmt.Errorf("You cannot specify a Machine Image and also Machine Name filter")) } if c.MachineNetworks == nil { diff --git a/builder/triton/source_machine_config_test.go b/builder/triton/source_machine_config_test.go index aeb1977a5..6a960d4fe 100644 --- a/builder/triton/source_machine_config_test.go +++ b/builder/triton/source_machine_config_test.go @@ -24,13 +24,6 @@ func TestSourceMachineConfig_Prepare(t *testing.T) { if errs == nil { t.Fatalf("should error: %#v", sc) } - - sc = testSourceMachineConfig(t) - sc.MachineImage = "" - errs = sc.Prepare(nil) - if errs == nil { - t.Fatalf("should error: %#v", sc) - } } func testSourceMachineConfig(t *testing.T) SourceMachineConfig { diff --git a/builder/triton/step_create_source_machine.go b/builder/triton/step_create_source_machine.go index 46b7d789c..ae51fc60e 100644 --- a/builder/triton/step_create_source_machine.go +++ b/builder/triton/step_create_source_machine.go @@ -17,7 +17,16 @@ func (s *StepCreateSourceMachine) Run(state multistep.StateBag) multistep.StepAc driver := state.Get("driver").(Driver) ui := state.Get("ui").(packer.Ui) - ui.Say("Creating source machine...") + if !config.MachineImageFilters.Empty() { + ui.Say("Selecting an image based on search criteria") + imageId, err := driver.GetImage(config) + if err != nil { + state.Put("error", fmt.Errorf("Problem selecting an image based on an search criteria: %s", err)) + return multistep.ActionHalt + } + ui.Say(fmt.Sprintf("Based, on given search criteria, Machine ID is: %q", imageId)) + config.MachineImage = imageId + } machineId, err := driver.CreateMachine(config) if err != nil { @@ -33,7 +42,6 @@ func (s *StepCreateSourceMachine) Run(state multistep.StateBag) multistep.StepAc } state.Put("machine", machineId) - return multistep.ActionContinue } diff --git a/website/source/docs/builders/triton.html.md b/website/source/docs/builders/triton.html.md index f1f670eb8..80c54a5b4 100644 --- a/website/source/docs/builders/triton.html.md +++ b/website/source/docs/builders/triton.html.md @@ -64,7 +64,8 @@ builder. base image automatically decides the brand. On the Joyent public cloud a valid `source_machine_image` could for example be `70e3ae72-96b6-11e6-9056-9737fd4d0764` for version 16.3.1 of the 64bit - SmartOS base image (a 'joyent' brand image). + SmartOS base image (a 'joyent' brand image). `source_machine_image_filter` can + be used to populate this UUID. - `source_machine_package` (string) - The Triton package to use while building the image. Does not affect (and does not have to be the same) as the package @@ -133,6 +134,19 @@ builder. information about the image. Maximum 128 characters. - `image_tags` (object of key/value strings) - Tag applied to the image. +- `source_machine_image_filter` (object) - Filters used to populate the `source_machine_image` field. + Example: + + ``` json + { + "source_machine_image_filter": { + "name": "ubuntu-16.04", + "type": "lx-dataset", + "most_recent": true + } + } + ``` + ## Basic Example Below is a minimal example to create an joyent-brand image on the Joyent public @@ -149,7 +163,11 @@ cloud: "source_machine_name": "image-builder", "source_machine_package": "g4-highcpu-128M", - "source_machine_image": "f6acf198-2037-11e7-8863-8fdd4ce58b6a", + "source_machine_image_filter": { + "name": "ubuntu-16.04", + "type": "lx-dataset", + "most_recent": "true" + }, "ssh_username": "root",