Merge pull request #6490 from tcarrio/most-recent-image-openstack

OpenStack source image search filter
This commit is contained in:
Rickard von Essen 2018-08-23 12:41:06 +02:00 committed by GitHub
commit 70cfafb75c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 313 additions and 18 deletions

View File

@ -86,6 +86,12 @@ 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.RunConfig.SourceImage,
SourceImageName: b.config.RunConfig.SourceImageName,
SourceImageOpts: b.config.RunConfig.sourceImageOpts,
SourceMostRecent: b.config.SourceImageFilters.MostRecent,
},
&StepCreateVolume{
UseBlockStorageVolume: b.config.UseBlockStorageVolume,
SourceImage: b.config.SourceImage,

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/hashicorp/packer/common/uuid"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/template/interpolate"
@ -20,6 +21,7 @@ type RunConfig struct {
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"`
@ -47,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 {
@ -75,8 +123,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.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."))
}
@ -111,5 +159,34 @@ 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 {
listOpts, filterErr := c.SourceImageFilters.Filters.Build()
if filterErr != nil {
errs = append(errs, filterErr)
}
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,7 +4,9 @@ import (
"os"
"testing"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/hashicorp/packer/helper/communicator"
"github.com/mitchellh/mapstructure"
)
func init() {
@ -127,3 +129,84 @@ 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)
}
}
func TestBuildBadImageFilter(t *testing.T) {
filterMap := map[string]interface{}{
"limit": "3",
"size_min": "25",
}
filters := ImageFilterOptions{}
mapstructure.Decode(filterMap, &filters)
listOpts, err := filters.Build()
if err != nil {
t.Errorf("Error returned processing image filter: %s", err.Error())
return // we cannot trust listOpts to not cause unexpected behaviour
}
if listOpts.Limit == filterMap["limit"] {
t.Errorf("Limit was parsed into ListOpts: %d", listOpts.Limit)
}
if listOpts.SizeMin != 0 {
t.Errorf("SizeMin was parsed into ListOpts: %d", listOpts.SizeMin)
}
if listOpts.Sort != "created_at:desc" {
t.Errorf("Sort was not applied: %s", listOpts.Sort)
}
if !filters.Empty() {
t.Errorf("The filters should be empty due to lack of input")
}
}
// Tests that the Empty method on ImageFilterOptions works as expected
func TestImageFiltersEmpty(t *testing.T) {
filledFilters := ImageFilterOptions{
Name: "Ubuntu 16.04",
Visibility: "public",
Owner: "1234567890",
Tags: []string{"prod", "ready"},
}
if filledFilters.Empty() {
t.Errorf("Expected filled filters to be non-empty: %v", filledFilters)
}
emptyFilters := ImageFilterOptions{}
if !emptyFilters.Empty() {
t.Errorf("Expected default filter to be empty: %v", emptyFilters)
}
}

View File

@ -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, ok := state.GetOk("source_image"); ok {
serverOpts.ImageRef = imageID.(string)
}
var serverOptsExt servers.CreateOptsBuilder
// Create root volume in the Block Storage service if required.

View File

@ -0,0 +1,76 @@
package openstack
import (
"context"
"fmt"
"log"
"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
SourceImageOpts images.ListOpts
SourceMostRecent bool
}
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)
image := &images.Image{}
err = images.List(client, s.SourceImageOpts).EachPage(func(page pagination.Page) (bool, error) {
i, err := images.ExtractImages(page)
if err != nil {
return false, err
}
switch len(i) {
case 1:
*image = i[0]
return false, nil
default:
if s.SourceMostRecent {
*image = i[0]
return false, nil
}
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",
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 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
}
ui.Message(fmt.Sprintf("Found Image ID: %s", image.ID))
state.Put("source_image", image.ID)
return multistep.ActionContinue
}
func (s *StepSourceImageInfo) Cleanup(state multistep.StateBag) {
// No cleanup required for backout
}

View File

@ -70,6 +70,11 @@ builder.
is an alternative way of providing `source_image` and only either of them
can be specified.
- `source_image_filter` (map) - The search filters for determining the base
image to use. This is an alternative way of providing `source_image` and
only one of these methods can be used. `source_image` will override the
filters.
- `username` or `user_id` (string) - The username or id used to connect to
the OpenStack service. If not specified, Packer will use the environment
variable `OS_USERNAME` or `OS_USERID`, if set. This is not required if
@ -153,7 +158,7 @@ builder.
Defaults to false.
- `region` (string) - The name of the region, such as "DFW", in which to
launch the server to create the AMI. If not specified, Packer will use the
launch the server to create the image. If not specified, Packer will use the
environment variable `OS_REGION_NAME`, if set.
- `reuse_ips` (boolean) - Whether or not to attempt to reuse existing
@ -166,6 +171,48 @@ builder.
- `security_groups` (array of strings) - A list of security groups by name to
add to this instance.
- `source_image_filter` (object) - Filters used to populate filter options.
Example:
``` json
{
"source_image_filter": {
"filters": {
"name": "ubuntu-16.04",
"visibility": "protected",
"owner": "d1a588cf4b0743344508dc145649372d1",
"tags": ["prod", "ready"]
},
"most_recent": true
}
}
```
This selects the most recent production Ubuntu 16.04 shared to you by the given owner.
NOTE: This will fail unless *exactly* one image is returned, or `most_recent` is set to true.
In the example of multiple returned images, `most_recent` will cause this to succeed by selecting
the newest image of the returned images.
- `filters` (map of strings) - filters used to select a `source_image`.
NOTE: This will fail unless *exactly* one image is returned, or `most_recent` is set to true.
Of the filters described in [ImageService](https://developer.openstack.org/api-ref/image/v2/), the following
are valid:
- name (string)
- owner (string)
- tags (array of strings)
- visibility (string)
- `most_recent` (boolean) - Selects the newest created image when true.
This is most useful for selecting a daily distro build.
You may set use this in place of `source_image` If `source_image_filter` is provided
alongside `source_image`, the `source_image` will override the filter. The filter
will not be used in this case.
- `ssh_interface` (string) - The type of interface to connect via SSH. Values
useful for Rackspace are "public" or "private", and the default behavior is
to connect via whichever is returned first from the OpenStack API.