From 33caed3531ecc79769781e26771e290ad3df735e Mon Sep 17 00:00:00 2001 From: Tom Carrio Date: Sat, 14 Jul 2018 00:25:26 -0400 Subject: [PATCH] Completed filters and most_recent processing using OpenStack imageservice API --- builder/openstack/image_query.go | 147 ++++++++++++++++++++ builder/openstack/run_config.go | 2 +- builder/openstack/step_run_source_server.go | 6 + builder/openstack/step_source_image_info.go | 87 ++++++++++++ 4 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 builder/openstack/image_query.go create mode 100644 builder/openstack/step_source_image_info.go diff --git a/builder/openstack/image_query.go b/builder/openstack/image_query.go new file mode 100644 index 000000000..5b2db0655 --- /dev/null +++ b/builder/openstack/image_query.go @@ -0,0 +1,147 @@ +package openstack + +import ( + "fmt" + "reflect" + "strings" + "time" + "strconv" + + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/go-multierror" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" +) + +const ( + descendingSort = "desc" + createdAtKey = "created_at" +) + +// Retrieve the specific ImageDateFilter using the exported const from images +func getDateFilter(s string) (images.ImageDateFilter, error) { + filters := [...]images.ImageDateFilter{ + images.FilterGT, + images.FilterGTE, + images.FilterLT, + images.FilterLTE, + images.FilterNEQ, + images.FilterEQ, + } + + for _, filter := range filters { + if string(filter) == s { + return filter, nil + } + } + + return images.ImageDateFilter(nil), fmt.Errorf("No ImageDateFilter found for %s", s) +} + +// Allows construction of all fields from ListOpts using the "q" tags and +// type detection to set all fields within a provided ListOpts struct +func buildImageFilters(input map[string]string, listOpts *images.ListOpts) *packer.MultiError { + + // fill each field in the ListOpts based on tag/type + metaOpts := reflect.ValueOf(listOpts).Elem() + + multiErr := packer.MultiError{} + + for i := 0; i < metaOpts.Type().NumField(); i++ { + vField := metaOpts.Field(i) + tField := metaOpts.Type().Field(i) + fieldName := tField.Name + key := metaOpts.Type().Field(i).Tag.Get("q") + + // get key from the map and set values if they exist + if val, exists := input[key]; exists && vField.CanSet() { + switch vField.Kind() { + + case reflect.Int64: + iVal, err := strconv.Atoi(val) + if err != nil { + multierror.Append(err, multiErr.Errors...) + } else { + vField.Set(reflect.ValueOf(iVal)) + } + + case reflect.String: + vField.Set(reflect.ValueOf(val)) + + case reflect.Slice: + typeOfSlice := reflect.TypeOf(vField).Elem() + fieldArray := reflect.MakeSlice(reflect.SliceOf(typeOfSlice), 0, 0) + for _, s := range strings.Split(val, ",") { + if len(s) > 0 { + fieldArray = reflect.Append(fieldArray, reflect.ValueOf(s)) + } + } + vField.Set(fieldArray) + + default: + multierror.Append( + fmt.Errorf("Unsupported struct type %s", vField.Type().Name), + multiErr.Errors...) + } + + } else if fieldName == reflect.TypeOf(images.ListOpts{}.CreatedAtQuery).Name() || + fieldName == reflect.TypeOf(images.ListOpts{}.UpdatedAtQuery).Name() { + // get ImageDateQuery from string and set to this field + query, err := dateToImageDateQuery(&key, &val) + if err != nil { + multierror.Append(err, multiErr.Errors...) + continue + } + vField.Set(reflect.ValueOf(query)) + } + } + + return &multiErr +} + +// Apply most recent filtering logic to ListOpts where user has filled fields. +// This does not check whether both are filled. Allow OpenStack to determine which to use. +// It is suggested that users use the newest sort field +// See https://developer.openstack.org/api-ref/image/v2/ +func applyMostRecent(listOpts *images.ListOpts) { + // apply to old sorting properties if user used them. This overwrites previous values? + if listOpts.SortDir == "" && listOpts.SortKey != "" { + listOpts.SortDir = descendingSort + listOpts.SortKey = createdAtKey + } + + // apply to new sorting property + if listOpts.Sort != "" { + listOpts.Sort = fmt.Sprintf("%s:%s,%s", createdAtKey, descendingSort, listOpts.Sort) + } else { + listOpts.Sort = fmt.Sprintf("%s:%s", createdAtKey, descendingSort) + } + + return +} + +// Converts a given date entry to ImageDateQuery for use in ListOpts +func dateToImageDateQuery(val *string, key *string) (*images.ImageDateQuery, error) { + q := images.ImageDateQuery{} + sep := ":" + entries := strings.Split(*val, sep) + + if len(entries) > 3 { + filter, err := getDateFilter(entries[0]) + if err != nil { + return nil, fmt.Errorf("Failed to parse date filter for %s", key) + } else { + q.Filter = filter + } + + date, err := time.Parse((*val)[len(entries[0]):], time.RFC3339) + if err != nil { + return nil, fmt.Errorf("Failed to parse date format for %s", key) + } else { + q.Date = date + } + + return &q, nil + } + + return nil, fmt.Errorf("Incorrect date query format for %s", key) +} diff --git a/builder/openstack/run_config.go b/builder/openstack/run_config.go index b98b65ea8..c96eb46bb 100644 --- a/builder/openstack/run_config.go +++ b/builder/openstack/run_config.go @@ -76,7 +76,7 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { } if c.SourceImage == "" && c.SourceImageName == "" { - errs = append(errs, errors.New("Either a source_image or a source_image_name must be specified")) + errs = append(errs, errors.New("Either a source_image or a source_image_`name 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.")) } diff --git a/builder/openstack/step_run_source_server.go b/builder/openstack/step_run_source_server.go index e56218467..c14061efc 100644 --- a/builder/openstack/step_run_source_server.go +++ b/builder/openstack/step_run_source_server.go @@ -76,6 +76,12 @@ func (s *StepRunSourceServer) Run(_ context.Context, state multistep.StateBag) m ServiceClient: computeClient, Metadata: s.InstanceMetadata, } + + // check if image filter returned a source image ID and replace + if imageID := state.Get("source_image").(string); imageID != "" { + serverOpts.ImageRef = imageID + } + var serverOptsExt servers.CreateOptsBuilder // Create root volume in the Block Storage service if required. diff --git a/builder/openstack/step_source_image_info.go b/builder/openstack/step_source_image_info.go new file mode 100644 index 000000000..c3bf4486d --- /dev/null +++ b/builder/openstack/step_source_image_info.go @@ -0,0 +1,87 @@ +package openstack + +import ( + "log" + "fmt" + "context" + + + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/helper/multistep" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" + "github.com/gophercloud/gophercloud/pagination" +) + +type StepSourceImageInfo struct { + SourceImage string + SourceImageName string + ImageFilters ImageFilterOptions +} + +type ImageFilterOptions struct { + Filters map[string]string + 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) + + client, err := config.computeV2Client() + + // if an ID is provided we skip the filter since that will return a single image + if s.SourceImage != "" { + state.Put("source_image", s.SourceImage) + return multistep.ActionContinue + } + + params := &images.ListOpts{} + + // build ListOpts from filters + if len(s.ImageFilters.Filters) > 0 { + err = buildImageFilters(s.ImageFilters.Filters, params) + if err != nil { + err := fmt.Errorf("Errors encountered in filter parsing.\n%s" + err.Error()) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + if s.ImageFilters.MostRecent { + applyMostRecent(params) + } + + log.Printf("Using Image Filters %v", params) + image := &images.Image{} + err = images.List(client, params).EachPage(func (page pagination.Page) (bool, error) { + i, err := images.ExtractImages(page) + if err != nil { + return false, err + } + + switch len(i) { + case 0: + return false, fmt.Errorf("No image was found matching filters: %v", ) + case 1: + *image = i[0] + return true, nil + default: + return false, fmt.Errorf("Your query returned more than one result. Please try a more specific search, or set most_recent to true.") + } + + return true, nil + }) + + if err != nil { + err := fmt.Errorf("Error querying image: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Message(fmt.Sprintf("Found Image ID: %s", image.ID)) + + state.Put("source_image", image) + return multistep.ActionContinue +} \ No newline at end of file