From 7776bf596b2cf1c207c1ea45b85a103d34d8b88b Mon Sep 17 00:00:00 2001 From: stack72 Date: Mon, 30 Oct 2017 19:26:42 +0200 Subject: [PATCH] builder/triton: Add a data source for source_machine_image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes: #5476 Based on this new template addition: ``` { "variables": { "image_version": "", "triton_account": "", "triton_key_id": "", "triton_key_material": "" }, "builders": [{ "type": "triton", "triton_account": "{{user `triton_account`}}", "triton_key_id": "{{user `triton_key_id`}}", "triton_key_material": "{{user `triton_key_material`}}", "source_machine_package": "g4-highcpu-128M", "source_machine_image_filter": { "name": "ubuntu-16.04", "most_recent": "true" }, "ssh_username": "root", "image_version": "{{user `image_version`}}", "image_name": "teamcity-server" }], "provisioners": [ { "type": "shell", "start_retry_timeout": "10m", "inline": [ "sudo apt-get update -y", "sudo apt-get install -y nginx" ] } ] } ``` I got the following output from packer: ``` packer-testing % make image packer build \ -var "triton_account=stack72_joyent" \ -var "triton_key_id=40:9d:d3:f9:0b:86:62:48:f4:2e:a5:8e:43:00:2a:9b" \ -var "triton_key_material=""" \ -var "image_version=1.0.0" \ new-template.json triton output will be in this color. ==> triton: Selecting an image based on search criteria ==> triton: Based, on given search criteria, Machine ID is: "7b5981c4-1889-11e7-b4c5-3f3bdfc9b88b" ==> triton: Waiting for source machine to become available... ==> triton: Waiting for SSH to become available... ==> triton: Connected to SSH! ==> triton: Provisioning with shell script: /var/folders/_p/2_zj9lqn4n11fx20qy787p7c0000gn/T/packer-shell797317310 triton: Get:1 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB] triton: Hit:2 http://archive.ubuntu.com/ubuntu xenial InRelease ``` I can verify from the triton cli tools that the id `7b5981c4` (from the packer output) is indeed the correct ID ``` terraform [master●] % triton images name=~ubuntu-16.04 SHORTID NAME VERSION FLAGS OS TYPE PUBDATE 49b22aec ubuntu-16.04 20160427 P linux lx-dataset 2016-04-27 675834a0 ubuntu-16.04 20160505 P linux lx-dataset 2016-05-05 4edaa46a ubuntu-16.04 20160516 P linux lx-dataset 2016-05-16 05140a7e ubuntu-16.04 20160601 P linux lx-dataset 2016-06-01 e331b22a ubuntu-16.04 20161004 P linux lx-dataset 2016-10-04 8879c758 ubuntu-16.04 20161213 P linux lx-dataset 2016-12-13 7b5981c4 ubuntu-16.04 20170403 P linux lx-dataset 2017-04-03 <------- THIS IS THE LATEST UBUNTU IMAGE ``` --- builder/triton/driver.go | 1 + builder/triton/driver_mock.go | 11 ++++ builder/triton/driver_triton.go | 58 ++++++++++++++++++++ builder/triton/source_machine_config.go | 34 +++++++++--- builder/triton/source_machine_config_test.go | 7 --- builder/triton/step_create_source_machine.go | 12 +++- website/source/docs/builders/triton.html.md | 22 +++++++- 7 files changed, 125 insertions(+), 20 deletions(-) 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",