From 7ef09bba137f44d9f60ceb37b25f455e2b97c117 Mon Sep 17 00:00:00 2001 From: Marin Salinas Date: Mon, 4 Feb 2019 10:37:31 -0600 Subject: [PATCH] feature: bsusurrogate, add clean volumes and run source vm step --- builder/osc/bsusurrogate/builder.go | 22 ++ builder/osc/bsusurrogate/builder_acc_test.go | 18 +- builder/osc/common/block_device.go | 52 ++- builder/osc/common/interpolate_build_info.go | 2 +- builder/osc/common/run_config.go | 4 +- builder/osc/common/state.go | 40 +++ builder/osc/common/step_cleanup_volumes.go | 96 ++++++ builder/osc/common/step_network_info.go | 2 +- builder/osc/common/step_run_source_vm.go | 319 +++++++++++++++++++ builder/osc/common/tags.go | 10 +- 10 files changed, 546 insertions(+), 19 deletions(-) create mode 100644 builder/osc/common/step_cleanup_volumes.go create mode 100644 builder/osc/common/step_run_source_vm.go diff --git a/builder/osc/bsusurrogate/builder.go b/builder/osc/bsusurrogate/builder.go index 945332962..7ecc2fe87 100644 --- a/builder/osc/bsusurrogate/builder.go +++ b/builder/osc/bsusurrogate/builder.go @@ -164,6 +164,28 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe CommConfig: &b.config.RunConfig.Comm, TemporarySGSourceCidr: b.config.TemporarySGSourceCidr, }, + &osccommon.StepCleanupVolumes{ + BlockDevices: b.config.BlockDevices, + }, + &osccommon.StepRunSourceVm{ + AssociatePublicIpAddress: b.config.AssociatePublicIpAddress, + BlockDevices: b.config.BlockDevices, + Comm: &b.config.RunConfig.Comm, + Ctx: b.config.ctx, + Debug: b.config.PackerDebug, + BsuOptimized: b.config.BsuOptimized, + EnableT2Unlimited: b.config.EnableT2Unlimited, + ExpectedRootDevice: "ebs", // should it be bsu + IamVmProfile: b.config.IamVmProfile, + VmInitiatedShutdownBehavior: b.config.VmInitiatedShutdownBehavior, + VmType: b.config.VmType, + IsRestricted: false, + SourceOMI: b.config.SourceOmi, + Tags: b.config.RunTags, + UserData: b.config.UserData, + UserDataFile: b.config.UserDataFile, + VolumeTags: b.config.VolumeRunTags, + }, } b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) diff --git a/builder/osc/bsusurrogate/builder_acc_test.go b/builder/osc/bsusurrogate/builder_acc_test.go index 00afeb182..04b397f2a 100644 --- a/builder/osc/bsusurrogate/builder_acc_test.go +++ b/builder/osc/bsusurrogate/builder_acc_test.go @@ -26,25 +26,27 @@ const testBuilderAccBasic = ` "builders": [{ "type": "test", "region": "eu-west-2", - "vm_type": "m3.medium", + "vm_type": "c4.large", "source_omi": "ami-46260446", "ssh_username": "ubuntu", "omi_name": "packer-test {{timestamp}}", "omi_virtualization_type": "hvm", + "subregion_name": "eu-west-2a", "launch_block_device_mappings" : [ { - "volume_type" : "gp2", - "device_name" : "/dev/sda1", + "volume_type" : "io1", + "device_name" : "/dev/xvdf", "delete_on_vm_deletion" : false, - "volume_size" : 10 + "volume_size" : 10, + "iops": 300 } ], "omi_root_device":{ - "source_device_name": "/dev/sda1", - "device_name": "/dev/sda2", + "source_device_name": "/dev/xvdf", + "device_name": "/dev/sda1", "delete_on_vm_deletion": true, - "volume_size": 16, - "volume_type": "gp2" + "volume_size": 10, + "volume_type": "standard" } }] diff --git a/builder/osc/common/block_device.go b/builder/osc/common/block_device.go index fa2a16f4e..8d6ce9535 100644 --- a/builder/osc/common/block_device.go +++ b/builder/osc/common/block_device.go @@ -83,6 +83,54 @@ func buildBlockDevices(b []BlockDevice) []*oapi.BlockDeviceMapping { return blockDevices } +func buildBlockDevicesVmCreation(b []BlockDevice) []oapi.BlockDeviceMappingVmCreation { + var blockDevices []oapi.BlockDeviceMappingVmCreation + + for _, blockDevice := range b { + mapping := oapi.BlockDeviceMappingVmCreation{ + DeviceName: blockDevice.DeviceName, + } + + if blockDevice.NoDevice { + mapping.NoDevice = "" + } else if blockDevice.VirtualName != "" { + if strings.HasPrefix(blockDevice.VirtualName, "ephemeral") { + mapping.VirtualDeviceName = blockDevice.VirtualName + } + } else { + bsu := oapi.BsuToCreate{ + DeleteOnVmDeletion: blockDevice.DeleteOnVmDeletion, + } + + if blockDevice.VolumeType != "" { + bsu.VolumeType = blockDevice.VolumeType + } + + if blockDevice.VolumeSize > 0 { + bsu.VolumeSize = blockDevice.VolumeSize + } + + // IOPS is only valid for io1 type + if blockDevice.VolumeType == "io1" { + bsu.Iops = blockDevice.IOPS + } + + if blockDevice.SnapshotId != "" { + bsu.SnapshotId = blockDevice.SnapshotId + } + + //missing + //BlockDevice Encrypted + //KmsKeyId + + mapping.Bsu = bsu + } + + blockDevices = append(blockDevices, mapping) + } + return blockDevices +} + func (b *BlockDevice) Prepare(ctx *interpolate.Context) error { if b.DeviceName == "" { return fmt.Errorf("The `device_name` must be specified " + @@ -114,6 +162,6 @@ func (b *OMIBlockDevices) BuildOMIDevices() []*oapi.BlockDeviceMapping { return buildBlockDevices(b.OMIMappings) } -func (b *LaunchBlockDevices) BuildLaunchDevices() []*oapi.BlockDeviceMapping { - return buildBlockDevices(b.LaunchMappings) +func (b *LaunchBlockDevices) BuildLaunchDevices() []oapi.BlockDeviceMappingVmCreation { + return buildBlockDevicesVmCreation(b.LaunchMappings) } diff --git a/builder/osc/common/interpolate_build_info.go b/builder/osc/common/interpolate_build_info.go index 2515e9f33..a3d62a00c 100644 --- a/builder/osc/common/interpolate_build_info.go +++ b/builder/osc/common/interpolate_build_info.go @@ -20,7 +20,7 @@ func extractBuildInfo(region string, state multistep.StateBag) *BuildInfoTemplat } } - sourceOMI := rawSourceOMI.(*oapi.Image) + sourceOMI := rawSourceOMI.(oapi.Image) sourceOMITags := make(map[string]string, len(sourceOMI.Tags)) for _, tag := range sourceOMI.Tags { sourceOMITags[tag.Key] = tag.Value diff --git a/builder/osc/common/run_config.go b/builder/osc/common/run_config.go index b781a1a4b..acd13ae64 100644 --- a/builder/osc/common/run_config.go +++ b/builder/osc/common/run_config.go @@ -59,10 +59,10 @@ func (d *SecurityGroupFilterOptions) Empty() bool { // AMI and details on how to access that launched image. type RunConfig struct { AssociatePublicIpAddress bool `mapstructure:"associate_public_ip_address"` - Subregion string `mapstructure:"availability_zone"` + Subregion string `mapstructure:"subregion_name"` BlockDurationMinutes int64 `mapstructure:"block_duration_minutes"` DisableStopVm bool `mapstructure:"disable_stop_vm"` - BsuOptimized bool `mapstructure:"ebs_optimized"` + BsuOptimized bool `mapstructure:"bsu_optimized"` EnableT2Unlimited bool `mapstructure:"enable_t2_unlimited"` IamVmProfile string `mapstructure:"iam_vm_profile"` VmInitiatedShutdownBehavior string `mapstructure:"shutdown_behavior"` diff --git a/builder/osc/common/state.go b/builder/osc/common/state.go index 701a4b293..67a48b2b0 100644 --- a/builder/osc/common/state.go +++ b/builder/osc/common/state.go @@ -17,6 +17,19 @@ func waitForSecurityGroup(conn *oapi.Client, securityGroupID string) error { return err } +func waitUntilForVmRunning(conn *oapi.Client, vmID string) error { + errCh := make(chan error, 1) + go waitForState(errCh, "running", waitUntilVmStateFunc(conn, vmID)) + err := <-errCh + return err +} + +func waitUntilVmDeleted(conn *oapi.Client, vmID string) error { + errCh := make(chan error, 1) + go waitForState(errCh, "terminated", waitUntilVmStateFunc(conn, vmID)) + return <-errCh +} + func waitForState(errCh chan<- error, target string, refresh stateRefreshFunc) error { err := common.Retry(2, 2, 0, func(_ uint) (bool, error) { state, err := refresh() @@ -31,6 +44,33 @@ func waitForState(errCh chan<- error, target string, refresh stateRefreshFunc) e return err } +func waitUntilVmStateFunc(conn *oapi.Client, id string) stateRefreshFunc { + return func() (string, error) { + log.Printf("[Debug] Check if SG with id %s exists", id) + resp, err := conn.POST_ReadVms(oapi.ReadVmsRequest{ + Filters: oapi.FiltersVm{ + VmIds: []string{id}, + }, + }) + + log.Printf("[Debug] Read Response %+v", resp.OK) + + if err != nil { + return "", err + } + + if resp.OK == nil { + return "", fmt.Errorf("Vm with ID %s. Not Found", id) + } + + if len(resp.OK.Vms) == 0 { + return "pending", nil + } + + return resp.OK.Vms[0].State, nil + } +} + func securityGroupWaitFunc(conn *oapi.Client, id string) stateRefreshFunc { return func() (string, error) { log.Printf("[Debug] Check if SG with id %s exists", id) diff --git a/builder/osc/common/step_cleanup_volumes.go b/builder/osc/common/step_cleanup_volumes.go new file mode 100644 index 000000000..e49afe91d --- /dev/null +++ b/builder/osc/common/step_cleanup_volumes.go @@ -0,0 +1,96 @@ +package common + +import ( + "context" + "fmt" + "reflect" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/outscale/osc-go/oapi" +) + +// stepCleanupVolumes cleans up any orphaned volumes that were not designated to +// remain after termination of the vm. These volumes are typically ones +// that are marked as "delete on terminate:false" in the source_ami of a build. +type StepCleanupVolumes struct { + BlockDevices BlockDevices +} + +func (s *StepCleanupVolumes) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + // stepCleanupVolumes is for Cleanup only + return multistep.ActionContinue +} + +func (s *StepCleanupVolumes) Cleanup(state multistep.StateBag) { + oapiconn := state.Get("oapi").(*oapi.Client) + vmRaw := state.Get("vm") + var vm *oapi.Vm + if vmRaw != nil { + vm = vmRaw.(*oapi.Vm) + } + ui := state.Get("ui").(packer.Ui) + if vm == nil { + ui.Say("No volumes to clean up, skipping") + return + } + + ui.Say("Cleaning up any extra volumes...") + + // Collect Volume information from the cached Vm as a map of volume-id + // to device name, to compare with save list below + var vl []string + volList := make(map[string]string) + for _, bdm := range vm.BlockDeviceMappings { + if !reflect.DeepEqual(bdm.Bsu, oapi.BsuCreated{}) { + vl = append(vl, bdm.Bsu.VolumeId) + volList[bdm.Bsu.VolumeId] = bdm.DeviceName + } + } + + // Using the volume list from the cached Vm, check with Outscale for up to + // date information on them + resp, err := oapiconn.POST_ReadVolumes(oapi.ReadVolumesRequest{ + Filters: oapi.FiltersVolume{ + VolumeIds: vl, + }, + }) + + if err != nil { + ui.Say(fmt.Sprintf("Error describing volumes: %s", err)) + return + } + + // If any of the returned volumes are in a "deleting" stage or otherwise not + // available, remove them from the list of volumes + for _, v := range resp.OK.Volumes { + if v.State != "" && v.State != "available" { + delete(volList, v.VolumeId) + } + } + + if len(resp.OK.Volumes) == 0 { + ui.Say("No volumes to clean up, skipping") + return + } + + // Filter out any devices created as part of the launch mappings, since + // we'll let amazon follow the `delete_on_termination` setting. + for _, b := range s.BlockDevices.LaunchMappings { + for volKey, volName := range volList { + if volName == b.DeviceName { + delete(volList, volKey) + } + } + } + + // Destroy remaining volumes + for k := range volList { + ui.Say(fmt.Sprintf("Destroying volume (%s)...", k)) + _, err := oapiconn.POST_DeleteVolume(oapi.DeleteVolumeRequest{VolumeId: k}) + if err != nil { + ui.Say(fmt.Sprintf("Error deleting volume: %s", err)) + } + + } +} diff --git a/builder/osc/common/step_network_info.go b/builder/osc/common/step_network_info.go index 6f5c4e501..937758bae 100644 --- a/builder/osc/common/step_network_info.go +++ b/builder/osc/common/step_network_info.go @@ -150,7 +150,7 @@ func (s *StepNetworkInfo) Run(_ context.Context, state multistep.StateBag) multi } state.Put("net_id", s.NetId) - state.Put("availability_zone", s.SubregionName) + state.Put("subregion_name", s.SubregionName) state.Put("subnet_id", s.SubnetId) return multistep.ActionContinue } diff --git a/builder/osc/common/step_run_source_vm.go b/builder/osc/common/step_run_source_vm.go new file mode 100644 index 000000000..0251c759e --- /dev/null +++ b/builder/osc/common/step_run_source_vm.go @@ -0,0 +1,319 @@ +package common + +import ( + "context" + "encoding/base64" + "fmt" + "io/ioutil" + "log" + "reflect" + + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/outscale/osc-go/oapi" + + retry "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" +) + +type StepRunSourceVm struct { + AssociatePublicIpAddress bool + BlockDevices BlockDevices + Comm *communicator.Config + Ctx interpolate.Context + Debug bool + BsuOptimized bool + EnableT2Unlimited bool + ExpectedRootDevice string + IamVmProfile string + VmInitiatedShutdownBehavior string + VmType string + IsRestricted bool + SourceOMI string + Tags TagMap + UserData string + UserDataFile string + VolumeTags TagMap + + vmId string +} + +func (s *StepRunSourceVm) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + oapiconn := state.Get("oapi").(*oapi.Client) + + securityGroupIds := state.Get("securityGroupIds").([]string) + ui := state.Get("ui").(packer.Ui) + + userData := s.UserData + if s.UserDataFile != "" { + contents, err := ioutil.ReadFile(s.UserDataFile) + if err != nil { + state.Put("error", fmt.Errorf("Problem reading user data file: %s", err)) + return multistep.ActionHalt + } + + userData = string(contents) + } + + // Test if it is encoded already, and if not, encode it + if _, err := base64.StdEncoding.DecodeString(userData); err != nil { + log.Printf("[DEBUG] base64 encoding user data...") + userData = base64.StdEncoding.EncodeToString([]byte(userData)) + } + + ui.Say("Launching a source OUTSCALE vm...") + image, ok := state.Get("source_image").(oapi.Image) + if !ok { + state.Put("error", fmt.Errorf("source_image type assertion failed")) + return multistep.ActionHalt + } + s.SourceOMI = image.ImageId + + if s.ExpectedRootDevice != "" && image.RootDeviceType != s.ExpectedRootDevice { + state.Put("error", fmt.Errorf( + "The provided source OMI has an invalid root device type.\n"+ + "Expected '%s', got '%s'.", + s.ExpectedRootDevice, image.RootDeviceType)) + return multistep.ActionHalt + } + + var vmId string + + ui.Say("Adding tags to source vm") + if _, exists := s.Tags["Name"]; !exists { + s.Tags["Name"] = "Packer Builder" + } + + oapiTags, err := s.Tags.OAPITags(s.Ctx, oapiconn.GetConfig().Region, state) + if err != nil { + err := fmt.Errorf("Error tagging source vm: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // volTags, err := s.VolumeTags.OAPITags(s.Ctx, oapiconn.GetConfig().Region, state) + // if err != nil { + // err := fmt.Errorf("Error tagging volumes: %s", err) + // state.Put("error", err) + // ui.Error(err.Error()) + // return multistep.ActionHalt + // } + + subregion := state.Get("subregion_name").(string) + runOpts := oapi.CreateVmsRequest{ + ImageId: s.SourceOMI, + VmType: s.VmType, + UserData: userData, + MaxVmsCount: 1, + MinVmsCount: 1, + Placement: oapi.Placement{SubregionName: subregion}, + BsuOptimized: s.BsuOptimized, + BlockDeviceMappings: s.BlockDevices.BuildLaunchDevices(), + //IamVmProfile: oapi.IamVmProfileSpecification{Name: &s.IamVmProfile}, + } + + // if s.EnableT2Unlimited { + // creditOption := "unlimited" + // runOpts.CreditSpecification = &oapi.CreditSpecificationRequest{CpuCredits: &creditOption} + // } + + // Collect tags for tagging on resource creation + // var tagSpecs []oapi.ResourceTag + + // if len(oapiTags) > 0 { + // runTags := &oapi.ResourceTag{ + // ResourceType: aws.String("vm"), + // Tags: oapiTags, + // } + + // tagSpecs = append(tagSpecs, runTags) + // } + + // if len(volTags) > 0 { + // runVolTags := &oapi.TagSpecification{ + // ResourceType: aws.String("volume"), + // Tags: volTags, + // } + + // tagSpecs = append(tagSpecs, runVolTags) + // } + + // // If our region supports it, set tag specifications + // if len(tagSpecs) > 0 && !s.IsRestricted { + // runOpts.SetTagSpecifications(tagSpecs) + // oapiTags.Report(ui) + // volTags.Report(ui) + // } + + if s.Comm.SSHKeyPairName != "" { + runOpts.KeypairName = s.Comm.SSHKeyPairName + } + + subnetId := state.Get("subnet_id").(string) + + if subnetId != "" && s.AssociatePublicIpAddress { + runOpts.Nics = []oapi.NicForVmCreation{ + { + DeviceNumber: 0, + //AssociatePublicIpAddress: s.AssociatePublicIpAddress, + SubnetId: subnetId, + SecurityGroupIds: securityGroupIds, + DeleteOnVmDeletion: true, + }, + } + } else { + runOpts.SubnetId = subnetId + runOpts.SecurityGroupIds = securityGroupIds + } + + if s.ExpectedRootDevice == "bsu" { + runOpts.VmInitiatedShutdownBehavior = s.VmInitiatedShutdownBehavior + } + + runResp, err := oapiconn.POST_CreateVms(runOpts) + if err != nil { + err := fmt.Errorf("Error launching source vm: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + vmId = runResp.OK.Vms[0].VmId + + // Set the vm ID so that the cleanup works properly + s.vmId = vmId + + ui.Message(fmt.Sprintf("Vm ID: %s", vmId)) + ui.Say(fmt.Sprintf("Waiting for vm (%v) to become ready...", vmId)) + + request := oapi.ReadVmsRequest{ + Filters: oapi.FiltersVm{ + VmIds: []string{vmId}, + }, + } + if err := waitUntilForVmRunning(oapiconn, vmId); err != nil { + err := fmt.Errorf("Error waiting for vm (%s) to become ready: %s", vmId, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + //TODO:Set Vm and Volume Tags, + //TODO: LinkPublicIp i + + resp, err := oapiconn.POST_ReadVms(request) + + r := resp.OK + + if err != nil || len(r.Vms) == 0 { + err := fmt.Errorf("Error finding source vm.") + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + vm := r.Vms[0] + + if s.Debug { + if vm.PublicDnsName != "" { + ui.Message(fmt.Sprintf("Public DNS: %s", vm.PublicDnsName)) + } + + if vm.PublicIp != "" { + ui.Message(fmt.Sprintf("Public IP: %s", vm.PublicIp)) + } + + if vm.PrivateIp != "" { + ui.Message(fmt.Sprintf("Private IP: %s", vm.PublicIp)) + } + } + + state.Put("vm", vm) + + // If we're in a region that doesn't support tagging on vm creation, + // do that now. + + if s.IsRestricted { + oapiTags.Report(ui) + // Retry creating tags for about 2.5 minutes + err = retry.Retry(0.2, 30, 11, func(_ uint) (bool, error) { + _, err := oapiconn.POST_CreateTags(oapi.CreateTagsRequest{ + Tags: oapiTags, + ResourceIds: []string{vmId}, + }) + if err == nil { + return true, nil + } + //TODO: improve error + if awsErr, ok := err.(awserr.Error); ok { + if awsErr.Code() == "InvalidVmID.NotFound" { + return false, nil + } + } + return true, err + }) + + if err != nil { + err := fmt.Errorf("Error tagging source vm: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Now tag volumes + + volumeIds := make([]string, 0) + for _, v := range vm.BlockDeviceMappings { + if bsu := v.Bsu; !reflect.DeepEqual(bsu, oapi.BsuCreated{}) { + volumeIds = append(volumeIds, bsu.VolumeId) + } + } + + if len(volumeIds) > 0 && s.VolumeTags.IsSet() { + ui.Say("Adding tags to source BSU Volumes") + + volumeTags, err := s.VolumeTags.OAPITags(s.Ctx, oapiconn.GetConfig().Region, state) + if err != nil { + err := fmt.Errorf("Error tagging source BSU Volumes on %s: %s", vm.VmId, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + volumeTags.Report(ui) + + _, err = oapiconn.POST_CreateTags(oapi.CreateTagsRequest{ + ResourceIds: volumeIds, + Tags: volumeTags, + }) + + if err != nil { + err := fmt.Errorf("Error tagging source BSU Volumes on %s: %s", vm.VmId, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + } + + return multistep.ActionContinue +} + +func (s *StepRunSourceVm) Cleanup(state multistep.StateBag) { + + oapiconn := state.Get("oapi").(*oapi.Client) + ui := state.Get("ui").(packer.Ui) + + // Terminate the source vm if it exists + if s.vmId != "" { + ui.Say("Terminating the source OUTSCALE vm...") + if _, err := oapiconn.POST_DeleteVms(oapi.DeleteVmsRequest{VmIds: []string{s.vmId}}); err != nil { + ui.Error(fmt.Sprintf("Error terminating vm, may still be around: %s", err)) + return + } + + if err := waitUntilVmDeleted(oapiconn, s.vmId); err != nil { + ui.Error(err.Error()) + } + } +} diff --git a/builder/osc/common/tags.go b/builder/osc/common/tags.go index ff58c0d67..0f70bac4a 100644 --- a/builder/osc/common/tags.go +++ b/builder/osc/common/tags.go @@ -10,9 +10,9 @@ import ( ) type TagMap map[string]string -type OAPI []*oapi.ResourceTag +type OAPITags []oapi.ResourceTag -func (t OAPI) Report(ui packer.Ui) { +func (t OAPITags) Report(ui packer.Ui) { for _, tag := range t { ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", tag.Key, tag.Value)) @@ -23,8 +23,8 @@ func (t TagMap) IsSet() bool { return len(t) > 0 } -func (t TagMap) OAPI(ctx interpolate.Context, region string, state multistep.StateBag) (OAPI, error) { - var oapiTags []*oapi.ResourceTag +func (t TagMap) OAPITags(ctx interpolate.Context, region string, state multistep.StateBag) (OAPITags, error) { + var oapiTags []oapi.ResourceTag ctx.Data = extractBuildInfo(region, state) for key, value := range t { @@ -36,7 +36,7 @@ func (t TagMap) OAPI(ctx interpolate.Context, region string, state multistep.Sta if err != nil { return nil, fmt.Errorf("Error processing tag: %s:%s - %s", key, value, err) } - oapiTags = append(oapiTags, &oapi.ResourceTag{ + oapiTags = append(oapiTags, oapi.ResourceTag{ Key: interpolatedKey, Value: interpolatedValue, })