From 0c7e73b1cf46e48460c1c2414f51b399297617b9 Mon Sep 17 00:00:00 2001 From: Arthur Burkart Date: Tue, 18 Oct 2016 22:46:41 -0400 Subject: [PATCH] Implements Snapshot tagging While implementing my acceptance test, I stumbled upon a comment stating that snapshot deletion should also be implemented, so I snuck that in. I can't help but wonder if there is some generic logic that is implemented a few times throughout the packer code base that could maybe better serve us if it were abstracted to the common package. --- builder/amazon/common/artifact.go | 13 ++ builder/amazon/common/step_create_tags.go | 173 +++++++++++----------- builder/amazon/ebs/tags_acc_test.go | 43 ++++-- 3 files changed, 133 insertions(+), 96 deletions(-) diff --git a/builder/amazon/common/artifact.go b/builder/amazon/common/artifact.go index bee5fe604..28331a4ec 100644 --- a/builder/amazon/common/artifact.go +++ b/builder/amazon/common/artifact.go @@ -79,6 +79,19 @@ func (a *Artifact) Destroy() error { } 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{ ImageId: &imageId, } diff --git a/builder/amazon/common/step_create_tags.go b/builder/amazon/common/step_create_tags.go index 925bf4362..a8234b5d3 100644 --- a/builder/amazon/common/step_create_tags.go +++ b/builder/amazon/common/step_create_tags.go @@ -22,99 +22,100 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packer.Ui) amis := state.Get("amis").(map[string]string) - if len(s.Tags) > 0 || len(s.SnapshotTags) > 0 { - for region, ami := range amis { - ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", ami)) + if len(s.Tags) == 0 && len(s.SnapshotTags) == 0 { + return multistep.ActionContinue + } - // Convert tags to ec2.Tag format - ec2Tags := ConvertToEC2Tags(s.Tags, ui) - snapshotTags := ConvertToEC2Tags(s.SnapshotTags, ui) + // Adds tags to AMIs and snapshots + for region, ami := range amis { + ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", ami)) - // Declare list of resources to tag - awsConfig := aws.Config{ - Credentials: ec2conn.Config.Credentials, - Region: aws.String(region), + // Convert tags to ec2.Tag format + amiTags := ConvertToEC2Tags(s.Tags, ui) + ui.Say(fmt.Sprintf("Snapshot tags:")) + snapshotTags := ConvertToEC2Tags(s.SnapshotTags, ui) + + // Declare list of resources to tag + awsConfig := aws.Config{ + Credentials: ec2conn.Config.Credentials, + Region: aws.String(region), + } + session, err := session.NewSession(&awsConfig) + if err != nil { + err := fmt.Errorf("Error creating AWS session: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + regionconn := ec2.New(session) + + // Retrieve image list for given AMI + resourceIds := []*string{&ami} + imageResp, err := regionconn.DescribeImages(&ec2.DescribeImagesInput{ + ImageIds: resourceIds, + }) + + if err != nil { + err := fmt.Errorf("Error retrieving details for AMI (%s): %s", ami, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if len(imageResp.Images) == 0 { + err := fmt.Errorf("Error retrieving details for AMI (%s), no images found", ami) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + image := imageResp.Images[0] + snapshotIds := []*string{} + + // Add only those with a Snapshot ID, i.e. not Ephemeral + for _, device := range image.BlockDeviceMappings { + if device.Ebs != nil && device.Ebs.SnapshotId != nil { + ui.Say(fmt.Sprintf("Tagging snapshot: %s", *device.Ebs.SnapshotId)) + resourceIds = append(resourceIds, device.Ebs.SnapshotId) + snapshotIds = append(snapshotIds, device.Ebs.SnapshotId) } - session, err := session.NewSession(&awsConfig) - if err != nil { - err := fmt.Errorf("Error creating AWS session: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - regionconn := ec2.New(session) + } - // Retrieve image list for given AMI - resourceIds := []*string{&ami} - imageResp, err := regionconn.DescribeImages(&ec2.DescribeImagesInput{ - ImageIds: resourceIds, + // Retry creating tags for about 2.5 minutes + err = retry.Retry(0.2, 30, 11, func() (bool, error) { + // Tag images and snapshots + _, err := regionconn.CreateTags(&ec2.CreateTagsInput{ + Resources: resourceIds, + Tags: amiTags, }) - - if err != nil { - err := fmt.Errorf("Error retrieving details for AMI (%s): %s", ami, err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - if len(imageResp.Images) == 0 { - err := fmt.Errorf("Error retrieving details for AMI (%s), no images found", ami) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - image := imageResp.Images[0] - snapshotIds := []*string{} - - // Add only those with a Snapshot ID, i.e. not Ephemeral - for _, device := range image.BlockDeviceMappings { - if device.Ebs != nil && device.Ebs.SnapshotId != nil { - ui.Say(fmt.Sprintf("Tagging snapshot: %s", *device.Ebs.SnapshotId)) - resourceIds = append(resourceIds, device.Ebs.SnapshotId) - snapshotIds = append(snapshotIds, device.Ebs.SnapshotId) + if awsErr, ok := err.(awserr.Error); ok { + if awsErr.Code() == "InvalidAMIID.NotFound" || + awsErr.Code() == "InvalidSnapshot.NotFound" { + return false, nil } } - // Retry creating tags for about 2.5 minutes - err = retry.Retry(0.2, 30, 11, func() (bool, error) { - // Tag images and snapshots - _, err := regionconn.CreateTags(&ec2.CreateTagsInput{ - Resources: resourceIds, - Tags: ec2Tags, - }) - if err == nil { - return true, nil - } - 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 { - return true, nil - } - if awsErr, ok := err.(awserr.Error); ok { - if awsErr.Code() == "InvalidSnapshot.NotFound" { - return false, nil - } - } - return true, err + // Override tags on snapshots + _, err = regionconn.CreateTags(&ec2.CreateTagsInput{ + Resources: snapshotIds, + Tags: snapshotTags, }) - - if err != nil { - err := fmt.Errorf("Error adding tags to Resources (%#v): %s", resourceIds, err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt + if err == nil { + return true, nil } + if awsErr, ok := err.(awserr.Error); ok { + if awsErr.Code() == "InvalidSnapshot.NotFound" { + return false, nil + } + } + return true, err + }) + + if err != nil { + err := fmt.Errorf("Error adding tags to Resources (%#v): %s", resourceIds, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt } } @@ -126,13 +127,13 @@ func (s *StepCreateTags) Cleanup(state multistep.StateBag) { } func ConvertToEC2Tags(tags map[string]string, ui packer.Ui) []*ec2.Tag { - var ec2Tags []*ec2.Tag + var amiTags []*ec2.Tag for key, value := range tags { ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value)) - ec2Tags = append(ec2Tags, &ec2.Tag{ + amiTags = append(amiTags, &ec2.Tag{ Key: aws.String(key), Value: aws.String(value), }) } - return ec2Tags + return amiTags } diff --git a/builder/amazon/ebs/tags_acc_test.go b/builder/amazon/ebs/tags_acc_test.go index 3027eff7f..5d7866c89 100644 --- a/builder/amazon/ebs/tags_acc_test.go +++ b/builder/amazon/ebs/tags_acc_test.go @@ -1,6 +1,7 @@ package ebs import ( + "encoding/json" "fmt" "testing" @@ -11,6 +12,21 @@ import ( "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) { builderT.Test(t, builderT.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -26,9 +42,10 @@ func checkTags() builderT.TestCheckFunc { return fmt.Errorf("more than 1 artifact") } - tags := make(map[string]string) - tags["OS_Version"] = "Ubuntu" - tags["Release"] = "Latest" + config := TFConfig{} + json.Unmarshal([]byte(testBuilderTagsAccBasic), &config) + tags := config.Builders[0].Tags + snapshotTags := config.Builders[0].SnapshotTags // Get the actual *Artifact pointer so we can access the AMIs directly artifactRaw := artifacts[0] @@ -37,18 +54,18 @@ func checkTags() builderT.TestCheckFunc { 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() imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ ImageIds: []*string{aws.String(artifact.Amis["us-east-1"])}, }) 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 { - 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] @@ -61,7 +78,7 @@ func checkTags() builderT.TestCheckFunc { } } - // grab matching snapshot info + // Grab matching snapshot info resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{ SnapshotIds: snapshots, }) @@ -74,12 +91,14 @@ func checkTags() builderT.TestCheckFunc { 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 { expected := len(tags) for _, t := range s.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-- } } @@ -106,7 +125,11 @@ const testBuilderTagsAccBasic = ` "ami_name": "packer-tags-testing-{{timestamp}}", "tags": { "OS_Version": "Ubuntu", - "Release": "Latest" + "Release": "Latest", + "Name": "Bleep" + }, + "snapshot_tags": { + "Name": "Foobar" } } ]