diff --git a/aws/provider.go b/aws/provider.go index 0df7f3c57797..d642b3a61419 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -424,6 +424,7 @@ func Provider() terraform.ResourceProvider { "aws_s3_bucket_object": resourceAwsS3BucketObject(), "aws_s3_bucket_notification": resourceAwsS3BucketNotification(), "aws_security_group": resourceAwsSecurityGroup(), + "aws_network_interface_sg_attachment": resourceAwsNetworkInterfaceSGAttachment(), "aws_default_security_group": resourceAwsDefaultSecurityGroup(), "aws_security_group_rule": resourceAwsSecurityGroupRule(), "aws_simpledb_domain": resourceAwsSimpleDBDomain(), diff --git a/aws/resource_aws_network_interface_sg_attachment.go b/aws/resource_aws_network_interface_sg_attachment.go new file mode 100644 index 000000000000..2c36a97f9e34 --- /dev/null +++ b/aws/resource_aws_network_interface_sg_attachment.go @@ -0,0 +1,183 @@ +package aws + +import ( + "fmt" + "log" + "reflect" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsNetworkInterfaceSGAttachment() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsNetworkInterfaceSGAttachmentCreate, + Read: resourceAwsNetworkInterfaceSGAttachmentRead, + Delete: resourceAwsNetworkInterfaceSGAttachmentDelete, + Schema: map[string]*schema.Schema{ + "security_group_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "network_interface_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsNetworkInterfaceSGAttachmentCreate(d *schema.ResourceData, meta interface{}) error { + // Get a lock to prevent races on other SG attachments/detatchments on this + // interface ID. This lock is released when the function exits, regardless of + // success or failure. + // + // The lock here - in the create function - deliberately covers the + // post-creation read as well, which is normally not covered as Read is + // otherwise only performed on refresh. Locking on it here prevents + // inconsistencies that could be caused by other attachments that will be + // operating on the interface, ensuring that Create gets a full lay of the + // land before moving on. + mk := "network_interface_sg_attachment_" + d.Get("network_interface_id").(string) + awsMutexKV.Lock(mk) + defer awsMutexKV.Unlock(mk) + + sgID := d.Get("security_group_id").(string) + interfaceID := d.Get("network_interface_id").(string) + + conn := meta.(*AWSClient).ec2conn + + // Fetch the network interface we will be working with. + iface, err := fetchNetworkInterface(conn, interfaceID) + if err != nil { + return err + } + + // Add the security group to the network interface. + log.Printf("[DEBUG] Attaching security group %s to network interface ID %s", sgID, interfaceID) + + if sgExistsInENI(sgID, iface) { + return fmt.Errorf("security group %s already attached to interface ID %s", sgID, *iface.NetworkInterfaceId) + } + var groupIDs []string + for _, v := range iface.Groups { + groupIDs = append(groupIDs, *v.GroupId) + } + groupIDs = append(groupIDs, sgID) + params := &ec2.ModifyNetworkInterfaceAttributeInput{ + NetworkInterfaceId: iface.NetworkInterfaceId, + Groups: aws.StringSlice(groupIDs), + } + + _, err = conn.ModifyNetworkInterfaceAttribute(params) + if err != nil { + return err + } + + log.Printf("[DEBUG] Successful attachment of security group %s to network interface ID %s", sgID, interfaceID) + + return resourceAwsNetworkInterfaceSGAttachmentRead(d, meta) +} + +func resourceAwsNetworkInterfaceSGAttachmentRead(d *schema.ResourceData, meta interface{}) error { + sgID := d.Get("security_group_id").(string) + interfaceID := d.Get("network_interface_id").(string) + + log.Printf("[DEBUG] Checking association of security group %s to network interface ID %s", sgID, interfaceID) + + conn := meta.(*AWSClient).ec2conn + + iface, err := fetchNetworkInterface(conn, interfaceID) + if err != nil { + return err + } + + if sgExistsInENI(sgID, iface) { + d.SetId(fmt.Sprintf("%s_%s", sgID, interfaceID)) + } else { + // The assocation does not exist when it should, taint this resource. + log.Printf("[WARN] Security group %s not associated with network interface ID %s, tainting", sgID, interfaceID) + d.SetId("") + } + return nil +} + +func resourceAwsNetworkInterfaceSGAttachmentDelete(d *schema.ResourceData, meta interface{}) error { + // Get a lock to prevent races on other SG attachments/detatchments on this + // interface ID. This lock is released when the function exits, regardless of + // success or failure. + mk := "network_interface_sg_attachment_" + d.Get("network_interface_id").(string) + awsMutexKV.Lock(mk) + defer awsMutexKV.Unlock(mk) + + sgID := d.Get("security_group_id").(string) + interfaceID := d.Get("network_interface_id").(string) + + log.Printf("[DEBUG] Removing security group %s from interface ID %s", sgID, interfaceID) + + conn := meta.(*AWSClient).ec2conn + + iface, err := fetchNetworkInterface(conn, interfaceID) + if err != nil { + return err + } + + if err := delSGFromENI(conn, sgID, iface); err != nil { + return err + } + + d.SetId("") + return nil +} + +// fetchNetworkInterface is a utility function used by Read and Delete to fetch +// the full ENI details for a specific interface ID. +func fetchNetworkInterface(conn *ec2.EC2, ifaceID string) (*ec2.NetworkInterface, error) { + log.Printf("[DEBUG] Fetching information for interface ID %s", ifaceID) + dniParams := &ec2.DescribeNetworkInterfacesInput{ + NetworkInterfaceIds: aws.StringSlice([]string{ifaceID}), + } + + dniResp, err := conn.DescribeNetworkInterfaces(dniParams) + if err != nil { + return nil, err + } + return dniResp.NetworkInterfaces[0], nil +} + +func delSGFromENI(conn *ec2.EC2, sgID string, iface *ec2.NetworkInterface) error { + old := iface.Groups + var new []*string + for _, v := range iface.Groups { + if *v.GroupId == sgID { + continue + } + new = append(new, v.GroupId) + } + if reflect.DeepEqual(old, new) { + // The interface already didn't have the security group, nothing to do + return nil + } + + params := &ec2.ModifyNetworkInterfaceAttributeInput{ + NetworkInterfaceId: iface.NetworkInterfaceId, + Groups: new, + } + + _, err := conn.ModifyNetworkInterfaceAttribute(params) + return err +} + +// sgExistsInENI is a utility function that can be used to quickly check to +// see if a security group exists in an *ec2.NetworkInterface. +func sgExistsInENI(sgID string, iface *ec2.NetworkInterface) bool { + for _, v := range iface.Groups { + if *v.GroupId == sgID { + return true + } + } + return false +} diff --git a/aws/resource_aws_network_interface_sg_attachment_test.go b/aws/resource_aws_network_interface_sg_attachment_test.go new file mode 100644 index 000000000000..55ca9da418a8 --- /dev/null +++ b/aws/resource_aws_network_interface_sg_attachment_test.go @@ -0,0 +1,228 @@ +package aws + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAwsNetworkInterfaceSGAttachment(t *testing.T) { + cases := []struct { + Name string + ResourceAttr string + Config func(bool) string + }{ + { + Name: "instance primary interface", + ResourceAttr: "primary_network_interface_id", + Config: testAccAwsNetworkInterfaceSGAttachmentConfigViaInstance, + }, + { + Name: "externally supplied instance through data source", + ResourceAttr: "network_interface_id", + Config: testAccAwsNetworkInterfaceSGAttachmentConfigViaDataSource, + }, + } + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: tc.Config(true), + Check: checkSecurityGroupAttached(tc.ResourceAttr, true), + }, + resource.TestStep{ + Config: tc.Config(false), + Check: checkSecurityGroupAttached(tc.ResourceAttr, false), + }, + }, + }) + }) + } +} + +func checkSecurityGroupAttached(attr string, expected bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + interfaceID := s.Modules[0].Resources["aws_instance.instance"].Primary.Attributes[attr] + sgID := s.Modules[0].Resources["aws_security_group.sg"].Primary.ID + + iface, err := fetchNetworkInterface(conn, interfaceID) + if err != nil { + return err + } + actual := sgExistsInENI(sgID, iface) + if expected != actual { + return fmt.Errorf("expected existence of security group in ENI to be %t, got %t", expected, actual) + } + return nil + } +} + +func testAccAwsNetworkInterfaceSGAttachmentConfigViaInstance(attachmentEnabled bool) string { + return fmt.Sprintf(` +variable "sg_attachment_enabled" { + type = "string" + default = "%t" +} + +data "aws_ami" "ami" { + most_recent = true + + filter { + name = "name" + values = ["amzn-ami-hvm-*"] + } + + owners = ["amazon"] +} + +resource "aws_instance" "instance" { + instance_type = "t2.micro" + ami = "${data.aws_ami.ami.id}" + + tags = { + "type" = "terraform-test-instance" + } +} + +resource "aws_security_group" "sg" { + tags = { + "type" = "terraform-test-security-group" + } +} + +resource "aws_network_interface_sg_attachment" "sg_attachment" { + count = "${var.sg_attachment_enabled == "true" ? 1 : 0}" + security_group_id = "${aws_security_group.sg.id}" + network_interface_id = "${aws_instance.instance.primary_network_interface_id}" +} +`, attachmentEnabled) +} + +func testAccAwsNetworkInterfaceSGAttachmentConfigViaDataSource(attachmentEnabled bool) string { + return fmt.Sprintf(` +variable "sg_attachment_enabled" { + type = "string" + default = "%t" +} + +data "aws_ami" "ami" { + most_recent = true + + filter { + name = "name" + values = ["amzn-ami-hvm-*"] + } + + owners = ["amazon"] +} + +resource "aws_instance" "instance" { + instance_type = "t2.micro" + ami = "${data.aws_ami.ami.id}" + + tags = { + "type" = "terraform-test-instance" + } +} + +data "aws_instance" "external_instance" { + instance_id = "${aws_instance.instance.id}" +} + +resource "aws_security_group" "sg" { + tags = { + "type" = "terraform-test-security-group" + } +} + +resource "aws_network_interface_sg_attachment" "sg_attachment" { + count = "${var.sg_attachment_enabled == "true" ? 1 : 0}" + security_group_id = "${aws_security_group.sg.id}" + network_interface_id = "${data.aws_instance.external_instance.network_interface_id}" +} +`, attachmentEnabled) +} + +func TestAccAwsNetworkInterfaceSGAttachmentRaceCheck(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAwsNetworkInterfaceSGAttachmentRaceCheckConfig(), + Check: checkSecurityGroupAttachmentRace(), + }, + }, + }) +} + +// sgRaceCheckCount specifies the amount of security groups to create in the +// race check. This should be the maximum amount of security groups that can be +// attached to an interface at once, minus the default (we don't remove it in +// the config). +const sgRaceCheckCount = 4 + +func checkSecurityGroupAttachmentRace() resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + interfaceID := s.Modules[0].Resources["aws_network_interface.interface"].Primary.ID + for i := 0; i < sgRaceCheckCount; i++ { + sgID := s.Modules[0].Resources["aws_security_group.sg."+strconv.Itoa(i)].Primary.ID + iface, err := fetchNetworkInterface(conn, interfaceID) + if err != nil { + return err + } + if !sgExistsInENI(sgID, iface) { + return fmt.Errorf("security group ID %s was not attached to ENI ID %s", sgID, interfaceID) + } + } + return nil + } +} + +func testAccAwsNetworkInterfaceSGAttachmentRaceCheckConfig() string { + return fmt.Sprintf(` +variable "security_group_count" { + type = "string" + default = "%d" +} + +data "aws_availability_zones" "available" {} + +data "aws_subnet" "subnet" { + availability_zone = "${data.aws_availability_zones.available.names[0]}" + default_for_az = "true" +} + +resource "aws_network_interface" "interface" { + subnet_id = "${data.aws_subnet.subnet.id}" + + tags = { + "type" = "terraform-test-network-interface" + } +} + +resource "aws_security_group" "sg" { + count = "${var.security_group_count}" + + tags = { + "type" = "terraform-test-security-group" + } +} + +resource "aws_network_interface_sg_attachment" "sg_attachment" { + count = "${var.security_group_count}" + security_group_id = "${aws_security_group.sg.*.id[count.index]}" + network_interface_id = "${aws_network_interface.interface.id}" +} +`, sgRaceCheckCount) +} diff --git a/website/aws.erb b/website/aws.erb index 3b974d574fc6..a11685c6085d 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1349,7 +1349,7 @@ - > + > VPC Resources