Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

provider/aws: Add FlowLog resource #2384

Merged
merged 11 commits into from
Jun 23, 2015
3 changes: 2 additions & 1 deletion builtin/providers/aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func Provider() terraform.ResourceProvider {
"aws_db_parameter_group": resourceAwsDbParameterGroup(),
"aws_db_security_group": resourceAwsDbSecurityGroup(),
"aws_db_subnet_group": resourceAwsDbSubnetGroup(),
"aws_dynamodb_table": resourceAwsDynamoDbTable(),
"aws_dynamodb_table": resourceAwsDynamoDbTable(),
"aws_ebs_volume": resourceAwsEbsVolume(),
"aws_ecs_cluster": resourceAwsEcsCluster(),
"aws_ecs_service": resourceAwsEcsService(),
Expand All @@ -101,6 +101,7 @@ func Provider() terraform.ResourceProvider {
"aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(),
"aws_elasticache_subnet_group": resourceAwsElasticacheSubnetGroup(),
"aws_elb": resourceAwsElb(),
"aws_flow_log": resourceAwsFlowLog(),
"aws_iam_access_key": resourceAwsIamAccessKey(),
"aws_iam_group_policy": resourceAwsIamGroupPolicy(),
"aws_iam_group": resourceAwsIamGroup(),
Expand Down
167 changes: 167 additions & 0 deletions builtin/providers/aws/resource_aws_flow_log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package aws

import (
"fmt"
"log"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/schema"
)

func resourceAwsFlowLog() *schema.Resource {
return &schema.Resource{
Create: resourceAwsLogFlowCreate,
Read: resourceAwsLogFlowRead,
Delete: resourceAwsLogFlowDelete,

Schema: map[string]*schema.Schema{
"iam_role_arn": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},

"log_group_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},

"vpc_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"subnet_id", "eni_id"},
},

"subnet_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"eni_id", "vpc_id"},
},

"eni_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"subnet_id", "vpc_id"},
},

"traffic_type": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},

"flow_log_status": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},

"deliver_log_status": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}

func resourceAwsLogFlowCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn

types := []struct {
ID string
Type string
}{
{ID: d.Get("vpc_id").(string), Type: "VPC"},
{ID: d.Get("subnet_id").(string), Type: "Subnet"},
{ID: d.Get("eni_id").(string), Type: "NetworkInterface"},
}

var resourceId string
var resourceType string
for _, t := range types {
if t.ID != "" {
resourceId = t.ID
resourceType = t.Type
break
}
}

if resourceId == "" || resourceType == "" {
return fmt.Errorf("Error: Flow Logs require either a VPC, Subnet, or ENI ID")
}

opts := &ec2.CreateFlowLogsInput{
DeliverLogsPermissionARN: aws.String(d.Get("iam_role_arn").(string)),
LogGroupName: aws.String(d.Get("log_group_name").(string)),
ResourceIDs: []*string{aws.String(resourceId)},
ResourceType: aws.String(resourceType),
TrafficType: aws.String(d.Get("traffic_type").(string)),
}

log.Printf(
"[DEBUG] Flow Log Create configuration: %s", awsutil.StringValue(opts))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! 👍 Isn't this basically addressing #2179 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awsutil.StringValue does, yes, but to close #2179 we need to use it in All The Places™

resp, err := conn.CreateFlowLogs(opts)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log the create with the awsutil.StringValue of opts here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 285a88b

if err != nil {
return fmt.Errorf("Error creating Flow Log for (%s), error: %s", resourceId, err)
}

if len(resp.FlowLogIDs) > 1 {
return fmt.Errorf("Error: multiple Flow Logs created for (%s)", resourceId)
}

d.SetId(*resp.FlowLogIDs[0])

return resourceAwsLogFlowRead(d, meta)
}

func resourceAwsLogFlowRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn

opts := &ec2.DescribeFlowLogsInput{
FlowLogIDs: []*string{aws.String(d.Id())},
}

resp, err := conn.DescribeFlowLogs(opts)
if err != nil {
log.Printf("[WARN] Error describing Flow Logs for id (%s)", d.Id())
d.SetId("")
return nil
}

if len(resp.FlowLogs) == 0 {
log.Printf("[WARN] No Flow Logs found for id (%s)", d.Id())
d.SetId("")
return nil
}

fl := resp.FlowLogs[0]

d.Set("traffic_type", fl.TrafficType)
d.Set("log_group_name", fl.LogGroupName)
d.Set("iam_role_arn", fl.DeliverLogsPermissionARN)
d.Set("flow_log_status", fl.FlowLogStatus)
d.Set("deliver_log_status", fl.DeliverLogsStatus)

return nil
}

func resourceAwsLogFlowDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn

log.Printf(
"[DEBUG] Flow Log Destroy: %s", d.Id())
_, err := conn.DeleteFlowLogs(&ec2.DeleteFlowLogsInput{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log the delete here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 285a88b

FlowLogIDs: []*string{aws.String(d.Id())},
})

if err != nil {
return fmt.Errorf("[WARN] Error deleting Flow Log with ID (%s), error: %s", d.Id(), err)
}

return nil
}
212 changes: 212 additions & 0 deletions builtin/providers/aws/resource_aws_flow_log_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package aws

import (
"fmt"
"os"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

func TestAccFlowLog_basic(t *testing.T) {
var flowLog ec2.FlowLog
lgn := os.Getenv("LOG_GROUP_NAME")

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckFlowLogDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: fmt.Sprintf(testAccFlowLogConfig_basic, lgn),
Check: resource.ComposeTestCheckFunc(
testAccCheckFlowLogExists("aws_flow_log.test_flow_log", &flowLog),
testAccCheckAWSFlowLogAttributes(&flowLog),
),
},
},
})
}

func TestAccFlowLog_subnet(t *testing.T) {
var flowLog ec2.FlowLog
lgn := os.Getenv("LOG_GROUP_NAME")

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckFlowLogDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: fmt.Sprintf(testAccFlowLogConfig_subnet, lgn),
Check: resource.ComposeTestCheckFunc(
testAccCheckFlowLogExists("aws_flow_log.test_flow_log_subnet", &flowLog),
testAccCheckAWSFlowLogAttributes(&flowLog),
),
},
},
})
}

func testAccCheckFlowLogExists(n string, flowLog *ec2.FlowLog) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}

if rs.Primary.ID == "" {
return fmt.Errorf("No Flow Log ID is set")
}

conn := testAccProvider.Meta().(*AWSClient).ec2conn
describeOpts := &ec2.DescribeFlowLogsInput{
FlowLogIDs: []*string{aws.String(rs.Primary.ID)},
}
resp, err := conn.DescribeFlowLogs(describeOpts)
if err != nil {
return err
}

if len(resp.FlowLogs) > 0 {
*flowLog = *resp.FlowLogs[0]
return nil
}
return fmt.Errorf("No Flow Logs found for id (%s)", rs.Primary.ID)
}
}

func testAccCheckAWSFlowLogAttributes(flowLog *ec2.FlowLog) resource.TestCheckFunc {
return func(s *terraform.State) error {
if flowLog.FlowLogStatus != nil && *flowLog.FlowLogStatus == "ACTIVE" {
return nil
}
if flowLog.FlowLogStatus == nil {
return fmt.Errorf("Flow Log status is not ACTIVE, is nil")
} else {
return fmt.Errorf("Flow Log status is not ACTIVE, got: %s", *flowLog.FlowLogStatus)
}
}
}

func testAccCheckFlowLogDestroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_flow_log" {
continue
}

return nil
}

return nil
}

var testAccFlowLogConfig_basic = `
resource "aws_vpc" "default" {
cidr_block = "10.0.0.0/16"
tags {
Name = "tf-flow-log-test"
}
}

resource "aws_subnet" "test_subnet" {
vpc_id = "${aws_vpc.default.id}"
cidr_block = "10.0.1.0/24"

tags {
Name = "tf-flow-test"
}
}

resource "aws_iam_role" "test_role" {
name = "test_role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"ec2.amazonaws.com"
]
},
"Action": [
"sts:AssumeRole"
]
}
]
}
EOF
}

resource "aws_flow_log" "test_flow_log" {
# log_group_name needs to exist before hand
# until we have a CloudWatch Log Group Resource
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we plan to release this without the CloudWatch Log Group resource, there should IMO be a warning/note in docs mentioning it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the docs to mention that it must exist already. I did see your Log Group PR too (thanks!), which will help when that gets completed

log_group_name = "tf-test-log-group"
iam_role_arn = "${aws_iam_role.test_role.arn}"
vpc_id = "${aws_vpc.default.id}"
traffic_type = "ALL"
}

resource "aws_flow_log" "test_flow_log_subnet" {
# log_group_name needs to exist before hand
# until we have a CloudWatch Log Group Resource
log_group_name = "%s"
iam_role_arn = "${aws_iam_role.test_role.arn}"
subnet_id = "${aws_subnet.test_subnet.id}"
traffic_type = "ALL"
}
`

var testAccFlowLogConfig_subnet = `
resource "aws_vpc" "default" {
cidr_block = "10.0.0.0/16"
tags {
Name = "tf-flow-log-test"
}
}

resource "aws_subnet" "test_subnet" {
vpc_id = "${aws_vpc.default.id}"
cidr_block = "10.0.1.0/24"

tags {
Name = "tf-flow-test"
}
}

resource "aws_iam_role" "test_role" {
name = "test_role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"ec2.amazonaws.com"
]
},
"Action": [
"sts:AssumeRole"
]
}
]
}
EOF
}

resource "aws_flow_log" "test_flow_log_subnet" {
# log_group_name needs to exist before hand
# until we have a CloudWatch Log Group Resource
log_group_name = "%s"
iam_role_arn = "${aws_iam_role.test_role.arn}"
subnet_id = "${aws_subnet.test_subnet.id}"
traffic_type = "ALL"
}
`
Loading