Merge pull request #6490 from tcarrio/most-recent-image-openstack
OpenStack source image search filter
This commit is contained in:
commit
70cfafb75c
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue