Merge pull request #6819 from hashicorp/f-aws-vpc-data-sources

provider/aws: data sources for AWS network planning
This commit is contained in:
Clint 2016-10-13 14:17:55 -05:00 committed by GitHub
commit 46ee2ef51a
35 changed files with 1583 additions and 10 deletions

View File

@ -0,0 +1,89 @@
package aws
import (
"fmt"
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/schema"
)
func dataSourceAwsAvailabilityZone() *schema.Resource {
return &schema.Resource{
Read: dataSourceAwsAvailabilityZoneRead,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"region": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"name_suffix": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"state": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
},
}
}
func dataSourceAwsAvailabilityZoneRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
req := &ec2.DescribeAvailabilityZonesInput{}
if name := d.Get("name"); name != "" {
req.ZoneNames = []*string{aws.String(name.(string))}
}
req.Filters = buildEC2AttributeFilterList(
map[string]string{
"state": d.Get("state").(string),
},
)
if len(req.Filters) == 0 {
// Don't send an empty filters list; the EC2 API won't accept it.
req.Filters = nil
}
log.Printf("[DEBUG] DescribeAvailabilityZones %s\n", req)
resp, err := conn.DescribeAvailabilityZones(req)
if err != nil {
return err
}
if resp == nil || len(resp.AvailabilityZones) == 0 {
return fmt.Errorf("no matching AZ found")
}
if len(resp.AvailabilityZones) > 1 {
return fmt.Errorf("multiple AZs matched; use additional constraints to reduce matches to a single AZ")
}
az := resp.AvailabilityZones[0]
// As a convenience when working with AZs generically, we expose
// the AZ suffix alone, without the region name.
// This can be used e.g. to create lookup tables by AZ letter that
// work regardless of region.
nameSuffix := (*az.ZoneName)[len(*az.RegionName):]
d.SetId(*az.ZoneName)
d.Set("id", az.ZoneName)
d.Set("name", az.ZoneName)
d.Set("name_suffix", nameSuffix)
d.Set("region", az.RegionName)
d.Set("state", az.State)
return nil
}

View File

@ -0,0 +1,57 @@
package aws
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccDataSourceAwsAvailabilityZone(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDataSourceAwsAvailabilityZoneConfig,
Check: resource.ComposeTestCheckFunc(
testAccDataSourceAwsAvailabilityZoneCheck("data.aws_availability_zone.by_name"),
),
},
},
})
}
func testAccDataSourceAwsAvailabilityZoneCheck(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("root module has no resource called %s", name)
}
attr := rs.Primary.Attributes
if attr["name"] != "us-west-2a" {
return fmt.Errorf("bad name %s", attr["name"])
}
if attr["name_suffix"] != "a" {
return fmt.Errorf("bad name_suffix %s", attr["name_suffix"])
}
if attr["region"] != "us-west-2" {
return fmt.Errorf("bad region %s", attr["region"])
}
return nil
}
}
const testAccDataSourceAwsAvailabilityZoneConfig = `
provider "aws" {
region = "us-west-2"
}
data "aws_availability_zone" "by_name" {
name = "us-west-2a"
}
`

View File

@ -0,0 +1,84 @@
package aws
import (
"fmt"
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/schema"
)
func dataSourceAwsRegion() *schema.Resource {
return &schema.Resource{
Read: dataSourceAwsRegionRead,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"current": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Computed: true,
},
"endpoint": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
},
}
}
func dataSourceAwsRegionRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
currentRegion := meta.(*AWSClient).region
req := &ec2.DescribeRegionsInput{}
req.RegionNames = make([]*string, 0, 2)
if name := d.Get("name").(string); name != "" {
req.RegionNames = append(req.RegionNames, aws.String(name))
}
if d.Get("current").(bool) {
req.RegionNames = append(req.RegionNames, aws.String(currentRegion))
}
req.Filters = buildEC2AttributeFilterList(
map[string]string{
"endpoint": d.Get("endpoint").(string),
},
)
if len(req.Filters) == 0 {
// Don't send an empty filters list; the EC2 API won't accept it.
req.Filters = nil
}
log.Printf("[DEBUG] DescribeRegions %s\n", req)
resp, err := conn.DescribeRegions(req)
if err != nil {
return err
}
if resp == nil || len(resp.Regions) == 0 {
return fmt.Errorf("no matching regions found")
}
if len(resp.Regions) > 1 {
return fmt.Errorf("multiple regions matched; use additional constraints to reduce matches to a single region")
}
region := resp.Regions[0]
d.SetId(*region.RegionName)
d.Set("id", region.RegionName)
d.Set("name", region.RegionName)
d.Set("endpoint", region.Endpoint)
d.Set("current", *region.RegionName == currentRegion)
return nil
}

View File

@ -0,0 +1,64 @@
package aws
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccDataSourceAwsRegion(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDataSourceAwsRegionConfig,
Check: resource.ComposeTestCheckFunc(
testAccDataSourceAwsRegionCheck("data.aws_region.by_name_current", "us-west-2", "true"),
testAccDataSourceAwsRegionCheck("data.aws_region.by_name_other", "us-west-1", "false"),
testAccDataSourceAwsRegionCheck("data.aws_region.by_current", "us-west-2", "true"),
),
},
},
})
}
func testAccDataSourceAwsRegionCheck(name, region, current string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("root module has no resource called %s", name)
}
attr := rs.Primary.Attributes
if attr["name"] != region {
return fmt.Errorf("bad name %s", attr["name"])
}
if attr["current"] != current {
return fmt.Errorf("bad current %s; want %s", attr["current"], current)
}
return nil
}
}
const testAccDataSourceAwsRegionConfig = `
provider "aws" {
region = "us-west-2"
}
data "aws_region" "by_name_current" {
name = "us-west-2"
}
data "aws_region" "by_name_other" {
name = "us-west-1"
}
data "aws_region" "by_current" {
current = true
}
`

View File

@ -0,0 +1,123 @@
package aws
import (
"fmt"
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/schema"
)
func dataSourceAwsSubnet() *schema.Resource {
return &schema.Resource{
Read: dataSourceAwsSubnetRead,
Schema: map[string]*schema.Schema{
"availability_zone": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"cidr_block": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"default_for_az": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Computed: true,
},
"filter": ec2CustomFiltersSchema(),
"id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"state": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"tags": tagsSchemaComputed(),
"vpc_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
},
}
}
func dataSourceAwsSubnetRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
req := &ec2.DescribeSubnetsInput{}
if id := d.Get("id"); id != "" {
req.SubnetIds = []*string{aws.String(id.(string))}
}
// We specify default_for_az as boolean, but EC2 filters want
// it to be serialized as a string. Note that setting it to
// "false" here does not actually filter by it *not* being
// the default, because Terraform can't distinguish between
// "false" and "not set".
defaultForAzStr := ""
if d.Get("default_for_az").(bool) {
defaultForAzStr = "true"
}
req.Filters = buildEC2AttributeFilterList(
map[string]string{
"availabilityZone": d.Get("availability_zone").(string),
"cidrBlock": d.Get("cidr_block").(string),
"defaultForAz": defaultForAzStr,
"state": d.Get("state").(string),
"vpc-id": d.Get("vpc_id").(string),
},
)
req.Filters = append(req.Filters, buildEC2TagFilterList(
tagsFromMap(d.Get("tags").(map[string]interface{})),
)...)
req.Filters = append(req.Filters, buildEC2CustomFilterList(
d.Get("filter").(*schema.Set),
)...)
if len(req.Filters) == 0 {
// Don't send an empty filters list; the EC2 API won't accept it.
req.Filters = nil
}
log.Printf("[DEBUG] DescribeSubnets %s\n", req)
resp, err := conn.DescribeSubnets(req)
if err != nil {
return err
}
if resp == nil || len(resp.Subnets) == 0 {
return fmt.Errorf("no matching subnet found")
}
if len(resp.Subnets) > 1 {
return fmt.Errorf("multiple subnets matched; use additional constraints to reduce matches to a single subnet")
}
subnet := resp.Subnets[0]
d.SetId(*subnet.SubnetId)
d.Set("id", subnet.SubnetId)
d.Set("vpc_id", subnet.VpcId)
d.Set("availability_zone", subnet.AvailabilityZone)
d.Set("cidr_block", subnet.CidrBlock)
d.Set("default_for_az", subnet.DefaultForAz)
d.Set("state", subnet.State)
d.Set("tags", tagsToMap(subnet.Tags))
return nil
}

View File

@ -0,0 +1,125 @@
package aws
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccDataSourceAwsSubnet(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDataSourceAwsSubnetConfig,
Check: resource.ComposeTestCheckFunc(
testAccDataSourceAwsSubnetCheck("data.aws_subnet.by_id"),
testAccDataSourceAwsSubnetCheck("data.aws_subnet.by_cidr"),
testAccDataSourceAwsSubnetCheck("data.aws_subnet.by_tag"),
testAccDataSourceAwsSubnetCheck("data.aws_subnet.by_vpc"),
testAccDataSourceAwsSubnetCheck("data.aws_subnet.by_filter"),
),
},
},
})
}
func testAccDataSourceAwsSubnetCheck(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("root module has no resource called %s", name)
}
vpcRs, ok := s.RootModule().Resources["aws_vpc.test"]
if !ok {
return fmt.Errorf("can't find aws_vpc.test in state")
}
subnetRs, ok := s.RootModule().Resources["aws_subnet.test"]
if !ok {
return fmt.Errorf("can't find aws_subnet.test in state")
}
attr := rs.Primary.Attributes
if attr["id"] != subnetRs.Primary.Attributes["id"] {
return fmt.Errorf(
"id is %s; want %s",
attr["id"],
subnetRs.Primary.Attributes["id"],
)
}
if attr["vpc_id"] != vpcRs.Primary.Attributes["id"] {
return fmt.Errorf(
"vpc_id is %s; want %s",
attr["vpc_id"],
vpcRs.Primary.Attributes["id"],
)
}
if attr["cidr_block"] != "172.16.123.0/24" {
return fmt.Errorf("bad cidr_block %s", attr["cidr_block"])
}
if attr["availability_zone"] != "us-west-2a" {
return fmt.Errorf("bad availability_zone %s", attr["availability_zone"])
}
if attr["tags.Name"] != "terraform-testacc-subnet-data-source" {
return fmt.Errorf("bad Name tag %s", attr["tags.Name"])
}
return nil
}
}
const testAccDataSourceAwsSubnetConfig = `
provider "aws" {
region = "us-west-2"
}
resource "aws_vpc" "test" {
cidr_block = "172.16.0.0/16"
tags {
Name = "terraform-testacc-subnet-data-source"
}
}
resource "aws_subnet" "test" {
vpc_id = "${aws_vpc.test.id}"
cidr_block = "172.16.123.0/24"
availability_zone = "us-west-2a"
tags {
Name = "terraform-testacc-subnet-data-source"
}
}
data "aws_subnet" "by_id" {
id = "${aws_subnet.test.id}"
}
data "aws_subnet" "by_cidr" {
cidr_block = "${aws_subnet.test.cidr_block}"
}
data "aws_subnet" "by_tag" {
tags {
Name = "${aws_subnet.test.tags["Name"]}"
}
}
data "aws_subnet" "by_vpc" {
vpc_id = "${aws_subnet.test.vpc_id}"
}
data "aws_subnet" "by_filter" {
filter {
name = "vpc-id"
values = ["${aws_subnet.test.vpc_id}"]
}
}
`

View File

@ -0,0 +1,121 @@
package aws
import (
"fmt"
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/schema"
)
func dataSourceAwsVpc() *schema.Resource {
return &schema.Resource{
Read: dataSourceAwsVpcRead,
Schema: map[string]*schema.Schema{
"cidr_block": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"dhcp_options_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"default": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Computed: true,
},
"filter": ec2CustomFiltersSchema(),
"id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"instance_tenancy": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"state": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"tags": tagsSchemaComputed(),
},
}
}
func dataSourceAwsVpcRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
req := &ec2.DescribeVpcsInput{}
if id := d.Get("id"); id != "" {
req.VpcIds = []*string{aws.String(id.(string))}
}
// We specify "default" as boolean, but EC2 filters want
// it to be serialized as a string. Note that setting it to
// "false" here does not actually filter by it *not* being
// the default, because Terraform can't distinguish between
// "false" and "not set".
isDefaultStr := ""
if d.Get("default").(bool) {
isDefaultStr = "true"
}
req.Filters = buildEC2AttributeFilterList(
map[string]string{
"cidr": d.Get("cidr_block").(string),
"dhcp-options-id": d.Get("dhcp_options_id").(string),
"isDefault": isDefaultStr,
"state": d.Get("state").(string),
},
)
req.Filters = append(req.Filters, buildEC2TagFilterList(
tagsFromMap(d.Get("tags").(map[string]interface{})),
)...)
req.Filters = append(req.Filters, buildEC2CustomFilterList(
d.Get("filter").(*schema.Set),
)...)
if len(req.Filters) == 0 {
// Don't send an empty filters list; the EC2 API won't accept it.
req.Filters = nil
}
log.Printf("[DEBUG] DescribeVpcs %s\n", req)
resp, err := conn.DescribeVpcs(req)
if err != nil {
return err
}
if resp == nil || len(resp.Vpcs) == 0 {
return fmt.Errorf("no matching VPC found")
}
if len(resp.Vpcs) > 1 {
return fmt.Errorf("multiple VPCs matched; use additional constraints to reduce matches to a single VPC")
}
vpc := resp.Vpcs[0]
d.SetId(*vpc.VpcId)
d.Set("id", vpc.VpcId)
d.Set("cidr_block", vpc.CidrBlock)
d.Set("dhcp_options_id", vpc.DhcpOptionsId)
d.Set("instance_tenancy", vpc.InstanceTenancy)
d.Set("default", vpc.IsDefault)
d.Set("state", vpc.State)
d.Set("tags", tagsToMap(vpc.Tags))
return nil
}

View File

@ -0,0 +1,95 @@
package aws
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccDataSourceAwsVpc(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDataSourceAwsVpcConfig,
Check: resource.ComposeTestCheckFunc(
testAccDataSourceAwsVpcCheck("data.aws_vpc.by_id"),
testAccDataSourceAwsVpcCheck("data.aws_vpc.by_cidr"),
testAccDataSourceAwsVpcCheck("data.aws_vpc.by_tag"),
testAccDataSourceAwsVpcCheck("data.aws_vpc.by_filter"),
),
},
},
})
}
func testAccDataSourceAwsVpcCheck(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("root module has no resource called %s", name)
}
vpcRs, ok := s.RootModule().Resources["aws_vpc.test"]
if !ok {
return fmt.Errorf("can't find aws_vpc.test in state")
}
attr := rs.Primary.Attributes
if attr["id"] != vpcRs.Primary.Attributes["id"] {
return fmt.Errorf(
"id is %s; want %s",
attr["id"],
vpcRs.Primary.Attributes["id"],
)
}
if attr["cidr_block"] != "172.16.0.0/16" {
return fmt.Errorf("bad cidr_block %s", attr["cidr_block"])
}
if attr["tags.Name"] != "terraform-testacc-vpc-data-source" {
return fmt.Errorf("bad Name tag %s", attr["tags.Name"])
}
return nil
}
}
const testAccDataSourceAwsVpcConfig = `
provider "aws" {
region = "us-west-2"
}
resource "aws_vpc" "test" {
cidr_block = "172.16.0.0/16"
tags {
Name = "terraform-testacc-vpc-data-source"
}
}
data "aws_vpc" "by_id" {
id = "${aws_vpc.test.id}"
}
data "aws_vpc" "by_cidr" {
cidr_block = "${aws_vpc.test.cidr_block}"
}
data "aws_vpc" "by_tag" {
tags {
Name = "${aws_vpc.test.tags["Name"]}"
}
}
data "aws_vpc" "by_filter" {
filter {
name = "cidr"
values = ["${aws_vpc.test.cidr_block}"]
}
}
`

View File

@ -0,0 +1,153 @@
package aws
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/schema"
)
// buildEC2AttributeFilterList takes a flat map of scalar attributes (most
// likely values extracted from a *schema.ResourceData on an EC2-querying
// data source) and produces a []*ec2.Filter representing an exact match
// for each of the given non-empty attributes.
//
// The keys of the given attributes map are the attribute names expected
// by the EC2 API, which are usually either in camelcase or with dash-separated
// words. We conventionally map these to underscore-separated identifiers
// with the same words when presenting these as data source query attributes
// in Terraform.
//
// It's the callers responsibility to transform any non-string values into
// the appropriate string serialization required by the AWS API when
// encoding the given filter. Any attributes given with empty string values
// are ignored, assuming that the user wishes to leave that attribute
// unconstrained while filtering.
//
// The purpose of this function is to create values to pass in
// for the "Filters" attribute on most of the "Describe..." API functions in
// the EC2 API, to aid in the implementation of Terraform data sources that
// retrieve data about EC2 objects.
func buildEC2AttributeFilterList(attrs map[string]string) []*ec2.Filter {
filters := make([]*ec2.Filter, 0, len(attrs))
for filterName, value := range attrs {
if value == "" {
continue
}
filters = append(filters, &ec2.Filter{
Name: aws.String(filterName),
Values: []*string{aws.String(value)},
})
}
return filters
}
// buildEC2TagFilterList takes a []*ec2.Tag and produces a []*ec2.Filter that
// represents exact matches for all of the tag key/value pairs given in
// the tag set.
//
// The purpose of this function is to create values to pass in for
// the "Filters" attribute on most of the "Describe..." API functions
// in the EC2 API, to implement filtering by tag values e.g. in Terraform
// data sources that retrieve data about EC2 objects.
//
// It is conventional for an EC2 data source to include an attribute called
// "tags" which conforms to the schema returned by the tagsSchema() function.
// The value of this can then be converted to a tags slice using tagsFromMap,
// and the result finally passed in to this function.
//
// In Terraform configuration this would then look like this, to constrain
// results by name:
//
// tags {
// Name = "my-awesome-subnet"
// }
func buildEC2TagFilterList(tags []*ec2.Tag) []*ec2.Filter {
filters := make([]*ec2.Filter, len(tags))
for i, tag := range tags {
filters[i] = &ec2.Filter{
Name: aws.String(fmt.Sprintf("tag:%s", *tag.Key)),
Values: []*string{tag.Value},
}
}
return filters
}
// ec2CustomFiltersSchema returns a *schema.Schema that represents
// a set of custom filtering criteria that a user can specify as input
// to a data source that wraps one of the many "Describe..." API calls
// in the EC2 API.
//
// It is conventional for an attribute of this type to be included
// as a top-level attribute called "filter". This is the "catch all" for
// filter combinations that are not possible to express using scalar
// attributes or tags. In Terraform configuration, the custom filter blocks
// then look like this:
//
// filter {
// name = "availabilityZone"
// values = ["us-west-2a", "us-west-2b"]
// }
func ec2CustomFiltersSchema() *schema.Schema {
return &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"values": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
},
}
}
// buildEC2CustomFilterList takes the set value extracted from a schema
// attribute conforming to the schema returned by ec2CustomFiltersSchema,
// and transforms it into a []*ec2.Filter representing the same filter
// expressions which is ready to pass into the "Filters" attribute on most
// of the "Describe..." functions in the EC2 API.
//
// This function is intended only to be used in conjunction with
// ec2CustomFitlersSchema. See the docs on that function for more details
// on the configuration pattern this is intended to support.
func buildEC2CustomFilterList(filterSet *schema.Set) []*ec2.Filter {
if filterSet == nil {
return []*ec2.Filter{}
}
customFilters := filterSet.List()
filters := make([]*ec2.Filter, len(customFilters))
for filterIdx, customFilterI := range customFilters {
customFilterMapI := customFilterI.(map[string]interface{})
name := customFilterMapI["name"].(string)
valuesI := customFilterMapI["values"].(*schema.Set).List()
values := make([]*string, len(valuesI))
for valueIdx, valueI := range valuesI {
values[valueIdx] = aws.String(valueI.(string))
}
filters[filterIdx] = &ec2.Filter{
Name: &name,
Values: values,
}
}
return filters
}

View File

@ -0,0 +1,158 @@
package aws
import (
"reflect"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/schema"
)
func TestBuildEC2AttributeFilterList(t *testing.T) {
type TestCase struct {
Attrs map[string]string
Expected []*ec2.Filter
}
testCases := []TestCase{
{
map[string]string{
"foo": "bar",
"baz": "boo",
},
[]*ec2.Filter{
{
Name: aws.String("foo"),
Values: []*string{aws.String("bar")},
},
{
Name: aws.String("baz"),
Values: []*string{aws.String("boo")},
},
},
},
{
map[string]string{
"foo": "bar",
"baz": "",
},
[]*ec2.Filter{
{
Name: aws.String("foo"),
Values: []*string{aws.String("bar")},
},
},
},
}
for i, testCase := range testCases {
result := buildEC2AttributeFilterList(testCase.Attrs)
if !reflect.DeepEqual(result, testCase.Expected) {
t.Errorf(
"test case %d: got %#v, but want %#v",
i, result, testCase.Expected,
)
}
}
}
func TestBuildEC2TagFilterList(t *testing.T) {
type TestCase struct {
Tags []*ec2.Tag
Expected []*ec2.Filter
}
testCases := []TestCase{
{
[]*ec2.Tag{
{
Key: aws.String("foo"),
Value: aws.String("bar"),
},
{
Key: aws.String("baz"),
Value: aws.String("boo"),
},
},
[]*ec2.Filter{
{
Name: aws.String("tag:foo"),
Values: []*string{aws.String("bar")},
},
{
Name: aws.String("tag:baz"),
Values: []*string{aws.String("boo")},
},
},
},
}
for i, testCase := range testCases {
result := buildEC2TagFilterList(testCase.Tags)
if !reflect.DeepEqual(result, testCase.Expected) {
t.Errorf(
"test case %d: got %#v, but want %#v",
i, result, testCase.Expected,
)
}
}
}
func TestBuildEC2CustomFilterList(t *testing.T) {
// We need to get a set with the appropriate hash function,
// so we'll use the schema to help us produce what would
// be produced in the normal case.
filtersSchema := ec2CustomFiltersSchema()
// The zero value of this schema will be an interface{}
// referring to a new, empty *schema.Set with the
// appropriate hash function configured.
filters := filtersSchema.ZeroValue().(*schema.Set)
// We also need an appropriately-configured set for
// the list of values.
valuesSchema := filtersSchema.Elem.(*schema.Resource).Schema["values"]
valuesSet := func(vals ...string) *schema.Set {
ret := valuesSchema.ZeroValue().(*schema.Set)
for _, val := range vals {
ret.Add(val)
}
return ret
}
filters.Add(map[string]interface{}{
"name": "foo",
"values": valuesSet("bar", "baz"),
})
filters.Add(map[string]interface{}{
"name": "pizza",
"values": valuesSet("cheese"),
})
expected := []*ec2.Filter{
// These are produced in the deterministic order guaranteed
// by schema.Set.List(), which happens to produce them in
// the following order for our current input. If this test
// evolves with different input data in future then they
// will likely be emitted in a different order, which is fine.
{
Name: aws.String("pizza"),
Values: []*string{aws.String("cheese")},
},
{
Name: aws.String("foo"),
Values: []*string{aws.String("bar"), aws.String("baz")},
},
}
result := buildEC2CustomFilterList(filters)
if !reflect.DeepEqual(result, expected) {
t.Errorf(
"got %#v, but want %#v",
result, expected,
)
}
}

View File

@ -144,6 +144,7 @@ func Provider() terraform.ResourceProvider {
DataSourcesMap: map[string]*schema.Resource{
"aws_ami": dataSourceAwsAmi(),
"aws_availability_zone": dataSourceAwsAvailabilityZone(),
"aws_availability_zones": dataSourceAwsAvailabilityZones(),
"aws_billing_service_account": dataSourceAwsBillingServiceAccount(),
"aws_caller_identity": dataSourceAwsCallerIdentity(),
@ -153,7 +154,10 @@ func Provider() terraform.ResourceProvider {
"aws_iam_policy_document": dataSourceAwsIamPolicyDocument(),
"aws_ip_ranges": dataSourceAwsIPRanges(),
"aws_redshift_service_account": dataSourceAwsRedshiftServiceAccount(),
"aws_region": dataSourceAwsRegion(),
"aws_s3_bucket_object": dataSourceAwsS3BucketObject(),
"aws_subnet": dataSourceAwsSubnet(),
"aws_vpc": dataSourceAwsVpc(),
},
ResourcesMap: map[string]*schema.Resource{

View File

@ -22,6 +22,14 @@ func tagsSchema() *schema.Schema {
}
}
func tagsSchemaComputed() *schema.Schema {
return &schema.Schema{
Type: schema.TypeMap,
Optional: true,
Computed: true,
}
}
func setElbV2Tags(conn *elbv2.ELBV2, d *schema.ResourceData) error {
if d.HasChange("tags") {
oraw, nraw := d.GetChange("tags")

3
examples/aws-networking/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
terraform.tfstate
terraform.tfstate.backup
.terraform/*

View File

@ -0,0 +1,11 @@
# AWS Networking Example
This example creates AWS VPC resources, making a VPC in each of two regions and
then two subnets in each VPC in two different availability zones.
This example also demonstrates the use of modules to create several copies of
the same resource set with different arguments. The child modules in this
directory are:
* `region`: container module for all of the network resources within a region. This is instantiated once per region.
* `subnet`: represents a subnet within a given availability zone. This is instantiated twice per region, using the first two availability zones supported within the target AWS account.

View File

@ -0,0 +1,27 @@
variable "region_numbers" {
default = {
us-east-1 = 1
us-west-1 = 2
us-west-2 = 3
eu-west-1 = 4
}
}
variable "az_numbers" {
default = {
a = 1
b = 2
c = 3
d = 4
e = 5
f = 6
g = 7
h = 8
i = 9
j = 10
k = 11
l = 12
m = 13
n = 14
}
}

View File

@ -0,0 +1 @@
../numbering/variables.tf

View File

@ -0,0 +1,11 @@
output "vpc_id" {
value = "${aws_vpc.main.id}"
}
output "primary_subnet_id" {
value = "${module.primary_subnet.subnet_id}"
}
output "secondary_subnet_id" {
value = "${module.secondary_subnet.subnet_id}"
}

View File

@ -0,0 +1,25 @@
resource "aws_security_group" "region" {
name = "region"
description = "Open access within this region"
vpc_id = "${aws_vpc.main.id}"
ingress {
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["${aws_vpc.main.cidr_block}"]
}
}
resource "aws_security_group" "internal-all" {
name = "internal-all"
description = "Open access within the full internal network"
vpc_id = "${aws_vpc.main.id}"
ingress {
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["${var.base_cidr_block}"]
}
}

View File

@ -0,0 +1,14 @@
data "aws_availability_zones" "all" {
}
module "primary_subnet" {
source = "../subnet"
vpc_id = "${aws_vpc.main.id}"
availability_zone = "${data.aws_availability_zones.all.names[0]}"
}
module "secondary_subnet" {
source = "../subnet"
vpc_id = "${aws_vpc.main.id}"
availability_zone = "${data.aws_availability_zones.all.names[1]}"
}

View File

@ -0,0 +1,9 @@
variable "region" {
description = "The name of the AWS region to set up a network within"
}
variable "base_cidr_block" {}
provider "aws" {
region = "${var.region}"
}

View File

@ -0,0 +1,7 @@
resource "aws_vpc" "main" {
cidr_block = "${cidrsubnet(var.base_cidr_block, 4, lookup(var.region_numbers, var.region))}"
}
resource "aws_internet_gateway" "main" {
vpc_id = "${aws_vpc.main.id}"
}

View File

@ -0,0 +1,11 @@
module "us-east-1" {
source = "./region"
region = "us-east-1"
base_cidr_block = "${var.base_cidr_block}"
}
module "us-west-2" {
source = "./region"
region = "us-west-2"
base_cidr_block = "${var.base_cidr_block}"
}

View File

@ -0,0 +1 @@
../numbering/variables.tf

View File

@ -0,0 +1,3 @@
output "subnet_id" {
value = "${aws_subnet.main.id}"
}

View File

@ -0,0 +1,12 @@
resource "aws_security_group" "az" {
name = "az-${data.aws_availability_zone.target.name}"
description = "Open access within the AZ ${data.aws_availability_zone.target.name}"
vpc_id = "${var.vpc_id}"
ingress {
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["${aws_subnet.main.cidr_block}"]
}
}

View File

@ -0,0 +1,13 @@
resource "aws_subnet" "main" {
cidr_block = "${cidrsubnet(data.aws_vpc.target.cidr_block, 4, lookup(var.az_numbers, data.aws_availability_zone.target.name_suffix))}"
vpc_id = "${var.vpc_id}"
}
resource "aws_route_table" "main" {
vpc_id = "${var.vpc_id}"
}
resource "aws_route_table_association" "main" {
subnet_id = "${aws_subnet.main.id}"
route_table_id = "${aws_route_table.main.id}"
}

View File

@ -0,0 +1,11 @@
variable "vpc_id" {}
variable "availability_zone" {}
data "aws_availability_zone" "target" {
name = "${var.availability_zone}"
}
data "aws_vpc" "target" {
id = "${var.vpc_id}"
}

View File

@ -0,0 +1,3 @@
variable "base_cidr_block" {
default = "10.0.0.0/12"
}

View File

@ -47,14 +47,17 @@ func (n *EvalReadDataDiff) Eval(ctx EvalContext) (interface{}, error) {
diff = new(InstanceDiff)
}
// id is always computed, because we're always "creating a new resource"
// if id isn't explicitly set then it's always computed, because we're
// always "creating a new resource".
diff.init()
diff.SetAttribute("id", &ResourceAttrDiff{
Old: "",
NewComputed: true,
RequiresNew: true,
Type: DiffAttrOutput,
})
if _, ok := diff.Attributes["id"]; !ok {
diff.SetAttribute("id", &ResourceAttrDiff{
Old: "",
NewComputed: true,
RequiresNew: true,
Type: DiffAttrOutput,
})
}
}
err = ctx.Hook(func(h Hook) (HookAction, error) {

View File

@ -0,0 +1,98 @@
---
layout: "aws"
page_title: "AWS: aws_availability_zone"
sidebar_current: "docs-aws-datasource-availability-zone"
description: |-
Provides details about a specific availability zone
---
# aws\_availability\_zone
`aws_availability_zone` provides details about a specific availablity zone (AZ)
in the current region.
This can be used both to validate an availability zone given in a variable
and to split the AZ name into its component parts of an AWS region and an
AZ identifier letter. The latter may be useful e.g. for implementing a
consistent subnet numbering scheme across several regions by mapping both
the region and the subnet letter to network numbers.
This is different from the `aws_availability_zones` (plural) data source,
which provides a list of the available zones.
## Example Usage
The following example shows how this data source might be used to derive
VPC and subnet CIDR prefixes systematically for an availability zone.
```
variable "region_number" {
# Arbitrary mapping of region name to number to use in
# a VPC's CIDR prefix.
default = {
us-east-1 = 1
us-west-1 = 2
us-west-2 = 3
eu-central-1 = 4
ap-northeast-1 = 5
}
}
variable "az_number" {
# Assign a number to each AZ letter used in our configuration
default = {
a = 1
b = 2
c = 3
d = 4
e = 5
f = 6
}
}
# Retrieve the AZ where we want to create network resources
# This must be in the region selected on the AWS provider.
data "aws_availability_zone" "example" {
name = "eu-central-1a"
}
# Create a VPC for the region associated with the AZ
resource "aws_vpc" "example" {
cidr_block = "${cidrsubnet("10.0.0.0/8", 4, var.region_number[data.aws_availability_zone.example.region])}"
}
# Create a subnet for the AZ within the regional VPC
resource "aws_subnet" "example" {
vpc_id = "${aws_vpc.example.id}"
cidr_block = "${cidrsubnet(aws_vpc.example.cidr_block, 4, var.az_number[data.aws_availability_zone.name_suffix])}"
}
```
## Argument Reference
The arguments of this data source act as filters for querying the available
availability zones. The given filters must match exactly one availability
zone whose data will be exported as attributes.
* `name` - (Optional) The full name of the availability zone to select.
* `state` - (Optional) A specific availability zone state to require. May
be any of `"available"`, `"information"`, `"impaired"` or `"available"`.
All reasonable uses of this data source will specify `name`, since `state`
alone would match a single AZ only in a region that itself has only one AZ.
## Attributes Reference
The following attributes are exported:
* `name` - The name of the selected availability zone.
* `region` - The region where the selected availability zone resides.
This is always the region selected on the provider, since this data source
searches only within that region.
* `name_suffix` - The part of the AZ name that appears after the region name,
uniquely identifying the AZ within its region.
* `state` - The current state of the AZ.

View File

@ -12,6 +12,9 @@ The Availability Zones data source allows access to the list of AWS
Availability Zones which can be accessed by an AWS account within the region
configured in the provider.
This is different from the `aws_availability_zone` (singular) data source,
which provides some details about a specific availability zone.
## Example Usage
```

View File

@ -0,0 +1,54 @@
---
layout: "aws"
page_title: "AWS: aws_region"
sidebar_current: "docs-aws-datasource-region"
description: |-
Provides details about a specific service region
---
# aws\_region
`aws_region` provides details about a specific AWS region.
As well as validating a given region name (and optionally obtaining its
endpoint) this resource can be used to discover the name of the region
configured within the provider. The latter can be useful in a child module
which is inheriting an AWS provider configuration from its parent module.
## Example Usage
The following example shows how the resource might be used to obtain
the name of the AWS region configured on the provider.
```
data "aws_region" "current" {
current = true
}
```
## Argument Reference
The arguments of this data source act as filters for querying the available
regions. The given filters must match exactly one region whose data will be
exported as attributes.
* `name` - (Optional) The full name of the region to select.
* `current` - (Optional) Set to `true` to match only the region configured
in the provider. (It is not meaningful to set this to `false`.)
* `endpoint` - (Optional) The endpoint of the region to select.
At least one of the above attributes should be provided to ensure that only
one region is matched.
## Attributes Reference
The following attributes are exported:
* `name` - The name of the selected region.
* `current` - `true` if the selected region is the one configured on the
provider, or `false` otherwise.
* `endpoint` - The endpoint for the selected region.

View File

@ -0,0 +1,81 @@
---
layout: "aws"
page_title: "AWS: aws_subnet"
sidebar_current: "docs-aws-datasource-subnet"
description: |-
Provides details about a specific VPC subnet
---
# aws\_subnet
`aws_subnet` provides details about a specific VPC subnet.
This resource can prove useful when a module accepts a subnet id as
an input variable and needs to, for example, determine the id of the
VPC that the subnet belongs to.
## Example Usage
The following example shows how one might accept a subnet id as a variable
and use this data source to obtain the data necessary to create a security
group that allows connections from hosts in that subnet.
```
variable "subnet_id" {}
data "aws_subnet" "selected" {
id = "${var.subnet_id}"
}
resource "aws_security_group" "subnet" {
vpc_id = "${aws_subnet.selected.vpc_id}"
ingress {
cidr_blocks = ["${aws_subnet.selected.cidr_block}"]
from_port = 80
to_port = 80
protocol = "tcp"
}
}
```
## Argument Reference
The arguments of this data source act as filters for querying the available
subnets in the current region. The given filters must match exactly one
subnet whose data will be exported as attributes.
* `availability_zone` - (Optional) The availability zone where the
subnet must reside.
* `cidr_block` - (Optional) The cidr block of the desired subnet.
* `default_for_az` - (Optional) Boolean constraint for whether the desired
subnet must be the default subnet for its associated availability zone.
* `filter` - (Optional) Custom filter block as described below.
* `id` - (Optional) The id of the specific subnet to retrieve.
* `state` - (Optional) The state that the desired subnet must have.
* `tags` - (Optional) A mapping of tags, each pair of which must exactly match
a pair on the desired subnet.
* `vpc_id` - (Optional) The id of the VPC that the desired subnet belongs to.
More complex filters can be expressed using one or more `filter` sub-blocks,
which take the following arguments:
* `name` - (Required) The name of the field to filter by, as defined by
[the underlying AWS API](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSubnets.html).
* `values` - (Required) Set of values that are accepted for the given field.
A subnet will be selected if any one of the given values matches.
## Attributes Reference
All of the argument attributes except `filter` blocks are also exported as
result attributes. This data source will complete the data by populating
any fields that are not included in the configuration with the data for
the selected subnet.

View File

@ -0,0 +1,79 @@
---
layout: "aws"
page_title: "AWS: aws_vpc"
sidebar_current: "docs-aws-datasource-vpc"
description: |-
Provides details about a specific VPC
---
# aws\_vpc
`aws_vpc` provides details about a specific VPC.
This resource can prove useful when a module accepts a vpc id as
an input variable and needs to, for example, determine the CIDR block of that
VPC.
## Example Usage
The following example shows how one might accept a VPC id as a variable
and use this data source to obtain the data necessary to create a subnet
within it.
```
variable "vpc_id" {}
data "aws_vpc" "selected" {
id = "${var.vpc_id}"
}
resource "aws_subnet" "example" {
vpc_id = "${aws_vpc.selected.id}"
availability_zone = "us-west-2a"
cidr_block = "${cidrsubnet(aws_vpc.selected.cidr_block, 4, 1)}"
}
```
## Argument Reference
The arguments of this data source act as filters for querying the available
VPCs in the current region. The given filters must match exactly one
VPC whose data will be exported as attributes.
* `cidr_block` - (Optional) The cidr block of the desired VPC.
* `dhcp_options_id` - (Optional) The DHCP options id of the desired VPC.
* `default` - (Optional) Boolean constraint on whether the desired VPC is
the default VPC for the region.
* `filter` - (Optional) Custom filter block as described below.
* `id` - (Optional) The id of the specific VPC to retrieve.
* `state` - (Optional) The current state of the desired VPC.
Can be either `"pending"` or `"available"`.
* `tags` - (Optional) A mapping of tags, each pair of which must exactly match
a pair on the desired VPC.
More complex filters can be expressed using one or more `filter` sub-blocks,
which take the following arguments:
* `name` - (Required) The name of the field to filter by, as defined by
[the underlying AWS API](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpcs.html).
* `values` - (Required) Set of values that are accepted for the given field.
A VPC will be selected if any one of the given values matches.
## Attributes Reference
All of the argument attributes except `filter` blocks are also exported as
result attributes. This data source will complete the data by populating
any fields that are not included in the configuration with the data for
the selected VPC.
The following attribute is additionally exported:
* `instance_tenancy` - The allowed tenancy of instances launched into the
selected VPC. May be any of `"default"`, `"dedicated"`, or `"host"`.

View File

@ -17,6 +17,9 @@
<li<%= sidebar_current("docs-aws-datasource-ami") %>>
<a href="/docs/providers/aws/d/ami.html">aws_ami</a>
</li>
<li<%= sidebar_current("docs-aws-datasource-availability-zone") %>>
<a href="/docs/providers/aws/d/availability_zone.html">aws_availability_zone</a>
</li>
<li<%= sidebar_current("docs-aws-datasource-availability-zones") %>>
<a href="/docs/providers/aws/d/availability_zones.html">aws_availability_zones</a>
</li>
@ -38,12 +41,21 @@
<li<%= sidebar_current("docs-aws-datasource-ip_ranges") %>>
<a href="/docs/providers/aws/d/ip_ranges.html">aws_ip_ranges</a>
</li>
<li<%= sidebar_current("docs-aws-datasource-redshift-service-account") %>>
<a href="/docs/providers/aws/d/redshift_service_account.html">aws_redshift_service_account</a>
</li>
<li<%= sidebar_current("docs-aws-datasource-redshift-service-account") %>>
<a href="/docs/providers/aws/d/redshift_service_account.html">aws_redshift_service_account</a>
</li>
<li<%= sidebar_current("docs-aws-datasource-region") %>>
<a href="/docs/providers/aws/d/region.html">aws_region</a>
</li>
<li<%= sidebar_current("docs-aws-datasource-s3-bucket-object") %>>
<a href="/docs/providers/aws/d/s3_bucket_object.html">aws_s3_bucket_object</a>
</li>
<li<%= sidebar_current("docs-aws-datasource-subnet") %>>
<a href="/docs/providers/aws/d/subnet.html">aws_subnet</a>
</li>
<li<%= sidebar_current("docs-aws-datasource-vpc") %>>
<a href="/docs/providers/aws/d/vpc.html">aws_vpc</a>
</li>
</ul>
</li>