OpenStack: refactored how source_image_filter is handled to remove reflection

This commit is contained in:
Rickard von Essen 2018-08-22 13:37:43 +02:00
parent 3eb4151599
commit 3b49caaf40
7 changed files with 129 additions and 272 deletions

View File

@ -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{

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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