diff --git a/builder/openstack/builder.go b/builder/openstack/builder.go index 8e4c97ba6..638dcc8ba 100644 --- a/builder/openstack/builder.go +++ b/builder/openstack/builder.go @@ -87,9 +87,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe SSHAgentAuth: b.config.RunConfig.Comm.SSHAgentAuth, }, &StepSourceImageInfo{ - SourceImage: b.config.SourceImage, - SourceImageName: b.config.SourceImageName, - SourceImageOpts: b.config.SourceImageOpts, + SourceImage: b.config.RunConfig.SourceImage, + SourceImageName: b.config.RunConfig.SourceImageName, + SourceImageOpts: b.config.RunConfig.sourceImageOpts, SourceMostRecent: b.config.SourceImageFilters.MostRecent, }, &StepCreateVolume{ diff --git a/builder/openstack/image_query.go b/builder/openstack/image_query.go deleted file mode 100644 index 1d1ef0d27..000000000 --- a/builder/openstack/image_query.go +++ /dev/null @@ -1,120 +0,0 @@ -package openstack - -import ( - "fmt" - "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" - "github.com/hashicorp/packer/packer" - "reflect" -) - -const ( - mostRecentSort = "created_at:desc" -) - -var validFields = map[string]string{ - "Name": "name", - "Visibility": "visibility", - "Owner": "owner", - "Tags": "tags", -} - -// Retrieve the specific ImageVisibility using the exported const from images -func getImageVisibility(s string) (images.ImageVisibility, error) { - visibilities := [...]images.ImageVisibility{ - images.ImageVisibilityPublic, - images.ImageVisibilityPrivate, - images.ImageVisibilityCommunity, - images.ImageVisibilityShared, - } - - for _, visibility := range visibilities { - if string(visibility) == s { - return visibility, nil - } - } - - var nilVisibility images.ImageVisibility - return nilVisibility, fmt.Errorf("No valid ImageVisibility found for %s", s) -} - -// Allows construction of all supported fields from ListOpts -// The `input` map will be modified but is not reused further in the builder -func buildImageFilters(input map[string]interface{}, listOpts *images.ListOpts) *packer.MultiError { - - // fill each field in the ListOpts based on tag/type - metaOpts := reflect.Indirect(reflect.ValueOf(listOpts)) - multiErr := packer.MultiError{} - - for i := 0; i < metaOpts.Type().NumField(); i++ { - vField := metaOpts.Field(i) - fieldName := metaOpts.Type().Field(i).Name - - // check the valid fields map and whether we can set this field - if key, exists := validFields[fieldName]; exists { - - // check that this key was provided by the user, then set the field and have compatible types - if val, exists := input[key]; exists { - - // non-settable field - if !vField.CanSet() { - multiErr.Errors = append(multiErr.Errors, fmt.Errorf("Unsettable field: %s", fieldName)) - - // remove key from input filters so we can go over them after - delete(input, key) - continue - } - - switch key { - case "owner", "name", "tags": - if valType := reflect.TypeOf(val); valType != vField.Type() { - multiErr.Errors = append(multiErr.Errors, - fmt.Errorf("Invalid type '%v' for field %s (%s)", - valType, - key, - fieldName, - )) - break - } - vField.Set(reflect.ValueOf(val)) - - case "visibility": - visibility, err := getImageVisibility(val.(string)) - if err != nil { - multiErr.Errors = append(multiErr.Errors, err) - break - } - vField.Set(reflect.ValueOf(visibility)) - - } - - // remove key from input filters so we can go over them after - delete(input, key) - } - } - } - - // error any invalid filters - for key, value := range input { - multiErr.Errors = append(multiErr.Errors, fmt.Errorf("Invalid filter: %s: %v (type: %v)", - key, - value, - reflect.TypeOf(value), - )) - } - - // Set defaults for status and member_status - listOpts.Status = images.ImageStatusActive - listOpts.MemberStatus = images.ImageMemberStatusAccepted - - return &multiErr -} - -// Apply most recent filtering logic to ListOpts where user has filled fields. -// See https://developer.openstack.org/api-ref/image/v2/ -func applyMostRecent(listOpts *images.ListOpts) { - // Sort isn't supported through our API so there should be no existing values. - // Overwriting ListOpts.Sort is okay. - listOpts.Sort = mostRecentSort - - return -} diff --git a/builder/openstack/image_query_test.go b/builder/openstack/image_query_test.go deleted file mode 100644 index 71ffe3b37..000000000 --- a/builder/openstack/image_query_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package openstack - -import ( - "testing" - - "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" - "github.com/mitchellh/mapstructure" -) - -func TestImageFilterOptionsDecode(t *testing.T) { - opts := ImageFilterOptions{} - input := map[string]interface{}{ - "most_recent": true, - "filters": map[string]interface{}{ - "visibility": "protected", - "tag": []string{"prod", "ready"}, - "name": "ubuntu 16.04", - "owner": "tcarrio", - }, - } - err := mapstructure.Decode(input, &opts) - if err != nil { - t.Errorf("Did not successfully generate ImageFilterOptions from %v.\nContains %v", input, opts) - } -} - -// This test case confirms that only allowed fields will be set to values -// The checked values are non-nil for their target type -func TestBuildImageFilter(t *testing.T) { - testOpts := images.ListOpts{} - - filters := map[string]interface{}{ - "limit": "3", - "name": "Ubuntu 16.04", - "visibility": "public", - "status": "active", - "size_min": "25", - "sort": "created_at:desc", - "tags": []string{"prod", "ready"}, - } - - // copy of original filters to pass to build function - passedFilters := make(map[string]interface{}) - for k, v := range filters { - passedFilters[k] = v - } - - buildImageFilters(passedFilters, &testOpts) - - if testOpts.Limit != 0 { - t.Errorf("Limit was parsed: %d", testOpts.Limit) - } - - if testOpts.Name != filters["name"] { - t.Errorf("Name did not parse correctly: %s", testOpts.Name) - } - - if testOpts.Visibility != images.ImageVisibilityPublic { - t.Errorf("Visibility did not parse correctly: %v", testOpts.Visibility) - } - - if testOpts.Status != images.ImageStatusActive { - t.Errorf("Image status did not parse correctly: %s", testOpts.Status) - } - - if testOpts.SizeMin != 0 { - t.Errorf("Size min was parsed: %d", testOpts.SizeMin) - } - - if len(testOpts.Sort) > 0 { - t.Errorf("Sort was parsed: %s", testOpts.Sort) - } -} - -// This test case confirms that invalid filter input are caught and do not result in a panic -func TestInvalidFilterInput(t *testing.T) { - - testOpts := images.ListOpts{} - - filters := map[string]interface{}{ - "tags": "prod", // supposed to be a []string - "owner": 12345, // supposed to be a string - "invalid_field": 0, // not a valid field in ListOpts - } - - numFields := len(filters) - - multiErr := buildImageFilters(filters, &testOpts) - if len(multiErr.Errors) != numFields { - t.Errorf("Failed to catch all %d invalid types/fields in filters", numFields) - for _, err := range multiErr.Errors { - t.Log(err.Error()) - } - } -} - -func TestApplyMostRecent(t *testing.T) { - testSortOpts := images.ListOpts{ - Name: "RHEL 7.0", - Tags: []string{"prod", "ready"}, - } - - applyMostRecent(&testSortOpts) - - if testSortOpts.Sort != "created_at:desc" { - t.Errorf("Error applying most recent filter: sort") - } -} diff --git a/builder/openstack/run_config.go b/builder/openstack/run_config.go index 7df9a65d8..ccc87dffc 100644 --- a/builder/openstack/run_config.go +++ b/builder/openstack/run_config.go @@ -19,23 +19,22 @@ type RunConfig struct { SSHInterface string `mapstructure:"ssh_interface"` SSHIPVersion string `mapstructure:"ssh_ip_version"` - SourceImage string `mapstructure:"source_image"` - SourceImageName string `mapstructure:"source_image_name"` - SourceImageFilters ImageFilterOptions `mapstructure:"source_image_filter"` - SourceImageOpts images.ListOpts `mapstructure:""` - Flavor string `mapstructure:"flavor"` - AvailabilityZone string `mapstructure:"availability_zone"` - RackconnectWait bool `mapstructure:"rackconnect_wait"` - FloatingIPNetwork string `mapstructure:"floating_ip_network"` - FloatingIP string `mapstructure:"floating_ip"` - ReuseIPs bool `mapstructure:"reuse_ips"` - SecurityGroups []string `mapstructure:"security_groups"` - Networks []string `mapstructure:"networks"` - Ports []string `mapstructure:"ports"` - UserData string `mapstructure:"user_data"` - UserDataFile string `mapstructure:"user_data_file"` - InstanceName string `mapstructure:"instance_name"` - InstanceMetadata map[string]string `mapstructure:"instance_metadata"` + SourceImage string `mapstructure:"source_image"` + SourceImageName string `mapstructure:"source_image_name"` + SourceImageFilters ImageFilter `mapstructure:"source_image_filter"` + Flavor string `mapstructure:"flavor"` + AvailabilityZone string `mapstructure:"availability_zone"` + RackconnectWait bool `mapstructure:"rackconnect_wait"` + FloatingIPNetwork string `mapstructure:"floating_ip_network"` + FloatingIP string `mapstructure:"floating_ip"` + ReuseIPs bool `mapstructure:"reuse_ips"` + SecurityGroups []string `mapstructure:"security_groups"` + Networks []string `mapstructure:"networks"` + Ports []string `mapstructure:"ports"` + UserData string `mapstructure:"user_data"` + UserDataFile string `mapstructure:"user_data_file"` + InstanceName string `mapstructure:"instance_name"` + InstanceMetadata map[string]string `mapstructure:"instance_metadata"` ConfigDrive bool `mapstructure:"config_drive"` @@ -50,6 +49,52 @@ type RunConfig struct { // Not really used, but here for BC OpenstackProvider string `mapstructure:"openstack_provider"` UseFloatingIp bool `mapstructure:"use_floating_ip"` + + sourceImageOpts images.ListOpts +} + +type ImageFilter struct { + Filters ImageFilterOptions `mapstructure:"filters"` + MostRecent bool `mapstructure:"most_recent"` +} + +type ImageFilterOptions struct { + Name string `mapstructure:"name"` + Owner string `mapstructure:"owner"` + Tags []string `mapstructure:"tags"` + Visibility string `mapstructure:"visibility"` +} + +func (f *ImageFilterOptions) Empty() bool { + return f.Name == "" && f.Owner == "" && len(f.Tags) == 0 && f.Visibility == "" +} + +func (f *ImageFilterOptions) Build() (*images.ListOpts, error) { + opts := images.ListOpts{} + // Set defaults for status, member_status, and sort + opts.Status = images.ImageStatusActive + opts.MemberStatus = images.ImageMemberStatusAccepted + opts.Sort = "created_at:desc" + + var err error + + if f.Name != "" { + opts.Name = f.Name + } + if f.Owner != "" { + opts.Owner = f.Owner + } + if len(f.Tags) > 0 { + opts.Tags = f.Tags + } + if f.Visibility != "" { + v, err := getImageVisibility(f.Visibility) + if err == nil { + opts.Visibility = *v + } + } + + return &opts, err } func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { @@ -78,7 +123,7 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { } } - if c.SourceImage == "" && c.SourceImageName == "" && c.SourceImageFilters.Filters == nil { + if c.SourceImage == "" && c.SourceImageName == "" && c.SourceImageFilters.Filters.Empty() { errs = append(errs, errors.New("Either a source_image, a source_image_name, or source_image_filter must be specified")) } else if len(c.SourceImage) > 0 && len(c.SourceImageName) > 0 { errs = append(errs, errors.New("Only a source_image or a source_image_name can be specified, not both.")) @@ -116,21 +161,32 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { // if neither ID or image name is provided outside the filter, build the filter if len(c.SourceImage) == 0 && len(c.SourceImageName) == 0 { - params := &images.ListOpts{} - if len(c.SourceImageFilters.Filters) > 0 { - filterErrs := buildImageFilters(c.SourceImageFilters.Filters, params) - if len(filterErrs.Errors) > 0 { - errs = append(errs, filterErrs.Errors...) - } + listOpts, filterErr := c.SourceImageFilters.Filters.Build() + + if filterErr != nil { + errs = append(errs, filterErr) } - - if c.SourceImageFilters.MostRecent { - applyMostRecent(params) - } - - c.SourceImageOpts = *params + c.sourceImageOpts = *listOpts } return errs } + +// Retrieve the specific ImageVisibility using the exported const from images +func getImageVisibility(visibility string) (*images.ImageVisibility, error) { + visibilities := [...]images.ImageVisibility{ + images.ImageVisibilityPublic, + images.ImageVisibilityPrivate, + images.ImageVisibilityCommunity, + images.ImageVisibilityShared, + } + + for _, v := range visibilities { + if string(v) == visibility { + return &v, nil + } + } + + return nil, fmt.Errorf("Not a valid visibility: %s", visibility) +} diff --git a/builder/openstack/run_config_test.go b/builder/openstack/run_config_test.go index 6ce0cf602..36b9e5716 100644 --- a/builder/openstack/run_config_test.go +++ b/builder/openstack/run_config_test.go @@ -4,6 +4,7 @@ import ( "os" "testing" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" "github.com/hashicorp/packer/helper/communicator" ) @@ -127,3 +128,32 @@ func TestRunConfigPrepare_FloatingIPPoolCompat(t *testing.T) { t.Fatalf("invalid value: %s", c.FloatingIPNetwork) } } + +// This test case confirms that only allowed fields will be set to values +// The checked values are non-nil for their target type +func TestBuildImageFilter(t *testing.T) { + + filters := ImageFilterOptions{ + Name: "Ubuntu 16.04", + Visibility: "public", + Owner: "1234567890", + Tags: []string{"prod", "ready"}, + } + + listOpts, err := filters.Build() + if err != nil { + t.Errorf("Building filter failed with: %s", err) + } + + if listOpts.Name != "Ubuntu 16.04" { + t.Errorf("Name did not build correctly: %s", listOpts.Name) + } + + if listOpts.Visibility != images.ImageVisibilityPublic { + t.Errorf("Visibility did not build correctly: %s", listOpts.Visibility) + } + + if listOpts.Owner != "1234567890" { + t.Errorf("Owner did not build correctly: %s", listOpts.Owner) + } +} diff --git a/builder/openstack/step_run_source_server.go b/builder/openstack/step_run_source_server.go index c14061efc..6bbb40eba 100644 --- a/builder/openstack/step_run_source_server.go +++ b/builder/openstack/step_run_source_server.go @@ -78,8 +78,8 @@ func (s *StepRunSourceServer) Run(_ context.Context, state multistep.StateBag) m } // check if image filter returned a source image ID and replace - if imageID := state.Get("source_image").(string); imageID != "" { - serverOpts.ImageRef = imageID + if imageID, ok := state.GetOk("source_image"); ok { + serverOpts.ImageRef = imageID.(string) } var serverOptsExt servers.CreateOptsBuilder diff --git a/builder/openstack/step_source_image_info.go b/builder/openstack/step_source_image_info.go index bdd0b25b5..6cf3500ae 100644 --- a/builder/openstack/step_source_image_info.go +++ b/builder/openstack/step_source_image_info.go @@ -18,15 +18,14 @@ type StepSourceImageInfo struct { SourceMostRecent bool } -type ImageFilterOptions struct { - Filters map[string]interface{} `mapstructure:"filters"` - MostRecent bool `mapstructure:"most_recent"` -} - func (s *StepSourceImageInfo) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { config := state.Get("config").(Config) ui := state.Get("ui").(packer.Ui) + if s.SourceImage != "" || s.SourceImageName != "" { + return multistep.ActionContinue + } + client, err := config.imageV2Client() log.Printf("Using Image Filters %v", s.SourceImageOpts) @@ -52,15 +51,15 @@ func (s *StepSourceImageInfo) Run(_ context.Context, state multistep.StateBag) m } }) - if image.ID == "" { - err := fmt.Errorf("No image was found matching filters: %v", s.SourceImageOpts) + if err != nil { + err := fmt.Errorf("Error querying image: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } - if err != nil { - err := fmt.Errorf("Error querying image: %s", err) + if image.ID == "" { + err := fmt.Errorf("No image was found matching filters: %v", s.SourceImageOpts) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt