Fixed step interface, added step to builder, passing all tests for new image_query.go

This commit is contained in:
tcarrio 2018-07-15 23:35:02 -04:00 committed by Tom Carrio
parent a87c8fec38
commit 94018c691c
5 changed files with 200 additions and 78 deletions

View File

@ -86,6 +86,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
SSHAgentAuth: b.config.RunConfig.Comm.SSHAgentAuth,
},
&StepSourceImageInfo{
SourceImage: b.config.SourceImage,
SourceImageName: b.config.SourceImageName,
ImageFilters: b.config.SourceImageFilters,
},
&StepCreateVolume{
UseBlockStorageVolume: b.config.UseBlockStorageVolume,
SourceImage: b.config.SourceImage,

View File

@ -3,13 +3,13 @@ package openstack
import (
"fmt"
"reflect"
"strconv"
"strings"
"time"
"strconv"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/go-multierror"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/packer/packer"
)
const (
@ -35,7 +35,47 @@ func getDateFilter(s string) (images.ImageDateFilter, error) {
}
var badFilter images.ImageDateFilter
return badFilter, fmt.Errorf("No ImageDateFilter found for %s", s)
return badFilter, fmt.Errorf("No valid ImageDateFilter found for %s", s)
}
// 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 ImageVisilibility found for %s", s)
}
// Retrieve the specific ImageVisibility using the exported const from images
func getImageStatus(s string) (images.ImageStatus, error) {
statuses := [...]images.ImageStatus{
images.ImageStatusActive,
images.ImageStatusDeactivated,
images.ImageStatusDeleted,
images.ImageStatusPendingDelete,
images.ImageStatusQueued,
images.ImageStatusSaving,
}
for _, status := range statuses {
if string(status) == s {
return status, nil
}
}
var nilStatus images.ImageStatus
return nilStatus, fmt.Errorf("No valid ImageVisilibility found for %s", s)
}
// Allows construction of all fields from ListOpts using the "q" tags and
@ -43,7 +83,7 @@ func getDateFilter(s string) (images.ImageDateFilter, error) {
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()
metaOpts := reflect.Indirect(reflect.ValueOf(listOpts))
multiErr := packer.MultiError{}
@ -57,17 +97,46 @@ func buildImageFilters(input map[string]string, listOpts *images.ListOpts) *pack
if val, exists := input[key]; exists && vField.CanSet() {
switch vField.Kind() {
case reflect.Int64:
// Handles integer types used in ListOpts
case reflect.Int64, reflect.Int:
iVal, err := strconv.Atoi(val)
if err != nil {
multierror.Append(err, multiErr.Errors...)
} else {
vField.Set(reflect.ValueOf(iVal))
continue
}
case reflect.String:
vField.Set(reflect.ValueOf(val))
if vField.Kind() == reflect.Int {
vField.Set(reflect.ValueOf(iVal))
} else {
var i64Val int64
i64Val = int64(iVal)
vField.Set(reflect.ValueOf(i64Val))
}
// Handles string and types using string
case reflect.String:
switch vField.Type() {
default:
vField.Set(reflect.ValueOf(val))
case reflect.TypeOf(images.ImageVisibility("")):
iv, err := getImageVisibility(val)
if err != nil {
multierror.Append(err, multiErr.Errors...)
continue
}
vField.Set(reflect.ValueOf(iv))
case reflect.TypeOf(images.ImageStatus("")):
is, err := getImageStatus(val)
if err != nil {
multierror.Append(err, multiErr.Errors...)
continue
}
vField.Set(reflect.ValueOf(is))
}
// Generates slice of strings for Tags
case reflect.Slice:
typeOfSlice := reflect.TypeOf(vField).Elem()
fieldArray := reflect.MakeSlice(reflect.SliceOf(typeOfSlice), 0, 0)
@ -80,18 +149,21 @@ func buildImageFilters(input map[string]string, listOpts *images.ListOpts) *pack
default:
multierror.Append(
fmt.Errorf("Unsupported struct type %s", vField.Type().Name),
fmt.Errorf("Unsupported kind %s", vField.Kind()),
multiErr.Errors...)
}
} else if fieldName == reflect.TypeOf(images.ListOpts{}.CreatedAtQuery).Name() ||
fieldName == reflect.TypeOf(images.ListOpts{}.UpdatedAtQuery).Name() {
// Handles ImageDateQuery types
} else if fieldName == reflect.TypeOf(listOpts.CreatedAtQuery).Name() ||
fieldName == reflect.TypeOf(listOpts.UpdatedAtQuery).Name() {
// get ImageDateQuery from string and set to this field
query, err := dateToImageDateQuery(&key, &val)
query, err := dateToImageDateQuery(key, val)
if err != nil {
multierror.Append(err, multiErr.Errors...)
continue
}
vField.Set(reflect.ValueOf(query))
}
}
@ -104,13 +176,12 @@ func buildImageFilters(input map[string]string, listOpts *images.ListOpts) *pack
// 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 old sorting properties if user used them. This overwrites previous values.
// The docs don't seem to mention more than one field being allowed here and how they would be
listOpts.SortDir = descendingSort
listOpts.SortKey = createdAtKey
// apply to new sorting property
// Apply to new sorting property.
if listOpts.Sort != "" {
listOpts.Sort = fmt.Sprintf("%s:%s,%s", createdAtKey, descendingSort, listOpts.Sort)
} else {
@ -121,10 +192,10 @@ func applyMostRecent(listOpts *images.ListOpts) {
}
// Converts a given date entry to ImageDateQuery for use in ListOpts
func dateToImageDateQuery(val *string, key *string) (*images.ImageDateQuery, error) {
func dateToImageDateQuery(val string, key string) (*images.ImageDateQuery, error) {
q := new(images.ImageDateQuery)
sep := ":"
entries := strings.Split(*val, sep)
entries := strings.Split(val, sep)
if len(entries) > 3 {
filter, err := getDateFilter(entries[0])
@ -134,9 +205,13 @@ func dateToImageDateQuery(val *string, key *string) (*images.ImageDateQuery, err
q.Filter = filter
}
date, err := time.Parse((*val)[len(entries[0]):], time.RFC3339)
dateSubstr := val[len(entries[0])+1:]
date, err := time.Parse(time.RFC3339, dateSubstr)
if err != nil {
return nil, fmt.Errorf("Failed to parse date format for %s", key)
return nil, fmt.Errorf("Failed to parse date format for %s.\nDate: %s.\nError: %s",
key,
dateSubstr,
err.Error())
} else {
q.Date = date
}

View File

@ -2,17 +2,19 @@ package openstack
import (
"testing"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/mitchellh/mapstructure"
)
func TestGetImageFilter(t *testing.T) {
passedExpectedMap := map[string]images.ImageDateFilter{
"gt": images.FilterGT,
"gte": images.FilterGTE,
"lt": images.FilterLT,
"lte": images.FilterLTE,
"neq": images.FilterNEQ,
"eq": images.FilterEQ,
"gt": images.FilterGT,
"gte": images.FilterGTE,
"lt": images.FilterLT,
"lte": images.FilterLTE,
"neq": images.FilterNEQ,
"eq": images.FilterEQ,
}
for passed, expected := range passedExpectedMap {
@ -29,12 +31,12 @@ func TestBuildImageFilter(t *testing.T) {
testOpts := images.ListOpts{}
filters := map[string]string{
"limit": "3",
"name": "Ubuntu 16.04",
"limit": "3",
"name": "Ubuntu 16.04",
"visibility": "public",
"image_status": "active",
"size_min": "0",
"sort": "created_at:desc",
"status": "active",
"size_min": "0",
"sort": "created_at:desc",
}
multiErr := buildImageFilters(filters, &testOpts)
@ -46,11 +48,11 @@ func TestBuildImageFilter(t *testing.T) {
}
if testOpts.Limit != 3 {
t.Errorf("Limit did not parse correctly")
t.Errorf("Limit did not parse correctly: %d", testOpts.Limit)
}
if testOpts.Name != filters["name"] {
t.Errorf("Name did not parse correctly")
t.Errorf("Name did not parse correctly: %")
}
var visibility images.ImageVisibility = "public"
@ -60,7 +62,7 @@ func TestBuildImageFilter(t *testing.T) {
var imageStatus images.ImageStatus = "active"
if testOpts.Status != imageStatus {
t.Errorf("Image status did not parse correctly")
t.Errorf("Image status did not parse correctly: %s", testOpts.Status)
}
if testOpts.SizeMin != 0 {
@ -73,30 +75,64 @@ func TestBuildImageFilter(t *testing.T) {
}
func TestApplyMostRecent(t *testing.T) {
testOpts := images.ListOpts{
Name: "RHEL 7.0",
testSortEmptyOpts := images.ListOpts{
Name: "RHEL 7.0",
SizeMin: 0,
}
applyMostRecent(&testOpts)
testSortFilledOpts := images.ListOpts{
Name: "Ubuntu 16.04",
SizeMin: 0,
Sort: "tags:ubuntu",
}
if testOpts.Sort != "created_at:desc" {
applyMostRecent(&testSortEmptyOpts)
if testSortEmptyOpts.Sort != "created_at:desc" {
t.Errorf("Error applying most recent filter: sort")
}
if testOpts.SortDir != "desc" || testOpts.SortKey != "created_at" {
t.Errorf("Error applying most recent filter: sort_dir/sort_key")
if testSortEmptyOpts.SortDir != "desc" || testSortEmptyOpts.SortKey != "created_at" {
t.Errorf("Error applying most recent filter: sort_dir/sort_key:\n{sort_dir: %s, sort_key: %s}",
testSortEmptyOpts.SortDir, testSortEmptyOpts.SortKey)
}
applyMostRecent(&testSortFilledOpts)
if testSortFilledOpts.Sort != "created_at:desc,tags:ubuntu" {
t.Errorf("Error applying most recent filter: sort")
}
if testSortFilledOpts.SortDir != "desc" || testSortFilledOpts.SortKey != "created_at" {
t.Errorf("Error applying most recent filter: sort_dir/sort_key:\n{sort_dir: %s, sort_key: %s}",
testSortFilledOpts.SortDir, testSortFilledOpts.SortKey)
}
}
func TestDateToImageDateQuery(t *testing.T) {
tests := [][2]string{
{"2006-01-02T15:04:05Z07:00", "created_at"},
{"gt:2012-11-01T22:08:41+00:00", "created_at"},
}
for _, test := range tests {
if _, err := dateToImageDateQuery(&test[0], &test[1]); err != nil {
if _, err := dateToImageDateQuery(test[0], test[1]); err != nil {
t.Error(err)
}
}
}
func TestImageFilterOptionsDecode(t *testing.T) {
opts := ImageFilterOptions{}
input := map[string]interface{}{
"most_recent": true,
"filters": map[string]interface{}{
"visibility": "protected",
"tag": "prod",
"name": "ubuntu 16.04",
},
}
err := mapstructure.Decode(input, &opts)
if err != nil {
t.Error("Did not successfully generate ImageFilterOptions from %v. Contains %v", input, opts)
}
}

View File

@ -18,21 +18,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"`
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 ImageFilterOptions `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"`
@ -75,8 +76,8 @@ 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"))
if c.SourceImage == "" && c.SourceImageName == "" && c.SourceImageFilters.Filters != nil {
errs = append(errs, errors.New("Either a source_image, a source_image_name, or 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."))
}

View File

@ -1,26 +1,25 @@
package openstack
import (
"log"
"fmt"
"context"
"fmt"
"log"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/helper/multistep"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/gophercloud/gophercloud/pagination"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type StepSourceImageInfo struct {
SourceImage string
SourceImageName string
ImageFilters ImageFilterOptions
SourceImage string
SourceImageName string
ImageFilters ImageFilterOptions
}
type ImageFilterOptions struct {
Filters map[string]string
MostRecent bool `mapstructure:"most_recent"`
Filters map[string]string `mapstructure:"filters"`
MostRecent bool `mapstructure:"most_recent"`
}
func (s *StepSourceImageInfo) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
@ -54,7 +53,7 @@ func (s *StepSourceImageInfo) Run(_ context.Context, state multistep.StateBag) m
log.Printf("Using Image Filters %v", params)
image := &images.Image{}
err = images.List(client, params).EachPage(func (page pagination.Page) (bool, error) {
err = images.List(client, params).EachPage(func(page pagination.Page) (bool, error) {
i, err := images.ExtractImages(page)
if err != nil {
return false, err
@ -62,12 +61,14 @@ func (s *StepSourceImageInfo) Run(_ context.Context, state multistep.StateBag) m
switch len(i) {
case 0:
return false, fmt.Errorf("No image was found matching filters: %v", )
return false, fmt.Errorf("No image was found matching filters: %v", params)
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 false, fmt.Errorf(
"Your query returned more than one result. Please try a more specific search, or set most_recent to true. Search filters: %v",
params)
}
return true, nil
@ -85,3 +86,7 @@ func (s *StepSourceImageInfo) Run(_ context.Context, state multistep.StateBag) m
state.Put("source_image", image)
return multistep.ActionContinue
}
func (s *StepSourceImageInfo) Cleanup(state multistep.StateBag) {
// No cleanup required for backout
}