Merge pull request #4015 from artburkart:closes_3908

Closes #3908: Adds snapshot tag overrides
This commit is contained in:
Rickard von Essen 2016-11-26 17:54:00 +01:00
commit a09f20f996
10 changed files with 164 additions and 90 deletions

View File

@ -263,6 +263,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}, },
&awscommon.StepCreateTags{ &awscommon.StepCreateTags{
Tags: b.config.AMITags, Tags: b.config.AMITags,
SnapshotTags: b.config.SnapshotTags,
}, },
) )

View File

@ -20,6 +20,7 @@ type AMIConfig struct {
AMIEnhancedNetworking bool `mapstructure:"enhanced_networking"` AMIEnhancedNetworking bool `mapstructure:"enhanced_networking"`
AMIForceDeregister bool `mapstructure:"force_deregister"` AMIForceDeregister bool `mapstructure:"force_deregister"`
AMIEncryptBootVolume bool `mapstructure:"encrypt_boot"` AMIEncryptBootVolume bool `mapstructure:"encrypt_boot"`
SnapshotTags map[string]string `mapstructure:"snapshot_tags"`
} }
func (c *AMIConfig) Prepare(ctx *interpolate.Context) []error { func (c *AMIConfig) Prepare(ctx *interpolate.Context) []error {

View File

@ -79,6 +79,19 @@ func (a *Artifact) Destroy() error {
} }
regionConn := ec2.New(session) regionConn := ec2.New(session)
// Get image metadata
imageResp, err := regionConn.DescribeImages(&ec2.DescribeImagesInput{
ImageIds: []*string{&imageId},
})
if err != nil {
errors = append(errors, err)
}
if len(imageResp.Images) == 0 {
err := fmt.Errorf("Error retrieving details for AMI (%s), no images found", imageId)
errors = append(errors, err)
}
// Deregister ami
input := &ec2.DeregisterImageInput{ input := &ec2.DeregisterImageInput{
ImageId: &imageId, ImageId: &imageId,
} }

View File

@ -14,6 +14,7 @@ import (
type StepCreateTags struct { type StepCreateTags struct {
Tags map[string]string Tags map[string]string
SnapshotTags map[string]string
} }
func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction { func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
@ -21,21 +22,20 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
amis := state.Get("amis").(map[string]string) amis := state.Get("amis").(map[string]string)
if len(s.Tags) > 0 { if len(s.Tags) == 0 && len(s.SnapshotTags) == 0 {
return multistep.ActionContinue
}
// Adds tags to AMIs and snapshots
for region, ami := range amis { for region, ami := range amis {
ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", ami)) ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", ami))
var ec2Tags []*ec2.Tag // Convert tags to ec2.Tag format
for key, value := range s.Tags { amiTags := ConvertToEC2Tags(s.Tags, ui)
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value)) ui.Say(fmt.Sprintf("Snapshot tags:"))
ec2Tags = append(ec2Tags, &ec2.Tag{ snapshotTags := ConvertToEC2Tags(s.SnapshotTags, ui)
Key: aws.String(key),
Value: aws.String(value),
})
}
// Declare list of resources to tag // Declare list of resources to tag
resourceIds := []*string{&ami}
awsConfig := aws.Config{ awsConfig := aws.Config{
Credentials: ec2conn.Config.Credentials, Credentials: ec2conn.Config.Credentials,
Region: aws.String(region), Region: aws.String(region),
@ -47,10 +47,10 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
regionconn := ec2.New(session) regionconn := ec2.New(session)
// Retrieve image list for given AMI // Retrieve image list for given AMI
resourceIds := []*string{&ami}
imageResp, err := regionconn.DescribeImages(&ec2.DescribeImagesInput{ imageResp, err := regionconn.DescribeImages(&ec2.DescribeImagesInput{
ImageIds: resourceIds, ImageIds: resourceIds,
}) })
@ -70,27 +70,41 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
} }
image := imageResp.Images[0] image := imageResp.Images[0]
snapshotIds := []*string{}
// Add only those with a Snapshot ID, i.e. not Ephemeral // Add only those with a Snapshot ID, i.e. not Ephemeral
for _, device := range image.BlockDeviceMappings { for _, device := range image.BlockDeviceMappings {
if device.Ebs != nil && device.Ebs.SnapshotId != nil { if device.Ebs != nil && device.Ebs.SnapshotId != nil {
ui.Say(fmt.Sprintf("Tagging snapshot: %s", *device.Ebs.SnapshotId)) ui.Say(fmt.Sprintf("Tagging snapshot: %s", *device.Ebs.SnapshotId))
resourceIds = append(resourceIds, device.Ebs.SnapshotId) resourceIds = append(resourceIds, device.Ebs.SnapshotId)
snapshotIds = append(snapshotIds, device.Ebs.SnapshotId)
} }
} }
// Retry creating tags for about 2.5 minutes // Retry creating tags for about 2.5 minutes
err = retry.Retry(0.2, 30, 11, func() (bool, error) { err = retry.Retry(0.2, 30, 11, func() (bool, error) {
// Tag images and snapshots
_, err := regionconn.CreateTags(&ec2.CreateTagsInput{ _, err := regionconn.CreateTags(&ec2.CreateTagsInput{
Resources: resourceIds, Resources: resourceIds,
Tags: ec2Tags, Tags: amiTags,
})
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "InvalidAMIID.NotFound" ||
awsErr.Code() == "InvalidSnapshot.NotFound" {
return false, nil
}
}
// Override tags on snapshots
_, err = regionconn.CreateTags(&ec2.CreateTagsInput{
Resources: snapshotIds,
Tags: snapshotTags,
}) })
if err == nil { if err == nil {
return true, nil return true, nil
} }
if awsErr, ok := err.(awserr.Error); ok { if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "InvalidAMIID.NotFound" || if awsErr.Code() == "InvalidSnapshot.NotFound" {
awsErr.Code() == "InvalidSnapshot.NotFound" {
return false, nil return false, nil
} }
} }
@ -104,7 +118,6 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt return multistep.ActionHalt
} }
} }
}
return multistep.ActionContinue return multistep.ActionContinue
} }
@ -112,3 +125,15 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
func (s *StepCreateTags) Cleanup(state multistep.StateBag) { func (s *StepCreateTags) Cleanup(state multistep.StateBag) {
// No cleanup... // No cleanup...
} }
func ConvertToEC2Tags(tags map[string]string, ui packer.Ui) []*ec2.Tag {
var amiTags []*ec2.Tag
for key, value := range tags {
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value))
amiTags = append(amiTags, &ec2.Tag{
Key: aws.String(key),
Value: aws.String(value),
})
}
return amiTags
}

View File

@ -183,6 +183,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}, },
&awscommon.StepCreateTags{ &awscommon.StepCreateTags{
Tags: b.config.AMITags, Tags: b.config.AMITags,
SnapshotTags: b.config.SnapshotTags,
}, },
} }

View File

@ -1,6 +1,7 @@
package ebs package ebs
import ( import (
"encoding/json"
"fmt" "fmt"
"testing" "testing"
@ -11,6 +12,21 @@ import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type TFBuilder struct {
Type string `json:"type"`
Region string `json:"region"`
SourceAmi string `json:"source_ami"`
InstanceType string `json:"instance_type"`
SshUsername string `json:"ssh_username"`
AmiName string `json:"ami_name"`
Tags map[string]string `json:"tags"`
SnapshotTags map[string]string `json:"snapshot_tags"`
}
type TFConfig struct {
Builders []TFBuilder `json:"builders"`
}
func TestBuilderTagsAcc_basic(t *testing.T) { func TestBuilderTagsAcc_basic(t *testing.T) {
builderT.Test(t, builderT.TestCase{ builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
@ -26,9 +42,10 @@ func checkTags() builderT.TestCheckFunc {
return fmt.Errorf("more than 1 artifact") return fmt.Errorf("more than 1 artifact")
} }
tags := make(map[string]string) config := TFConfig{}
tags["OS_Version"] = "Ubuntu" json.Unmarshal([]byte(testBuilderTagsAccBasic), &config)
tags["Release"] = "Latest" tags := config.Builders[0].Tags
snapshotTags := config.Builders[0].SnapshotTags
// Get the actual *Artifact pointer so we can access the AMIs directly // Get the actual *Artifact pointer so we can access the AMIs directly
artifactRaw := artifacts[0] artifactRaw := artifacts[0]
@ -37,18 +54,18 @@ func checkTags() builderT.TestCheckFunc {
return fmt.Errorf("unknown artifact: %#v", artifactRaw) return fmt.Errorf("unknown artifact: %#v", artifactRaw)
} }
// describe the image, get block devices with a snapshot // Describe the image, get block devices with a snapshot
ec2conn, _ := testEC2Conn() ec2conn, _ := testEC2Conn()
imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
ImageIds: []*string{aws.String(artifact.Amis["us-east-1"])}, ImageIds: []*string{aws.String(artifact.Amis["us-east-1"])},
}) })
if err != nil { if err != nil {
return fmt.Errorf("Error retrieving details for AMI Artifcat (%#v) in Tags Test: %s", artifact, err) return fmt.Errorf("Error retrieving details for AMI Artifact (%#v) in Tags Test: %s", artifact, err)
} }
if len(imageResp.Images) == 0 { if len(imageResp.Images) == 0 {
return fmt.Errorf("No images found for AMI Artifcat (%#v) in Tags Test: %s", artifact, err) return fmt.Errorf("No images found for AMI Artifact (%#v) in Tags Test: %s", artifact, err)
} }
image := imageResp.Images[0] image := imageResp.Images[0]
@ -61,7 +78,7 @@ func checkTags() builderT.TestCheckFunc {
} }
} }
// grab matching snapshot info // Grab matching snapshot info
resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{ resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
SnapshotIds: snapshots, SnapshotIds: snapshots,
}) })
@ -74,12 +91,14 @@ func checkTags() builderT.TestCheckFunc {
return fmt.Errorf("No Snapshots found for AMI Artifcat (%#v) in Tags Test", artifact) return fmt.Errorf("No Snapshots found for AMI Artifcat (%#v) in Tags Test", artifact)
} }
// grab the snapshots, check the tags // Grab the snapshots, check the tags
for _, s := range resp.Snapshots { for _, s := range resp.Snapshots {
expected := len(tags) expected := len(tags)
for _, t := range s.Tags { for _, t := range s.Tags {
for key, value := range tags { for key, value := range tags {
if key == *t.Key && value == *t.Value { if val, ok := snapshotTags[key]; ok && val == *t.Value {
expected--
} else if key == *t.Key && value == *t.Value {
expected-- expected--
} }
} }
@ -106,7 +125,11 @@ const testBuilderTagsAccBasic = `
"ami_name": "packer-tags-testing-{{timestamp}}", "ami_name": "packer-tags-testing-{{timestamp}}",
"tags": { "tags": {
"OS_Version": "Ubuntu", "OS_Version": "Ubuntu",
"Release": "Latest" "Release": "Latest",
"Name": "Bleep"
},
"snapshot_tags": {
"Name": "Foobar"
} }
} }
] ]

View File

@ -264,6 +264,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}, },
&awscommon.StepCreateTags{ &awscommon.StepCreateTags{
Tags: b.config.AMITags, Tags: b.config.AMITags,
SnapshotTags: b.config.SnapshotTags,
}, },
} }

View File

@ -200,6 +200,9 @@ each category, the available configuration keys are alphabetized.
- `most_recent` (bool) - Selects the newest created image when true. - `most_recent` (bool) - Selects the newest created image when true.
This is most useful for selecting a daily distro build. This is most useful for selecting a daily distro build.
- `snapshot_tags` (object of key/value strings) - Tags to apply to snapshot.
They will override AMI tags if already applied to snapshot.
- `tags` (object of key/value strings) - Tags applied to the AMI. - `tags` (object of key/value strings) - Tags applied to the AMI.
## Basic Example ## Basic Example

View File

@ -204,6 +204,9 @@ builder.
- `most_recent` (bool) - Selects the newest created image when true. - `most_recent` (bool) - Selects the newest created image when true.
This is most useful for selecting a daily distro build. This is most useful for selecting a daily distro build.
- `snapshot_tags` (object of key/value strings) - Tags to apply to snapshot.
They will override AMI tags if already applied to snapshot.
- `spot_price` (string) - The maximum hourly price to pay for a spot instance - `spot_price` (string) - The maximum hourly price to pay for a spot instance
to create the AMI. Spot instances are a type of instance that EC2 starts to create the AMI. Spot instances are a type of instance that EC2 starts
when the current spot price is less than the maximum price you specify. Spot when the current spot price is less than the maximum price you specify. Spot

View File

@ -219,6 +219,9 @@ builder.
- `most_recent` (bool) - Selects the newest created image when true. - `most_recent` (bool) - Selects the newest created image when true.
This is most useful for selecting a daily distro build. This is most useful for selecting a daily distro build.
- `snapshot_tags` (object of key/value strings) - Tags to apply to snapshot.
They will override AMI tags if already applied to snapshot.
- `spot_price` (string) - The maximum hourly price to launch a spot instance - `spot_price` (string) - The maximum hourly price to launch a spot instance
to create the AMI. It is a type of instances that EC2 starts when the to create the AMI. It is a type of instances that EC2 starts when the
maximum price that you specify exceeds the current spot price. Spot price maximum price that you specify exceeds the current spot price. Spot price