Merge remote-tracking branch 'upstream/master' into b-fix-aws-subnet-map-public-change

* upstream/master: (295 commits)
  Update CHANGELOG.md
  provider/aws: Allow DB Parameter group to change in RDS
  return error if failed to set tags on Route 53 zone
  core: [tests] fix order dependent test
  Fix hashcode for ASG test
  provider/aws: Fix issue with tainted ASG groups failing to re-create
  Don't error when reading s3 bucket with no tags
  Avoid panics when DBName is not set
  Add floating IP association in aceptance tests
  Use env var OS_POOL_NAME as default for pool attribute
  providers/heroku: Add heroku-postgres to example
  docs: resource addressing
  providers/heroku: Document environment variables
  providers/heroku: Add region to example
  Bugfix on floating IP assignment
  Update CHANGELOG.md
  update CHANGELOG
  website: note on docker
  core: formalize resource addressing
  core: fill out context tests for targeted ops
  ...
This commit is contained in:
Clint Shryock 2015-04-01 16:24:38 -05:00
commit 85c0910165
231 changed files with 15674 additions and 610 deletions

View File

@ -6,20 +6,32 @@ BACKWARDS INCOMPATIBILITIES:
the `remote` command: `terraform remote push` and `terraform remote pull`. the `remote` command: `terraform remote push` and `terraform remote pull`.
The old `remote` functionality is now at `terraform remote config`. This The old `remote` functionality is now at `terraform remote config`. This
consolidates all remote state management under one command. consolidates all remote state management under one command.
* Period-prefixed configuration files are now ignored. This might break
existing Terraform configurations if you had period-prefixed files.
FEATURES: FEATURES:
* **New provider: `dme` (DNSMadeEasy)** [GH-855] * **New provider: `dme` (DNSMadeEasy)** [GH-855]
* **New provider: `docker` (Docker)** - Manage container lifecycle
using the standard Docker API. [GH-855]
* **New provider: `openstack` (OpenStack)** - Interact with the many resources
provided by OpenStack. [GH-924]
* **New command: `taint`** - Manually mark a resource as tainted, causing * **New command: `taint`** - Manually mark a resource as tainted, causing
a destroy and recreate on the next plan/apply. a destroy and recreate on the next plan/apply.
* **New resource: `aws_vpn_gateway`** [GH-1137]
* **New resource: `aws_elastic_network_interfaces`** [GH-1149]
* **Self-variables** can be used to reference the current resource's * **Self-variables** can be used to reference the current resource's
attributes within a provisioner. Ex. `${self.private_ip_address}` [GH-1033] attributes within a provisioner. Ex. `${self.private_ip_address}` [GH-1033]
* **Continous state** saving during `terraform apply`. The state file is * **Continuous state** saving during `terraform apply`. The state file is
continously updated as apply is running, meaning that the state is continuously updated as apply is running, meaning that the state is
less likely to become corrupt in a catastrophic case: terraform panic less likely to become corrupt in a catastrophic case: terraform panic
or system killing Terraform. or system killing Terraform.
* **Math operations** in interpolations. You can now do things like * **Math operations** in interpolations. You can now do things like
`${count.index+1}`. [GH-1068] `${count.index+1}`. [GH-1068]
* **New AWS SDK:** Move to `aws-sdk-go` (hashicorp/aws-sdk-go),
a fork of the offical `awslabs` repo. We forked for stability while
`awslabs` refactored the library, and will move back to the officially
supported version in the next release.
IMPROVEMENTS: IMPROVEMENTS:
@ -31,10 +43,23 @@ IMPROVEMENTS:
* **New config function: `split`** - Split a value based on a delimiter. * **New config function: `split`** - Split a value based on a delimiter.
This is useful for faking lists as parameters to modules. This is useful for faking lists as parameters to modules.
* **New resource: `digitalocean_ssh_key`** [GH-1074] * **New resource: `digitalocean_ssh_key`** [GH-1074]
* config: Expand `~` with homedir in `file()` paths [GH-1338]
* core: The serial of the state is only updated if there is an actual * core: The serial of the state is only updated if there is an actual
change. This will lower the amount of state changing on things change. This will lower the amount of state changing on things
like refresh. like refresh.
* core: Autoload `terraform.tfvars.json` as well as `terraform.tfvars` [GH-1030] * core: Autoload `terraform.tfvars.json` as well as `terraform.tfvars` [GH-1030]
* core: `.tf` files that start with a period are now ignored. [GH-1227]
* command/remote-config: After enabling remote state, a `pull` is
automatically done initially.
* providers/google: Add `size` option to disk blocks for instances. [GH-1284]
* providers/aws: Improve support for tagging resources.
* providers/aws: Add a short syntax for Route 53 Record names, e.g.
`www` instead of `www.example.com`.
* providers/aws: Improve dependency violation error handling, when deleting
Internet Gateways or Auto Scaling groups [GH-1325].
* provider/aws: Add non-destructive updates to AWS RDS. You can now upgrade
`egine_version`, `parameter_group_name`, and `multi_az` without forcing
a new database to be created.[GH-1341]
BUG FIXES: BUG FIXES:
@ -47,13 +72,31 @@ BUG FIXES:
a computed attribute was used as part of a set parameter. [GH-1073] a computed attribute was used as part of a set parameter. [GH-1073]
* core: Fix edge case where state containing both "resource" and * core: Fix edge case where state containing both "resource" and
"resource.0" would ignore the latter completely. [GH-1086] "resource.0" would ignore the latter completely. [GH-1086]
* core: Modules with a source of a relative file path moving up
directories work properly, i.e. "../a" [GH-1232]
* providers/aws: manually deleted VPC removes it from the state * providers/aws: manually deleted VPC removes it from the state
* providers/aws: `source_dest_check` regression fixed (now works). [GH-1020] * providers/aws: `source_dest_check` regression fixed (now works). [GH-1020]
* providers/aws: Longer wait times for DB instances. * providers/aws: Longer wait times for DB instances.
* providers/aws: Longer wait times for route53 records (30 mins). [GH-1164] * providers/aws: Longer wait times for route53 records (30 mins). [GH-1164]
* providers/aws: Fix support for TXT records in Route 53. [GH-1213]
* providers/aws: Fix support for wildcard records in Route 53. [GH-1222]
* providers/aws: Fix issue with ignoring the 'self' attribute of a
Security Group rule. [GH-1223]
* providers/aws: Fix issue with `sql_mode` in RDS parameter group always
causing an update. [GH-1225]
* providers/aws: Fix dependency violation with subnets and security groups
[GH-1252]
* providers/aws: Fix issue with refreshing `db_subnet_groups` causing an error
instead of updating state [GH-1254]
* providers/aws: Prevent empty string to be used as default
`health_check_type` [GH-1052]
* providers/aws: Add tags on AWS IG creation, not just on update [GH-1176]
* providers/digitalocean: Waits until droplet is ready to be destroyed [GH-1057] * providers/digitalocean: Waits until droplet is ready to be destroyed [GH-1057]
* providers/digitalocean: More lenient about 404's while waiting [GH-1062] * providers/digitalocean: More lenient about 404's while waiting [GH-1062]
* providers/digitalocean: FQDN for domain records in CNAME, MX, NS, etc.
Also fixes invalid updates in plans. [GH-863]
* providers/google: Network data in state was not being stored. [GH-1095] * providers/google: Network data in state was not being stored. [GH-1095]
* providers/heroku: Fix panic when config vars block was empty. [GH-1211]
PLUGIN CHANGES: PLUGIN CHANGES:
@ -80,7 +123,7 @@ IMPROVEMENTS:
* provider/aws: The `aws_db_instance` resource no longer requires both * provider/aws: The `aws_db_instance` resource no longer requires both
`final_snapshot_identifier` and `skip_final_snapshot`; the presence or `final_snapshot_identifier` and `skip_final_snapshot`; the presence or
absence of the former now implies the latter. [GH-874] absence of the former now implies the latter. [GH-874]
* provider/aws: Avoid unecessary update of `aws_subnet` when * provider/aws: Avoid unnecessary update of `aws_subnet` when
`map_public_ip_on_launch` is not specified in config. [GH-898] `map_public_ip_on_launch` is not specified in config. [GH-898]
* provider/aws: Add `apply_method` to `aws_db_parameter_group` [GH-897] * provider/aws: Add `apply_method` to `aws_db_parameter_group` [GH-897]
* provider/aws: Add `storage_type` to `aws_db_instance` [GH-896] * provider/aws: Add `storage_type` to `aws_db_instance` [GH-896]
@ -113,7 +156,7 @@ BUG FIXES:
* command/apply: Fix regression where user variables weren't asked [GH-736] * command/apply: Fix regression where user variables weren't asked [GH-736]
* helper/hashcode: Update `hash.String()` to always return a positive index. * helper/hashcode: Update `hash.String()` to always return a positive index.
Fixes issue where specific strings would convert to a negative index Fixes issue where specific strings would convert to a negative index
and be ommited when creating Route53 records. [GH-967] and be omitted when creating Route53 records. [GH-967]
* provider/aws: Automatically suffix the Route53 zone name on record names. [GH-312] * provider/aws: Automatically suffix the Route53 zone name on record names. [GH-312]
* provider/aws: Instance should ignore root EBS devices. [GH-877] * provider/aws: Instance should ignore root EBS devices. [GH-877]
* provider/aws: Fix `aws_db_instance` to not recreate each time. [GH-874] * provider/aws: Fix `aws_db_instance` to not recreate each time. [GH-874]
@ -519,3 +562,4 @@ BUG FIXES:
* Initial release * Initial release

View File

@ -53,8 +53,8 @@ If you have never worked with Go before, you will have to complete the
following steps in order to be able to compile and test Terraform (or following steps in order to be able to compile and test Terraform (or
use the Vagrantfile in this repo to stand up a dev VM). use the Vagrantfile in this repo to stand up a dev VM).
1. Install Go. Make sure the Go version is at least Go 1.2. Terraform will not work with anything less than 1. Install Go. Make sure the Go version is at least Go 1.4. Terraform will not work with anything less than
Go 1.2. On a Mac, you can `brew install go` to install Go 1.2. Go 1.4. On a Mac, you can `brew install go` to install Go 1.4.
2. Set and export the `GOPATH` environment variable and update your `PATH`. 2. Set and export the `GOPATH` environment variable and update your `PATH`.
For example, you can add to your `.bash_profile`. For example, you can add to your `.bash_profile`.

View File

@ -0,0 +1,12 @@
package main
import (
"github.com/hashicorp/terraform/builtin/providers/docker"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: docker.Provider,
})
}

View File

@ -0,0 +1 @@
package main

View File

@ -0,0 +1,12 @@
package main
import (
"github.com/hashicorp/terraform/builtin/providers/openstack"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: openstack.Provider,
})
}

View File

@ -0,0 +1,170 @@
package aws
import (
"bytes"
"fmt"
"log"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/autoscaling"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
)
// tagsSchema returns the schema to use for tags.
func autoscalingTagsSchema() *schema.Schema {
return &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"key": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"value": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"propagate_at_launch": &schema.Schema{
Type: schema.TypeBool,
Required: true,
},
},
},
Set: autoscalingTagsToHash,
}
}
func autoscalingTagsToHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["key"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["value"].(string)))
buf.WriteString(fmt.Sprintf("%t-", m["propagate_at_launch"].(bool)))
return hashcode.String(buf.String())
}
// setTags is a helper to set the tags for a resource. It expects the
// tags field to be named "tag"
func setAutoscalingTags(conn *autoscaling.AutoScaling, d *schema.ResourceData) error {
if d.HasChange("tag") {
oraw, nraw := d.GetChange("tag")
o := setToMapByKey(oraw.(*schema.Set), "key")
n := setToMapByKey(nraw.(*schema.Set), "key")
resourceID := d.Get("name").(string)
c, r := diffAutoscalingTags(
autoscalingTagsFromMap(o, resourceID),
autoscalingTagsFromMap(n, resourceID),
resourceID)
create := autoscaling.CreateOrUpdateTagsType{
Tags: c,
}
remove := autoscaling.DeleteTagsType{
Tags: r,
}
// Set tags
if len(r) > 0 {
log.Printf("[DEBUG] Removing autoscaling tags: %#v", r)
if err := conn.DeleteTags(&remove); err != nil {
return err
}
}
if len(c) > 0 {
log.Printf("[DEBUG] Creating autoscaling tags: %#v", c)
if err := conn.CreateOrUpdateTags(&create); err != nil {
return err
}
}
}
return nil
}
// diffTags takes our tags locally and the ones remotely and returns
// the set of tags that must be created, and the set of tags that must
// be destroyed.
func diffAutoscalingTags(oldTags, newTags []autoscaling.Tag, resourceID string) ([]autoscaling.Tag, []autoscaling.Tag) {
// First, we're creating everything we have
create := make(map[string]interface{})
for _, t := range newTags {
tag := map[string]interface{}{
"value": *t.Value,
"propagate_at_launch": *t.PropagateAtLaunch,
}
create[*t.Key] = tag
}
// Build the list of what to remove
var remove []autoscaling.Tag
for _, t := range oldTags {
old, ok := create[*t.Key].(map[string]interface{})
if !ok || old["value"] != *t.Value || old["propagate_at_launch"] != *t.PropagateAtLaunch {
// Delete it!
remove = append(remove, t)
}
}
return autoscalingTagsFromMap(create, resourceID), remove
}
// tagsFromMap returns the tags for the given map of data.
func autoscalingTagsFromMap(m map[string]interface{}, resourceID string) []autoscaling.Tag {
result := make([]autoscaling.Tag, 0, len(m))
for k, v := range m {
attr := v.(map[string]interface{})
result = append(result, autoscaling.Tag{
Key: aws.String(k),
Value: aws.String(attr["value"].(string)),
PropagateAtLaunch: aws.Boolean(attr["propagate_at_launch"].(bool)),
ResourceID: aws.String(resourceID),
ResourceType: aws.String("auto-scaling-group"),
})
}
return result
}
// autoscalingTagsToMap turns the list of tags into a map.
func autoscalingTagsToMap(ts []autoscaling.Tag) map[string]interface{} {
tags := make(map[string]interface{})
for _, t := range ts {
tag := map[string]interface{}{
"value": *t.Value,
"propagate_at_launch": *t.PropagateAtLaunch,
}
tags[*t.Key] = tag
}
return tags
}
// autoscalingTagDescriptionsToMap turns the list of tags into a map.
func autoscalingTagDescriptionsToMap(ts []autoscaling.TagDescription) map[string]map[string]interface{} {
tags := make(map[string]map[string]interface{})
for _, t := range ts {
tag := map[string]interface{}{
"value": *t.Value,
"propagate_at_launch": *t.PropagateAtLaunch,
}
tags[*t.Key] = tag
}
return tags
}
func setToMapByKey(s *schema.Set, key string) map[string]interface{} {
result := make(map[string]interface{})
for _, rawData := range s.List() {
data := rawData.(map[string]interface{})
result[data[key].(string)] = data
}
return result
}

View File

@ -0,0 +1,122 @@
package aws
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/aws-sdk-go/gen/autoscaling"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestDiffAutoscalingTags(t *testing.T) {
cases := []struct {
Old, New map[string]interface{}
Create, Remove map[string]interface{}
}{
// Basic add/remove
{
Old: map[string]interface{}{
"Name": map[string]interface{}{
"value": "bar",
"propagate_at_launch": true,
},
},
New: map[string]interface{}{
"DifferentTag": map[string]interface{}{
"value": "baz",
"propagate_at_launch": true,
},
},
Create: map[string]interface{}{
"DifferentTag": map[string]interface{}{
"value": "baz",
"propagate_at_launch": true,
},
},
Remove: map[string]interface{}{
"Name": map[string]interface{}{
"value": "bar",
"propagate_at_launch": true,
},
},
},
// Modify
{
Old: map[string]interface{}{
"Name": map[string]interface{}{
"value": "bar",
"propagate_at_launch": true,
},
},
New: map[string]interface{}{
"Name": map[string]interface{}{
"value": "baz",
"propagate_at_launch": false,
},
},
Create: map[string]interface{}{
"Name": map[string]interface{}{
"value": "baz",
"propagate_at_launch": false,
},
},
Remove: map[string]interface{}{
"Name": map[string]interface{}{
"value": "bar",
"propagate_at_launch": true,
},
},
},
}
var resourceID = "sample"
for i, tc := range cases {
awsTagsOld := autoscalingTagsFromMap(tc.Old, resourceID)
awsTagsNew := autoscalingTagsFromMap(tc.New, resourceID)
c, r := diffAutoscalingTags(awsTagsOld, awsTagsNew, resourceID)
cm := autoscalingTagsToMap(c)
rm := autoscalingTagsToMap(r)
if !reflect.DeepEqual(cm, tc.Create) {
t.Fatalf("%d: bad create: \n%#v\n%#v", i, cm, tc.Create)
}
if !reflect.DeepEqual(rm, tc.Remove) {
t.Fatalf("%d: bad remove: \n%#v\n%#v", i, rm, tc.Remove)
}
}
}
// testAccCheckTags can be used to check the tags on a resource.
func testAccCheckAutoscalingTags(
ts *[]autoscaling.TagDescription, key string, expected map[string]interface{}) resource.TestCheckFunc {
return func(s *terraform.State) error {
m := autoscalingTagDescriptionsToMap(*ts)
v, ok := m[key]
if !ok {
return fmt.Errorf("Missing tag: %s", key)
}
if v["value"] != expected["value"].(string) ||
v["propagate_at_launch"] != expected["propagate_at_launch"].(bool) {
return fmt.Errorf("%s: bad value: %s", key, v)
}
return nil
}
}
func testAccCheckAutoscalingTagNotExists(ts *[]autoscaling.TagDescription, key string) resource.TestCheckFunc {
return func(s *terraform.State) error {
m := autoscalingTagDescriptionsToMap(*ts)
if _, ok := m[key]; ok {
return fmt.Errorf("Tag exists when it should not: %s", key)
}
return nil
}
}

View File

@ -10,6 +10,7 @@ import (
"github.com/hashicorp/aws-sdk-go/gen/autoscaling" "github.com/hashicorp/aws-sdk-go/gen/autoscaling"
"github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/aws-sdk-go/gen/elb" "github.com/hashicorp/aws-sdk-go/gen/elb"
"github.com/hashicorp/aws-sdk-go/gen/iam"
"github.com/hashicorp/aws-sdk-go/gen/rds" "github.com/hashicorp/aws-sdk-go/gen/rds"
"github.com/hashicorp/aws-sdk-go/gen/route53" "github.com/hashicorp/aws-sdk-go/gen/route53"
"github.com/hashicorp/aws-sdk-go/gen/s3" "github.com/hashicorp/aws-sdk-go/gen/s3"
@ -30,6 +31,7 @@ type AWSClient struct {
r53conn *route53.Route53 r53conn *route53.Route53
region string region string
rdsconn *rds.RDS rdsconn *rds.RDS
iamconn *iam.IAM
} }
// Client configures and returns a fully initailized AWSClient // Client configures and returns a fully initailized AWSClient
@ -70,6 +72,8 @@ func (c *Config) Client() (interface{}, error) {
client.r53conn = route53.New(creds, "us-east-1", nil) client.r53conn = route53.New(creds, "us-east-1", nil)
log.Println("[INFO] Initializing EC2 Connection") log.Println("[INFO] Initializing EC2 Connection")
client.ec2conn = ec2.New(creds, c.Region, nil) client.ec2conn = ec2.New(creds, c.Region, nil)
client.iamconn = iam.New(creds, c.Region, nil)
} }
if len(errs) > 0 { if len(errs) > 0 {

View File

@ -58,6 +58,7 @@ func Provider() terraform.ResourceProvider {
"aws_launch_configuration": resourceAwsLaunchConfiguration(), "aws_launch_configuration": resourceAwsLaunchConfiguration(),
"aws_main_route_table_association": resourceAwsMainRouteTableAssociation(), "aws_main_route_table_association": resourceAwsMainRouteTableAssociation(),
"aws_network_acl": resourceAwsNetworkAcl(), "aws_network_acl": resourceAwsNetworkAcl(),
"aws_network_interface": resourceAwsNetworkInterface(),
"aws_route53_record": resourceAwsRoute53Record(), "aws_route53_record": resourceAwsRoute53Record(),
"aws_route53_zone": resourceAwsRoute53Zone(), "aws_route53_zone": resourceAwsRoute53Zone(),
"aws_route_table": resourceAwsRouteTable(), "aws_route_table": resourceAwsRouteTable(),
@ -67,6 +68,7 @@ func Provider() terraform.ResourceProvider {
"aws_subnet": resourceAwsSubnet(), "aws_subnet": resourceAwsSubnet(),
"aws_vpc": resourceAwsVpc(), "aws_vpc": resourceAwsVpc(),
"aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(), "aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(),
"aws_vpn_gateway": resourceAwsVpnGateway(),
}, },
ConfigureFunc: providerConfigure, ConfigureFunc: providerConfigure,

View File

@ -118,6 +118,8 @@ func resourceAwsAutoscalingGroup() *schema.Resource {
return hashcode.String(v.(string)) return hashcode.String(v.(string))
}, },
}, },
"tag": autoscalingTagsSchema(),
}, },
} }
} }
@ -133,11 +135,16 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{})
autoScalingGroupOpts.AvailabilityZones = expandStringList( autoScalingGroupOpts.AvailabilityZones = expandStringList(
d.Get("availability_zones").(*schema.Set).List()) d.Get("availability_zones").(*schema.Set).List())
if v, ok := d.GetOk("tag"); ok {
autoScalingGroupOpts.Tags = autoscalingTagsFromMap(
setToMapByKey(v.(*schema.Set), "key"), d.Get("name").(string))
}
if v, ok := d.GetOk("default_cooldown"); ok { if v, ok := d.GetOk("default_cooldown"); ok {
autoScalingGroupOpts.DefaultCooldown = aws.Integer(v.(int)) autoScalingGroupOpts.DefaultCooldown = aws.Integer(v.(int))
} }
if v, ok := d.GetOk("health_check"); ok && v.(string) != "" { if v, ok := d.GetOk("health_check_type"); ok && v.(string) != "" {
autoScalingGroupOpts.HealthCheckType = aws.String(v.(string)) autoScalingGroupOpts.HealthCheckType = aws.String(v.(string))
} }
@ -186,15 +193,16 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e
} }
d.Set("availability_zones", g.AvailabilityZones) d.Set("availability_zones", g.AvailabilityZones)
d.Set("default_cooldown", *g.DefaultCooldown) d.Set("default_cooldown", g.DefaultCooldown)
d.Set("desired_capacity", *g.DesiredCapacity) d.Set("desired_capacity", g.DesiredCapacity)
d.Set("health_check_grace_period", *g.HealthCheckGracePeriod) d.Set("health_check_grace_period", g.HealthCheckGracePeriod)
d.Set("health_check_type", *g.HealthCheckType) d.Set("health_check_type", g.HealthCheckType)
d.Set("launch_configuration", *g.LaunchConfigurationName) d.Set("launch_configuration", g.LaunchConfigurationName)
d.Set("load_balancers", g.LoadBalancerNames) d.Set("load_balancers", g.LoadBalancerNames)
d.Set("min_size", *g.MinSize) d.Set("min_size", g.MinSize)
d.Set("max_size", *g.MaxSize) d.Set("max_size", g.MaxSize)
d.Set("name", *g.AutoScalingGroupName) d.Set("name", g.AutoScalingGroupName)
d.Set("tag", g.Tags)
d.Set("vpc_zone_identifier", strings.Split(*g.VPCZoneIdentifier, ",")) d.Set("vpc_zone_identifier", strings.Split(*g.VPCZoneIdentifier, ","))
d.Set("termination_policies", g.TerminationPolicies) d.Set("termination_policies", g.TerminationPolicies)
@ -224,6 +232,12 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{})
opts.MaxSize = aws.Integer(d.Get("max_size").(int)) opts.MaxSize = aws.Integer(d.Get("max_size").(int))
} }
if err := setAutoscalingTags(autoscalingconn, d); err != nil {
return err
} else {
d.SetPartial("tag")
}
log.Printf("[DEBUG] AutoScaling Group update configuration: %#v", opts) log.Printf("[DEBUG] AutoScaling Group update configuration: %#v", opts)
err := autoscalingconn.UpdateAutoScalingGroup(&opts) err := autoscalingconn.UpdateAutoScalingGroup(&opts)
if err != nil { if err != nil {
@ -273,7 +287,12 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{})
return err return err
} }
return nil return resource.Retry(5*time.Minute, func() error {
if g, _ = getAwsAutoscalingGroup(d, meta); g != nil {
return fmt.Errorf("Auto Scaling Group still exists")
}
return nil
})
} }
func getAwsAutoscalingGroup( func getAwsAutoscalingGroup(

View File

@ -2,6 +2,7 @@ package aws
import ( import (
"fmt" "fmt"
"reflect"
"testing" "testing"
"github.com/hashicorp/aws-sdk-go/aws" "github.com/hashicorp/aws-sdk-go/aws"
@ -53,6 +54,44 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) {
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_autoscaling_group.bar", "desired_capacity", "5"), "aws_autoscaling_group.bar", "desired_capacity", "5"),
testLaunchConfigurationName("aws_autoscaling_group.bar", &lc), testLaunchConfigurationName("aws_autoscaling_group.bar", &lc),
testAccCheckAutoscalingTags(&group.Tags, "Bar", map[string]interface{}{
"value": "bar-foo",
"propagate_at_launch": true,
}),
),
},
},
})
}
func TestAccAWSAutoScalingGroup_tags(t *testing.T) {
var group autoscaling.AutoScalingGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSAutoScalingGroupConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group),
testAccCheckAutoscalingTags(&group.Tags, "Foo", map[string]interface{}{
"value": "foo-bar",
"propagate_at_launch": true,
}),
),
},
resource.TestStep{
Config: testAccAWSAutoScalingGroupConfigUpdate,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group),
testAccCheckAutoscalingTagNotExists(&group.Tags, "Foo"),
testAccCheckAutoscalingTags(&group.Tags, "Bar", map[string]interface{}{
"value": "bar-foo",
"propagate_at_launch": true,
}),
), ),
}, },
}, },
@ -130,7 +169,7 @@ func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.AutoScalingGro
} }
if *group.HealthCheckType != "ELB" { if *group.HealthCheckType != "ELB" {
return fmt.Errorf("Bad health_check_type: %s", *group.HealthCheckType) return fmt.Errorf("Bad health_check_type,\nexpected: %s\ngot: %s", "ELB", *group.HealthCheckType)
} }
if *group.HealthCheckGracePeriod != 300 { if *group.HealthCheckGracePeriod != 300 {
@ -145,6 +184,21 @@ func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.AutoScalingGro
return fmt.Errorf("Bad launch configuration name: %s", *group.LaunchConfigurationName) return fmt.Errorf("Bad launch configuration name: %s", *group.LaunchConfigurationName)
} }
t := autoscaling.TagDescription{
Key: aws.String("Foo"),
Value: aws.String("foo-bar"),
PropagateAtLaunch: aws.Boolean(true),
ResourceType: aws.String("auto-scaling-group"),
ResourceID: group.AutoScalingGroupName,
}
if !reflect.DeepEqual(group.Tags[0], t) {
return fmt.Errorf(
"Got:\n\n%#v\n\nExpected:\n\n%#v\n",
group.Tags[0],
t)
}
return nil return nil
} }
} }
@ -226,6 +280,12 @@ resource "aws_autoscaling_group" "bar" {
termination_policies = ["OldestInstance"] termination_policies = ["OldestInstance"]
launch_configuration = "${aws_launch_configuration.foobar.name}" launch_configuration = "${aws_launch_configuration.foobar.name}"
tag {
key = "Foo"
value = "foo-bar"
propagate_at_launch = true
}
} }
` `
@ -253,6 +313,12 @@ resource "aws_autoscaling_group" "bar" {
force_delete = true force_delete = true
launch_configuration = "${aws_launch_configuration.new.name}" launch_configuration = "${aws_launch_configuration.new.name}"
tag {
key = "Bar"
value = "bar-foo"
propagate_at_launch = true
}
} }
` `

View File

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/hashicorp/aws-sdk-go/aws" "github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/iam"
"github.com/hashicorp/aws-sdk-go/gen/rds" "github.com/hashicorp/aws-sdk-go/gen/rds"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
@ -17,6 +18,7 @@ func resourceAwsDbInstance() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
Create: resourceAwsDbInstanceCreate, Create: resourceAwsDbInstanceCreate,
Read: resourceAwsDbInstanceRead, Read: resourceAwsDbInstanceRead,
Update: resourceAwsDbInstanceUpdate,
Delete: resourceAwsDbInstanceDelete, Delete: resourceAwsDbInstanceDelete,
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
@ -47,7 +49,6 @@ func resourceAwsDbInstance() *schema.Resource {
"engine_version": &schema.Schema{ "engine_version": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true,
}, },
"storage_encrypted": &schema.Schema{ "storage_encrypted": &schema.Schema{
@ -119,7 +120,6 @@ func resourceAwsDbInstance() *schema.Resource {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true,
}, },
"port": &schema.Schema{ "port": &schema.Schema{
@ -138,6 +138,7 @@ func resourceAwsDbInstance() *schema.Resource {
"vpc_security_group_ids": &schema.Schema{ "vpc_security_group_ids": &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString}, Elem: &schema.Schema{Type: schema.TypeString},
Set: func(v interface{}) int { Set: func(v interface{}) int {
return hashcode.String(v.(string)) return hashcode.String(v.(string))
@ -162,13 +163,13 @@ func resourceAwsDbInstance() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
ForceNew: true, ForceNew: true,
Computed: true,
}, },
"parameter_group_name": &schema.Schema{ "parameter_group_name": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true,
}, },
"address": &schema.Schema{ "address": &schema.Schema{
@ -185,12 +186,24 @@ func resourceAwsDbInstance() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Computed: true, Computed: true,
}, },
// apply_immediately is used to determine when the update modifications
// take place.
// See http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html
"apply_immediately": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Computed: true,
},
"tags": tagsSchema(),
}, },
} }
} }
func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error { func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).rdsconn conn := meta.(*AWSClient).rdsconn
tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{}))
opts := rds.CreateDBInstanceMessage{ opts := rds.CreateDBInstanceMessage{
AllocatedStorage: aws.Integer(d.Get("allocated_storage").(int)), AllocatedStorage: aws.Integer(d.Get("allocated_storage").(int)),
DBInstanceClass: aws.String(d.Get("instance_class").(string)), DBInstanceClass: aws.String(d.Get("instance_class").(string)),
@ -201,6 +214,7 @@ func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error
Engine: aws.String(d.Get("engine").(string)), Engine: aws.String(d.Get("engine").(string)),
EngineVersion: aws.String(d.Get("engine_version").(string)), EngineVersion: aws.String(d.Get("engine_version").(string)),
StorageEncrypted: aws.Boolean(d.Get("storage_encrypted").(bool)), StorageEncrypted: aws.Boolean(d.Get("storage_encrypted").(bool)),
Tags: tags,
} }
if attr, ok := d.GetOk("storage_type"); ok { if attr, ok := d.GetOk("storage_type"); ok {
@ -304,7 +318,11 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error {
return nil return nil
} }
d.Set("name", *v.DBName) if v.DBName != nil {
d.Set("name", *v.DBName)
} else {
d.Set("name", "")
}
d.Set("username", *v.MasterUsername) d.Set("username", *v.MasterUsername)
d.Set("engine", *v.Engine) d.Set("engine", *v.Engine)
d.Set("engine_version", *v.EngineVersion) d.Set("engine_version", *v.EngineVersion)
@ -328,6 +346,28 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("status", *v.DBInstanceStatus) d.Set("status", *v.DBInstanceStatus)
d.Set("storage_encrypted", *v.StorageEncrypted) d.Set("storage_encrypted", *v.StorageEncrypted)
// list tags for resource
// set tags
conn := meta.(*AWSClient).rdsconn
arn, err := buildRDSARN(d, meta)
if err != nil {
log.Printf("[DEBUG] Error building ARN for DB Instance, not setting Tags for DB %s", *v.DBName)
} else {
resp, err := conn.ListTagsForResource(&rds.ListTagsForResourceMessage{
ResourceName: aws.String(arn),
})
if err != nil {
log.Printf("[DEBUG] Error retreiving tags for ARN: %s", arn)
}
var dt []rds.Tag
if len(resp.TagList) > 0 {
dt = resp.TagList
}
d.Set("tags", tagsToMapRDS(dt))
}
// Create an empty schema.Set to hold all vpc security group ids // Create an empty schema.Set to hold all vpc security group ids
ids := &schema.Set{ ids := &schema.Set{
F: func(v interface{}) int { F: func(v interface{}) int {
@ -390,6 +430,56 @@ func resourceAwsDbInstanceDelete(d *schema.ResourceData, meta interface{}) error
return nil return nil
} }
func resourceAwsDbInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).rdsconn
d.Partial(true)
// Change is used to determine if a ModifyDBInstanceMessage request actually
// gets sent.
change := false
req := &rds.ModifyDBInstanceMessage{
ApplyImmediately: aws.Boolean(d.Get("apply_immediately").(bool)),
DBInstanceIdentifier: aws.String(d.Id()),
}
if d.HasChange("engine_version") {
change = true
d.SetPartial("engine_version")
req.EngineVersion = aws.String(d.Get("engine_version").(string))
}
if d.HasChange("multi_az") {
change = true
d.SetPartial("multi_az")
req.MultiAZ = aws.Boolean(d.Get("multi_az").(bool))
}
if d.HasChange("parameter_group_name") {
change = true
d.SetPartial("parameter_group_name")
req.DBParameterGroupName = aws.String(d.Get("parameter_group_name").(string))
}
if change {
log.Printf("[DEBUG] DB Instance Modification request: %#v", req)
_, err := conn.ModifyDBInstance(req)
if err != nil {
return fmt.Errorf("Error mofigying DB Instance %s: %s", d.Id(), err)
}
}
if arn, err := buildRDSARN(d, meta); err == nil {
if err := setTagsRDS(conn, d, arn); err != nil {
return err
} else {
d.SetPartial("tags")
}
}
d.Partial(false)
return resourceAwsDbInstanceRead(d, meta)
}
func resourceAwsBbInstanceRetrieve( func resourceAwsBbInstanceRetrieve(
d *schema.ResourceData, meta interface{}) (*rds.DBInstance, error) { d *schema.ResourceData, meta interface{}) (*rds.DBInstance, error) {
conn := meta.(*AWSClient).rdsconn conn := meta.(*AWSClient).rdsconn
@ -439,3 +529,16 @@ func resourceAwsDbInstanceStateRefreshFunc(
return v, *v.DBInstanceStatus, nil return v, *v.DBInstanceStatus, nil
} }
} }
func buildRDSARN(d *schema.ResourceData, meta interface{}) (string, error) {
iamconn := meta.(*AWSClient).iamconn
region := meta.(*AWSClient).region
// An zero value GetUserRequest{} defers to the currently logged in user
resp, err := iamconn.GetUser(&iam.GetUserRequest{})
if err != nil {
return "", err
}
user := resp.User
arn := fmt.Sprintf("arn:aws:rds:%s:%s:db:%s", region, *user.UserID, d.Id())
return arn, nil
}

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"log" "log"
"strings"
"time" "time"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
@ -220,7 +221,8 @@ func resourceAwsDbParameterHash(v interface{}) int {
var buf bytes.Buffer var buf bytes.Buffer
m := v.(map[string]interface{}) m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["value"].(string))) // Store the value as a lower case string, to match how we store them in flattenParameters
buf.WriteString(fmt.Sprintf("%s-", strings.ToLower(m["value"].(string))))
return hashcode.String(buf.String()) return hashcode.String(buf.String())
} }

View File

@ -79,6 +79,11 @@ func resourceAwsDbSubnetGroupRead(d *schema.ResourceData, meta interface{}) erro
describeResp, err := rdsconn.DescribeDBSubnetGroups(&describeOpts) describeResp, err := rdsconn.DescribeDBSubnetGroups(&describeOpts)
if err != nil { if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "DBSubnetGroupNotFoundFault" {
// Update state to indicate the db subnet no longer exists.
d.SetId("")
return nil
}
return err return err
} }

View File

@ -154,6 +154,8 @@ func resourceAwsElb() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Computed: true, Computed: true,
}, },
"tags": tagsSchema(),
}, },
} }
} }
@ -167,11 +169,12 @@ func resourceAwsElbCreate(d *schema.ResourceData, meta interface{}) error {
return err return err
} }
tags := tagsFromMapELB(d.Get("tags").(map[string]interface{}))
// Provision the elb // Provision the elb
elbOpts := &elb.CreateAccessPointInput{ elbOpts := &elb.CreateAccessPointInput{
LoadBalancerName: aws.String(d.Get("name").(string)), LoadBalancerName: aws.String(d.Get("name").(string)),
Listeners: listeners, Listeners: listeners,
Tags: tags,
} }
if scheme, ok := d.GetOk("internal"); ok && scheme.(bool) { if scheme, ok := d.GetOk("internal"); ok && scheme.(bool) {
@ -208,6 +211,8 @@ func resourceAwsElbCreate(d *schema.ResourceData, meta interface{}) error {
d.SetPartial("security_groups") d.SetPartial("security_groups")
d.SetPartial("subnets") d.SetPartial("subnets")
d.Set("tags", tagsToMapELB(tags))
if d.HasChange("health_check") { if d.HasChange("health_check") {
vs := d.Get("health_check").(*schema.Set).List() vs := d.Get("health_check").(*schema.Set).List()
if len(vs) > 0 { if len(vs) > 0 {
@ -267,6 +272,15 @@ func resourceAwsElbRead(d *schema.ResourceData, meta interface{}) error {
d.Set("security_groups", lb.SecurityGroups) d.Set("security_groups", lb.SecurityGroups)
d.Set("subnets", lb.Subnets) d.Set("subnets", lb.Subnets)
resp, err := elbconn.DescribeTags(&elb.DescribeTagsInput{
LoadBalancerNames: []string{*lb.LoadBalancerName},
})
var et []elb.Tag
if len(resp.TagDescriptions) > 0 {
et = resp.TagDescriptions[0].Tags
}
d.Set("tags", tagsToMapELB(et))
// There's only one health check, so save that to state as we // There's only one health check, so save that to state as we
// currently can // currently can
if *lb.HealthCheck.Target != "" { if *lb.HealthCheck.Target != "" {
@ -357,6 +371,11 @@ func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
} }
} }
if err := setTagsELB(elbconn, d); err != nil {
return err
} else {
d.SetPartial("tags")
}
d.Partial(false) d.Partial(false)
return resourceAwsElbRead(d, meta) return resourceAwsElbRead(d, meta)

View File

@ -53,6 +53,61 @@ func TestAccAWSELB_basic(t *testing.T) {
}) })
} }
func TestAccAWSELB_tags(t *testing.T) {
var conf elb.LoadBalancerDescription
var td elb.TagDescription
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSELBDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSELBConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSELBExists("aws_elb.bar", &conf),
testAccCheckAWSELBAttributes(&conf),
resource.TestCheckResourceAttr(
"aws_elb.bar", "name", "foobar-terraform-test"),
testAccLoadTags(&conf, &td),
testAccCheckELBTags(&td.Tags, "bar", "baz"),
),
},
resource.TestStep{
Config: testAccAWSELBConfig_TagUpdate,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSELBExists("aws_elb.bar", &conf),
testAccCheckAWSELBAttributes(&conf),
resource.TestCheckResourceAttr(
"aws_elb.bar", "name", "foobar-terraform-test"),
testAccLoadTags(&conf, &td),
testAccCheckELBTags(&td.Tags, "foo", "bar"),
testAccCheckELBTags(&td.Tags, "new", "type"),
),
},
},
})
}
func testAccLoadTags(conf *elb.LoadBalancerDescription, td *elb.TagDescription) resource.TestCheckFunc {
return func(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).elbconn
describe, err := conn.DescribeTags(&elb.DescribeTagsInput{
LoadBalancerNames: []string{*conf.LoadBalancerName},
})
if err != nil {
return err
}
if len(describe.TagDescriptions) > 0 {
*td = describe.TagDescriptions[0]
}
return nil
}
}
func TestAccAWSELB_InstanceAttaching(t *testing.T) { func TestAccAWSELB_InstanceAttaching(t *testing.T) {
var conf elb.LoadBalancerDescription var conf elb.LoadBalancerDescription
@ -288,6 +343,31 @@ resource "aws_elb" "bar" {
lb_protocol = "http" lb_protocol = "http"
} }
tags {
bar = "baz"
}
cross_zone_load_balancing = true
}
`
const testAccAWSELBConfig_TagUpdate = `
resource "aws_elb" "bar" {
name = "foobar-terraform-test"
availability_zones = ["us-west-2a", "us-west-2b", "us-west-2c"]
listener {
instance_port = 8000
instance_protocol = "http"
lb_port = 80
lb_protocol = "http"
}
tags {
foo = "bar"
new = "type"
}
cross_zone_load_balancing = true cross_zone_load_balancing = true
} }
` `

View File

@ -24,6 +24,9 @@ func resourceAwsInstance() *schema.Resource {
Update: resourceAwsInstanceUpdate, Update: resourceAwsInstanceUpdate,
Delete: resourceAwsInstanceDelete, Delete: resourceAwsInstanceDelete,
SchemaVersion: 1,
MigrateState: resourceAwsInstanceMigrateState,
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"ami": &schema.Schema{ "ami": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -127,53 +130,28 @@ func resourceAwsInstance() *schema.Resource {
ForceNew: true, ForceNew: true,
Optional: true, Optional: true,
}, },
"tenancy": &schema.Schema{ "tenancy": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"tags": tagsSchema(), "tags": tagsSchema(),
"block_device": &schema.Schema{ "block_device": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
Removed: "Split out into three sub-types; see Changelog and Docs",
},
"ebs_block_device": &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true, Computed: true,
Elem: &schema.Resource{ Elem: &schema.Resource{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"device_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"virtual_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"snapshot_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_size": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
"delete_on_termination": &schema.Schema{ "delete_on_termination": &schema.Schema{
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
@ -181,6 +159,12 @@ func resourceAwsInstance() *schema.Resource {
ForceNew: true, ForceNew: true,
}, },
"device_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"encrypted": &schema.Schema{ "encrypted": &schema.Schema{
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
@ -194,36 +178,12 @@ func resourceAwsInstance() *schema.Resource {
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
},
},
Set: resourceAwsInstanceBlockDevicesHash,
},
"root_block_device": &schema.Schema{ "snapshot_id": &schema.Schema{
// TODO: This is a list because we don't support singleton
// sub-resources today. We'll enforce that the list only ever has
// length zero or one below. When TF gains support for
// sub-resources this can be converted.
Type: schema.TypeList,
Optional: true,
Computed: true,
Elem: &schema.Resource{
// "You can only modify the volume size, volume type, and Delete on
// Termination flag on the block device mapping entry for the root
// device volume." - bit.ly/ec2bdmap
Schema: map[string]*schema.Schema{
"delete_on_termination": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
ForceNew: true,
},
"device_name": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true,
ForceNew: true, ForceNew: true,
Default: "/dev/sda1",
}, },
"volume_size": &schema.Schema{ "volume_size": &schema.Schema{
@ -239,6 +199,71 @@ func resourceAwsInstance() *schema.Resource {
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
},
},
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
buf.WriteString(fmt.Sprintf("%t-", m["encrypted"].(bool)))
// NOTE: Not considering IOPS in hash; when using gp2, IOPS can come
// back set to something like "33", which throws off the set
// calculation and generates an unresolvable diff.
// buf.WriteString(fmt.Sprintf("%d-", m["iops"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string)))
buf.WriteString(fmt.Sprintf("%d-", m["volume_size"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["volume_type"].(string)))
return hashcode.String(buf.String())
},
},
"ephemeral_block_device": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"device_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"virtual_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},
},
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string)))
return hashcode.String(buf.String())
},
},
"root_block_device": &schema.Schema{
// TODO: This is a set because we don't support singleton
// sub-resources today. We'll enforce that the set only ever has
// length zero or one below. When TF gains support for
// sub-resources this can be converted.
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Resource{
// "You can only modify the volume size, volume type, and Delete on
// Termination flag on the block device mapping entry for the root
// device volume." - bit.ly/ec2bdmap
Schema: map[string]*schema.Schema{
"delete_on_termination": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
ForceNew: true,
},
"iops": &schema.Schema{ "iops": &schema.Schema{
Type: schema.TypeInt, Type: schema.TypeInt,
@ -246,8 +271,32 @@ func resourceAwsInstance() *schema.Resource {
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"volume_size": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
}, },
}, },
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
// See the NOTE in "ebs_block_device" for why we skip iops here.
// buf.WriteString(fmt.Sprintf("%d-", m["iops"].(int)))
buf.WriteString(fmt.Sprintf("%d-", m["volume_size"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["volume_type"].(string)))
return hashcode.String(buf.String())
},
}, },
}, },
} }
@ -262,9 +311,21 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
userData = base64.StdEncoding.EncodeToString([]byte(v.(string))) userData = base64.StdEncoding.EncodeToString([]byte(v.(string)))
} }
// check for non-default Subnet, and cast it to a String
var hasSubnet bool
subnet, hasSubnet := d.GetOk("subnet_id")
subnetID := subnet.(string)
placement := &ec2.Placement{ placement := &ec2.Placement{
AvailabilityZone: aws.String(d.Get("availability_zone").(string)), AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
Tenancy: aws.String(d.Get("tenancy").(string)), }
if hasSubnet {
// Tenancy is only valid inside a VPC
// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Placement.html
if v := d.Get("tenancy").(string); v != "" {
placement.Tenancy = aws.String(v)
}
} }
iam := &ec2.IAMInstanceProfileSpecification{ iam := &ec2.IAMInstanceProfileSpecification{
@ -288,11 +349,6 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
associatePublicIPAddress = v.(bool) associatePublicIPAddress = v.(bool)
} }
// check for non-default Subnet, and cast it to a String
var hasSubnet bool
subnet, hasSubnet := d.GetOk("subnet_id")
subnetID := subnet.(string)
var groups []string var groups []string
if v := d.Get("security_groups"); v != nil { if v := d.Get("security_groups"); v != nil {
// Security group names. // Security group names.
@ -347,46 +403,88 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
runOpts.KeyName = aws.String(v.(string)) runOpts.KeyName = aws.String(v.(string))
} }
blockDevices := make([]interface{}, 0) blockDevices := make([]ec2.BlockDeviceMapping, 0)
if v := d.Get("block_device"); v != nil { if v, ok := d.GetOk("ebs_block_device"); ok {
blockDevices = append(blockDevices, v.(*schema.Set).List()...) vL := v.(*schema.Set).List()
} for _, v := range vL {
if v := d.Get("root_block_device"); v != nil {
rootBlockDevices := v.([]interface{})
if len(rootBlockDevices) > 1 {
return fmt.Errorf("Cannot specify more than one root_block_device.")
}
blockDevices = append(blockDevices, rootBlockDevices...)
}
if len(blockDevices) > 0 {
runOpts.BlockDeviceMappings = make([]ec2.BlockDeviceMapping, len(blockDevices))
for i, v := range blockDevices {
bd := v.(map[string]interface{}) bd := v.(map[string]interface{})
runOpts.BlockDeviceMappings[i].DeviceName = aws.String(bd["device_name"].(string)) ebs := &ec2.EBSBlockDevice{
runOpts.BlockDeviceMappings[i].EBS = &ec2.EBSBlockDevice{
VolumeType: aws.String(bd["volume_type"].(string)),
VolumeSize: aws.Integer(bd["volume_size"].(int)),
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)), DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
} }
if v, ok := bd["virtual_name"].(string); ok {
runOpts.BlockDeviceMappings[i].VirtualName = aws.String(v)
}
if v, ok := bd["snapshot_id"].(string); ok && v != "" { if v, ok := bd["snapshot_id"].(string); ok && v != "" {
runOpts.BlockDeviceMappings[i].EBS.SnapshotID = aws.String(v) ebs.SnapshotID = aws.String(v)
} }
if v, ok := bd["encrypted"].(bool); ok {
runOpts.BlockDeviceMappings[i].EBS.Encrypted = aws.Boolean(v) if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Integer(v)
} }
if v, ok := bd["volume_type"].(string); ok && v != "" {
ebs.VolumeType = aws.String(v)
}
if v, ok := bd["iops"].(int); ok && v > 0 { if v, ok := bd["iops"].(int); ok && v > 0 {
runOpts.BlockDeviceMappings[i].EBS.IOPS = aws.Integer(v) ebs.IOPS = aws.Integer(v)
}
blockDevices = append(blockDevices, ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
EBS: ebs,
})
}
}
if v, ok := d.GetOk("ephemeral_block_device"); ok {
vL := v.(*schema.Set).List()
for _, v := range vL {
bd := v.(map[string]interface{})
blockDevices = append(blockDevices, ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
VirtualName: aws.String(bd["virtual_name"].(string)),
})
}
}
if v, ok := d.GetOk("root_block_device"); ok {
vL := v.(*schema.Set).List()
if len(vL) > 1 {
return fmt.Errorf("Cannot specify more than one root_block_device.")
}
for _, v := range vL {
bd := v.(map[string]interface{})
ebs := &ec2.EBSBlockDevice{
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
}
if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Integer(v)
}
if v, ok := bd["volume_type"].(string); ok && v != "" {
ebs.VolumeType = aws.String(v)
}
if v, ok := bd["iops"].(int); ok && v > 0 {
ebs.IOPS = aws.Integer(v)
}
if dn, err := fetchRootDeviceName(d.Get("ami").(string), ec2conn); err == nil {
blockDevices = append(blockDevices, ec2.BlockDeviceMapping{
DeviceName: dn,
EBS: ebs,
})
} else {
return err
} }
} }
} }
if len(blockDevices) > 0 {
runOpts.BlockDeviceMappings = blockDevices
}
// Create the instance // Create the instance
log.Printf("[DEBUG] Run configuration: %#v", runOpts) log.Printf("[DEBUG] Run configuration: %#v", runOpts)
runResp, err := ec2conn.RunInstances(runOpts) runResp, err := ec2conn.RunInstances(runOpts)
@ -473,13 +571,18 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
return nil return nil
} }
d.Set("availability_zone", instance.Placement.AvailabilityZone) if instance.Placement != nil {
d.Set("availability_zone", instance.Placement.AvailabilityZone)
}
if instance.Placement.Tenancy != nil {
d.Set("tenancy", instance.Placement.Tenancy)
}
d.Set("key_name", instance.KeyName) d.Set("key_name", instance.KeyName)
d.Set("public_dns", instance.PublicDNSName) d.Set("public_dns", instance.PublicDNSName)
d.Set("public_ip", instance.PublicIPAddress) d.Set("public_ip", instance.PublicIPAddress)
d.Set("private_dns", instance.PrivateDNSName) d.Set("private_dns", instance.PrivateDNSName)
d.Set("private_ip", instance.PrivateIPAddress) d.Set("private_ip", instance.PrivateIPAddress)
d.Set("subnet_id", instance.SubnetID)
if len(instance.NetworkInterfaces) > 0 { if len(instance.NetworkInterfaces) > 0 {
d.Set("subnet_id", instance.NetworkInterfaces[0].SubnetID) d.Set("subnet_id", instance.NetworkInterfaces[0].SubnetID)
} else { } else {
@ -487,14 +590,13 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
} }
d.Set("ebs_optimized", instance.EBSOptimized) d.Set("ebs_optimized", instance.EBSOptimized)
d.Set("tags", tagsToMap(instance.Tags)) d.Set("tags", tagsToMap(instance.Tags))
d.Set("tenancy", instance.Placement.Tenancy)
// Determine whether we're referring to security groups with // Determine whether we're referring to security groups with
// IDs or names. We use a heuristic to figure this out. By default, // IDs or names. We use a heuristic to figure this out. By default,
// we use IDs if we're in a VPC. However, if we previously had an // we use IDs if we're in a VPC. However, if we previously had an
// all-name list of security groups, we use names. Or, if we had any // all-name list of security groups, we use names. Or, if we had any
// IDs, we use IDs. // IDs, we use IDs.
useID := *instance.SubnetID != "" useID := instance.SubnetID != nil && *instance.SubnetID != ""
if v := d.Get("security_groups"); v != nil { if v := d.Get("security_groups"); v != nil {
match := false match := false
for _, v := range v.(*schema.Set).List() { for _, v := range v.(*schema.Set).List() {
@ -518,67 +620,28 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
} }
d.Set("security_groups", sgs) d.Set("security_groups", sgs)
blockDevices := make(map[string]ec2.InstanceBlockDeviceMapping) if err := readBlockDevices(d, instance, ec2conn); err != nil {
for _, bd := range instance.BlockDeviceMappings {
blockDevices[*bd.EBS.VolumeID] = bd
}
volIDs := make([]string, 0, len(blockDevices))
for _, vol := range blockDevices {
volIDs = append(volIDs, *vol.EBS.VolumeID)
}
volResp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesRequest{
VolumeIDs: volIDs,
})
if err != nil {
return err return err
} }
nonRootBlockDevices := make([]map[string]interface{}, 0)
rootBlockDevice := make([]interface{}, 0, 1)
for _, vol := range volResp.Volumes {
blockDevice := make(map[string]interface{})
blockDevice["device_name"] = *blockDevices[*vol.VolumeID].DeviceName
blockDevice["volume_type"] = *vol.VolumeType
blockDevice["volume_size"] = *vol.Size
if vol.IOPS != nil {
blockDevice["iops"] = *vol.IOPS
}
blockDevice["delete_on_termination"] =
*blockDevices[*vol.VolumeID].EBS.DeleteOnTermination
// If this is the root device, save it. We stop here since we
// can't put invalid keys into this map.
if blockDevice["device_name"] == *instance.RootDeviceName {
rootBlockDevice = []interface{}{blockDevice}
continue
}
blockDevice["snapshot_id"] = *vol.SnapshotID
blockDevice["encrypted"] = *vol.Encrypted
nonRootBlockDevices = append(nonRootBlockDevices, blockDevice)
}
d.Set("block_device", nonRootBlockDevices)
d.Set("root_block_device", rootBlockDevice)
return nil return nil
} }
func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error { func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).ec2conn
opts := new(ec2.ModifyInstanceAttributeRequest)
log.Printf("[INFO] Modifying instance %s: %#v", d.Id(), opts) // SourceDestCheck can only be set on VPC instances
err := ec2conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeRequest{ if d.Get("subnet_id").(string) != "" {
InstanceID: aws.String(d.Id()), log.Printf("[INFO] Modifying instance %s", d.Id())
SourceDestCheck: &ec2.AttributeBooleanValue{ err := ec2conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeRequest{
Value: aws.Boolean(d.Get("source_dest_check").(bool)), InstanceID: aws.String(d.Id()),
}, SourceDestCheck: &ec2.AttributeBooleanValue{
}) Value: aws.Boolean(d.Get("source_dest_check").(bool)),
},
if err != nil { })
return err if err != nil {
return err
}
} }
// TODO(mitchellh): wait for the attributes we modified to // TODO(mitchellh): wait for the attributes we modified to
@ -656,11 +719,111 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRe
} }
} }
func resourceAwsInstanceBlockDevicesHash(v interface{}) int { func readBlockDevices(d *schema.ResourceData, instance *ec2.Instance, ec2conn *ec2.EC2) error {
var buf bytes.Buffer ibds, err := readBlockDevicesFromInstance(instance, ec2conn)
m := v.(map[string]interface{}) if err != nil {
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) return err
buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) }
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
return hashcode.String(buf.String()) if err := d.Set("ebs_block_device", ibds["ebs"]); err != nil {
return err
}
if ibds["root"] != nil {
if err := d.Set("root_block_device", []interface{}{ibds["root"]}); err != nil {
return err
}
}
return nil
}
func readBlockDevicesFromInstance(instance *ec2.Instance, ec2conn *ec2.EC2) (map[string]interface{}, error) {
blockDevices := make(map[string]interface{})
blockDevices["ebs"] = make([]map[string]interface{}, 0)
blockDevices["root"] = nil
instanceBlockDevices := make(map[string]ec2.InstanceBlockDeviceMapping)
for _, bd := range instance.BlockDeviceMappings {
if bd.EBS != nil {
instanceBlockDevices[*(bd.EBS.VolumeID)] = bd
}
}
if len(instanceBlockDevices) == 0 {
return nil, nil
}
volIDs := make([]string, 0, len(instanceBlockDevices))
for volID := range instanceBlockDevices {
volIDs = append(volIDs, volID)
}
// Need to call DescribeVolumes to get volume_size and volume_type for each
// EBS block device
volResp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesRequest{
VolumeIDs: volIDs,
})
if err != nil {
return nil, err
}
for _, vol := range volResp.Volumes {
instanceBd := instanceBlockDevices[*vol.VolumeID]
bd := make(map[string]interface{})
if instanceBd.EBS != nil && instanceBd.EBS.DeleteOnTermination != nil {
bd["delete_on_termination"] = *instanceBd.EBS.DeleteOnTermination
}
if vol.Size != nil {
bd["volume_size"] = *vol.Size
}
if vol.VolumeType != nil {
bd["volume_type"] = *vol.VolumeType
}
if vol.IOPS != nil {
bd["iops"] = *vol.IOPS
}
if blockDeviceIsRoot(instanceBd, instance) {
blockDevices["root"] = bd
} else {
if instanceBd.DeviceName != nil {
bd["device_name"] = *instanceBd.DeviceName
}
if vol.Encrypted != nil {
bd["encrypted"] = *vol.Encrypted
}
if vol.SnapshotID != nil {
bd["snapshot_id"] = *vol.SnapshotID
}
blockDevices["ebs"] = append(blockDevices["ebs"].([]map[string]interface{}), bd)
}
}
return blockDevices, nil
}
func blockDeviceIsRoot(bd ec2.InstanceBlockDeviceMapping, instance *ec2.Instance) bool {
return (bd.DeviceName != nil &&
instance.RootDeviceName != nil &&
*bd.DeviceName == *instance.RootDeviceName)
}
func fetchRootDeviceName(ami string, conn *ec2.EC2) (aws.StringValue, error) {
if ami == "" {
return nil, fmt.Errorf("Cannot fetch root device name for blank AMI ID.")
}
log.Printf("[DEBUG] Describing AMI %q to get root block device name", ami)
req := &ec2.DescribeImagesRequest{ImageIDs: []string{ami}}
if res, err := conn.DescribeImages(req); err == nil {
if len(res.Images) == 1 {
return res.Images[0].RootDeviceName, nil
} else {
return nil, fmt.Errorf("Expected 1 AMI for ID: %s, got: %#v", ami, res.Images)
}
} else {
return nil, err
}
} }

View File

@ -0,0 +1,113 @@
package aws
import (
"fmt"
"log"
"strconv"
"strings"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/terraform"
)
func resourceAwsInstanceMigrateState(
v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
switch v {
case 0:
log.Println("[INFO] Found AWS Instance State v0; migrating to v1")
return migrateStateV0toV1(is)
default:
return is, fmt.Errorf("Unexpected schema version: %d", v)
}
return is, nil
}
func migrateStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) {
if is.Empty() {
log.Println("[DEBUG] Empty InstanceState; nothing to migrate.")
return is, nil
}
log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes)
// Delete old count
delete(is.Attributes, "block_device.#")
oldBds, err := readV0BlockDevices(is)
if err != nil {
return is, err
}
// seed count fields for new types
is.Attributes["ebs_block_device.#"] = "0"
is.Attributes["ephemeral_block_device.#"] = "0"
// depending on if state was v0.3.7 or an earlier version, it might have
// root_block_device defined already
if _, ok := is.Attributes["root_block_device.#"]; !ok {
is.Attributes["root_block_device.#"] = "0"
}
for _, oldBd := range oldBds {
if err := writeV1BlockDevice(is, oldBd); err != nil {
return is, err
}
}
log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes)
return is, nil
}
func readV0BlockDevices(is *terraform.InstanceState) (map[string]map[string]string, error) {
oldBds := make(map[string]map[string]string)
for k, v := range is.Attributes {
if !strings.HasPrefix(k, "block_device.") {
continue
}
path := strings.Split(k, ".")
if len(path) != 3 {
return oldBds, fmt.Errorf("Found unexpected block_device field: %#v", k)
}
hashcode, attribute := path[1], path[2]
oldBd, ok := oldBds[hashcode]
if !ok {
oldBd = make(map[string]string)
oldBds[hashcode] = oldBd
}
oldBd[attribute] = v
delete(is.Attributes, k)
}
return oldBds, nil
}
func writeV1BlockDevice(
is *terraform.InstanceState, oldBd map[string]string) error {
code := hashcode.String(oldBd["device_name"])
bdType := "ebs_block_device"
if vn, ok := oldBd["virtual_name"]; ok && strings.HasPrefix(vn, "ephemeral") {
bdType = "ephemeral_block_device"
} else if dn, ok := oldBd["device_name"]; ok && dn == "/dev/sda1" {
bdType = "root_block_device"
}
switch bdType {
case "ebs_block_device":
delete(oldBd, "virtual_name")
case "root_block_device":
delete(oldBd, "virtual_name")
delete(oldBd, "encrypted")
delete(oldBd, "snapshot_id")
case "ephemeral_block_device":
delete(oldBd, "delete_on_termination")
delete(oldBd, "encrypted")
delete(oldBd, "iops")
delete(oldBd, "volume_size")
delete(oldBd, "volume_type")
}
for attr, val := range oldBd {
attrKey := fmt.Sprintf("%s.%d.%s", bdType, code, attr)
is.Attributes[attrKey] = val
}
countAttr := fmt.Sprintf("%s.#", bdType)
count, _ := strconv.Atoi(is.Attributes[countAttr])
is.Attributes[countAttr] = strconv.Itoa(count + 1)
return nil
}

View File

@ -0,0 +1,159 @@
package aws
import (
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestAWSInstanceMigrateState(t *testing.T) {
cases := map[string]struct {
StateVersion int
Attributes map[string]string
Expected map[string]string
Meta interface{}
}{
"v0.3.6 and earlier": {
StateVersion: 0,
Attributes: map[string]string{
// EBS
"block_device.#": "2",
"block_device.3851383343.delete_on_termination": "true",
"block_device.3851383343.device_name": "/dev/sdx",
"block_device.3851383343.encrypted": "false",
"block_device.3851383343.snapshot_id": "",
"block_device.3851383343.virtual_name": "",
"block_device.3851383343.volume_size": "5",
"block_device.3851383343.volume_type": "standard",
// Ephemeral
"block_device.3101711606.delete_on_termination": "false",
"block_device.3101711606.device_name": "/dev/sdy",
"block_device.3101711606.encrypted": "false",
"block_device.3101711606.snapshot_id": "",
"block_device.3101711606.virtual_name": "ephemeral0",
"block_device.3101711606.volume_size": "",
"block_device.3101711606.volume_type": "",
// Root
"block_device.56575650.delete_on_termination": "true",
"block_device.56575650.device_name": "/dev/sda1",
"block_device.56575650.encrypted": "false",
"block_device.56575650.snapshot_id": "",
"block_device.56575650.volume_size": "10",
"block_device.56575650.volume_type": "standard",
},
Expected: map[string]string{
"ebs_block_device.#": "1",
"ebs_block_device.3851383343.delete_on_termination": "true",
"ebs_block_device.3851383343.device_name": "/dev/sdx",
"ebs_block_device.3851383343.encrypted": "false",
"ebs_block_device.3851383343.snapshot_id": "",
"ebs_block_device.3851383343.volume_size": "5",
"ebs_block_device.3851383343.volume_type": "standard",
"ephemeral_block_device.#": "1",
"ephemeral_block_device.2458403513.device_name": "/dev/sdy",
"ephemeral_block_device.2458403513.virtual_name": "ephemeral0",
"root_block_device.#": "1",
"root_block_device.3018388612.delete_on_termination": "true",
"root_block_device.3018388612.device_name": "/dev/sda1",
"root_block_device.3018388612.snapshot_id": "",
"root_block_device.3018388612.volume_size": "10",
"root_block_device.3018388612.volume_type": "standard",
},
},
"v0.3.7": {
StateVersion: 0,
Attributes: map[string]string{
// EBS
"block_device.#": "2",
"block_device.3851383343.delete_on_termination": "true",
"block_device.3851383343.device_name": "/dev/sdx",
"block_device.3851383343.encrypted": "false",
"block_device.3851383343.snapshot_id": "",
"block_device.3851383343.virtual_name": "",
"block_device.3851383343.volume_size": "5",
"block_device.3851383343.volume_type": "standard",
"block_device.3851383343.iops": "",
// Ephemeral
"block_device.3101711606.delete_on_termination": "false",
"block_device.3101711606.device_name": "/dev/sdy",
"block_device.3101711606.encrypted": "false",
"block_device.3101711606.snapshot_id": "",
"block_device.3101711606.virtual_name": "ephemeral0",
"block_device.3101711606.volume_size": "",
"block_device.3101711606.volume_type": "",
"block_device.3101711606.iops": "",
// Root
"root_block_device.#": "1",
"root_block_device.3018388612.delete_on_termination": "true",
"root_block_device.3018388612.device_name": "/dev/sda1",
"root_block_device.3018388612.snapshot_id": "",
"root_block_device.3018388612.volume_size": "10",
"root_block_device.3018388612.volume_type": "io1",
"root_block_device.3018388612.iops": "1000",
},
Expected: map[string]string{
"ebs_block_device.#": "1",
"ebs_block_device.3851383343.delete_on_termination": "true",
"ebs_block_device.3851383343.device_name": "/dev/sdx",
"ebs_block_device.3851383343.encrypted": "false",
"ebs_block_device.3851383343.snapshot_id": "",
"ebs_block_device.3851383343.volume_size": "5",
"ebs_block_device.3851383343.volume_type": "standard",
"ephemeral_block_device.#": "1",
"ephemeral_block_device.2458403513.device_name": "/dev/sdy",
"ephemeral_block_device.2458403513.virtual_name": "ephemeral0",
"root_block_device.#": "1",
"root_block_device.3018388612.delete_on_termination": "true",
"root_block_device.3018388612.device_name": "/dev/sda1",
"root_block_device.3018388612.snapshot_id": "",
"root_block_device.3018388612.volume_size": "10",
"root_block_device.3018388612.volume_type": "io1",
"root_block_device.3018388612.iops": "1000",
},
},
}
for tn, tc := range cases {
is := &terraform.InstanceState{
ID: "i-abc123",
Attributes: tc.Attributes,
}
is, err := resourceAwsInstanceMigrateState(
tc.StateVersion, is, tc.Meta)
if err != nil {
t.Fatalf("bad: %s, err: %#v", tn, err)
}
for k, v := range tc.Expected {
if is.Attributes[k] != v {
t.Fatalf(
"bad: %s\n\n expected: %#v -> %#v\n got: %#v -> %#v\n in: %#v",
tn, k, v, k, is.Attributes[k], is.Attributes)
}
}
}
}
func TestAWSInstanceMigrateState_empty(t *testing.T) {
var is *terraform.InstanceState
var meta interface{}
// should handle nil
is, err := resourceAwsInstanceMigrateState(0, is, meta)
if err != nil {
t.Fatalf("err: %#v", err)
}
if is != nil {
t.Fatalf("expected nil instancestate, got: %#v", is)
}
// should handle non-nil but empty
is = &terraform.InstanceState{}
is, err = resourceAwsInstanceMigrateState(0, is, meta)
if err != nil {
t.Fatalf("err: %#v", err)
}
}

View File

@ -14,6 +14,7 @@ import (
func TestAccAWSInstance_normal(t *testing.T) { func TestAccAWSInstance_normal(t *testing.T) {
var v ec2.Instance var v ec2.Instance
var vol *ec2.Volume
testCheck := func(*terraform.State) error { testCheck := func(*terraform.State) error {
if *v.Placement.AvailabilityZone != "us-west-2a" { if *v.Placement.AvailabilityZone != "us-west-2a" {
@ -35,6 +36,21 @@ func TestAccAWSInstance_normal(t *testing.T) {
Providers: testAccProviders, Providers: testAccProviders,
CheckDestroy: testAccCheckInstanceDestroy, CheckDestroy: testAccCheckInstanceDestroy,
Steps: []resource.TestStep{ Steps: []resource.TestStep{
// Create a volume to cover #1249
resource.TestStep{
// Need a resource in this config so the provisioner will be available
Config: testAccInstanceConfig_pre,
Check: func(*terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
var err error
vol, err = conn.CreateVolume(&ec2.CreateVolumeRequest{
AvailabilityZone: aws.String("us-west-2a"),
Size: aws.Integer(5),
})
return err
},
},
resource.TestStep{ resource.TestStep{
Config: testAccInstanceConfig, Config: testAccInstanceConfig,
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
@ -45,6 +61,8 @@ func TestAccAWSInstance_normal(t *testing.T) {
"aws_instance.foo", "aws_instance.foo",
"user_data", "user_data",
"3dc39dda39be1205215e776bad998da361a5955d"), "3dc39dda39be1205215e776bad998da361a5955d"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "ebs_block_device.#", "0"),
), ),
}, },
@ -61,8 +79,19 @@ func TestAccAWSInstance_normal(t *testing.T) {
"aws_instance.foo", "aws_instance.foo",
"user_data", "user_data",
"3dc39dda39be1205215e776bad998da361a5955d"), "3dc39dda39be1205215e776bad998da361a5955d"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "ebs_block_device.#", "0"),
), ),
}, },
// Clean up volume created above
resource.TestStep{
Config: testAccInstanceConfig,
Check: func(*terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
return conn.DeleteVolume(&ec2.DeleteVolumeRequest{VolumeID: vol.VolumeID})
},
},
}, },
}) })
} }
@ -111,31 +140,31 @@ func TestAccAWSInstance_blockDevices(t *testing.T) {
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.#", "1"), "aws_instance.foo", "root_block_device.#", "1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.0.device_name", "/dev/sda1"), "aws_instance.foo", "root_block_device.1023169747.volume_size", "11"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.0.volume_size", "11"), "aws_instance.foo", "root_block_device.1023169747.volume_type", "gp2"),
// this one is important because it's the only root_block_device
// attribute that comes back from the API. so checking it verifies
// that we set state properly
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.0.volume_type", "gp2"), "aws_instance.foo", "ebs_block_device.#", "2"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.#", "2"), "aws_instance.foo", "ebs_block_device.2225977507.device_name", "/dev/sdb"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.172787947.device_name", "/dev/sdb"), "aws_instance.foo", "ebs_block_device.2225977507.volume_size", "9"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.172787947.volume_size", "9"), "aws_instance.foo", "ebs_block_device.2225977507.volume_type", "standard"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.172787947.iops", "0"), "aws_instance.foo", "ebs_block_device.1977224956.device_name", "/dev/sdc"),
// Check provisioned SSD device
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.volume_type", "io1"), "aws_instance.foo", "ebs_block_device.1977224956.volume_size", "10"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.device_name", "/dev/sdc"), "aws_instance.foo", "ebs_block_device.1977224956.volume_type", "io1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.volume_size", "10"), "aws_instance.foo", "ebs_block_device.1977224956.iops", "100"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.iops", "100"), "aws_instance.foo", "ephemeral_block_device.#", "1"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "ephemeral_block_device.1692014856.device_name", "/dev/sde"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "ephemeral_block_device.1692014856.virtual_name", "ephemeral0"),
testCheck(), testCheck(),
), ),
}, },
@ -391,6 +420,20 @@ func TestInstanceTenancySchema(t *testing.T) {
} }
} }
const testAccInstanceConfig_pre = `
resource "aws_security_group" "tf_test_foo" {
name = "tf_test_foo"
description = "foo"
ingress {
protocol = "icmp"
from_port = -1
to_port = -1
cidr_blocks = ["0.0.0.0/0"]
}
}
`
const testAccInstanceConfig = ` const testAccInstanceConfig = `
resource "aws_security_group" "tf_test_foo" { resource "aws_security_group" "tf_test_foo" {
name = "tf_test_foo" name = "tf_test_foo"
@ -420,21 +463,25 @@ resource "aws_instance" "foo" {
# us-west-2 # us-west-2
ami = "ami-55a7ea65" ami = "ami-55a7ea65"
instance_type = "m1.small" instance_type = "m1.small"
root_block_device { root_block_device {
device_name = "/dev/sda1"
volume_type = "gp2" volume_type = "gp2"
volume_size = 11 volume_size = 11
} }
block_device { ebs_block_device {
device_name = "/dev/sdb" device_name = "/dev/sdb"
volume_size = 9 volume_size = 9
} }
block_device { ebs_block_device {
device_name = "/dev/sdc" device_name = "/dev/sdc"
volume_size = 10 volume_size = 10
volume_type = "io1" volume_type = "io1"
iops = 100 iops = 100
} }
ephemeral_block_device {
device_name = "/dev/sde"
virtual_name = "ephemeral0"
}
} }
` `

View File

@ -199,39 +199,14 @@ func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{})
d.Id(), d.Id(),
vpcID.(string)) vpcID.(string))
wait := true
err := ec2conn.DetachInternetGateway(&ec2.DetachInternetGatewayRequest{
InternetGatewayID: aws.String(d.Id()),
VPCID: aws.String(vpcID.(string)),
})
if err != nil {
ec2err, ok := err.(aws.APIError)
if ok {
if ec2err.Code == "InvalidInternetGatewayID.NotFound" {
err = nil
wait = false
} else if ec2err.Code == "Gateway.NotAttached" {
err = nil
wait = false
}
}
if err != nil {
return err
}
}
if !wait {
return nil
}
// Wait for it to be fully detached before continuing // Wait for it to be fully detached before continuing
log.Printf("[DEBUG] Waiting for internet gateway (%s) to detach", d.Id()) log.Printf("[DEBUG] Waiting for internet gateway (%s) to detach", d.Id())
stateConf := &resource.StateChangeConf{ stateConf := &resource.StateChangeConf{
Pending: []string{"attached", "detaching", "available"}, Pending: []string{"detaching"},
Target: "detached", Target: "detached",
Refresh: IGAttachStateRefreshFunc(ec2conn, d.Id(), "detached"), Refresh: detachIGStateRefreshFunc(ec2conn, d.Id(), vpcID.(string)),
Timeout: 1 * time.Minute, Timeout: 2 * time.Minute,
Delay: 10 * time.Second,
} }
if _, err := stateConf.WaitForState(); err != nil { if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf( return fmt.Errorf(
@ -242,6 +217,32 @@ func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{})
return nil return nil
} }
// InstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
// an EC2 instance.
func detachIGStateRefreshFunc(conn *ec2.EC2, instanceID, vpcID string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
err := conn.DetachInternetGateway(&ec2.DetachInternetGatewayRequest{
InternetGatewayID: aws.String(instanceID),
VPCID: aws.String(vpcID),
})
if err != nil {
ec2err, ok := err.(aws.APIError)
if ok {
if ec2err.Code == "InvalidInternetGatewayID.NotFound" {
return nil, "Not Found", err
} else if ec2err.Code == "Gateway.NotAttached" {
return "detached", "detached", nil
} else if ec2err.Code == "DependencyViolation" {
return nil, "detaching", nil
}
}
}
// DetachInternetGateway only returns an error, so if it's nil, assume we're
// detached
return "detached", "detached", nil
}
}
// IGStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch // IGStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
// an internet gateway. // an internet gateway.
func IGStateRefreshFunc(ec2conn *ec2.EC2, id string) resource.StateRefreshFunc { func IGStateRefreshFunc(ec2conn *ec2.EC2, id string) resource.StateRefreshFunc {
@ -300,10 +301,6 @@ func IGAttachStateRefreshFunc(ec2conn *ec2.EC2, id string, expected string) reso
ig := &resp.InternetGateways[0] ig := &resp.InternetGateways[0]
if time.Now().Sub(start) > 10*time.Second {
return ig, expected, nil
}
if len(ig.Attachments) == 0 { if len(ig.Attachments) == 0 {
// No attachments, we're detached // No attachments, we're detached
return ig, "detached", nil return ig, "detached", nil

View File

@ -0,0 +1,271 @@
package aws
import (
"bytes"
"fmt"
"log"
"strconv"
"time"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsNetworkInterface() *schema.Resource {
return &schema.Resource{
Create: resourceAwsNetworkInterfaceCreate,
Read: resourceAwsNetworkInterfaceRead,
Update: resourceAwsNetworkInterfaceUpdate,
Delete: resourceAwsNetworkInterfaceDelete,
Schema: map[string]*schema.Schema{
"subnet_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"private_ips": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: func(v interface{}) int {
return hashcode.String(v.(string))
},
},
"security_groups": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: func(v interface{}) int {
return hashcode.String(v.(string))
},
},
"attachment": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"instance": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"device_index": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"attachment_id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
},
Set: resourceAwsEniAttachmentHash,
},
"tags": tagsSchema(),
},
}
}
func resourceAwsNetworkInterfaceCreate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
request := &ec2.CreateNetworkInterfaceRequest{
Groups: expandStringList(d.Get("security_groups").(*schema.Set).List()),
SubnetID: aws.String(d.Get("subnet_id").(string)),
PrivateIPAddresses: expandPrivateIPAddesses(d.Get("private_ips").(*schema.Set).List()),
}
log.Printf("[DEBUG] Creating network interface")
resp, err := ec2conn.CreateNetworkInterface(request)
if err != nil {
return fmt.Errorf("Error creating ENI: %s", err)
}
d.SetId(*resp.NetworkInterface.NetworkInterfaceID)
log.Printf("[INFO] ENI ID: %s", d.Id())
return resourceAwsNetworkInterfaceUpdate(d, meta)
}
func resourceAwsNetworkInterfaceRead(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
describe_network_interfaces_request := &ec2.DescribeNetworkInterfacesRequest{
NetworkInterfaceIDs: []string{d.Id()},
}
describeResp, err := ec2conn.DescribeNetworkInterfaces(describe_network_interfaces_request)
if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidNetworkInterfaceID.NotFound" {
// The ENI is gone now, so just remove it from the state
d.SetId("")
return nil
}
return fmt.Errorf("Error retrieving ENI: %s", err)
}
if len(describeResp.NetworkInterfaces) != 1 {
return fmt.Errorf("Unable to find ENI: %#v", describeResp.NetworkInterfaces)
}
eni := describeResp.NetworkInterfaces[0]
d.Set("subnet_id", eni.SubnetID)
d.Set("private_ips", flattenNetworkInterfacesPrivateIPAddesses(eni.PrivateIPAddresses))
d.Set("security_groups", flattenGroupIdentifiers(eni.Groups))
// Tags
d.Set("tags", tagsToMap(eni.TagSet))
if eni.Attachment != nil {
attachment := []map[string]interface{}{flattenAttachment(eni.Attachment)}
d.Set("attachment", attachment)
} else {
d.Set("attachment", nil)
}
return nil
}
func networkInterfaceAttachmentRefreshFunc(ec2conn *ec2.EC2, id string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
describe_network_interfaces_request := &ec2.DescribeNetworkInterfacesRequest{
NetworkInterfaceIDs: []string{id},
}
describeResp, err := ec2conn.DescribeNetworkInterfaces(describe_network_interfaces_request)
if err != nil {
log.Printf("[ERROR] Could not find network interface %s. %s", id, err)
return nil, "", err
}
eni := describeResp.NetworkInterfaces[0]
hasAttachment := strconv.FormatBool(eni.Attachment != nil)
log.Printf("[DEBUG] ENI %s has attachment state %s", id, hasAttachment)
return eni, hasAttachment, nil
}
}
func resourceAwsNetworkInterfaceDetach(oa *schema.Set, meta interface{}, eniId string) error {
// if there was an old attachment, remove it
if oa != nil && len(oa.List()) > 0 {
old_attachment := oa.List()[0].(map[string]interface{})
detach_request := &ec2.DetachNetworkInterfaceRequest{
AttachmentID: aws.String(old_attachment["attachment_id"].(string)),
Force: aws.Boolean(true),
}
ec2conn := meta.(*AWSClient).ec2conn
detach_err := ec2conn.DetachNetworkInterface(detach_request)
if detach_err != nil {
return fmt.Errorf("Error detaching ENI: %s", detach_err)
}
log.Printf("[DEBUG] Waiting for ENI (%s) to become dettached", eniId)
stateConf := &resource.StateChangeConf{
Pending: []string{"true"},
Target: "false",
Refresh: networkInterfaceAttachmentRefreshFunc(ec2conn, eniId),
Timeout: 10 * time.Minute,
}
if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf(
"Error waiting for ENI (%s) to become dettached: %s", eniId, err)
}
}
return nil
}
func resourceAwsNetworkInterfaceUpdate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
d.Partial(true)
if d.HasChange("attachment") {
ec2conn := meta.(*AWSClient).ec2conn
oa, na := d.GetChange("attachment")
detach_err := resourceAwsNetworkInterfaceDetach(oa.(*schema.Set), meta, d.Id())
if detach_err != nil {
return detach_err
}
// if there is a new attachment, attach it
if na != nil && len(na.(*schema.Set).List()) > 0 {
new_attachment := na.(*schema.Set).List()[0].(map[string]interface{})
attach_request := &ec2.AttachNetworkInterfaceRequest{
DeviceIndex: aws.Integer(new_attachment["device_index"].(int)),
InstanceID: aws.String(new_attachment["instance"].(string)),
NetworkInterfaceID: aws.String(d.Id()),
}
_, attach_err := ec2conn.AttachNetworkInterface(attach_request)
if attach_err != nil {
return fmt.Errorf("Error attaching ENI: %s", attach_err)
}
}
d.SetPartial("attachment")
}
if d.HasChange("security_groups") {
request := &ec2.ModifyNetworkInterfaceAttributeRequest{
NetworkInterfaceID: aws.String(d.Id()),
Groups: expandStringList(d.Get("security_groups").(*schema.Set).List()),
}
err := ec2conn.ModifyNetworkInterfaceAttribute(request)
if err != nil {
return fmt.Errorf("Failure updating ENI: %s", err)
}
d.SetPartial("security_groups")
}
if err := setTags(ec2conn, d); err != nil {
return err
} else {
d.SetPartial("tags")
}
d.Partial(false)
return resourceAwsNetworkInterfaceRead(d, meta)
}
func resourceAwsNetworkInterfaceDelete(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
log.Printf("[INFO] Deleting ENI: %s", d.Id())
detach_err := resourceAwsNetworkInterfaceDetach(d.Get("attachment").(*schema.Set), meta, d.Id())
if detach_err != nil {
return detach_err
}
deleteEniOpts := ec2.DeleteNetworkInterfaceRequest{
NetworkInterfaceID: aws.String(d.Id()),
}
if err := ec2conn.DeleteNetworkInterface(&deleteEniOpts); err != nil {
return fmt.Errorf("Error deleting ENI: %s", err)
}
return nil
}
func resourceAwsEniAttachmentHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["instance"].(string)))
buf.WriteString(fmt.Sprintf("%d-", m["device_index"].(int)))
return hashcode.String(buf.String())
}

View File

@ -0,0 +1,239 @@
package aws
import (
"fmt"
"testing"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSENI_basic(t *testing.T) {
var conf ec2.NetworkInterface
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSENIDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSENIConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSENIExists("aws_network_interface.bar", &conf),
testAccCheckAWSENIAttributes(&conf),
resource.TestCheckResourceAttr(
"aws_network_interface.bar", "private_ips.#", "1"),
resource.TestCheckResourceAttr(
"aws_network_interface.bar", "tags.Name", "bar_interface"),
),
},
},
})
}
func TestAccAWSENI_attached(t *testing.T) {
var conf ec2.NetworkInterface
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSENIDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSENIConfigWithAttachment,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSENIExists("aws_network_interface.bar", &conf),
testAccCheckAWSENIAttributesWithAttachment(&conf),
resource.TestCheckResourceAttr(
"aws_network_interface.bar", "private_ips.#", "1"),
resource.TestCheckResourceAttr(
"aws_network_interface.bar", "tags.Name", "bar_interface"),
),
},
},
})
}
func testAccCheckAWSENIExists(n string, res *ec2.NetworkInterface) 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 ENI ID is set")
}
ec2conn := testAccProvider.Meta().(*AWSClient).ec2conn
describe_network_interfaces_request := &ec2.DescribeNetworkInterfacesRequest{
NetworkInterfaceIDs: []string{rs.Primary.ID},
}
describeResp, err := ec2conn.DescribeNetworkInterfaces(describe_network_interfaces_request)
if err != nil {
return err
}
if len(describeResp.NetworkInterfaces) != 1 ||
*describeResp.NetworkInterfaces[0].NetworkInterfaceID != rs.Primary.ID {
return fmt.Errorf("ENI not found")
}
*res = describeResp.NetworkInterfaces[0]
return nil
}
}
func testAccCheckAWSENIAttributes(conf *ec2.NetworkInterface) resource.TestCheckFunc {
return func(s *terraform.State) error {
if conf.Attachment != nil {
return fmt.Errorf("expected attachment to be nil")
}
if *conf.AvailabilityZone != "us-west-2a" {
return fmt.Errorf("expected availability_zone to be us-west-2a, but was %s", *conf.AvailabilityZone)
}
if len(conf.Groups) != 1 && *conf.Groups[0].GroupName != "foo" {
return fmt.Errorf("expected security group to be foo, but was %#v", conf.Groups)
}
if *conf.PrivateIPAddress != "172.16.10.100" {
return fmt.Errorf("expected private ip to be 172.16.10.100, but was %s", *conf.PrivateIPAddress)
}
if len(conf.TagSet) == 0 {
return fmt.Errorf("expected tags")
}
return nil
}
}
func testAccCheckAWSENIAttributesWithAttachment(conf *ec2.NetworkInterface) resource.TestCheckFunc {
return func(s *terraform.State) error {
if conf.Attachment == nil {
return fmt.Errorf("expected attachment to be set, but was nil")
}
if *conf.Attachment.DeviceIndex != 1 {
return fmt.Errorf("expected attachment device index to be 1, but was %d", *conf.Attachment.DeviceIndex)
}
if *conf.AvailabilityZone != "us-west-2a" {
return fmt.Errorf("expected availability_zone to be us-west-2a, but was %s", *conf.AvailabilityZone)
}
if len(conf.Groups) != 1 && *conf.Groups[0].GroupName != "foo" {
return fmt.Errorf("expected security group to be foo, but was %#v", conf.Groups)
}
if *conf.PrivateIPAddress != "172.16.10.100" {
return fmt.Errorf("expected private ip to be 172.16.10.100, but was %s", *conf.PrivateIPAddress)
}
return nil
}
}
func testAccCheckAWSENIDestroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_network_interface" {
continue
}
ec2conn := testAccProvider.Meta().(*AWSClient).ec2conn
describe_network_interfaces_request := &ec2.DescribeNetworkInterfacesRequest{
NetworkInterfaceIDs: []string{rs.Primary.ID},
}
_, err := ec2conn.DescribeNetworkInterfaces(describe_network_interfaces_request)
if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidNetworkInterfaceID.NotFound" {
return nil
}
return err
}
}
return nil
}
const testAccAWSENIConfig = `
resource "aws_vpc" "foo" {
cidr_block = "172.16.0.0/16"
}
resource "aws_subnet" "foo" {
vpc_id = "${aws_vpc.foo.id}"
cidr_block = "172.16.10.0/24"
availability_zone = "us-west-2a"
}
resource "aws_security_group" "foo" {
vpc_id = "${aws_vpc.foo.id}"
description = "foo"
name = "foo"
}
resource "aws_network_interface" "bar" {
subnet_id = "${aws_subnet.foo.id}"
private_ips = ["172.16.10.100"]
security_groups = ["${aws_security_group.foo.id}"]
tags {
Name = "bar_interface"
}
}
`
const testAccAWSENIConfigWithAttachment = `
resource "aws_vpc" "foo" {
cidr_block = "172.16.0.0/16"
}
resource "aws_subnet" "foo" {
vpc_id = "${aws_vpc.foo.id}"
cidr_block = "172.16.10.0/24"
availability_zone = "us-west-2a"
}
resource "aws_subnet" "bar" {
vpc_id = "${aws_vpc.foo.id}"
cidr_block = "172.16.11.0/24"
availability_zone = "us-west-2a"
}
resource "aws_security_group" "foo" {
vpc_id = "${aws_vpc.foo.id}"
description = "foo"
name = "foo"
}
resource "aws_instance" "foo" {
ami = "ami-c5eabbf5"
instance_type = "t2.micro"
subnet_id = "${aws_subnet.bar.id}"
associate_public_ip_address = false
private_ip = "172.16.11.50"
}
resource "aws_network_interface" "bar" {
subnet_id = "${aws_subnet.foo.id}"
private_ips = ["172.16.10.100"]
security_groups = ["${aws_security_group.foo.id}"]
attachment {
instance = "${aws_instance.foo.id}"
device_index = 1
}
tags {
Name = "bar_interface"
}
}
`

View File

@ -67,17 +67,8 @@ func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) er
return err return err
} }
// Check if the current record name contains the zone suffix.
// If it does not, add the zone name to form a fully qualified name
// and keep AWS happy.
recordName := d.Get("name").(string)
zoneName := strings.Trim(*zoneRecord.HostedZone.Name, ".")
if !strings.HasSuffix(recordName, zoneName) {
d.Set("name", strings.Join([]string{recordName, zoneName}, "."))
}
// Get the record // Get the record
rec, err := resourceAwsRoute53RecordBuildSet(d) rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name)
if err != nil { if err != nil {
return err return err
} }
@ -101,7 +92,7 @@ func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) er
} }
log.Printf("[DEBUG] Creating resource records for zone: %s, name: %s", log.Printf("[DEBUG] Creating resource records for zone: %s, name: %s",
zone, d.Get("name").(string)) zone, *rec.Name)
wait := resource.StateChangeConf{ wait := resource.StateChangeConf{
Pending: []string{"rejected"}, Pending: []string{"rejected"},
@ -111,10 +102,12 @@ func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) er
Refresh: func() (interface{}, string, error) { Refresh: func() (interface{}, string, error) {
resp, err := conn.ChangeResourceRecordSets(req) resp, err := conn.ChangeResourceRecordSets(req)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "PriorRequestNotComplete") { if r53err, ok := err.(aws.APIError); ok {
// There is some pending operation, so just retry if r53err.Code == "PriorRequestNotComplete" {
// in a bit. // There is some pending operation, so just retry
return nil, "rejected", nil // in a bit.
return nil, "rejected", nil
}
} }
return nil, "failure", err return nil, "failure", err
@ -159,9 +152,17 @@ func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) erro
conn := meta.(*AWSClient).r53conn conn := meta.(*AWSClient).r53conn
zone := d.Get("zone_id").(string) zone := d.Get("zone_id").(string)
// get expanded name
zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(zone)})
if err != nil {
return err
}
en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name)
lopts := &route53.ListResourceRecordSetsRequest{ lopts := &route53.ListResourceRecordSetsRequest{
HostedZoneID: aws.String(cleanZoneID(zone)), HostedZoneID: aws.String(cleanZoneID(zone)),
StartRecordName: aws.String(d.Get("name").(string)), StartRecordName: aws.String(en),
StartRecordType: aws.String(d.Get("type").(string)), StartRecordType: aws.String(d.Get("type").(string)),
} }
@ -202,9 +203,12 @@ func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) er
zone := d.Get("zone_id").(string) zone := d.Get("zone_id").(string)
log.Printf("[DEBUG] Deleting resource records for zone: %s, name: %s", log.Printf("[DEBUG] Deleting resource records for zone: %s, name: %s",
zone, d.Get("name").(string)) zone, d.Get("name").(string))
zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(zone)})
if err != nil {
return err
}
// Get the records // Get the records
rec, err := resourceAwsRoute53RecordBuildSet(d) rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name)
if err != nil { if err != nil {
return err return err
} }
@ -260,16 +264,30 @@ func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) er
return nil return nil
} }
func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData) (*route53.ResourceRecordSet, error) { func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) (*route53.ResourceRecordSet, error) {
recs := d.Get("records").(*schema.Set).List() recs := d.Get("records").(*schema.Set).List()
records := make([]route53.ResourceRecord, 0, len(recs)) records := make([]route53.ResourceRecord, 0, len(recs))
typeStr := d.Get("type").(string)
for _, r := range recs { for _, r := range recs {
records = append(records, route53.ResourceRecord{Value: aws.String(r.(string))}) switch typeStr {
case "TXT":
str := fmt.Sprintf("\"%s\"", r.(string))
records = append(records, route53.ResourceRecord{Value: aws.String(str)})
default:
records = append(records, route53.ResourceRecord{Value: aws.String(r.(string))})
}
} }
// get expanded name
en := expandRecordName(d.Get("name").(string), zoneName)
// Create the RecordSet request with the fully expanded name, e.g.
// sub.domain.com. Route 53 requires a fully qualified domain name, but does
// not require the trailing ".", which it will itself, so we don't call FQDN
// here.
rec := &route53.ResourceRecordSet{ rec := &route53.ResourceRecordSet{
Name: aws.String(d.Get("name").(string)), Name: aws.String(en),
Type: aws.String(d.Get("type").(string)), Type: aws.String(d.Get("type").(string)),
TTL: aws.Long(int64(d.Get("ttl").(int))), TTL: aws.Long(int64(d.Get("ttl").(int))),
ResourceRecords: records, ResourceRecords: records,
@ -297,3 +315,15 @@ func cleanRecordName(name string) string {
} }
return str return str
} }
// Check if the current record name contains the zone suffix.
// If it does not, add the zone name to form a fully qualified name
// and keep AWS happy.
func expandRecordName(name, zone string) string {
rn := strings.TrimSuffix(name, ".")
zone = strings.TrimSuffix(zone, ".")
if !strings.HasSuffix(rn, zone) {
rn = strings.Join([]string{name, zone}, ".")
}
return rn
}

View File

@ -29,6 +29,27 @@ func TestCleanRecordName(t *testing.T) {
} }
} }
func TestExpandRecordName(t *testing.T) {
cases := []struct {
Input, Output string
}{
{"www", "www.nonexample.com"},
{"dev.www", "dev.www.nonexample.com"},
{"*", "*.nonexample.com"},
{"nonexample.com", "nonexample.com"},
{"test.nonexample.com", "test.nonexample.com"},
{"test.nonexample.com.", "test.nonexample.com"},
}
zone_name := "nonexample.com"
for _, tc := range cases {
actual := expandRecordName(tc.Input, zone_name)
if actual != tc.Output {
t.Fatalf("input: %s\noutput: %s", tc.Input, actual)
}
}
}
func TestAccRoute53Record(t *testing.T) { func TestAccRoute53Record(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
@ -45,6 +66,22 @@ func TestAccRoute53Record(t *testing.T) {
}) })
} }
func TestAccRoute53Record_txtSupport(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckRoute53RecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccRoute53RecordConfigTXT,
Check: resource.ComposeTestCheckFunc(
testAccCheckRoute53RecordExists("aws_route53_record.default"),
),
},
},
})
}
func TestAccRoute53Record_generatesSuffix(t *testing.T) { func TestAccRoute53Record_generatesSuffix(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
@ -135,9 +172,11 @@ func testAccCheckRoute53RecordExists(n string) resource.TestCheckFunc {
name := parts[1] name := parts[1]
rType := parts[2] rType := parts[2]
en := expandRecordName(name, "notexample.com")
lopts := &route53.ListResourceRecordSetsRequest{ lopts := &route53.ListResourceRecordSetsRequest{
HostedZoneID: aws.String(cleanZoneID(zone)), HostedZoneID: aws.String(cleanZoneID(zone)),
StartRecordName: aws.String(name), StartRecordName: aws.String(en),
StartRecordType: aws.String(rType), StartRecordType: aws.String(rType),
} }
@ -151,7 +190,7 @@ func testAccCheckRoute53RecordExists(n string) resource.TestCheckFunc {
// rec := resp.ResourceRecordSets[0] // rec := resp.ResourceRecordSets[0]
for _, rec := range resp.ResourceRecordSets { for _, rec := range resp.ResourceRecordSets {
recName := cleanRecordName(*rec.Name) recName := cleanRecordName(*rec.Name)
if FQDN(recName) == FQDN(name) && *rec.Type == rType { if FQDN(recName) == FQDN(en) && *rec.Type == rType {
return nil return nil
} }
} }
@ -230,3 +269,16 @@ resource "aws_route53_record" "wildcard" {
records = ["127.0.0.1"] records = ["127.0.0.1"]
} }
` `
const testAccRoute53RecordConfigTXT = `
resource "aws_route53_zone" "main" {
name = "notexample.com"
}
resource "aws_route53_record" "default" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "subdomain"
type = "TXT"
ttl = "30"
records = ["lalalala"]
}
`

View File

@ -16,6 +16,7 @@ func resourceAwsRoute53Zone() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
Create: resourceAwsRoute53ZoneCreate, Create: resourceAwsRoute53ZoneCreate,
Read: resourceAwsRoute53ZoneRead, Read: resourceAwsRoute53ZoneRead,
Update: resourceAwsRoute53ZoneUpdate,
Delete: resourceAwsRoute53ZoneDelete, Delete: resourceAwsRoute53ZoneDelete,
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
@ -29,6 +30,8 @@ func resourceAwsRoute53Zone() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Computed: true, Computed: true,
}, },
"tags": tagsSchema(),
}, },
} }
} }
@ -72,7 +75,7 @@ func resourceAwsRoute53ZoneCreate(d *schema.ResourceData, meta interface{}) erro
if err != nil { if err != nil {
return err return err
} }
return nil return resourceAwsRoute53ZoneUpdate(d, meta)
} }
func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error { func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error {
@ -87,9 +90,41 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error
return err return err
} }
// get tags
req := &route53.ListTagsForResourceRequest{
ResourceID: aws.String(d.Id()),
ResourceType: aws.String("hostedzone"),
}
resp, err := r53.ListTagsForResource(req)
if err != nil {
return err
}
var tags []route53.Tag
if resp.ResourceTagSet != nil {
tags = resp.ResourceTagSet.Tags
}
if err := d.Set("tags", tagsToMapR53(tags)); err != nil {
return err
}
return nil return nil
} }
func resourceAwsRoute53ZoneUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).r53conn
if err := setTagsR53(conn, d); err != nil {
return err
} else {
d.SetPartial("tags")
}
return resourceAwsRoute53ZoneRead(d, meta)
}
func resourceAwsRoute53ZoneDelete(d *schema.ResourceData, meta interface{}) error { func resourceAwsRoute53ZoneDelete(d *schema.ResourceData, meta interface{}) error {
r53 := meta.(*AWSClient).r53conn r53 := meta.(*AWSClient).r53conn

View File

@ -63,6 +63,9 @@ func TestCleanChangeID(t *testing.T) {
} }
func TestAccRoute53Zone(t *testing.T) { func TestAccRoute53Zone(t *testing.T) {
var zone route53.HostedZone
var td route53.ResourceTagSet
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,
@ -71,7 +74,9 @@ func TestAccRoute53Zone(t *testing.T) {
resource.TestStep{ resource.TestStep{
Config: testAccRoute53ZoneConfig, Config: testAccRoute53ZoneConfig,
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckRoute53ZoneExists("aws_route53_zone.main"), testAccCheckRoute53ZoneExists("aws_route53_zone.main", &zone),
testAccLoadTagsR53(&zone, &td),
testAccCheckTagsR53(&td.Tags, "foo", "bar"),
), ),
}, },
}, },
@ -93,7 +98,7 @@ func testAccCheckRoute53ZoneDestroy(s *terraform.State) error {
return nil return nil
} }
func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc { func testAccCheckRoute53ZoneExists(n string, zone *route53.HostedZone) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n] rs, ok := s.RootModule().Resources[n]
if !ok { if !ok {
@ -105,10 +110,34 @@ func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc {
} }
conn := testAccProvider.Meta().(*AWSClient).r53conn conn := testAccProvider.Meta().(*AWSClient).r53conn
_, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(rs.Primary.ID)}) resp, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(rs.Primary.ID)})
if err != nil { if err != nil {
return fmt.Errorf("Hosted zone err: %v", err) return fmt.Errorf("Hosted zone err: %v", err)
} }
*zone = *resp.HostedZone
return nil
}
}
func testAccLoadTagsR53(zone *route53.HostedZone, td *route53.ResourceTagSet) resource.TestCheckFunc {
return func(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).r53conn
zone := cleanZoneID(*zone.ID)
req := &route53.ListTagsForResourceRequest{
ResourceID: aws.String(zone),
ResourceType: aws.String("hostedzone"),
}
resp, err := conn.ListTagsForResource(req)
if err != nil {
return err
}
if resp.ResourceTagSet != nil {
*td = *resp.ResourceTagSet
}
return nil return nil
} }
} }
@ -116,5 +145,10 @@ func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc {
const testAccRoute53ZoneConfig = ` const testAccRoute53ZoneConfig = `
resource "aws_route53_zone" "main" { resource "aws_route53_zone" "main" {
name = "hashicorp.com" name = "hashicorp.com"
tags {
foo = "bar"
Name = "tf-route53-tag-test"
}
} }
` `

View File

@ -107,6 +107,7 @@ func resourceAwsRouteTableRead(d *schema.ResourceData, meta interface{}) error {
return err return err
} }
if rtRaw == nil { if rtRaw == nil {
d.SetId("")
return nil return nil
} }

View File

@ -14,6 +14,7 @@ func resourceAwsS3Bucket() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
Create: resourceAwsS3BucketCreate, Create: resourceAwsS3BucketCreate,
Read: resourceAwsS3BucketRead, Read: resourceAwsS3BucketRead,
Update: resourceAwsS3BucketUpdate,
Delete: resourceAwsS3BucketDelete, Delete: resourceAwsS3BucketDelete,
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
@ -29,6 +30,8 @@ func resourceAwsS3Bucket() *schema.Resource {
Optional: true, Optional: true,
ForceNew: true, ForceNew: true,
}, },
"tags": tagsSchema(),
}, },
} }
} }
@ -64,7 +67,15 @@ func resourceAwsS3BucketCreate(d *schema.ResourceData, meta interface{}) error {
// Assign the bucket name as the resource ID // Assign the bucket name as the resource ID
d.SetId(bucket) d.SetId(bucket)
return nil return resourceAwsS3BucketUpdate(d, meta)
}
func resourceAwsS3BucketUpdate(d *schema.ResourceData, meta interface{}) error {
s3conn := meta.(*AWSClient).s3conn
if err := setTagsS3(s3conn, d); err != nil {
return err
}
return resourceAwsS3BucketRead(d, meta)
} }
func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error { func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
@ -76,6 +87,16 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
if err != nil { if err != nil {
return err return err
} }
tagSet, err := getTagSetS3(s3conn, d.Id())
if err != nil {
return err
}
if err := d.Set("tags", tagsToMapS3(tagSet)); err != nil {
return err
}
return nil return nil
} }

View File

@ -285,6 +285,7 @@ func resourceAwsSecurityGroupRuleHash(v interface{}) int {
buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int))) buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int)))
buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int))) buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string))) buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string)))
buf.WriteString(fmt.Sprintf("%t-", m["self"].(bool)))
// We need to make sure to sort the strings below so that we always // We need to make sure to sort the strings below so that we always
// generate the same hash code no matter what is in the set. // generate the same hash code no matter what is in the set.

View File

@ -30,15 +30,15 @@ func TestAccAWSSecurityGroup_normal(t *testing.T) {
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "description", "Used in the terraform acceptance tests"), "aws_security_group.web", "description", "Used in the terraform acceptance tests"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "ingress.332851786.protocol", "tcp"), "aws_security_group.web", "ingress.3629188364.protocol", "tcp"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "ingress.332851786.from_port", "80"), "aws_security_group.web", "ingress.3629188364.from_port", "80"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "ingress.332851786.to_port", "8000"), "aws_security_group.web", "ingress.3629188364.to_port", "8000"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "ingress.332851786.cidr_blocks.#", "1"), "aws_security_group.web", "ingress.3629188364.cidr_blocks.#", "1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "ingress.332851786.cidr_blocks.0", "10.0.0.0/8"), "aws_security_group.web", "ingress.3629188364.cidr_blocks.0", "10.0.0.0/8"),
), ),
}, },
}, },
@ -116,25 +116,25 @@ func TestAccAWSSecurityGroup_vpc(t *testing.T) {
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "description", "Used in the terraform acceptance tests"), "aws_security_group.web", "description", "Used in the terraform acceptance tests"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "ingress.332851786.protocol", "tcp"), "aws_security_group.web", "ingress.3629188364.protocol", "tcp"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "ingress.332851786.from_port", "80"), "aws_security_group.web", "ingress.3629188364.from_port", "80"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "ingress.332851786.to_port", "8000"), "aws_security_group.web", "ingress.3629188364.to_port", "8000"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "ingress.332851786.cidr_blocks.#", "1"), "aws_security_group.web", "ingress.3629188364.cidr_blocks.#", "1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "ingress.332851786.cidr_blocks.0", "10.0.0.0/8"), "aws_security_group.web", "ingress.3629188364.cidr_blocks.0", "10.0.0.0/8"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "egress.332851786.protocol", "tcp"), "aws_security_group.web", "egress.3629188364.protocol", "tcp"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "egress.332851786.from_port", "80"), "aws_security_group.web", "egress.3629188364.from_port", "80"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "egress.332851786.to_port", "8000"), "aws_security_group.web", "egress.3629188364.to_port", "8000"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "egress.332851786.cidr_blocks.#", "1"), "aws_security_group.web", "egress.3629188364.cidr_blocks.#", "1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_security_group.web", "egress.332851786.cidr_blocks.0", "10.0.0.0/8"), "aws_security_group.web", "egress.3629188364.cidr_blocks.0", "10.0.0.0/8"),
testCheck, testCheck,
), ),
}, },

View File

@ -159,17 +159,38 @@ func resourceAwsSubnetDelete(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).ec2conn
log.Printf("[INFO] Deleting subnet: %s", d.Id()) log.Printf("[INFO] Deleting subnet: %s", d.Id())
req := &ec2.DeleteSubnetRequest{
err := ec2conn.DeleteSubnet(&ec2.DeleteSubnetRequest{
SubnetID: aws.String(d.Id()), SubnetID: aws.String(d.Id()),
}) }
if err != nil { wait := resource.StateChangeConf{
ec2err, ok := err.(aws.APIError) Pending: []string{"pending"},
if ok && ec2err.Code == "InvalidSubnetID.NotFound" { Target: "destroyed",
return nil Timeout: 5 * time.Minute,
} MinTimeout: 1 * time.Second,
Refresh: func() (interface{}, string, error) {
err := ec2conn.DeleteSubnet(req)
if err != nil {
if apiErr, ok := err.(aws.APIError); ok {
if apiErr.Code == "DependencyViolation" {
// There is some pending operation, so just retry
// in a bit.
return 42, "pending", nil
}
if apiErr.Code == "InvalidSubnetID.NotFound" {
return 42, "destroyed", nil
}
}
return 42, "failure", err
}
return 42, "destroyed", nil
},
}
if _, err := wait.WaitForState(); err != nil {
return fmt.Errorf("Error deleting subnet: %s", err) return fmt.Errorf("Error deleting subnet: %s", err)
} }

View File

@ -185,29 +185,32 @@ func resourceAwsVpcUpdate(d *schema.ResourceData, meta interface{}) error {
// Turn on partial mode // Turn on partial mode
d.Partial(true) d.Partial(true)
vpcid := d.Id() vpcid := d.Id()
modifyOpts := &ec2.ModifyVPCAttributeRequest{
VPCID: &vpcid,
}
if d.HasChange("enable_dns_hostnames") { if d.HasChange("enable_dns_hostnames") {
val := d.Get("enable_dns_hostnames").(bool) val := d.Get("enable_dns_hostnames").(bool)
modifyOpts.EnableDNSHostnames = &ec2.AttributeBooleanValue{ modifyOpts := &ec2.ModifyVPCAttributeRequest{
Value: &val, VPCID: &vpcid,
EnableDNSHostnames: &ec2.AttributeBooleanValue{
Value: &val,
},
} }
log.Printf( log.Printf(
"[INFO] Modifying enable_dns_hostnames vpc attribute for %s: %#v", "[INFO] Modifying enable_dns_support vpc attribute for %s: %#v",
d.Id(), modifyOpts) d.Id(), modifyOpts)
if err := ec2conn.ModifyVPCAttribute(modifyOpts); err != nil { if err := ec2conn.ModifyVPCAttribute(modifyOpts); err != nil {
return err return err
} }
d.SetPartial("enable_dns_hostnames") d.SetPartial("enable_dns_support")
} }
if d.HasChange("enable_dns_support") { if d.HasChange("enable_dns_support") {
val := d.Get("enable_dns_hostnames").(bool) val := d.Get("enable_dns_support").(bool)
modifyOpts.EnableDNSSupport = &ec2.AttributeBooleanValue{ modifyOpts := &ec2.ModifyVPCAttributeRequest{
Value: &val, VPCID: &vpcid,
EnableDNSSupport: &ec2.AttributeBooleanValue{
Value: &val,
},
} }
log.Printf( log.Printf(
@ -238,7 +241,7 @@ func resourceAwsVpcDelete(d *schema.ResourceData, meta interface{}) error {
} }
log.Printf("[INFO] Deleting VPC: %s", d.Id()) log.Printf("[INFO] Deleting VPC: %s", d.Id())
if err := ec2conn.DeleteVPC(DeleteVpcOpts); err != nil { if err := ec2conn.DeleteVPC(DeleteVpcOpts); err != nil {
ec2err, ok := err.(*aws.APIError) ec2err, ok := err.(aws.APIError)
if ok && ec2err.Code == "InvalidVpcID.NotFound" { if ok && ec2err.Code == "InvalidVpcID.NotFound" {
return nil return nil
} }
@ -258,7 +261,7 @@ func VPCStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc {
} }
resp, err := conn.DescribeVPCs(DescribeVpcOpts) resp, err := conn.DescribeVPCs(DescribeVpcOpts)
if err != nil { if err != nil {
if ec2err, ok := err.(*aws.APIError); ok && ec2err.Code == "InvalidVpcID.NotFound" { if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidVpcID.NotFound" {
resp = nil resp = nil
} else { } else {
log.Printf("Error on VPCStateRefresh: %s", err) log.Printf("Error on VPCStateRefresh: %s", err)

View File

@ -2,11 +2,12 @@ package aws
import ( import (
"fmt" "fmt"
"testing"
"github.com/hashicorp/aws-sdk-go/aws" "github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"testing"
) )
func TestAccVpc_basic(t *testing.T) { func TestAccVpc_basic(t *testing.T) {
@ -132,7 +133,7 @@ func testAccCheckVpcDestroy(s *terraform.State) error {
} }
// Verify the error is what we want // Verify the error is what we want
ec2err, ok := err.(*aws.APIError) ec2err, ok := err.(aws.APIError)
if !ok { if !ok {
return err return err
} }
@ -184,6 +185,26 @@ func testAccCheckVpcExists(n string, vpc *ec2.VPC) resource.TestCheckFunc {
} }
} }
// https://github.com/hashicorp/terraform/issues/1301
func TestAccVpc_bothDnsOptionsSet(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckVpcDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccVpcConfig_BothDnsOptions,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"aws_vpc.bar", "enable_dns_hostnames", "true"),
resource.TestCheckResourceAttr(
"aws_vpc.bar", "enable_dns_support", "true"),
),
},
},
})
}
const testAccVpcConfig = ` const testAccVpcConfig = `
resource "aws_vpc" "foo" { resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16" cidr_block = "10.1.0.0/16"
@ -223,3 +244,12 @@ resource "aws_vpc" "bar" {
cidr_block = "10.2.0.0/16" cidr_block = "10.2.0.0/16"
} }
` `
const testAccVpcConfig_BothDnsOptions = `
resource "aws_vpc" "bar" {
cidr_block = "10.2.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
}
`

View File

@ -0,0 +1,318 @@
package aws
import (
"fmt"
"log"
"time"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsVpnGateway() *schema.Resource {
return &schema.Resource{
Create: resourceAwsVpnGatewayCreate,
Read: resourceAwsVpnGatewayRead,
Update: resourceAwsVpnGatewayUpdate,
Delete: resourceAwsVpnGatewayDelete,
Schema: map[string]*schema.Schema{
"availability_zone": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"vpc_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"tags": tagsSchema(),
},
}
}
func resourceAwsVpnGatewayCreate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
createOpts := &ec2.CreateVPNGatewayRequest{
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
Type: aws.String("ipsec.1"),
}
// Create the VPN gateway
log.Printf("[DEBUG] Creating VPN gateway")
resp, err := ec2conn.CreateVPNGateway(createOpts)
if err != nil {
return fmt.Errorf("Error creating VPN gateway: %s", err)
}
// Get the ID and store it
vpnGateway := resp.VPNGateway
d.SetId(*vpnGateway.VPNGatewayID)
log.Printf("[INFO] VPN Gateway ID: %s", *vpnGateway.VPNGatewayID)
// Attach the VPN gateway to the correct VPC
return resourceAwsVpnGatewayUpdate(d, meta)
}
func resourceAwsVpnGatewayRead(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
vpnGatewayRaw, _, err := vpnGatewayStateRefreshFunc(ec2conn, d.Id())()
if err != nil {
return err
}
if vpnGatewayRaw == nil {
// Seems we have lost our VPN gateway
d.SetId("")
return nil
}
vpnGateway := vpnGatewayRaw.(*ec2.VPNGateway)
if len(vpnGateway.VPCAttachments) == 0 {
// Gateway exists but not attached to the VPC
d.Set("vpc_id", "")
} else {
d.Set("vpc_id", vpnGateway.VPCAttachments[0].VPCID)
}
d.Set("availability_zone", vpnGateway.AvailabilityZone)
d.Set("tags", tagsToMap(vpnGateway.Tags))
return nil
}
func resourceAwsVpnGatewayUpdate(d *schema.ResourceData, meta interface{}) error {
if d.HasChange("vpc_id") {
// If we're already attached, detach it first
if err := resourceAwsVpnGatewayDetach(d, meta); err != nil {
return err
}
// Attach the VPN gateway to the new vpc
if err := resourceAwsVpnGatewayAttach(d, meta); err != nil {
return err
}
}
ec2conn := meta.(*AWSClient).ec2conn
if err := setTags(ec2conn, d); err != nil {
return err
}
d.SetPartial("tags")
return resourceAwsVpnGatewayRead(d, meta)
}
func resourceAwsVpnGatewayDelete(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
// Detach if it is attached
if err := resourceAwsVpnGatewayDetach(d, meta); err != nil {
return err
}
log.Printf("[INFO] Deleting VPN gateway: %s", d.Id())
return resource.Retry(5*time.Minute, func() error {
err := ec2conn.DeleteVPNGateway(&ec2.DeleteVPNGatewayRequest{
VPNGatewayID: aws.String(d.Id()),
})
if err == nil {
return nil
}
ec2err, ok := err.(aws.APIError)
if !ok {
return err
}
switch ec2err.Code {
case "InvalidVpnGatewayID.NotFound":
return nil
case "IncorrectState":
return err // retry
}
return resource.RetryError{Err: err}
})
}
func resourceAwsVpnGatewayAttach(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
if d.Get("vpc_id").(string) == "" {
log.Printf(
"[DEBUG] Not attaching VPN Gateway '%s' as no VPC ID is set",
d.Id())
return nil
}
log.Printf(
"[INFO] Attaching VPN Gateway '%s' to VPC '%s'",
d.Id(),
d.Get("vpc_id").(string))
_, err := ec2conn.AttachVPNGateway(&ec2.AttachVPNGatewayRequest{
VPNGatewayID: aws.String(d.Id()),
VPCID: aws.String(d.Get("vpc_id").(string)),
})
if err != nil {
return err
}
// A note on the states below: the AWS docs (as of July, 2014) say
// that the states would be: attached, attaching, detached, detaching,
// but when running, I noticed that the state is usually "available" when
// it is attached.
// Wait for it to be fully attached before continuing
log.Printf("[DEBUG] Waiting for VPN gateway (%s) to attach", d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"detached", "attaching"},
Target: "available",
Refresh: VpnGatewayAttachStateRefreshFunc(ec2conn, d.Id(), "available"),
Timeout: 1 * time.Minute,
}
if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf(
"Error waiting for VPN gateway (%s) to attach: %s",
d.Id(), err)
}
return nil
}
func resourceAwsVpnGatewayDetach(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
// Get the old VPC ID to detach from
vpcID, _ := d.GetChange("vpc_id")
if vpcID.(string) == "" {
log.Printf(
"[DEBUG] Not detaching VPN Gateway '%s' as no VPC ID is set",
d.Id())
return nil
}
log.Printf(
"[INFO] Detaching VPN Gateway '%s' from VPC '%s'",
d.Id(),
vpcID.(string))
wait := true
err := ec2conn.DetachVPNGateway(&ec2.DetachVPNGatewayRequest{
VPNGatewayID: aws.String(d.Id()),
VPCID: aws.String(d.Get("vpc_id").(string)),
})
if err != nil {
ec2err, ok := err.(aws.APIError)
if ok {
if ec2err.Code == "InvalidVpnGatewayID.NotFound" {
err = nil
wait = false
} else if ec2err.Code == "InvalidVpnGatewayAttachment.NotFound" {
err = nil
wait = false
}
}
if err != nil {
return err
}
}
if !wait {
return nil
}
// Wait for it to be fully detached before continuing
log.Printf("[DEBUG] Waiting for VPN gateway (%s) to detach", d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"attached", "detaching", "available"},
Target: "detached",
Refresh: VpnGatewayAttachStateRefreshFunc(ec2conn, d.Id(), "detached"),
Timeout: 1 * time.Minute,
}
if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf(
"Error waiting for vpn gateway (%s) to detach: %s",
d.Id(), err)
}
return nil
}
// vpnGatewayStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch a VPNGateway.
func vpnGatewayStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.DescribeVPNGateways(&ec2.DescribeVPNGatewaysRequest{
VPNGatewayIDs: []string{id},
})
if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidVpnGatewayID.NotFound" {
resp = nil
} else {
log.Printf("[ERROR] Error on VpnGatewayStateRefresh: %s", err)
return nil, "", err
}
}
if resp == nil {
// Sometimes AWS just has consistency issues and doesn't see
// our instance yet. Return an empty state.
return nil, "", nil
}
vpnGateway := &resp.VPNGateways[0]
return vpnGateway, *vpnGateway.State, nil
}
}
// VpnGatewayAttachStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
// the state of a VPN gateway's attachment
func VpnGatewayAttachStateRefreshFunc(conn *ec2.EC2, id string, expected string) resource.StateRefreshFunc {
var start time.Time
return func() (interface{}, string, error) {
if start.IsZero() {
start = time.Now()
}
resp, err := conn.DescribeVPNGateways(&ec2.DescribeVPNGatewaysRequest{
VPNGatewayIDs: []string{id},
})
if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidVpnGatewayID.NotFound" {
resp = nil
} else {
log.Printf("[ERROR] Error on VpnGatewayStateRefresh: %s", err)
return nil, "", err
}
}
if resp == nil {
// Sometimes AWS just has consistency issues and doesn't see
// our instance yet. Return an empty state.
return nil, "", nil
}
vpnGateway := &resp.VPNGateways[0]
if time.Now().Sub(start) > 10*time.Second {
return vpnGateway, expected, nil
}
if len(vpnGateway.VPCAttachments) == 0 {
// No attachments, we're detached
return vpnGateway, "detached", nil
}
return vpnGateway, *vpnGateway.VPCAttachments[0].State, nil
}
}

View File

@ -0,0 +1,232 @@
package aws
import (
"fmt"
"testing"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSVpnGateway(t *testing.T) {
var v, v2 ec2.VPNGateway
testNotEqual := func(*terraform.State) error {
if len(v.VPCAttachments) == 0 {
return fmt.Errorf("VPN gateway A is not attached")
}
if len(v2.VPCAttachments) == 0 {
return fmt.Errorf("VPN gateway B is not attached")
}
id1 := v.VPCAttachments[0].VPCID
id2 := v2.VPCAttachments[0].VPCID
if id1 == id2 {
return fmt.Errorf("Both attachment IDs are the same")
}
return nil
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckVpnGatewayDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccVpnGatewayConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckVpnGatewayExists(
"aws_vpn_gateway.foo", &v),
),
},
resource.TestStep{
Config: testAccVpnGatewayConfigChangeVPC,
Check: resource.ComposeTestCheckFunc(
testAccCheckVpnGatewayExists(
"aws_vpn_gateway.foo", &v2),
testNotEqual,
),
},
},
})
}
func TestAccAWSVpnGateway_delete(t *testing.T) {
var vpnGateway ec2.VPNGateway
testDeleted := func(r string) resource.TestCheckFunc {
return func(s *terraform.State) error {
_, ok := s.RootModule().Resources[r]
if ok {
return fmt.Errorf("VPN Gateway %q should have been deleted", r)
}
return nil
}
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckVpnGatewayDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccVpnGatewayConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckVpnGatewayExists("aws_vpn_gateway.foo", &vpnGateway)),
},
resource.TestStep{
Config: testAccNoVpnGatewayConfig,
Check: resource.ComposeTestCheckFunc(testDeleted("aws_vpn_gateway.foo")),
},
},
})
}
func TestAccVpnGateway_tags(t *testing.T) {
var v ec2.VPNGateway
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckVpnGatewayDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckVpnGatewayConfigTags,
Check: resource.ComposeTestCheckFunc(
testAccCheckVpnGatewayExists("aws_vpn_gateway.foo", &v),
testAccCheckTags(&v.Tags, "foo", "bar"),
),
},
resource.TestStep{
Config: testAccCheckVpnGatewayConfigTagsUpdate,
Check: resource.ComposeTestCheckFunc(
testAccCheckVpnGatewayExists("aws_vpn_gateway.foo", &v),
testAccCheckTags(&v.Tags, "foo", ""),
testAccCheckTags(&v.Tags, "bar", "baz"),
),
},
},
})
}
func testAccCheckVpnGatewayDestroy(s *terraform.State) error {
ec2conn := testAccProvider.Meta().(*AWSClient).ec2conn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_vpn_gateway" {
continue
}
// Try to find the resource
resp, err := ec2conn.DescribeVPNGateways(&ec2.DescribeVPNGatewaysRequest{
VPNGatewayIDs: []string{rs.Primary.ID},
})
if err == nil {
if len(resp.VPNGateways) > 0 {
return fmt.Errorf("still exists")
}
return nil
}
// Verify the error is what we want
ec2err, ok := err.(aws.APIError)
if !ok {
return err
}
if ec2err.Code != "InvalidVpnGatewayID.NotFound" {
return err
}
}
return nil
}
func testAccCheckVpnGatewayExists(n string, ig *ec2.VPNGateway) 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 ID is set")
}
ec2conn := testAccProvider.Meta().(*AWSClient).ec2conn
resp, err := ec2conn.DescribeVPNGateways(&ec2.DescribeVPNGatewaysRequest{
VPNGatewayIDs: []string{rs.Primary.ID},
})
if err != nil {
return err
}
if len(resp.VPNGateways) == 0 {
return fmt.Errorf("VPNGateway not found")
}
*ig = resp.VPNGateways[0]
return nil
}
}
const testAccNoVpnGatewayConfig = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}
`
const testAccVpnGatewayConfig = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}
resource "aws_vpn_gateway" "foo" {
vpc_id = "${aws_vpc.foo.id}"
}
`
const testAccVpnGatewayConfigChangeVPC = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}
resource "aws_vpc" "bar" {
cidr_block = "10.2.0.0/16"
}
resource "aws_vpn_gateway" "foo" {
vpc_id = "${aws_vpc.bar.id}"
}
`
const testAccCheckVpnGatewayConfigTags = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}
resource "aws_vpn_gateway" "foo" {
vpc_id = "${aws_vpc.foo.id}"
tags {
foo = "bar"
}
}
`
const testAccCheckVpnGatewayConfigTagsUpdate = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}
resource "aws_vpn_gateway" "foo" {
vpc_id = "${aws_vpc.foo.id}"
tags {
bar = "baz"
}
}
`

View File

@ -0,0 +1,131 @@
package aws
import (
"crypto/md5"
"encoding/base64"
"encoding/xml"
"log"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/s3"
"github.com/hashicorp/terraform/helper/schema"
)
// setTags is a helper to set the tags for a resource. It expects the
// tags field to be named "tags"
func setTagsS3(conn *s3.S3, d *schema.ResourceData) error {
if d.HasChange("tags") {
oraw, nraw := d.GetChange("tags")
o := oraw.(map[string]interface{})
n := nraw.(map[string]interface{})
create, remove := diffTagsS3(tagsFromMapS3(o), tagsFromMapS3(n))
// Set tags
if len(remove) > 0 {
log.Printf("[DEBUG] Removing tags: %#v", remove)
err := conn.DeleteBucketTagging(&s3.DeleteBucketTaggingRequest{
Bucket: aws.String(d.Get("bucket").(string)),
})
if err != nil {
return err
}
}
if len(create) > 0 {
log.Printf("[DEBUG] Creating tags: %#v", create)
tagging := s3.Tagging{
TagSet: create,
XMLName: xml.Name{
Space: "http://s3.amazonaws.com/doc/2006-03-01/",
Local: "Tagging",
},
}
// AWS S3 API requires us to send a base64 encoded md5 hash of the
// content, which we need to build ourselves since aws-sdk-go does not.
b, err := xml.Marshal(tagging)
if err != nil {
return err
}
h := md5.New()
h.Write(b)
base := base64.StdEncoding.EncodeToString(h.Sum(nil))
req := &s3.PutBucketTaggingRequest{
Bucket: aws.String(d.Get("bucket").(string)),
ContentMD5: aws.String(base),
Tagging: &tagging,
}
err = conn.PutBucketTagging(req)
if err != nil {
return err
}
}
}
return nil
}
// diffTags takes our tags locally and the ones remotely and returns
// the set of tags that must be created, and the set of tags that must
// be destroyed.
func diffTagsS3(oldTags, newTags []s3.Tag) ([]s3.Tag, []s3.Tag) {
// First, we're creating everything we have
create := make(map[string]interface{})
for _, t := range newTags {
create[*t.Key] = *t.Value
}
// Build the list of what to remove
var remove []s3.Tag
for _, t := range oldTags {
old, ok := create[*t.Key]
if !ok || old != *t.Value {
// Delete it!
remove = append(remove, t)
}
}
return tagsFromMapS3(create), remove
}
// tagsFromMap returns the tags for the given map of data.
func tagsFromMapS3(m map[string]interface{}) []s3.Tag {
result := make([]s3.Tag, 0, len(m))
for k, v := range m {
result = append(result, s3.Tag{
Key: aws.String(k),
Value: aws.String(v.(string)),
})
}
return result
}
// tagsToMap turns the list of tags into a map.
func tagsToMapS3(ts []s3.Tag) map[string]string {
result := make(map[string]string)
for _, t := range ts {
result[*t.Key] = *t.Value
}
return result
}
// return a slice of s3 tags associated with the given s3 bucket. Essentially
// s3.GetBucketTagging, except returns an empty slice instead of an error when
// there are no tags.
func getTagSetS3(s3conn *s3.S3, bucket string) ([]s3.Tag, error) {
request := &s3.GetBucketTaggingRequest{
Bucket: aws.String(bucket),
}
response, err := s3conn.GetBucketTagging(request)
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "NoSuchTagSet" {
// There is no tag set associated with the bucket.
return []s3.Tag{}, nil
} else if err != nil {
return nil, err
}
return response.TagSet, nil
}

View File

@ -0,0 +1,85 @@
package aws
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/aws-sdk-go/gen/s3"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestDiffTagsS3(t *testing.T) {
cases := []struct {
Old, New map[string]interface{}
Create, Remove map[string]string
}{
// Basic add/remove
{
Old: map[string]interface{}{
"foo": "bar",
},
New: map[string]interface{}{
"bar": "baz",
},
Create: map[string]string{
"bar": "baz",
},
Remove: map[string]string{
"foo": "bar",
},
},
// Modify
{
Old: map[string]interface{}{
"foo": "bar",
},
New: map[string]interface{}{
"foo": "baz",
},
Create: map[string]string{
"foo": "baz",
},
Remove: map[string]string{
"foo": "bar",
},
},
}
for i, tc := range cases {
c, r := diffTagsS3(tagsFromMapS3(tc.Old), tagsFromMapS3(tc.New))
cm := tagsToMapS3(c)
rm := tagsToMapS3(r)
if !reflect.DeepEqual(cm, tc.Create) {
t.Fatalf("%d: bad create: %#v", i, cm)
}
if !reflect.DeepEqual(rm, tc.Remove) {
t.Fatalf("%d: bad remove: %#v", i, rm)
}
}
}
// testAccCheckTags can be used to check the tags on a resource.
func testAccCheckTagsS3(
ts *[]s3.Tag, key string, value string) resource.TestCheckFunc {
return func(s *terraform.State) error {
m := tagsToMapS3(*ts)
v, ok := m[key]
if value != "" && !ok {
return fmt.Errorf("Missing tag: %s", key)
} else if value == "" && ok {
return fmt.Errorf("Extra tag: %s", key)
}
if value == "" {
return nil
}
if v != value {
return fmt.Errorf("%s: bad value: %s", key, v)
}
return nil
}
}

View File

@ -207,3 +207,47 @@ func expandStringList(configured []interface{}) []string {
} }
return vs return vs
} }
//Flattens an array of private ip addresses into a []string, where the elements returned are the IP strings e.g. "192.168.0.0"
func flattenNetworkInterfacesPrivateIPAddesses(dtos []ec2.NetworkInterfacePrivateIPAddress) []string {
ips := make([]string, 0, len(dtos))
for _, v := range dtos {
ip := *v.PrivateIPAddress
ips = append(ips, ip)
}
return ips
}
//Flattens security group identifiers into a []string, where the elements returned are the GroupIDs
func flattenGroupIdentifiers(dtos []ec2.GroupIdentifier) []string {
ids := make([]string, 0, len(dtos))
for _, v := range dtos {
group_id := *v.GroupID
ids = append(ids, group_id)
}
return ids
}
//Expands an array of IPs into a ec2 Private IP Address Spec
func expandPrivateIPAddesses(ips []interface{}) []ec2.PrivateIPAddressSpecification {
dtos := make([]ec2.PrivateIPAddressSpecification, 0, len(ips))
for i, v := range ips {
new_private_ip := ec2.PrivateIPAddressSpecification{
PrivateIPAddress: aws.String(v.(string)),
}
new_private_ip.Primary = aws.Boolean(i == 0)
dtos = append(dtos, new_private_ip)
}
return dtos
}
//Flattens network interface attachment into a map[string]interface
func flattenAttachment(a *ec2.NetworkInterfaceAttachment) map[string]interface{} {
att := make(map[string]interface{})
att["instance"] = *a.InstanceID
att["device_index"] = *a.DeviceIndex
att["attachment_id"] = *a.AttachmentID
return att
}

View File

@ -346,3 +346,99 @@ func TestExpandInstanceString(t *testing.T) {
t.Fatalf("Expand Instance String output did not match.\nGot:\n%#v\n\nexpected:\n%#v", expanded, expected) t.Fatalf("Expand Instance String output did not match.\nGot:\n%#v\n\nexpected:\n%#v", expanded, expected)
} }
} }
func TestFlattenNetworkInterfacesPrivateIPAddesses(t *testing.T) {
expanded := []ec2.NetworkInterfacePrivateIPAddress{
ec2.NetworkInterfacePrivateIPAddress{PrivateIPAddress: aws.String("192.168.0.1")},
ec2.NetworkInterfacePrivateIPAddress{PrivateIPAddress: aws.String("192.168.0.2")},
}
result := flattenNetworkInterfacesPrivateIPAddesses(expanded)
if result == nil {
t.Fatal("result was nil")
}
if len(result) != 2 {
t.Fatalf("expected result had %d elements, but got %d", 2, len(result))
}
if result[0] != "192.168.0.1" {
t.Fatalf("expected ip to be 192.168.0.1, but was %s", result[0])
}
if result[1] != "192.168.0.2" {
t.Fatalf("expected ip to be 192.168.0.2, but was %s", result[1])
}
}
func TestFlattenGroupIdentifiers(t *testing.T) {
expanded := []ec2.GroupIdentifier{
ec2.GroupIdentifier{GroupID: aws.String("sg-001")},
ec2.GroupIdentifier{GroupID: aws.String("sg-002")},
}
result := flattenGroupIdentifiers(expanded)
if len(result) != 2 {
t.Fatalf("expected result had %d elements, but got %d", 2, len(result))
}
if result[0] != "sg-001" {
t.Fatalf("expected id to be sg-001, but was %s", result[0])
}
if result[1] != "sg-002" {
t.Fatalf("expected id to be sg-002, but was %s", result[1])
}
}
func TestExpandPrivateIPAddesses(t *testing.T) {
ip1 := "192.168.0.1"
ip2 := "192.168.0.2"
flattened := []interface{}{
ip1,
ip2,
}
result := expandPrivateIPAddesses(flattened)
if len(result) != 2 {
t.Fatalf("expected result had %d elements, but got %d", 2, len(result))
}
if *result[0].PrivateIPAddress != "192.168.0.1" || !*result[0].Primary {
t.Fatalf("expected ip to be 192.168.0.1 and Primary, but got %v, %t", *result[0].PrivateIPAddress, *result[0].Primary)
}
if *result[1].PrivateIPAddress != "192.168.0.2" || *result[1].Primary {
t.Fatalf("expected ip to be 192.168.0.2 and not Primary, but got %v, %t", *result[1].PrivateIPAddress, *result[1].Primary)
}
}
func TestFlattenAttachment(t *testing.T) {
expanded := &ec2.NetworkInterfaceAttachment{
InstanceID: aws.String("i-00001"),
DeviceIndex: aws.Integer(1),
AttachmentID: aws.String("at-002"),
}
result := flattenAttachment(expanded)
if result == nil {
t.Fatal("expected result to have value, but got nil")
}
if result["instance"] != "i-00001" {
t.Fatalf("expected instance to be i-00001, but got %s", result["instance"])
}
if result["device_index"] != 1 {
t.Fatalf("expected device_index to be 1, but got %d", result["device_index"])
}
if result["attachment_id"] != "at-002" {
t.Fatalf("expected attachment_id to be at-002, but got %s", result["attachment_id"])
}
}

View File

@ -0,0 +1,94 @@
package aws
import (
"log"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/elb"
"github.com/hashicorp/terraform/helper/schema"
)
// setTags is a helper to set the tags for a resource. It expects the
// tags field to be named "tags"
func setTagsELB(conn *elb.ELB, d *schema.ResourceData) error {
if d.HasChange("tags") {
oraw, nraw := d.GetChange("tags")
o := oraw.(map[string]interface{})
n := nraw.(map[string]interface{})
create, remove := diffTagsELB(tagsFromMapELB(o), tagsFromMapELB(n))
// Set tags
if len(remove) > 0 {
log.Printf("[DEBUG] Removing tags: %#v", remove)
k := make([]elb.TagKeyOnly, 0, len(remove))
for _, t := range remove {
k = append(k, elb.TagKeyOnly{Key: t.Key})
}
_, err := conn.RemoveTags(&elb.RemoveTagsInput{
LoadBalancerNames: []string{d.Get("name").(string)},
Tags: k,
})
if err != nil {
return err
}
}
if len(create) > 0 {
log.Printf("[DEBUG] Creating tags: %#v", create)
_, err := conn.AddTags(&elb.AddTagsInput{
LoadBalancerNames: []string{d.Get("name").(string)},
Tags: create,
})
if err != nil {
return err
}
}
}
return nil
}
// diffTags takes our tags locally and the ones remotely and returns
// the set of tags that must be created, and the set of tags that must
// be destroyed.
func diffTagsELB(oldTags, newTags []elb.Tag) ([]elb.Tag, []elb.Tag) {
// First, we're creating everything we have
create := make(map[string]interface{})
for _, t := range newTags {
create[*t.Key] = *t.Value
}
// Build the list of what to remove
var remove []elb.Tag
for _, t := range oldTags {
old, ok := create[*t.Key]
if !ok || old != *t.Value {
// Delete it!
remove = append(remove, t)
}
}
return tagsFromMapELB(create), remove
}
// tagsFromMap returns the tags for the given map of data.
func tagsFromMapELB(m map[string]interface{}) []elb.Tag {
result := make([]elb.Tag, 0, len(m))
for k, v := range m {
result = append(result, elb.Tag{
Key: aws.String(k),
Value: aws.String(v.(string)),
})
}
return result
}
// tagsToMap turns the list of tags into a map.
func tagsToMapELB(ts []elb.Tag) map[string]string {
result := make(map[string]string)
for _, t := range ts {
result[*t.Key] = *t.Value
}
return result
}

View File

@ -0,0 +1,85 @@
package aws
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/aws-sdk-go/gen/elb"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestDiffELBTags(t *testing.T) {
cases := []struct {
Old, New map[string]interface{}
Create, Remove map[string]string
}{
// Basic add/remove
{
Old: map[string]interface{}{
"foo": "bar",
},
New: map[string]interface{}{
"bar": "baz",
},
Create: map[string]string{
"bar": "baz",
},
Remove: map[string]string{
"foo": "bar",
},
},
// Modify
{
Old: map[string]interface{}{
"foo": "bar",
},
New: map[string]interface{}{
"foo": "baz",
},
Create: map[string]string{
"foo": "baz",
},
Remove: map[string]string{
"foo": "bar",
},
},
}
for i, tc := range cases {
c, r := diffTagsELB(tagsFromMapELB(tc.Old), tagsFromMapELB(tc.New))
cm := tagsToMapELB(c)
rm := tagsToMapELB(r)
if !reflect.DeepEqual(cm, tc.Create) {
t.Fatalf("%d: bad create: %#v", i, cm)
}
if !reflect.DeepEqual(rm, tc.Remove) {
t.Fatalf("%d: bad remove: %#v", i, rm)
}
}
}
// testAccCheckTags can be used to check the tags on a resource.
func testAccCheckELBTags(
ts *[]elb.Tag, key string, value string) resource.TestCheckFunc {
return func(s *terraform.State) error {
m := tagsToMapELB(*ts)
v, ok := m[key]
if value != "" && !ok {
return fmt.Errorf("Missing tag: %s", key)
} else if value == "" && ok {
return fmt.Errorf("Extra tag: %s", key)
}
if value == "" {
return nil
}
if v != value {
return fmt.Errorf("%s: bad value: %s", key, v)
}
return nil
}
}

View File

@ -0,0 +1,95 @@
package aws
import (
"log"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/rds"
"github.com/hashicorp/terraform/helper/schema"
)
// setTags is a helper to set the tags for a resource. It expects the
// tags field to be named "tags"
func setTagsRDS(conn *rds.RDS, d *schema.ResourceData, arn string) error {
if d.HasChange("tags") {
oraw, nraw := d.GetChange("tags")
o := oraw.(map[string]interface{})
n := nraw.(map[string]interface{})
create, remove := diffTagsRDS(tagsFromMapRDS(o), tagsFromMapRDS(n))
// Set tags
if len(remove) > 0 {
log.Printf("[DEBUG] Removing tags: %#v", remove)
k := make([]string, len(remove), len(remove))
for i, t := range remove {
k[i] = *t.Key
}
err := conn.RemoveTagsFromResource(&rds.RemoveTagsFromResourceMessage{
ResourceName: aws.String(arn),
TagKeys: k,
})
if err != nil {
return err
}
}
if len(create) > 0 {
log.Printf("[DEBUG] Creating tags: %#v", create)
err := conn.AddTagsToResource(&rds.AddTagsToResourceMessage{
ResourceName: aws.String(arn),
Tags: create,
})
if err != nil {
return err
}
}
}
return nil
}
// diffTags takes our tags locally and the ones remotely and returns
// the set of tags that must be created, and the set of tags that must
// be destroyed.
func diffTagsRDS(oldTags, newTags []rds.Tag) ([]rds.Tag, []rds.Tag) {
// First, we're creating everything we have
create := make(map[string]interface{})
for _, t := range newTags {
create[*t.Key] = *t.Value
}
// Build the list of what to remove
var remove []rds.Tag
for _, t := range oldTags {
old, ok := create[*t.Key]
if !ok || old != *t.Value {
// Delete it!
remove = append(remove, t)
}
}
return tagsFromMapRDS(create), remove
}
// tagsFromMap returns the tags for the given map of data.
func tagsFromMapRDS(m map[string]interface{}) []rds.Tag {
result := make([]rds.Tag, 0, len(m))
for k, v := range m {
result = append(result, rds.Tag{
Key: aws.String(k),
Value: aws.String(v.(string)),
})
}
return result
}
// tagsToMap turns the list of tags into a map.
func tagsToMapRDS(ts []rds.Tag) map[string]string {
result := make(map[string]string)
for _, t := range ts {
result[*t.Key] = *t.Value
}
return result
}

View File

@ -0,0 +1,85 @@
package aws
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/aws-sdk-go/gen/rds"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestDiffRDSTags(t *testing.T) {
cases := []struct {
Old, New map[string]interface{}
Create, Remove map[string]string
}{
// Basic add/remove
{
Old: map[string]interface{}{
"foo": "bar",
},
New: map[string]interface{}{
"bar": "baz",
},
Create: map[string]string{
"bar": "baz",
},
Remove: map[string]string{
"foo": "bar",
},
},
// Modify
{
Old: map[string]interface{}{
"foo": "bar",
},
New: map[string]interface{}{
"foo": "baz",
},
Create: map[string]string{
"foo": "baz",
},
Remove: map[string]string{
"foo": "bar",
},
},
}
for i, tc := range cases {
c, r := diffTagsRDS(tagsFromMapRDS(tc.Old), tagsFromMapRDS(tc.New))
cm := tagsToMapRDS(c)
rm := tagsToMapRDS(r)
if !reflect.DeepEqual(cm, tc.Create) {
t.Fatalf("%d: bad create: %#v", i, cm)
}
if !reflect.DeepEqual(rm, tc.Remove) {
t.Fatalf("%d: bad remove: %#v", i, rm)
}
}
}
// testAccCheckTags can be used to check the tags on a resource.
func testAccCheckRDSTags(
ts *[]rds.Tag, key string, value string) resource.TestCheckFunc {
return func(s *terraform.State) error {
m := tagsToMapRDS(*ts)
v, ok := m[key]
if value != "" && !ok {
return fmt.Errorf("Missing tag: %s", key)
} else if value == "" && ok {
return fmt.Errorf("Extra tag: %s", key)
}
if value == "" {
return nil
}
if v != value {
return fmt.Errorf("%s: bad value: %s", key, v)
}
return nil
}
}

View File

@ -0,0 +1,86 @@
package aws
import (
"log"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/route53"
"github.com/hashicorp/terraform/helper/schema"
)
// setTags is a helper to set the tags for a resource. It expects the
// tags field to be named "tags"
func setTagsR53(conn *route53.Route53, d *schema.ResourceData) error {
if d.HasChange("tags") {
oraw, nraw := d.GetChange("tags")
o := oraw.(map[string]interface{})
n := nraw.(map[string]interface{})
create, remove := diffTagsR53(tagsFromMapR53(o), tagsFromMapR53(n))
// Set tags
r := make([]string, len(remove))
for i, t := range remove {
r[i] = *t.Key
}
log.Printf("[DEBUG] Changing tags: \n\tadding: %#v\n\tremoving:%#v", create, remove)
req := &route53.ChangeTagsForResourceRequest{
AddTags: create,
RemoveTagKeys: r,
ResourceID: aws.String(d.Id()),
ResourceType: aws.String("hostedzone"),
}
_, err := conn.ChangeTagsForResource(req)
if err != nil {
return err
}
}
return nil
}
// diffTags takes our tags locally and the ones remotely and returns
// the set of tags that must be created, and the set of tags that must
// be destroyed.
func diffTagsR53(oldTags, newTags []route53.Tag) ([]route53.Tag, []route53.Tag) {
// First, we're creating everything we have
create := make(map[string]interface{})
for _, t := range newTags {
create[*t.Key] = *t.Value
}
// Build the list of what to remove
var remove []route53.Tag
for _, t := range oldTags {
old, ok := create[*t.Key]
if !ok || old != *t.Value {
// Delete it!
remove = append(remove, t)
}
}
return tagsFromMapR53(create), remove
}
// tagsFromMap returns the tags for the given map of data.
func tagsFromMapR53(m map[string]interface{}) []route53.Tag {
result := make([]route53.Tag, 0, len(m))
for k, v := range m {
result = append(result, route53.Tag{
Key: aws.String(k),
Value: aws.String(v.(string)),
})
}
return result
}
// tagsToMap turns the list of tags into a map.
func tagsToMapR53(ts []route53.Tag) map[string]string {
result := make(map[string]string)
for _, t := range ts {
result[*t.Key] = *t.Value
}
return result
}

View File

@ -0,0 +1,85 @@
package aws
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/aws-sdk-go/gen/route53"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestDiffTagsR53(t *testing.T) {
cases := []struct {
Old, New map[string]interface{}
Create, Remove map[string]string
}{
// Basic add/remove
{
Old: map[string]interface{}{
"foo": "bar",
},
New: map[string]interface{}{
"bar": "baz",
},
Create: map[string]string{
"bar": "baz",
},
Remove: map[string]string{
"foo": "bar",
},
},
// Modify
{
Old: map[string]interface{}{
"foo": "bar",
},
New: map[string]interface{}{
"foo": "baz",
},
Create: map[string]string{
"foo": "baz",
},
Remove: map[string]string{
"foo": "bar",
},
},
}
for i, tc := range cases {
c, r := diffTagsR53(tagsFromMapR53(tc.Old), tagsFromMapR53(tc.New))
cm := tagsToMapR53(c)
rm := tagsToMapR53(r)
if !reflect.DeepEqual(cm, tc.Create) {
t.Fatalf("%d: bad create: %#v", i, cm)
}
if !reflect.DeepEqual(rm, tc.Remove) {
t.Fatalf("%d: bad remove: %#v", i, rm)
}
}
}
// testAccCheckTags can be used to check the tags on a resource.
func testAccCheckTagsR53(
ts *[]route53.Tag, key string, value string) resource.TestCheckFunc {
return func(s *terraform.State) error {
m := tagsToMapR53(*ts)
v, ok := m[key]
if value != "" && !ok {
return fmt.Errorf("Missing tag: %s", key)
} else if value == "" && ok {
return fmt.Errorf("Extra tag: %s", key)
}
if value == "" {
return nil
}
if v != value {
return fmt.Errorf("%s: bad value: %s", key, v)
}
return nil
}
}

View File

@ -84,7 +84,7 @@ func resourceCloudStackDiskCreate(d *schema.ResourceData, meta interface{}) erro
if d.Get("size").(int) != 0 { if d.Get("size").(int) != 0 {
// Set the volume size // Set the volume size
p.SetSize(d.Get("size").(int)) p.SetSize(int64(d.Get("size").(int)))
} }
// Retrieve the zone UUID // Retrieve the zone UUID
@ -141,7 +141,7 @@ func resourceCloudStackDiskRead(d *schema.ResourceData, meta interface{}) error
d.Set("name", v.Name) d.Set("name", v.Name)
d.Set("attach", v.Attached != "") // If attached this will contain a timestamp when attached d.Set("attach", v.Attached != "") // If attached this will contain a timestamp when attached
d.Set("disk_offering", v.Diskofferingname) d.Set("disk_offering", v.Diskofferingname)
d.Set("size", v.Size/(1024*1024*1024)) // Needed to get GB's again d.Set("size", int(v.Size/(1024*1024*1024))) // Needed to get GB's again
d.Set("zone", v.Zonename) d.Set("zone", v.Zonename)
if v.Attached != "" { if v.Attached != "" {
@ -196,7 +196,7 @@ func resourceCloudStackDiskUpdate(d *schema.ResourceData, meta interface{}) erro
if d.Get("size").(int) != 0 { if d.Get("size").(int) != 0 {
// Set the size // Set the size
p.SetSize(d.Get("size").(int)) p.SetSize(int64(d.Get("size").(int)))
} }
// Set the shrink bit // Set the shrink bit
@ -367,7 +367,7 @@ func isAttached(cs *cloudstack.CloudStackClient, id string) (bool, error) {
return v.Attached != "", nil return v.Attached != "", nil
} }
func retrieveDeviceID(device string) int { func retrieveDeviceID(device string) int64 {
switch device { switch device {
case "/dev/xvdb", "D:": case "/dev/xvdb", "D:":
return 1 return 1
@ -402,7 +402,7 @@ func retrieveDeviceID(device string) int {
} }
} }
func retrieveDeviceName(device int, os string) string { func retrieveDeviceName(device int64, os string) string {
switch device { switch device {
case 1: case 1:
if os == "Windows" { if os == "Windows" {

View File

@ -87,11 +87,11 @@ func resourceCloudStackVPNCustomerGatewayCreate(d *schema.ResourceData, meta int
} }
if esplifetime, ok := d.GetOk("esp_lifetime"); ok { if esplifetime, ok := d.GetOk("esp_lifetime"); ok {
p.SetEsplifetime(esplifetime.(int)) p.SetEsplifetime(int64(esplifetime.(int)))
} }
if ikelifetime, ok := d.GetOk("ike_lifetime"); ok { if ikelifetime, ok := d.GetOk("ike_lifetime"); ok {
p.SetIkelifetime(ikelifetime.(int)) p.SetIkelifetime(int64(ikelifetime.(int)))
} }
// Create the new VPN Customer Gateway // Create the new VPN Customer Gateway
@ -128,8 +128,8 @@ func resourceCloudStackVPNCustomerGatewayRead(d *schema.ResourceData, meta inter
d.Set("ike_policy", v.Ikepolicy) d.Set("ike_policy", v.Ikepolicy)
d.Set("ipsec_psk", v.Ipsecpsk) d.Set("ipsec_psk", v.Ipsecpsk)
d.Set("dpd", v.Dpd) d.Set("dpd", v.Dpd)
d.Set("esp_lifetime", v.Esplifetime) d.Set("esp_lifetime", int(v.Esplifetime))
d.Set("ike_lifetime", v.Ikelifetime) d.Set("ike_lifetime", int(v.Ikelifetime))
return nil return nil
} }
@ -154,11 +154,11 @@ func resourceCloudStackVPNCustomerGatewayUpdate(d *schema.ResourceData, meta int
} }
if esplifetime, ok := d.GetOk("esp_lifetime"); ok { if esplifetime, ok := d.GetOk("esp_lifetime"); ok {
p.SetEsplifetime(esplifetime.(int)) p.SetEsplifetime(int64(esplifetime.(int)))
} }
if ikelifetime, ok := d.GetOk("ike_lifetime"); ok { if ikelifetime, ok := d.GetOk("ike_lifetime"); ok {
p.SetIkelifetime(ikelifetime.(int)) p.SetIkelifetime(int64(ikelifetime.(int)))
} }
// Update the VPN Customer Gateway // Update the VPN Customer Gateway

View File

@ -91,8 +91,9 @@ func resourceDigitalOceanRecordCreate(d *schema.ResourceData, meta interface{})
func resourceDigitalOceanRecordRead(d *schema.ResourceData, meta interface{}) error { func resourceDigitalOceanRecordRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*digitalocean.Client) client := meta.(*digitalocean.Client)
domain := d.Get("domain").(string)
rec, err := client.RetrieveRecord(d.Get("domain").(string), d.Id()) rec, err := client.RetrieveRecord(domain, d.Id())
if err != nil { if err != nil {
// If the record is somehow already destroyed, mark as // If the record is somehow already destroyed, mark as
// succesfully gone // succesfully gone
@ -104,6 +105,18 @@ func resourceDigitalOceanRecordRead(d *schema.ResourceData, meta interface{}) er
return err return err
} }
// Update response data for records with domain value
if t := rec.Type; t == "CNAME" || t == "MX" || t == "NS" || t == "SRV" {
// Append dot to response if resource value is absolute
if value := d.Get("value").(string); strings.HasSuffix(value, ".") {
rec.Data += "."
// If resource value ends with current domain, make response data absolute
if strings.HasSuffix(value, domain+".") {
rec.Data += domain + "."
}
}
}
d.Set("name", rec.Name) d.Set("name", rec.Name)
d.Set("type", rec.Type) d.Set("type", rec.Type)
d.Set("value", rec.Data) d.Set("value", rec.Data)

View File

@ -76,6 +76,87 @@ func TestAccDigitalOceanRecord_Updated(t *testing.T) {
}) })
} }
func TestAccDigitalOceanRecord_HostnameValue(t *testing.T) {
var record digitalocean.Record
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDigitalOceanRecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckDigitalOceanRecordConfig_cname,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanRecordExists("digitalocean_record.foobar", &record),
testAccCheckDigitalOceanRecordAttributesHostname("a", &record),
resource.TestCheckResourceAttr(
"digitalocean_record.foobar", "name", "terraform"),
resource.TestCheckResourceAttr(
"digitalocean_record.foobar", "domain", "foobar-test-terraform.com"),
resource.TestCheckResourceAttr(
"digitalocean_record.foobar", "value", "a.foobar-test-terraform.com."),
resource.TestCheckResourceAttr(
"digitalocean_record.foobar", "type", "CNAME"),
),
},
},
})
}
func TestAccDigitalOceanRecord_RelativeHostnameValue(t *testing.T) {
var record digitalocean.Record
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDigitalOceanRecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckDigitalOceanRecordConfig_relative_cname,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanRecordExists("digitalocean_record.foobar", &record),
testAccCheckDigitalOceanRecordAttributesHostname("a.b", &record),
resource.TestCheckResourceAttr(
"digitalocean_record.foobar", "name", "terraform"),
resource.TestCheckResourceAttr(
"digitalocean_record.foobar", "domain", "foobar-test-terraform.com"),
resource.TestCheckResourceAttr(
"digitalocean_record.foobar", "value", "a.b"),
resource.TestCheckResourceAttr(
"digitalocean_record.foobar", "type", "CNAME"),
),
},
},
})
}
func TestAccDigitalOceanRecord_ExternalHostnameValue(t *testing.T) {
var record digitalocean.Record
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDigitalOceanRecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckDigitalOceanRecordConfig_external_cname,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanRecordExists("digitalocean_record.foobar", &record),
testAccCheckDigitalOceanRecordAttributesHostname("a.foobar-test-terraform.net", &record),
resource.TestCheckResourceAttr(
"digitalocean_record.foobar", "name", "terraform"),
resource.TestCheckResourceAttr(
"digitalocean_record.foobar", "domain", "foobar-test-terraform.com"),
resource.TestCheckResourceAttr(
"digitalocean_record.foobar", "value", "a.foobar-test-terraform.net."),
resource.TestCheckResourceAttr(
"digitalocean_record.foobar", "type", "CNAME"),
),
},
},
})
}
func testAccCheckDigitalOceanRecordDestroy(s *terraform.State) error { func testAccCheckDigitalOceanRecordDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*digitalocean.Client) client := testAccProvider.Meta().(*digitalocean.Client)
@ -146,6 +227,17 @@ func testAccCheckDigitalOceanRecordExists(n string, record *digitalocean.Record)
} }
} }
func testAccCheckDigitalOceanRecordAttributesHostname(data string, record *digitalocean.Record) resource.TestCheckFunc {
return func(s *terraform.State) error {
if record.Data != data {
return fmt.Errorf("Bad value: expected %s, got %s", data, record.Data)
}
return nil
}
}
const testAccCheckDigitalOceanRecordConfig_basic = ` const testAccCheckDigitalOceanRecordConfig_basic = `
resource "digitalocean_domain" "foobar" { resource "digitalocean_domain" "foobar" {
name = "foobar-test-terraform.com" name = "foobar-test-terraform.com"
@ -173,3 +265,45 @@ resource "digitalocean_record" "foobar" {
value = "192.168.0.11" value = "192.168.0.11"
type = "A" type = "A"
}` }`
const testAccCheckDigitalOceanRecordConfig_cname = `
resource "digitalocean_domain" "foobar" {
name = "foobar-test-terraform.com"
ip_address = "192.168.0.10"
}
resource "digitalocean_record" "foobar" {
domain = "${digitalocean_domain.foobar.name}"
name = "terraform"
value = "a.foobar-test-terraform.com."
type = "CNAME"
}`
const testAccCheckDigitalOceanRecordConfig_relative_cname = `
resource "digitalocean_domain" "foobar" {
name = "foobar-test-terraform.com"
ip_address = "192.168.0.10"
}
resource "digitalocean_record" "foobar" {
domain = "${digitalocean_domain.foobar.name}"
name = "terraform"
value = "a.b"
type = "CNAME"
}`
const testAccCheckDigitalOceanRecordConfig_external_cname = `
resource "digitalocean_domain" "foobar" {
name = "foobar-test-terraform.com"
ip_address = "192.168.0.10"
}
resource "digitalocean_record" "foobar" {
domain = "${digitalocean_domain.foobar.name}"
name = "terraform"
value = "a.foobar-test-terraform.net."
type = "CNAME"
}`

View File

@ -0,0 +1,33 @@
package docker
import (
"path/filepath"
dc "github.com/fsouza/go-dockerclient"
)
// Config is the structure that stores the configuration to talk to a
// Docker API compatible host.
type Config struct {
Host string
CertPath string
}
// NewClient() returns a new Docker client.
func (c *Config) NewClient() (*dc.Client, error) {
// If there is no cert information, then just return the direct client
if c.CertPath == "" {
return dc.NewClient(c.Host)
}
// If there is cert information, load it and use it.
ca := filepath.Join(c.CertPath, "ca.pem")
cert := filepath.Join(c.CertPath, "cert.pem")
key := filepath.Join(c.CertPath, "key.pem")
return dc.NewTLSClient(c.Host, cert, key, ca)
}
// Data ia structure for holding data that we fetch from Docker.
type Data struct {
DockerImages map[string]*dc.APIImages
}

View File

@ -0,0 +1,54 @@
package docker
import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"host": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("DOCKER_HOST", "unix:/run/docker.sock"),
Description: "The Docker daemon address",
},
"cert_path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("DOCKER_CERT_PATH", nil),
Description: "Path to directory with Docker TLS config",
},
},
ResourcesMap: map[string]*schema.Resource{
"docker_container": resourceDockerContainer(),
"docker_image": resourceDockerImage(),
},
ConfigureFunc: providerConfigure,
}
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
Host: d.Get("host").(string),
CertPath: d.Get("cert_path").(string),
}
client, err := config.NewClient()
if err != nil {
return nil, fmt.Errorf("Error initializing Docker client: %s", err)
}
err = client.Ping()
if err != nil {
return nil, fmt.Errorf("Error pinging Docker server: %s", err)
}
return client, nil
}

View File

@ -0,0 +1,36 @@
package docker
import (
"os/exec"
"testing"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *schema.Provider
func init() {
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"docker": testAccProvider,
}
}
func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = Provider()
}
func testAccPreCheck(t *testing.T) {
cmd := exec.Command("docker", "version")
if err := cmd.Run(); err != nil {
t.Fatalf("Docker must be available: %s", err)
}
}

View File

@ -0,0 +1,222 @@
package docker
import (
"bytes"
"fmt"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceDockerContainer() *schema.Resource {
return &schema.Resource{
Create: resourceDockerContainerCreate,
Read: resourceDockerContainerRead,
Update: resourceDockerContainerUpdate,
Delete: resourceDockerContainerDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
// Indicates whether the container must be running.
//
// An assumption is made that configured containers
// should be running; if not, they should not be in
// the configuration. Therefore a stopped container
// should be started. Set to false to have the
// provider leave the container alone.
//
// Actively-debugged containers are likely to be
// stopped and started manually, and Docker has
// some provisions for restarting containers that
// stop. The utility here comes from the fact that
// this will delete and re-create the container
// following the principle that the containers
// should be pristine when started.
"must_run": &schema.Schema{
Type: schema.TypeBool,
Default: true,
Optional: true,
},
// ForceNew is not true for image because we need to
// sane this against Docker image IDs, as each image
// can have multiple names/tags attached do it.
"image": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"hostname": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"domainname": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"command": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"dns": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: stringSetHash,
},
"publish_all_ports": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"volumes": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: getVolumesElem(),
Set: resourceDockerVolumesHash,
},
"ports": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: getPortsElem(),
Set: resourceDockerPortsHash,
},
"env": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: stringSetHash,
},
},
}
}
func getVolumesElem() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"from_container": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"container_path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"host_path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"read_only": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
},
}
}
func getPortsElem() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"internal": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: true,
},
"external": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"ip": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"protocol": &schema.Schema{
Type: schema.TypeString,
Default: "tcp",
Optional: true,
ForceNew: true,
},
},
}
}
func resourceDockerPortsHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%v-", m["internal"].(int)))
if v, ok := m["external"]; ok {
buf.WriteString(fmt.Sprintf("%v-", v.(int)))
}
if v, ok := m["ip"]; ok {
buf.WriteString(fmt.Sprintf("%v-", v.(string)))
}
if v, ok := m["protocol"]; ok {
buf.WriteString(fmt.Sprintf("%v-", v.(string)))
}
return hashcode.String(buf.String())
}
func resourceDockerVolumesHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
if v, ok := m["from_container"]; ok {
buf.WriteString(fmt.Sprintf("%v-", v.(string)))
}
if v, ok := m["container_path"]; ok {
buf.WriteString(fmt.Sprintf("%v-", v.(string)))
}
if v, ok := m["host_path"]; ok {
buf.WriteString(fmt.Sprintf("%v-", v.(string)))
}
if v, ok := m["read_only"]; ok {
buf.WriteString(fmt.Sprintf("%v-", v.(bool)))
}
return hashcode.String(buf.String())
}
func stringSetHash(v interface{}) int {
return hashcode.String(v.(string))
}

View File

@ -0,0 +1,267 @@
package docker
import (
"errors"
"fmt"
"strconv"
"strings"
dc "github.com/fsouza/go-dockerclient"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) error {
var err error
client := meta.(*dc.Client)
var data Data
if err := fetchLocalImages(&data, client); err != nil {
return err
}
image := d.Get("image").(string)
if _, ok := data.DockerImages[image]; !ok {
if _, ok := data.DockerImages[image+":latest"]; !ok {
return fmt.Errorf("Unable to find image %s", image)
} else {
image = image + ":latest"
}
}
// The awesome, wonderful, splendiferous, sensical
// Docker API now lets you specify a HostConfig in
// CreateContainerOptions, but in my testing it still only
// actually applies HostConfig options set in StartContainer.
// How cool is that?
createOpts := dc.CreateContainerOptions{
Name: d.Get("name").(string),
Config: &dc.Config{
Image: image,
Hostname: d.Get("hostname").(string),
Domainname: d.Get("domainname").(string),
},
}
if v, ok := d.GetOk("env"); ok {
createOpts.Config.Env = stringSetToStringSlice(v.(*schema.Set))
}
if v, ok := d.GetOk("command"); ok {
createOpts.Config.Cmd = stringListToStringSlice(v.([]interface{}))
}
exposedPorts := map[dc.Port]struct{}{}
portBindings := map[dc.Port][]dc.PortBinding{}
if v, ok := d.GetOk("ports"); ok {
exposedPorts, portBindings = portSetToDockerPorts(v.(*schema.Set))
}
if len(exposedPorts) != 0 {
createOpts.Config.ExposedPorts = exposedPorts
}
volumes := map[string]struct{}{}
binds := []string{}
volumesFrom := []string{}
if v, ok := d.GetOk("volumes"); ok {
volumes, binds, volumesFrom, err = volumeSetToDockerVolumes(v.(*schema.Set))
if err != nil {
return fmt.Errorf("Unable to parse volumes: %s", err)
}
}
if len(volumes) != 0 {
createOpts.Config.Volumes = volumes
}
var retContainer *dc.Container
if retContainer, err = client.CreateContainer(createOpts); err != nil {
return fmt.Errorf("Unable to create container: %s", err)
}
if retContainer == nil {
return fmt.Errorf("Returned container is nil")
}
d.SetId(retContainer.ID)
hostConfig := &dc.HostConfig{
PublishAllPorts: d.Get("publish_all_ports").(bool),
}
if len(portBindings) != 0 {
hostConfig.PortBindings = portBindings
}
if len(binds) != 0 {
hostConfig.Binds = binds
}
if len(volumesFrom) != 0 {
hostConfig.VolumesFrom = volumesFrom
}
if v, ok := d.GetOk("dns"); ok {
hostConfig.DNS = stringSetToStringSlice(v.(*schema.Set))
}
if err := client.StartContainer(retContainer.ID, hostConfig); err != nil {
return fmt.Errorf("Unable to start container: %s", err)
}
return resourceDockerContainerRead(d, meta)
}
func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
apiContainer, err := fetchDockerContainer(d.Get("name").(string), client)
if err != nil {
return err
}
if apiContainer == nil {
// This container doesn't exist anymore
d.SetId("")
return nil
}
container, err := client.InspectContainer(apiContainer.ID)
if err != nil {
return fmt.Errorf("Error inspecting container %s: %s", apiContainer.ID, err)
}
if d.Get("must_run").(bool) && !container.State.Running {
return resourceDockerContainerDelete(d, meta)
}
return nil
}
func resourceDockerContainerUpdate(d *schema.ResourceData, meta interface{}) error {
return nil
}
func resourceDockerContainerDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
removeOpts := dc.RemoveContainerOptions{
ID: d.Id(),
RemoveVolumes: true,
Force: true,
}
if err := client.RemoveContainer(removeOpts); err != nil {
return fmt.Errorf("Error deleting container %s: %s", d.Id(), err)
}
d.SetId("")
return nil
}
func stringListToStringSlice(stringList []interface{}) []string {
ret := []string{}
for _, v := range stringList {
ret = append(ret, v.(string))
}
return ret
}
func stringSetToStringSlice(stringSet *schema.Set) []string {
ret := []string{}
if stringSet == nil {
return ret
}
for _, envVal := range stringSet.List() {
ret = append(ret, envVal.(string))
}
return ret
}
func fetchDockerContainer(name string, client *dc.Client) (*dc.APIContainers, error) {
apiContainers, err := client.ListContainers(dc.ListContainersOptions{All: true})
if err != nil {
return nil, fmt.Errorf("Error fetching container information from Docker: %s\n", err)
}
for _, apiContainer := range apiContainers {
// Sometimes the Docker API prefixes container names with /
// like it does in these commands. But if there's no
// set name, it just uses the ID without a /...ugh.
var dockerContainerName string
if len(apiContainer.Names) > 0 {
dockerContainerName = strings.TrimLeft(apiContainer.Names[0], "/")
} else {
dockerContainerName = apiContainer.ID
}
if dockerContainerName == name {
return &apiContainer, nil
}
}
return nil, nil
}
func portSetToDockerPorts(ports *schema.Set) (map[dc.Port]struct{}, map[dc.Port][]dc.PortBinding) {
retExposedPorts := map[dc.Port]struct{}{}
retPortBindings := map[dc.Port][]dc.PortBinding{}
for _, portInt := range ports.List() {
port := portInt.(map[string]interface{})
internal := port["internal"].(int)
protocol := port["protocol"].(string)
exposedPort := dc.Port(strconv.Itoa(internal) + "/" + protocol)
retExposedPorts[exposedPort] = struct{}{}
external, extOk := port["external"].(int)
ip, ipOk := port["ip"].(string)
if extOk {
portBinding := dc.PortBinding{
HostPort: strconv.Itoa(external),
}
if ipOk {
portBinding.HostIP = ip
}
retPortBindings[exposedPort] = append(retPortBindings[exposedPort], portBinding)
}
}
return retExposedPorts, retPortBindings
}
func volumeSetToDockerVolumes(volumes *schema.Set) (map[string]struct{}, []string, []string, error) {
retVolumeMap := map[string]struct{}{}
retHostConfigBinds := []string{}
retVolumeFromContainers := []string{}
for _, volumeInt := range volumes.List() {
volume := volumeInt.(map[string]interface{})
fromContainer := volume["from_container"].(string)
containerPath := volume["container_path"].(string)
hostPath := volume["host_path"].(string)
readOnly := volume["read_only"].(bool)
switch {
case len(fromContainer) == 0 && len(containerPath) == 0:
return retVolumeMap, retHostConfigBinds, retVolumeFromContainers, errors.New("Volume entry without container path or source container")
case len(fromContainer) != 0 && len(containerPath) != 0:
return retVolumeMap, retHostConfigBinds, retVolumeFromContainers, errors.New("Both a container and a path specified in a volume entry")
case len(fromContainer) != 0:
retVolumeFromContainers = append(retVolumeFromContainers, fromContainer)
case len(hostPath) != 0:
readWrite := "rw"
if readOnly {
readWrite = "ro"
}
retVolumeMap[containerPath] = struct{}{}
retHostConfigBinds = append(retHostConfigBinds, hostPath+":"+containerPath+":"+readWrite)
default:
retVolumeMap[containerPath] = struct{}{}
}
}
return retVolumeMap, retHostConfigBinds, retVolumeFromContainers, nil
}

View File

@ -0,0 +1,63 @@
package docker
import (
"fmt"
"testing"
dc "github.com/fsouza/go-dockerclient"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccDockerContainer_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDockerContainerConfig,
Check: resource.ComposeTestCheckFunc(
testAccContainerRunning("docker_container.foo"),
),
},
},
})
}
func testAccContainerRunning(n string) 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 ID is set")
}
client := testAccProvider.Meta().(*dc.Client)
containers, err := client.ListContainers(dc.ListContainersOptions{})
if err != nil {
return err
}
for _, c := range containers {
if c.ID == rs.Primary.ID {
return nil
}
}
return fmt.Errorf("Container not found: %s", rs.Primary.ID)
}
}
const testAccDockerContainerConfig = `
resource "docker_image" "foo" {
name = "ubuntu:trusty-20150320"
}
resource "docker_container" "foo" {
name = "tf-test"
image = "${docker_image.foo.latest}"
}
`

View File

@ -0,0 +1,31 @@
package docker
import (
"github.com/hashicorp/terraform/helper/schema"
)
func resourceDockerImage() *schema.Resource {
return &schema.Resource{
Create: resourceDockerImageCreate,
Read: resourceDockerImageRead,
Update: resourceDockerImageUpdate,
Delete: resourceDockerImageDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"keep_updated": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"latest": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}

View File

@ -0,0 +1,173 @@
package docker
import (
"fmt"
"strings"
dc "github.com/fsouza/go-dockerclient"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceDockerImageCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
apiImage, err := findImage(d, client)
if err != nil {
return fmt.Errorf("Unable to read Docker image into resource: %s", err)
}
d.SetId(apiImage.ID + d.Get("name").(string))
d.Set("latest", apiImage.ID)
return nil
}
func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
apiImage, err := findImage(d, client)
if err != nil {
return fmt.Errorf("Unable to read Docker image into resource: %s", err)
}
d.Set("latest", apiImage.ID)
return nil
}
func resourceDockerImageUpdate(d *schema.ResourceData, meta interface{}) error {
// We need to re-read in case switching parameters affects
// the value of "latest" or others
return resourceDockerImageRead(d, meta)
}
func resourceDockerImageDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}
func fetchLocalImages(data *Data, client *dc.Client) error {
images, err := client.ListImages(dc.ListImagesOptions{All: false})
if err != nil {
return fmt.Errorf("Unable to list Docker images: %s", err)
}
if data.DockerImages == nil {
data.DockerImages = make(map[string]*dc.APIImages)
}
// Docker uses different nomenclatures in different places...sometimes a short
// ID, sometimes long, etc. So we store both in the map so we can always find
// the same image object. We store the tags, too.
for i, image := range images {
data.DockerImages[image.ID[:12]] = &images[i]
data.DockerImages[image.ID] = &images[i]
for _, repotag := range image.RepoTags {
data.DockerImages[repotag] = &images[i]
}
}
return nil
}
func pullImage(data *Data, client *dc.Client, image string) error {
// TODO: Test local registry handling. It should be working
// based on the code that was ported over
pullOpts := dc.PullImageOptions{}
splitImageName := strings.Split(image, ":")
switch {
// It's in registry:port/repo:tag format
case len(splitImageName) == 3:
splitPortRepo := strings.Split(splitImageName[1], "/")
pullOpts.Registry = splitImageName[0] + ":" + splitPortRepo[0]
pullOpts.Repository = splitPortRepo[1]
pullOpts.Tag = splitImageName[2]
// It's either registry:port/repo or repo:tag with default registry
case len(splitImageName) == 2:
splitPortRepo := strings.Split(splitImageName[1], "/")
switch len(splitPortRepo) {
// registry:port/repo
case 2:
pullOpts.Registry = splitImageName[0] + ":" + splitPortRepo[0]
pullOpts.Repository = splitPortRepo[1]
pullOpts.Tag = "latest"
// repo:tag
case 1:
pullOpts.Repository = splitImageName[0]
pullOpts.Tag = splitImageName[1]
}
default:
pullOpts.Repository = image
}
if err := client.PullImage(pullOpts, dc.AuthConfiguration{}); err != nil {
return fmt.Errorf("Error pulling image %s: %s\n", image, err)
}
return fetchLocalImages(data, client)
}
func getImageTag(image string) string {
splitImageName := strings.Split(image, ":")
switch {
// It's in registry:port/repo:tag format
case len(splitImageName) == 3:
return splitImageName[2]
// It's either registry:port/repo or repo:tag with default registry
case len(splitImageName) == 2:
splitPortRepo := strings.Split(splitImageName[1], "/")
if len(splitPortRepo) == 2 {
return ""
} else {
return splitImageName[1]
}
}
return ""
}
func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error) {
var data Data
if err := fetchLocalImages(&data, client); err != nil {
return nil, err
}
imageName := d.Get("name").(string)
if imageName == "" {
return nil, fmt.Errorf("Empty image name is not allowed")
}
searchLocal := func() *dc.APIImages {
if apiImage, ok := data.DockerImages[imageName]; ok {
return apiImage
}
if apiImage, ok := data.DockerImages[imageName+":latest"]; ok {
imageName = imageName + ":latest"
return apiImage
}
return nil
}
foundImage := searchLocal()
if d.Get("keep_updated").(bool) || foundImage == nil {
if err := pullImage(&data, client, imageName); err != nil {
return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err)
}
}
foundImage = searchLocal()
if foundImage != nil {
return foundImage, nil
}
return nil, fmt.Errorf("Unable to find or pull image %s", imageName)
}

View File

@ -0,0 +1,32 @@
package docker
import (
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
func TestAccDockerImage_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDockerImageConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"docker_image.foo",
"latest",
"d0955f21bf24f5bfffd32d2d0bb669d0564701c271bc3dfc64cfc5adfdec2d07"),
),
},
},
})
}
const testAccDockerImageConfig = `
resource "docker_image" "foo" {
name = "ubuntu:trusty-20150320"
keep_updated = true
}
`

View File

@ -7,11 +7,10 @@ import (
"net/http" "net/http"
"os" "os"
"code.google.com/p/google-api-go-client/compute/v1"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/google" "golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt" "golang.org/x/oauth2/jwt"
"google.golang.org/api/compute/v1"
) )
// Config is the configuration structure used to instantiate the Google // Config is the configuration structure used to instantiate the Google

View File

@ -1,7 +1,7 @@
package google package google
import ( import (
"code.google.com/p/google-api-go-client/compute/v1" "google.golang.org/api/compute/v1"
) )
// readDiskType finds the disk type with the given name. // readDiskType finds the disk type with the given name.

View File

@ -4,7 +4,8 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"code.google.com/p/google-api-go-client/compute/v1" "google.golang.org/api/compute/v1"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
) )

View File

@ -5,9 +5,9 @@ import (
"log" "log"
"time" "time"
"code.google.com/p/google-api-go-client/compute/v1"
"code.google.com/p/google-api-go-client/googleapi"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
) )
func resourceComputeAddress() *schema.Resource { func resourceComputeAddress() *schema.Resource {

View File

@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"testing" "testing"
"code.google.com/p/google-api-go-client/compute/v1"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"google.golang.org/api/compute/v1"
) )
func TestAccComputeAddress_basic(t *testing.T) { func TestAccComputeAddress_basic(t *testing.T) {

View File

@ -5,9 +5,9 @@ import (
"log" "log"
"time" "time"
"code.google.com/p/google-api-go-client/compute/v1"
"code.google.com/p/google-api-go-client/googleapi"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
) )
func resourceComputeDisk() *schema.Resource { func resourceComputeDisk() *schema.Resource {

View File

@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"testing" "testing"
"code.google.com/p/google-api-go-client/compute/v1"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"google.golang.org/api/compute/v1"
) )
func TestAccComputeDisk_basic(t *testing.T) { func TestAccComputeDisk_basic(t *testing.T) {

View File

@ -6,10 +6,10 @@ import (
"sort" "sort"
"time" "time"
"code.google.com/p/google-api-go-client/compute/v1"
"code.google.com/p/google-api-go-client/googleapi"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
) )
func resourceComputeFirewall() *schema.Resource { func resourceComputeFirewall() *schema.Resource {

View File

@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"testing" "testing"
"code.google.com/p/google-api-go-client/compute/v1"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"google.golang.org/api/compute/v1"
) )
func TestAccComputeFirewall_basic(t *testing.T) { func TestAccComputeFirewall_basic(t *testing.T) {

View File

@ -5,9 +5,9 @@ import (
"log" "log"
"time" "time"
"code.google.com/p/google-api-go-client/compute/v1"
"code.google.com/p/google-api-go-client/googleapi"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
) )
func resourceComputeForwardingRule() *schema.Resource { func resourceComputeForwardingRule() *schema.Resource {

View File

@ -5,9 +5,9 @@ import (
"log" "log"
"time" "time"
"code.google.com/p/google-api-go-client/compute/v1"
"code.google.com/p/google-api-go-client/googleapi"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
) )
func resourceComputeHttpHealthCheck() *schema.Resource { func resourceComputeHttpHealthCheck() *schema.Resource {

View File

@ -5,10 +5,10 @@ import (
"log" "log"
"time" "time"
"code.google.com/p/google-api-go-client/compute/v1"
"code.google.com/p/google-api-go-client/googleapi"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
) )
func resourceComputeInstance() *schema.Resource { func resourceComputeInstance() *schema.Resource {
@ -72,6 +72,13 @@ func resourceComputeInstance() *schema.Resource {
"auto_delete": &schema.Schema{ "auto_delete": &schema.Schema{
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Default: true,
ForceNew: true,
},
"size": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true, ForceNew: true,
}, },
}, },
@ -283,11 +290,7 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err
disk.Type = "PERSISTENT" disk.Type = "PERSISTENT"
disk.Mode = "READ_WRITE" disk.Mode = "READ_WRITE"
disk.Boot = i == 0 disk.Boot = i == 0
disk.AutoDelete = true disk.AutoDelete = d.Get(prefix + ".auto_delete").(bool)
if v, ok := d.GetOk(prefix + ".auto_delete"); ok {
disk.AutoDelete = v.(bool)
}
// Load up the disk for this disk if specified // Load up the disk for this disk if specified
if v, ok := d.GetOk(prefix + ".disk"); ok { if v, ok := d.GetOk(prefix + ".disk"); ok {
@ -331,6 +334,11 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err
disk.InitializeParams.DiskType = diskType.SelfLink disk.InitializeParams.DiskType = diskType.SelfLink
} }
if v, ok := d.GetOk(prefix + ".size"); ok {
diskSizeGb := v.(int)
disk.InitializeParams.DiskSizeGb = int64(diskSizeGb)
}
disks = append(disks, &disk) disks = append(disks, &disk)
} }
@ -564,6 +572,7 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error
networkInterfaces = append(networkInterfaces, map[string]interface{}{ networkInterfaces = append(networkInterfaces, map[string]interface{}{
"name": iface.Name, "name": iface.Name,
"address": iface.NetworkIP, "address": iface.NetworkIP,
"network": iface.Network,
"access_config": accessConfigs, "access_config": accessConfigs,
}) })
} }

View File

@ -4,10 +4,10 @@ import (
"fmt" "fmt"
"time" "time"
"code.google.com/p/google-api-go-client/compute/v1"
"code.google.com/p/google-api-go-client/googleapi"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
) )
func resourceComputeInstanceTemplate() *schema.Resource { func resourceComputeInstanceTemplate() *schema.Resource {
@ -58,6 +58,7 @@ func resourceComputeInstanceTemplate() *schema.Resource {
"auto_delete": &schema.Schema{ "auto_delete": &schema.Schema{
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Default: true,
ForceNew: true, ForceNew: true,
}, },
@ -235,11 +236,7 @@ func buildDisks(d *schema.ResourceData, meta interface{}) []*compute.AttachedDis
disk.Mode = "READ_WRITE" disk.Mode = "READ_WRITE"
disk.Interface = "SCSI" disk.Interface = "SCSI"
disk.Boot = i == 0 disk.Boot = i == 0
disk.AutoDelete = true disk.AutoDelete = d.Get(prefix + ".auto_delete").(bool)
if v, ok := d.GetOk(prefix + ".auto_delete"); ok {
disk.AutoDelete = v.(bool)
}
if v, ok := d.GetOk(prefix + ".boot"); ok { if v, ok := d.GetOk(prefix + ".boot"); ok {
disk.Boot = v.(bool) disk.Boot = v.(bool)

View File

@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"testing" "testing"
"code.google.com/p/google-api-go-client/compute/v1"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"google.golang.org/api/compute/v1"
) )
func TestAccComputeInstanceTemplate_basic(t *testing.T) { func TestAccComputeInstanceTemplate_basic(t *testing.T) {
@ -65,7 +65,7 @@ func TestAccComputeInstanceTemplate_disks(t *testing.T) {
testAccCheckComputeInstanceTemplateExists( testAccCheckComputeInstanceTemplateExists(
"google_compute_instance_template.foobar", &instanceTemplate), "google_compute_instance_template.foobar", &instanceTemplate),
testAccCheckComputeInstanceTemplateDisk(&instanceTemplate, "debian-7-wheezy-v20140814", true, true), testAccCheckComputeInstanceTemplateDisk(&instanceTemplate, "debian-7-wheezy-v20140814", true, true),
testAccCheckComputeInstanceTemplateDisk(&instanceTemplate, "foo_existing_disk", false, false), testAccCheckComputeInstanceTemplateDisk(&instanceTemplate, "terraform-test-foobar", false, false),
), ),
}, },
}, },
@ -252,6 +252,14 @@ resource "google_compute_instance_template" "foobar" {
}` }`
const testAccComputeInstanceTemplate_disks = ` const testAccComputeInstanceTemplate_disks = `
resource "google_compute_disk" "foobar" {
name = "terraform-test-foobar"
image = "debian-7-wheezy-v20140814"
size = 10
type = "pd-ssd"
zone = "us-central1-a"
}
resource "google_compute_instance_template" "foobar" { resource "google_compute_instance_template" "foobar" {
name = "terraform-test" name = "terraform-test"
machine_type = "n1-standard-1" machine_type = "n1-standard-1"
@ -263,7 +271,7 @@ resource "google_compute_instance_template" "foobar" {
} }
disk { disk {
source = "foo_existing_disk" source = "terraform-test-foobar"
auto_delete = false auto_delete = false
boot = false boot = false
} }

View File

@ -5,9 +5,9 @@ import (
"strings" "strings"
"testing" "testing"
"code.google.com/p/google-api-go-client/compute/v1"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"google.golang.org/api/compute/v1"
) )
func TestAccComputeInstance_basic_deprecated_network(t *testing.T) { func TestAccComputeInstance_basic_deprecated_network(t *testing.T) {

View File

@ -5,9 +5,9 @@ import (
"log" "log"
"time" "time"
"code.google.com/p/google-api-go-client/compute/v1"
"code.google.com/p/google-api-go-client/googleapi"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
) )
func resourceComputeNetwork() *schema.Resource { func resourceComputeNetwork() *schema.Resource {

View File

@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"testing" "testing"
"code.google.com/p/google-api-go-client/compute/v1"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"google.golang.org/api/compute/v1"
) )
func TestAccComputeNetwork_basic(t *testing.T) { func TestAccComputeNetwork_basic(t *testing.T) {

View File

@ -5,10 +5,10 @@ import (
"log" "log"
"time" "time"
"code.google.com/p/google-api-go-client/compute/v1"
"code.google.com/p/google-api-go-client/googleapi"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
) )
func resourceComputeRoute() *schema.Resource { func resourceComputeRoute() *schema.Resource {

View File

@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"testing" "testing"
"code.google.com/p/google-api-go-client/compute/v1"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"google.golang.org/api/compute/v1"
) )
func TestAccComputeRoute_basic(t *testing.T) { func TestAccComputeRoute_basic(t *testing.T) {

View File

@ -6,9 +6,9 @@ import (
"strings" "strings"
"time" "time"
"code.google.com/p/google-api-go-client/compute/v1"
"code.google.com/p/google-api-go-client/googleapi"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
) )
func resourceComputeTargetPool() *schema.Resource { func resourceComputeTargetPool() *schema.Resource {

View File

@ -358,14 +358,18 @@ func updateConfigVars(
vars := make(map[string]*string) vars := make(map[string]*string)
for _, v := range o { for _, v := range o {
for k, _ := range v.(map[string]interface{}) { if v != nil {
vars[k] = nil for k, _ := range v.(map[string]interface{}) {
vars[k] = nil
}
} }
} }
for _, v := range n { for _, v := range n {
for k, v := range v.(map[string]interface{}) { if v != nil {
val := v.(string) for k, v := range v.(map[string]interface{}) {
vars[k] = &val val := v.(string)
vars[k] = &val
}
} }
} }

View File

@ -0,0 +1,67 @@
package openstack
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
)
type Config struct {
Username string
UserID string
Password string
APIKey string
IdentityEndpoint string
TenantID string
TenantName string
DomainID string
DomainName string
osClient *gophercloud.ProviderClient
}
func (c *Config) loadAndValidate() error {
ao := gophercloud.AuthOptions{
Username: c.Username,
UserID: c.UserID,
Password: c.Password,
APIKey: c.APIKey,
IdentityEndpoint: c.IdentityEndpoint,
TenantID: c.TenantID,
TenantName: c.TenantName,
DomainID: c.DomainID,
DomainName: c.DomainName,
}
client, err := openstack.AuthenticatedClient(ao)
if err != nil {
return err
}
c.osClient = client
return nil
}
func (c *Config) blockStorageV1Client(region string) (*gophercloud.ServiceClient, error) {
return openstack.NewBlockStorageV1(c.osClient, gophercloud.EndpointOpts{
Region: region,
})
}
func (c *Config) computeV2Client(region string) (*gophercloud.ServiceClient, error) {
return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{
Region: region,
})
}
func (c *Config) networkingV2Client(region string) (*gophercloud.ServiceClient, error) {
return openstack.NewNetworkV2(c.osClient, gophercloud.EndpointOpts{
Region: region,
})
}
func (c *Config) objectStorageV1Client(region string) (*gophercloud.ServiceClient, error) {
return openstack.NewObjectStorageV1(c.osClient, gophercloud.EndpointOpts{
Region: region,
})
}

View File

@ -0,0 +1,113 @@
package openstack
import (
"os"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
// Provider returns a schema.Provider for OpenStack.
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"auth_url": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: envDefaultFunc("OS_AUTH_URL"),
},
"user_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: envDefaultFunc("OS_USERNAME"),
},
"user_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
},
"tenant_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
},
"tenant_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: envDefaultFunc("OS_TENANT_NAME"),
},
"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: envDefaultFunc("OS_PASSWORD"),
},
"api_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
},
"domain_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
},
"domain_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
},
},
ResourcesMap: map[string]*schema.Resource{
"openstack_blockstorage_volume_v1": resourceBlockStorageVolumeV1(),
"openstack_compute_instance_v2": resourceComputeInstanceV2(),
"openstack_compute_keypair_v2": resourceComputeKeypairV2(),
"openstack_compute_secgroup_v2": resourceComputeSecGroupV2(),
"openstack_compute_floatingip_v2": resourceComputeFloatingIPV2(),
"openstack_fw_firewall_v1": resourceFWFirewallV1(),
"openstack_fw_policy_v1": resourceFWPolicyV1(),
"openstack_fw_rule_v1": resourceFWRuleV1(),
"openstack_lb_monitor_v1": resourceLBMonitorV1(),
"openstack_lb_pool_v1": resourceLBPoolV1(),
"openstack_lb_vip_v1": resourceLBVipV1(),
"openstack_networking_network_v2": resourceNetworkingNetworkV2(),
"openstack_networking_subnet_v2": resourceNetworkingSubnetV2(),
"openstack_networking_floatingip_v2": resourceNetworkingFloatingIPV2(),
"openstack_networking_router_v2": resourceNetworkingRouterV2(),
"openstack_networking_router_interface_v2": resourceNetworkingRouterInterfaceV2(),
"openstack_objectstorage_container_v1": resourceObjectStorageContainerV1(),
},
ConfigureFunc: configureProvider,
}
}
func configureProvider(d *schema.ResourceData) (interface{}, error) {
config := Config{
IdentityEndpoint: d.Get("auth_url").(string),
Username: d.Get("user_name").(string),
UserID: d.Get("user_id").(string),
Password: d.Get("password").(string),
APIKey: d.Get("api_key").(string),
TenantID: d.Get("tenant_id").(string),
TenantName: d.Get("tenant_name").(string),
DomainID: d.Get("domain_id").(string),
DomainName: d.Get("domain_name").(string),
}
if err := config.loadAndValidate(); err != nil {
return nil, err
}
return &config, nil
}
func envDefaultFunc(k string) schema.SchemaDefaultFunc {
return func() (interface{}, error) {
if v := os.Getenv(k); v != "" {
return v, nil
}
return nil, nil
}
}

View File

@ -0,0 +1,66 @@
package openstack
import (
"os"
"testing"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
var (
OS_REGION_NAME = ""
OS_POOL_NAME = ""
)
var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *schema.Provider
func init() {
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"openstack": testAccProvider,
}
}
func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = Provider()
}
func testAccPreCheck(t *testing.T) {
v := os.Getenv("OS_AUTH_URL")
if v == "" {
t.Fatal("OS_AUTH_URL must be set for acceptance tests")
}
v = os.Getenv("OS_REGION_NAME")
if v == "" {
t.Fatal("OS_REGION_NAME must be set for acceptance tests")
}
OS_REGION_NAME = v
v1 := os.Getenv("OS_IMAGE_ID")
v2 := os.Getenv("OS_IMAGE_NAME")
if v1 == "" && v2 == "" {
t.Fatal("OS_IMAGE_ID or OS_IMAGE_NAME must be set for acceptance tests")
}
v = os.Getenv("OS_POOL_NAME")
if v == "" {
t.Fatal("OS_POOL_NAME must be set for acceptance tests")
}
OS_POOL_NAME = v
v1 = os.Getenv("OS_FLAVOR_ID")
v2 = os.Getenv("OS_FLAVOR_NAME")
if v1 == "" && v2 == "" {
t.Fatal("OS_FLAVOR_ID or OS_FLAVOR_NAME must be set for acceptance tests")
}
}

View File

@ -0,0 +1,314 @@
package openstack
import (
"bytes"
"fmt"
"log"
"time"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"
)
func resourceBlockStorageVolumeV1() *schema.Resource {
return &schema.Resource{
Create: resourceBlockStorageVolumeV1Create,
Read: resourceBlockStorageVolumeV1Read,
Update: resourceBlockStorageVolumeV1Update,
Delete: resourceBlockStorageVolumeV1Delete,
Schema: map[string]*schema.Schema{
"region": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
DefaultFunc: envDefaultFunc("OS_REGION_NAME"),
},
"size": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
},
"metadata": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
ForceNew: false,
},
"snapshot_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"source_vol_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"image_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"volume_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"attachment": &schema.Schema{
Type: schema.TypeSet,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"instance_id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"device": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
},
Set: resourceVolumeAttachmentHash,
},
},
}
}
func resourceBlockStorageVolumeV1Create(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
blockStorageClient, err := config.blockStorageV1Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
}
createOpts := &volumes.CreateOpts{
Description: d.Get("description").(string),
Name: d.Get("name").(string),
Size: d.Get("size").(int),
SnapshotID: d.Get("snapshot_id").(string),
SourceVolID: d.Get("source_vol_id").(string),
ImageID: d.Get("image_id").(string),
VolumeType: d.Get("volume_type").(string),
Metadata: resourceContainerMetadataV2(d),
}
log.Printf("[DEBUG] Create Options: %#v", createOpts)
v, err := volumes.Create(blockStorageClient, createOpts).Extract()
if err != nil {
return fmt.Errorf("Error creating OpenStack volume: %s", err)
}
log.Printf("[INFO] Volume ID: %s", v.ID)
// Store the ID now
d.SetId(v.ID)
// Wait for the volume to become available.
log.Printf(
"[DEBUG] Waiting for volume (%s) to become available",
v.ID)
stateConf := &resource.StateChangeConf{
Target: "available",
Refresh: VolumeV1StateRefreshFunc(blockStorageClient, v.ID),
Timeout: 10 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return fmt.Errorf(
"Error waiting for volume (%s) to become ready: %s",
v.ID, err)
}
return resourceBlockStorageVolumeV1Read(d, meta)
}
func resourceBlockStorageVolumeV1Read(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
blockStorageClient, err := config.blockStorageV1Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
}
v, err := volumes.Get(blockStorageClient, d.Id()).Extract()
if err != nil {
return CheckDeleted(d, err, "volume")
}
log.Printf("[DEBUG] Retreived volume %s: %+v", d.Id(), v)
d.Set("size", v.Size)
d.Set("description", v.Description)
d.Set("name", v.Name)
d.Set("snapshot_id", v.SnapshotID)
d.Set("source_vol_id", v.SourceVolID)
d.Set("volume_type", v.VolumeType)
d.Set("metadata", v.Metadata)
if len(v.Attachments) > 0 {
attachments := make([]map[string]interface{}, len(v.Attachments))
for i, attachment := range v.Attachments {
attachments[i] = make(map[string]interface{})
attachments[i]["id"] = attachment["id"]
attachments[i]["instance_id"] = attachment["server_id"]
attachments[i]["device"] = attachment["device"]
log.Printf("[DEBUG] attachment: %v", attachment)
}
d.Set("attachment", attachments)
}
return nil
}
func resourceBlockStorageVolumeV1Update(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
blockStorageClient, err := config.blockStorageV1Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
}
updateOpts := volumes.UpdateOpts{
Name: d.Get("name").(string),
Description: d.Get("description").(string),
}
if d.HasChange("metadata") {
updateOpts.Metadata = resourceVolumeMetadataV1(d)
}
_, err = volumes.Update(blockStorageClient, d.Id(), updateOpts).Extract()
if err != nil {
return fmt.Errorf("Error updating OpenStack volume: %s", err)
}
return resourceBlockStorageVolumeV1Read(d, meta)
}
func resourceBlockStorageVolumeV1Delete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
blockStorageClient, err := config.blockStorageV1Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
}
v, err := volumes.Get(blockStorageClient, d.Id()).Extract()
if err != nil {
return CheckDeleted(d, err, "volume")
}
// make sure this volume is detached from all instances before deleting
if len(v.Attachments) > 0 {
log.Printf("[DEBUG] detaching volumes")
if computeClient, err := config.computeV2Client(d.Get("region").(string)); err != nil {
return err
} else {
for _, volumeAttachment := range v.Attachments {
log.Printf("[DEBUG] Attachment: %v", volumeAttachment)
if err := volumeattach.Delete(computeClient, volumeAttachment["server_id"].(string), volumeAttachment["id"].(string)).ExtractErr(); err != nil {
return err
}
}
stateConf := &resource.StateChangeConf{
Pending: []string{"in-use", "attaching"},
Target: "available",
Refresh: VolumeV1StateRefreshFunc(blockStorageClient, d.Id()),
Timeout: 10 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return fmt.Errorf(
"Error waiting for volume (%s) to become available: %s",
d.Id(), err)
}
}
}
err = volumes.Delete(blockStorageClient, d.Id()).ExtractErr()
if err != nil {
return fmt.Errorf("Error deleting OpenStack volume: %s", err)
}
// Wait for the volume to delete before moving on.
log.Printf("[DEBUG] Waiting for volume (%s) to delete", d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"deleting", "available"},
Target: "deleted",
Refresh: VolumeV1StateRefreshFunc(blockStorageClient, d.Id()),
Timeout: 10 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return fmt.Errorf(
"Error waiting for volume (%s) to delete: %s",
d.Id(), err)
}
d.SetId("")
return nil
}
func resourceVolumeMetadataV1(d *schema.ResourceData) map[string]string {
m := make(map[string]string)
for key, val := range d.Get("metadata").(map[string]interface{}) {
m[key] = val.(string)
}
return m
}
// VolumeV1StateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
// an OpenStack volume.
func VolumeV1StateRefreshFunc(client *gophercloud.ServiceClient, volumeID string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
v, err := volumes.Get(client, volumeID).Extract()
if err != nil {
errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if !ok {
return nil, "", err
}
if errCode.Actual == 404 {
return v, "deleted", nil
}
return nil, "", err
}
return v, v.Status, nil
}
}
func resourceVolumeAttachmentHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
if m["instance_id"] != nil {
buf.WriteString(fmt.Sprintf("%s-", m["instance_id"].(string)))
}
return hashcode.String(buf.String())
}

View File

@ -0,0 +1,138 @@
package openstack
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
)
func TestAccBlockStorageV1Volume_basic(t *testing.T) {
var volume volumes.Volume
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckBlockStorageV1VolumeDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccBlockStorageV1Volume_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckBlockStorageV1VolumeExists(t, "openstack_blockstorage_volume_v1.volume_1", &volume),
resource.TestCheckResourceAttr("openstack_blockstorage_volume_v1.volume_1", "name", "tf-test-volume"),
testAccCheckBlockStorageV1VolumeMetadata(&volume, "foo", "bar"),
),
},
resource.TestStep{
Config: testAccBlockStorageV1Volume_update,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("openstack_blockstorage_volume_v1.volume_1", "name", "tf-test-volume-updated"),
testAccCheckBlockStorageV1VolumeMetadata(&volume, "foo", "bar"),
),
},
},
})
}
func testAccCheckBlockStorageV1VolumeDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
blockStorageClient, err := config.blockStorageV1Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
}
for _, rs := range s.RootModule().Resources {
if rs.Type != "openstack_blockstorage_volume_v1" {
continue
}
_, err := volumes.Get(blockStorageClient, rs.Primary.ID).Extract()
if err == nil {
return fmt.Errorf("Volume still exists")
}
}
return nil
}
func testAccCheckBlockStorageV1VolumeExists(t *testing.T, n string, volume *volumes.Volume) 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 ID is set")
}
config := testAccProvider.Meta().(*Config)
blockStorageClient, err := config.blockStorageV1Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("Error creating OpenStack block storage client: %s", err)
}
found, err := volumes.Get(blockStorageClient, rs.Primary.ID).Extract()
if err != nil {
return err
}
if found.ID != rs.Primary.ID {
return fmt.Errorf("Volume not found")
}
*volume = *found
return nil
}
}
func testAccCheckBlockStorageV1VolumeMetadata(
volume *volumes.Volume, k string, v string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if volume.Metadata == nil {
return fmt.Errorf("No metadata")
}
for key, value := range volume.Metadata {
if k != key {
continue
}
if v == value {
return nil
}
return fmt.Errorf("Bad value for %s: %s", k, value)
}
return fmt.Errorf("Metadata not found: %s", k)
}
}
var testAccBlockStorageV1Volume_basic = fmt.Sprintf(`
resource "openstack_blockstorage_volume_v1" "volume_1" {
region = "%s"
name = "tf-test-volume"
description = "first test volume"
metadata{
foo = "bar"
}
size = 1
}`,
OS_REGION_NAME)
var testAccBlockStorageV1Volume_update = fmt.Sprintf(`
resource "openstack_blockstorage_volume_v1" "volume_1" {
region = "%s"
name = "tf-test-volume-updated"
description = "first test volume"
metadata{
foo = "bar"
}
size = 1
}`,
OS_REGION_NAME)

View File

@ -0,0 +1,107 @@
package openstack
import (
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
)
func resourceComputeFloatingIPV2() *schema.Resource {
return &schema.Resource{
Create: resourceComputeFloatingIPV2Create,
Read: resourceComputeFloatingIPV2Read,
Update: nil,
Delete: resourceComputeFloatingIPV2Delete,
Schema: map[string]*schema.Schema{
"region": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
DefaultFunc: envDefaultFunc("OS_REGION_NAME"),
},
"pool": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
DefaultFunc: envDefaultFunc("OS_POOL_NAME"),
},
"address": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"fixed_ip": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"instance_id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}
func resourceComputeFloatingIPV2Create(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
computeClient, err := config.computeV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
createOpts := &floatingip.CreateOpts{
Pool: d.Get("pool").(string),
}
log.Printf("[DEBUG] Create Options: %#v", createOpts)
newFip, err := floatingip.Create(computeClient, createOpts).Extract()
if err != nil {
return fmt.Errorf("Error creating Floating IP: %s", err)
}
d.SetId(newFip.ID)
return resourceComputeFloatingIPV2Read(d, meta)
}
func resourceComputeFloatingIPV2Read(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
computeClient, err := config.computeV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
fip, err := floatingip.Get(computeClient, d.Id()).Extract()
if err != nil {
return CheckDeleted(d, err, "floating ip")
}
log.Printf("[DEBUG] Retrieved Floating IP %s: %+v", d.Id(), fip)
d.Set("pool", fip.Pool)
d.Set("instance_id", fip.InstanceID)
d.Set("address", fip.IP)
d.Set("fixed_ip", fip.FixedIP)
return nil
}
func resourceComputeFloatingIPV2Delete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
computeClient, err := config.computeV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
log.Printf("[DEBUG] Deleting Floating IP %s", d.Id())
if err := floatingip.Delete(computeClient, d.Id()).ExtractErr(); err != nil {
return fmt.Errorf("Error deleting Floating IP: %s", err)
}
return nil
}

View File

@ -0,0 +1,91 @@
package openstack
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
)
func TestAccComputeV2FloatingIP_basic(t *testing.T) {
var floatingIP floatingip.FloatingIP
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckComputeV2FloatingIPDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccComputeV2FloatingIP_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckComputeV2FloatingIPExists(t, "openstack_compute_floatingip_v2.foo", &floatingIP),
),
},
},
})
}
func testAccCheckComputeV2FloatingIPDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
computeClient, err := config.computeV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckComputeV2FloatingIPDestroy) Error creating OpenStack compute client: %s", err)
}
for _, rs := range s.RootModule().Resources {
if rs.Type != "openstack_compute_floatingip_v2" {
continue
}
_, err := floatingip.Get(computeClient, rs.Primary.ID).Extract()
if err == nil {
return fmt.Errorf("FloatingIP still exists")
}
}
return nil
}
func testAccCheckComputeV2FloatingIPExists(t *testing.T, n string, kp *floatingip.FloatingIP) 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 ID is set")
}
config := testAccProvider.Meta().(*Config)
computeClient, err := config.computeV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckComputeV2FloatingIPExists) Error creating OpenStack compute client: %s", err)
}
found, err := floatingip.Get(computeClient, rs.Primary.ID).Extract()
if err != nil {
return err
}
if found.ID != rs.Primary.ID {
return fmt.Errorf("FloatingIP not found")
}
*kp = *found
return nil
}
}
var testAccComputeV2FloatingIP_basic = `
resource "openstack_compute_floatingip_v2" "foo" {
}
resource "openstack_compute_instance_v2" "bar" {
name = "terraform-acc-floating-ip-test"
floating_ip = "${openstack_compute_floatingip_v2.foo.address}"
}`

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,185 @@
package openstack
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"github.com/rackspace/gophercloud/pagination"
)
func TestAccComputeV2Instance_basic(t *testing.T) {
var instance servers.Server
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckComputeV2InstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccComputeV2Instance_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance),
testAccCheckComputeV2InstanceMetadata(&instance, "foo", "bar"),
),
},
},
})
}
func TestAccComputeV2Instance_volumeAttach(t *testing.T) {
var instance servers.Server
var volume volumes.Volume
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckComputeV2InstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccComputeV2Instance_volumeAttach,
Check: resource.ComposeTestCheckFunc(
testAccCheckBlockStorageV1VolumeExists(t, "openstack_blockstorage_volume_v1.myvol", &volume),
testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance),
testAccCheckComputeV2InstanceVolumeAttachment(&instance, &volume),
),
},
},
})
}
func testAccCheckComputeV2InstanceDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
computeClient, err := config.computeV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckComputeV2InstanceDestroy) Error creating OpenStack compute client: %s", err)
}
for _, rs := range s.RootModule().Resources {
if rs.Type != "openstack_compute_instance_v2" {
continue
}
_, err := servers.Get(computeClient, rs.Primary.ID).Extract()
if err == nil {
return fmt.Errorf("Instance still exists")
}
}
return nil
}
func testAccCheckComputeV2InstanceExists(t *testing.T, n string, instance *servers.Server) 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 ID is set")
}
config := testAccProvider.Meta().(*Config)
computeClient, err := config.computeV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckComputeV2InstanceExists) Error creating OpenStack compute client: %s", err)
}
found, err := servers.Get(computeClient, rs.Primary.ID).Extract()
if err != nil {
return err
}
if found.ID != rs.Primary.ID {
return fmt.Errorf("Instance not found")
}
*instance = *found
return nil
}
}
func testAccCheckComputeV2InstanceMetadata(
instance *servers.Server, k string, v string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if instance.Metadata == nil {
return fmt.Errorf("No metadata")
}
for key, value := range instance.Metadata {
if k != key {
continue
}
if v == value.(string) {
return nil
}
return fmt.Errorf("Bad value for %s: %s", k, value)
}
return fmt.Errorf("Metadata not found: %s", k)
}
}
func testAccCheckComputeV2InstanceVolumeAttachment(
instance *servers.Server, volume *volumes.Volume) resource.TestCheckFunc {
return func(s *terraform.State) error {
var attachments []volumeattach.VolumeAttachment
config := testAccProvider.Meta().(*Config)
computeClient, err := config.computeV2Client(OS_REGION_NAME)
if err != nil {
return err
}
err = volumeattach.List(computeClient, instance.ID).EachPage(func(page pagination.Page) (bool, error) {
actual, err := volumeattach.ExtractVolumeAttachments(page)
if err != nil {
return false, fmt.Errorf("Unable to lookup attachment: %s", err)
}
attachments = actual
return true, nil
})
for _, attachment := range attachments {
if attachment.VolumeID == volume.ID {
return nil
}
}
return fmt.Errorf("Volume not found: %s", volume.ID)
}
}
var testAccComputeV2Instance_basic = fmt.Sprintf(`
resource "openstack_compute_instance_v2" "foo" {
region = "%s"
name = "terraform-test"
metadata {
foo = "bar"
}
}`,
OS_REGION_NAME)
var testAccComputeV2Instance_volumeAttach = fmt.Sprintf(`
resource "openstack_blockstorage_volume_v1" "myvol" {
name = "myvol"
size = 1
}
resource "openstack_compute_instance_v2" "foo" {
region = "%s"
name = "terraform-test"
volume {
volume_id = "${openstack_blockstorage_volume_v1.myvol.id}"
}
}`,
OS_REGION_NAME)

View File

@ -0,0 +1,92 @@
package openstack
import (
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
)
func resourceComputeKeypairV2() *schema.Resource {
return &schema.Resource{
Create: resourceComputeKeypairV2Create,
Read: resourceComputeKeypairV2Read,
Delete: resourceComputeKeypairV2Delete,
Schema: map[string]*schema.Schema{
"region": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
DefaultFunc: envDefaultFunc("OS_REGION_NAME"),
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"public_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
}
}
func resourceComputeKeypairV2Create(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
computeClient, err := config.computeV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
createOpts := keypairs.CreateOpts{
Name: d.Get("name").(string),
PublicKey: d.Get("public_key").(string),
}
log.Printf("[DEBUG] Create Options: %#v", createOpts)
kp, err := keypairs.Create(computeClient, createOpts).Extract()
if err != nil {
return fmt.Errorf("Error creating OpenStack keypair: %s", err)
}
d.SetId(kp.Name)
return resourceComputeKeypairV2Read(d, meta)
}
func resourceComputeKeypairV2Read(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
computeClient, err := config.computeV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
kp, err := keypairs.Get(computeClient, d.Id()).Extract()
if err != nil {
return CheckDeleted(d, err, "keypair")
}
d.Set("name", kp.Name)
d.Set("public_key", kp.PublicKey)
return nil
}
func resourceComputeKeypairV2Delete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
computeClient, err := config.computeV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
err = keypairs.Delete(computeClient, d.Id()).ExtractErr()
if err != nil {
return fmt.Errorf("Error deleting OpenStack keypair: %s", err)
}
d.SetId("")
return nil
}

View File

@ -0,0 +1,90 @@
package openstack
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
)
func TestAccComputeV2Keypair_basic(t *testing.T) {
var keypair keypairs.KeyPair
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckComputeV2KeypairDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccComputeV2Keypair_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckComputeV2KeypairExists(t, "openstack_compute_keypair_v2.foo", &keypair),
),
},
},
})
}
func testAccCheckComputeV2KeypairDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
computeClient, err := config.computeV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckComputeV2KeypairDestroy) Error creating OpenStack compute client: %s", err)
}
for _, rs := range s.RootModule().Resources {
if rs.Type != "openstack_compute_keypair_v2" {
continue
}
_, err := keypairs.Get(computeClient, rs.Primary.ID).Extract()
if err == nil {
return fmt.Errorf("Keypair still exists")
}
}
return nil
}
func testAccCheckComputeV2KeypairExists(t *testing.T, n string, kp *keypairs.KeyPair) 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 ID is set")
}
config := testAccProvider.Meta().(*Config)
computeClient, err := config.computeV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckComputeV2KeypairExists) Error creating OpenStack compute client: %s", err)
}
found, err := keypairs.Get(computeClient, rs.Primary.ID).Extract()
if err != nil {
return err
}
if found.Name != rs.Primary.ID {
return fmt.Errorf("Keypair not found")
}
*kp = *found
return nil
}
}
var testAccComputeV2Keypair_basic = fmt.Sprintf(`
resource "openstack_compute_keypair_v2" "foo" {
region = "%s"
name = "test-keypair-tf"
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAjpC1hwiOCCmKEWxJ4qzTTsJbKzndLo1BCz5PcwtUnflmU+gHJtWMZKpuEGVi29h0A/+ydKek1O18k10Ff+4tyFjiHDQAT9+OfgWf7+b1yK+qDip3X1C0UPMbwHlTfSGWLGZquwhvEFx9k3h/M+VtMvwR1lJ9LUyTAImnNjWG7TAIPmui30HvM2UiFEmqkr4ijq45MyX2+fLIePLRIFuu1p4whjHAQYufqyno3BS48icQb4p6iVEZPo4AE2o9oIyQvj2mx4dk5Y8CgSETOZTYDOR3rU2fZTRDRgPJDH9FWvQjF5tA0p3d9CoWWd2s6GKKbfoUIi8R/Db1BSPJwkqB jrp-hp-pc"
}`,
OS_REGION_NAME)

View File

@ -0,0 +1,294 @@
package openstack
import (
"bytes"
"fmt"
"log"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
)
func resourceComputeSecGroupV2() *schema.Resource {
return &schema.Resource{
Create: resourceComputeSecGroupV2Create,
Read: resourceComputeSecGroupV2Read,
Update: resourceComputeSecGroupV2Update,
Delete: resourceComputeSecGroupV2Delete,
Schema: map[string]*schema.Schema{
"region": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
DefaultFunc: envDefaultFunc("OS_REGION_NAME"),
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: false,
},
"description": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: false,
},
"rule": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"from_port": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: false,
},
"to_port": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: false,
},
"ip_protocol": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: false,
},
"cidr": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
},
"from_group_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
},
"self": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
ForceNew: false,
},
},
},
},
},
}
}
func resourceComputeSecGroupV2Create(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
computeClient, err := config.computeV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
createOpts := secgroups.CreateOpts{
Name: d.Get("name").(string),
Description: d.Get("description").(string),
}
log.Printf("[DEBUG] Create Options: %#v", createOpts)
sg, err := secgroups.Create(computeClient, createOpts).Extract()
if err != nil {
return fmt.Errorf("Error creating OpenStack security group: %s", err)
}
d.SetId(sg.ID)
createRuleOptsList := resourceSecGroupRulesV2(d)
for _, createRuleOpts := range createRuleOptsList {
_, err := secgroups.CreateRule(computeClient, createRuleOpts).Extract()
if err != nil {
return fmt.Errorf("Error creating OpenStack security group rule: %s", err)
}
}
return resourceComputeSecGroupV2Read(d, meta)
}
func resourceComputeSecGroupV2Read(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
computeClient, err := config.computeV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
sg, err := secgroups.Get(computeClient, d.Id()).Extract()
if err != nil {
return CheckDeleted(d, err, "security group")
}
d.Set("name", sg.Name)
d.Set("description", sg.Description)
rtm := rulesToMap(sg.Rules)
for _, v := range rtm {
if v["group"] == d.Get("name") {
v["self"] = "1"
} else {
v["self"] = "0"
}
}
log.Printf("[DEBUG] rulesToMap(sg.Rules): %+v", rtm)
d.Set("rule", rtm)
return nil
}
func resourceComputeSecGroupV2Update(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
computeClient, err := config.computeV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
updateOpts := secgroups.UpdateOpts{
Name: d.Get("name").(string),
Description: d.Get("description").(string),
}
log.Printf("[DEBUG] Updating Security Group (%s) with options: %+v", d.Id(), updateOpts)
_, err = secgroups.Update(computeClient, d.Id(), updateOpts).Extract()
if err != nil {
return fmt.Errorf("Error updating OpenStack security group (%s): %s", d.Id(), err)
}
if d.HasChange("rule") {
oldSGRaw, newSGRaw := d.GetChange("rule")
oldSGRSlice, newSGRSlice := oldSGRaw.([]interface{}), newSGRaw.([]interface{})
oldSGRSet := schema.NewSet(secgroupRuleV2Hash, oldSGRSlice)
newSGRSet := schema.NewSet(secgroupRuleV2Hash, newSGRSlice)
secgrouprulesToAdd := newSGRSet.Difference(oldSGRSet)
secgrouprulesToRemove := oldSGRSet.Difference(newSGRSet)
log.Printf("[DEBUG] Security group rules to add: %v", secgrouprulesToAdd)
log.Printf("[DEBUG] Security groups rules to remove: %v", secgrouprulesToRemove)
for _, rawRule := range secgrouprulesToAdd.List() {
createRuleOpts := resourceSecGroupRuleCreateOptsV2(d, rawRule)
rule, err := secgroups.CreateRule(computeClient, createRuleOpts).Extract()
if err != nil {
return fmt.Errorf("Error adding rule to OpenStack security group (%s): %s", d.Id(), err)
}
log.Printf("[DEBUG] Added rule (%s) to OpenStack security group (%s) ", rule.ID, d.Id())
}
for _, r := range secgrouprulesToRemove.List() {
rule := resourceSecGroupRuleV2(d, r)
err := secgroups.DeleteRule(computeClient, rule.ID).ExtractErr()
if err != nil {
errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if !ok {
return fmt.Errorf("Error removing rule (%s) from OpenStack security group (%s): %s", rule.ID, d.Id(), err)
}
if errCode.Actual == 404 {
continue
} else {
return fmt.Errorf("Error removing rule (%s) from OpenStack security group (%s)", rule.ID, d.Id())
}
} else {
log.Printf("[DEBUG] Removed rule (%s) from OpenStack security group (%s): %s", rule.ID, d.Id(), err)
}
}
}
return resourceComputeSecGroupV2Read(d, meta)
}
func resourceComputeSecGroupV2Delete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
computeClient, err := config.computeV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
err = secgroups.Delete(computeClient, d.Id()).ExtractErr()
if err != nil {
return fmt.Errorf("Error deleting OpenStack security group: %s", err)
}
d.SetId("")
return nil
}
func resourceSecGroupRulesV2(d *schema.ResourceData) []secgroups.CreateRuleOpts {
rawRules := (d.Get("rule")).([]interface{})
createRuleOptsList := make([]secgroups.CreateRuleOpts, len(rawRules))
for i, raw := range rawRules {
rawMap := raw.(map[string]interface{})
groupId := rawMap["from_group_id"].(string)
if rawMap["self"].(bool) {
groupId = d.Id()
}
createRuleOptsList[i] = secgroups.CreateRuleOpts{
ParentGroupID: d.Id(),
FromPort: rawMap["from_port"].(int),
ToPort: rawMap["to_port"].(int),
IPProtocol: rawMap["ip_protocol"].(string),
CIDR: rawMap["cidr"].(string),
FromGroupID: groupId,
}
}
return createRuleOptsList
}
func resourceSecGroupRuleCreateOptsV2(d *schema.ResourceData, raw interface{}) secgroups.CreateRuleOpts {
rawMap := raw.(map[string]interface{})
groupId := rawMap["from_group_id"].(string)
if rawMap["self"].(bool) {
groupId = d.Id()
}
return secgroups.CreateRuleOpts{
ParentGroupID: d.Id(),
FromPort: rawMap["from_port"].(int),
ToPort: rawMap["to_port"].(int),
IPProtocol: rawMap["ip_protocol"].(string),
CIDR: rawMap["cidr"].(string),
FromGroupID: groupId,
}
}
func resourceSecGroupRuleV2(d *schema.ResourceData, raw interface{}) secgroups.Rule {
rawMap := raw.(map[string]interface{})
return secgroups.Rule{
ID: rawMap["id"].(string),
ParentGroupID: d.Id(),
FromPort: rawMap["from_port"].(int),
ToPort: rawMap["to_port"].(int),
IPProtocol: rawMap["ip_protocol"].(string),
IPRange: secgroups.IPRange{CIDR: rawMap["cidr"].(string)},
}
}
func rulesToMap(sgrs []secgroups.Rule) []map[string]interface{} {
sgrMap := make([]map[string]interface{}, len(sgrs))
for i, sgr := range sgrs {
sgrMap[i] = map[string]interface{}{
"id": sgr.ID,
"from_port": sgr.FromPort,
"to_port": sgr.ToPort,
"ip_protocol": sgr.IPProtocol,
"cidr": sgr.IPRange.CIDR,
"group": sgr.Group.Name,
}
}
return sgrMap
}
func secgroupRuleV2Hash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int)))
buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["ip_protocol"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["cidr"].(string)))
return hashcode.String(buf.String())
}

View File

@ -0,0 +1,90 @@
package openstack
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
)
func TestAccComputeV2SecGroup_basic(t *testing.T) {
var secgroup secgroups.SecurityGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckComputeV2SecGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccComputeV2SecGroup_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckComputeV2SecGroupExists(t, "openstack_compute_secgroup_v2.foo", &secgroup),
),
},
},
})
}
func testAccCheckComputeV2SecGroupDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
computeClient, err := config.computeV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckComputeV2SecGroupDestroy) Error creating OpenStack compute client: %s", err)
}
for _, rs := range s.RootModule().Resources {
if rs.Type != "openstack_compute_secgroup_v2" {
continue
}
_, err := secgroups.Get(computeClient, rs.Primary.ID).Extract()
if err == nil {
return fmt.Errorf("Security group still exists")
}
}
return nil
}
func testAccCheckComputeV2SecGroupExists(t *testing.T, n string, secgroup *secgroups.SecurityGroup) 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 ID is set")
}
config := testAccProvider.Meta().(*Config)
computeClient, err := config.computeV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckComputeV2SecGroupExists) Error creating OpenStack compute client: %s", err)
}
found, err := secgroups.Get(computeClient, rs.Primary.ID).Extract()
if err != nil {
return err
}
if found.ID != rs.Primary.ID {
return fmt.Errorf("Security group not found")
}
*secgroup = *found
return nil
}
}
var testAccComputeV2SecGroup_basic = fmt.Sprintf(`
resource "openstack_compute_secgroup_v2" "foo" {
region = "%s"
name = "test_group_1"
description = "first test security group"
}`,
OS_REGION_NAME)

View File

@ -0,0 +1,242 @@
package openstack
import (
"fmt"
"log"
"time"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls"
)
func resourceFWFirewallV1() *schema.Resource {
return &schema.Resource{
Create: resourceFWFirewallV1Create,
Read: resourceFWFirewallV1Read,
Update: resourceFWFirewallV1Update,
Delete: resourceFWFirewallV1Delete,
Schema: map[string]*schema.Schema{
"region": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
DefaultFunc: envDefaultFunc("OS_REGION_NAME"),
},
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"policy_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"admin_state_up": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"tenant_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
}
}
func resourceFWFirewallV1Create(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
adminStateUp := d.Get("admin_state_up").(bool)
firewallConfiguration := firewalls.CreateOpts{
Name: d.Get("name").(string),
Description: d.Get("description").(string),
PolicyID: d.Get("policy_id").(string),
AdminStateUp: &adminStateUp,
TenantID: d.Get("tenant_id").(string),
}
log.Printf("[DEBUG] Create firewall: %#v", firewallConfiguration)
firewall, err := firewalls.Create(networkingClient, firewallConfiguration).Extract()
if err != nil {
return err
}
log.Printf("[DEBUG] Firewall created: %#v", firewall)
stateConf := &resource.StateChangeConf{
Pending: []string{"PENDING_CREATE"},
Target: "ACTIVE",
Refresh: waitForFirewallActive(networkingClient, firewall.ID),
Timeout: 30 * time.Second,
Delay: 0,
MinTimeout: 2 * time.Second,
}
_, err = stateConf.WaitForState()
d.SetId(firewall.ID)
return resourceFWFirewallV1Read(d, meta)
}
func resourceFWFirewallV1Read(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG] Retrieve information about firewall: %s", d.Id())
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
firewall, err := firewalls.Get(networkingClient, d.Id()).Extract()
if err != nil {
return CheckDeleted(d, err, "LB pool")
}
d.Set("name", firewall.Name)
d.Set("description", firewall.Description)
d.Set("policy_id", firewall.PolicyID)
d.Set("admin_state_up", firewall.AdminStateUp)
d.Set("tenant_id", firewall.TenantID)
return nil
}
func resourceFWFirewallV1Update(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
opts := firewalls.UpdateOpts{}
if d.HasChange("name") {
opts.Name = d.Get("name").(string)
}
if d.HasChange("description") {
opts.Description = d.Get("description").(string)
}
if d.HasChange("policy_id") {
opts.PolicyID = d.Get("policy_id").(string)
}
if d.HasChange("admin_state_up") {
adminStateUp := d.Get("admin_state_up").(bool)
opts.AdminStateUp = &adminStateUp
}
log.Printf("[DEBUG] Updating firewall with id %s: %#v", d.Id(), opts)
stateConf := &resource.StateChangeConf{
Pending: []string{"PENDING_CREATE", "PENDING_UPDATE"},
Target: "ACTIVE",
Refresh: waitForFirewallActive(networkingClient, d.Id()),
Timeout: 30 * time.Second,
Delay: 0,
MinTimeout: 2 * time.Second,
}
_, err = stateConf.WaitForState()
err = firewalls.Update(networkingClient, d.Id(), opts).Err
if err != nil {
return err
}
return resourceFWFirewallV1Read(d, meta)
}
func resourceFWFirewallV1Delete(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG] Destroy firewall: %s", d.Id())
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
stateConf := &resource.StateChangeConf{
Pending: []string{"PENDING_CREATE", "PENDING_UPDATE"},
Target: "ACTIVE",
Refresh: waitForFirewallActive(networkingClient, d.Id()),
Timeout: 30 * time.Second,
Delay: 0,
MinTimeout: 2 * time.Second,
}
_, err = stateConf.WaitForState()
err = firewalls.Delete(networkingClient, d.Id()).Err
if err != nil {
return err
}
stateConf = &resource.StateChangeConf{
Pending: []string{"DELETING"},
Target: "DELETED",
Refresh: waitForFirewallDeletion(networkingClient, d.Id()),
Timeout: 2 * time.Minute,
Delay: 0,
MinTimeout: 2 * time.Second,
}
_, err = stateConf.WaitForState()
return err
}
func waitForFirewallActive(networkingClient *gophercloud.ServiceClient, id string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
fw, err := firewalls.Get(networkingClient, id).Extract()
log.Printf("[DEBUG] Get firewall %s => %#v", id, fw)
if err != nil {
return nil, "", err
}
return fw, fw.Status, nil
}
}
func waitForFirewallDeletion(networkingClient *gophercloud.ServiceClient, id string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
fw, err := firewalls.Get(networkingClient, id).Extract()
log.Printf("[DEBUG] Get firewall %s => %#v", id, fw)
if err != nil {
httpStatus := err.(*gophercloud.UnexpectedResponseCodeError)
log.Printf("[DEBUG] Get firewall %s status is %d", id, httpStatus.Actual)
if httpStatus.Actual == 404 {
log.Printf("[DEBUG] Firewall %s is actually deleted", id)
return "", "DELETED", nil
}
return nil, "", fmt.Errorf("Unexpected status code %d", httpStatus.Actual)
}
log.Printf("[DEBUG] Firewall %s deletion is pending", id)
return fw, "DELETING", nil
}
}

View File

@ -0,0 +1,139 @@
package openstack
import (
"fmt"
"testing"
"time"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls"
)
func TestAccFWFirewallV1(t *testing.T) {
var policyID *string
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckFWFirewallV1Destroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testFirewallConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckFWFirewallV1Exists("openstack_fw_firewall_v1.accept_test", "", "", policyID),
),
},
resource.TestStep{
Config: testFirewallConfigUpdated,
Check: resource.ComposeTestCheckFunc(
testAccCheckFWFirewallV1Exists("openstack_fw_firewall_v1.accept_test", "accept_test", "terraform acceptance test", policyID),
),
},
},
})
}
func testAccCheckFWFirewallV1Destroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckOpenstackFirewallDestroy) Error creating OpenStack networking client: %s", err)
}
for _, rs := range s.RootModule().Resources {
if rs.Type != "openstack_firewall" {
continue
}
_, err = firewalls.Get(networkingClient, rs.Primary.ID).Extract()
if err == nil {
return fmt.Errorf("Firewall (%s) still exists.", rs.Primary.ID)
}
httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if !ok || httpError.Actual != 404 {
return httpError
}
}
return nil
}
func testAccCheckFWFirewallV1Exists(n, expectedName, expectedDescription string, policyID *string) 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 ID is set")
}
config := testAccProvider.Meta().(*Config)
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckFirewallExists) Error creating OpenStack networking client: %s", err)
}
var found *firewalls.Firewall
for i := 0; i < 5; i++ {
// Firewall creation is asynchronous. Retry some times
// if we get a 404 error. Fail on any other error.
found, err = firewalls.Get(networkingClient, rs.Primary.ID).Extract()
if err != nil {
httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if !ok || httpError.Actual != 404 {
time.Sleep(time.Second)
continue
}
}
break
}
if err != nil {
return err
}
if found.Name != expectedName {
return fmt.Errorf("Expected Name to be <%s> but found <%s>", expectedName, found.Name)
}
if found.Description != expectedDescription {
return fmt.Errorf("Expected Description to be <%s> but found <%s>", expectedDescription, found.Description)
}
if found.PolicyID == "" {
return fmt.Errorf("Policy should not be empty")
}
if policyID != nil && found.PolicyID == *policyID {
return fmt.Errorf("Policy had not been correctly updated. Went from <%s> to <%s>", expectedName, found.Name)
}
policyID = &found.PolicyID
return nil
}
}
const testFirewallConfig = `
resource "openstack_fw_firewall_v1" "accept_test" {
policy_id = "${openstack_fw_policy_v1.accept_test_policy_1.id}"
}
resource "openstack_fw_policy_v1" "accept_test_policy_1" {
name = "policy-1"
}
`
const testFirewallConfigUpdated = `
resource "openstack_fw_firewall_v1" "accept_test" {
name = "accept_test"
description = "terraform acceptance test"
policy_id = "${openstack_fw_policy_v1.accept_test_policy_2.id}"
}
resource "openstack_fw_policy_v1" "accept_test_policy_2" {
name = "policy-2"
}
`

View File

@ -0,0 +1,200 @@
package openstack
import (
"fmt"
"log"
"time"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies"
)
func resourceFWPolicyV1() *schema.Resource {
return &schema.Resource{
Create: resourceFWPolicyV1Create,
Read: resourceFWPolicyV1Read,
Update: resourceFWPolicyV1Update,
Delete: resourceFWPolicyV1Delete,
Schema: map[string]*schema.Schema{
"region": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
DefaultFunc: envDefaultFunc("OS_REGION_NAME"),
},
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"audited": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"shared": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"tenant_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"rules": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: func(v interface{}) int {
return hashcode.String(v.(string))
},
},
},
}
}
func resourceFWPolicyV1Create(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
v := d.Get("rules").(*schema.Set)
log.Printf("[DEBUG] Rules found : %#v", v)
log.Printf("[DEBUG] Rules count : %d", v.Len())
rules := make([]string, v.Len())
for i, v := range v.List() {
rules[i] = v.(string)
}
audited := d.Get("audited").(bool)
shared := d.Get("shared").(bool)
opts := policies.CreateOpts{
Name: d.Get("name").(string),
Description: d.Get("description").(string),
Audited: &audited,
Shared: &shared,
TenantID: d.Get("tenant_id").(string),
Rules: rules,
}
log.Printf("[DEBUG] Create firewall policy: %#v", opts)
policy, err := policies.Create(networkingClient, opts).Extract()
if err != nil {
return err
}
log.Printf("[DEBUG] Firewall policy created: %#v", policy)
d.SetId(policy.ID)
return resourceFWPolicyV1Read(d, meta)
}
func resourceFWPolicyV1Read(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG] Retrieve information about firewall policy: %s", d.Id())
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
policy, err := policies.Get(networkingClient, d.Id()).Extract()
if err != nil {
return CheckDeleted(d, err, "LB pool")
}
d.Set("name", policy.Name)
d.Set("description", policy.Description)
d.Set("shared", policy.Shared)
d.Set("audited", policy.Audited)
d.Set("tenant_id", policy.TenantID)
return nil
}
func resourceFWPolicyV1Update(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
opts := policies.UpdateOpts{}
if d.HasChange("name") {
opts.Name = d.Get("name").(string)
}
if d.HasChange("description") {
opts.Description = d.Get("description").(string)
}
if d.HasChange("rules") {
v := d.Get("rules").(*schema.Set)
log.Printf("[DEBUG] Rules found : %#v", v)
log.Printf("[DEBUG] Rules count : %d", v.Len())
rules := make([]string, v.Len())
for i, v := range v.List() {
rules[i] = v.(string)
}
opts.Rules = rules
}
log.Printf("[DEBUG] Updating firewall policy with id %s: %#v", d.Id(), opts)
err = policies.Update(networkingClient, d.Id(), opts).Err
if err != nil {
return err
}
return resourceFWPolicyV1Read(d, meta)
}
func resourceFWPolicyV1Delete(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG] Destroy firewall policy: %s", d.Id())
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
for i := 0; i < 15; i++ {
err = policies.Delete(networkingClient, d.Id()).Err
if err == nil {
break
}
httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if !ok || httpError.Actual != 409 {
return err
}
// This error usualy means that the policy is attached
// to a firewall. At this point, the firewall is probably
// being delete. So, we retry a few times.
time.Sleep(time.Second * 2)
}
return err
}

View File

@ -0,0 +1,165 @@
package openstack
import (
"fmt"
"testing"
"time"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies"
)
func TestAccFWPolicyV1(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckFWPolicyV1Destroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testFirewallPolicyConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckFWPolicyV1Exists(
"openstack_fw_policy_v1.accept_test",
"", "", 0),
),
},
resource.TestStep{
Config: testFirewallPolicyConfigAddRules,
Check: resource.ComposeTestCheckFunc(
testAccCheckFWPolicyV1Exists(
"openstack_fw_policy_v1.accept_test",
"accept_test", "terraform acceptance test", 2),
),
},
resource.TestStep{
Config: testFirewallPolicyUpdateDeleteRule,
Check: resource.ComposeTestCheckFunc(
testAccCheckFWPolicyV1Exists(
"openstack_fw_policy_v1.accept_test",
"accept_test", "terraform acceptance test", 1),
),
},
},
})
}
func testAccCheckFWPolicyV1Destroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckOpenstackFirewallPolicyDestroy) Error creating OpenStack networking client: %s", err)
}
for _, rs := range s.RootModule().Resources {
if rs.Type != "openstack_fw_policy_v1" {
continue
}
_, err = policies.Get(networkingClient, rs.Primary.ID).Extract()
if err == nil {
return fmt.Errorf("Firewall policy (%s) still exists.", rs.Primary.ID)
}
httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if !ok || httpError.Actual != 404 {
return httpError
}
}
return nil
}
func testAccCheckFWPolicyV1Exists(n, name, description string, ruleCount int) 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 ID is set")
}
config := testAccProvider.Meta().(*Config)
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckFirewallPolicyExists) Error creating OpenStack networking client: %s", err)
}
var found *policies.Policy
for i := 0; i < 5; i++ {
// Firewall policy creation is asynchronous. Retry some times
// if we get a 404 error. Fail on any other error.
found, err = policies.Get(networkingClient, rs.Primary.ID).Extract()
if err != nil {
httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if !ok || httpError.Actual != 404 {
time.Sleep(time.Second)
continue
}
}
break
}
if err != nil {
return err
}
if name != found.Name {
return fmt.Errorf("Expected name <%s>, but found <%s>", name, found.Name)
}
if description != found.Description {
return fmt.Errorf("Expected description <%s>, but found <%s>", description, found.Description)
}
if ruleCount != len(found.Rules) {
return fmt.Errorf("Expected rule count <%d>, but found <%d>", ruleCount, len(found.Rules))
}
return nil
}
}
const testFirewallPolicyConfig = `
resource "openstack_fw_policy_v1" "accept_test" {
}
`
const testFirewallPolicyConfigAddRules = `
resource "openstack_fw_policy_v1" "accept_test" {
name = "accept_test"
description = "terraform acceptance test"
rules = [
"${openstack_fw_rule_v1.accept_test_udp_deny.id}",
"${openstack_fw_rule_v1.accept_test_tcp_allow.id}"
]
}
resource "openstack_fw_rule_v1" "accept_test_tcp_allow" {
protocol = "tcp"
action = "allow"
}
resource "openstack_fw_rule_v1" "accept_test_udp_deny" {
protocol = "udp"
action = "deny"
}
`
const testFirewallPolicyUpdateDeleteRule = `
resource "openstack_fw_policy_v1" "accept_test" {
name = "accept_test"
description = "terraform acceptance test"
rules = [
"${openstack_fw_rule_v1.accept_test_udp_deny.id}"
]
}
resource "openstack_fw_rule_v1" "accept_test_udp_deny" {
protocol = "udp"
action = "deny"
}
`

View File

@ -0,0 +1,223 @@
package openstack
import (
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules"
)
func resourceFWRuleV1() *schema.Resource {
return &schema.Resource{
Create: resourceFWRuleV1Create,
Read: resourceFWRuleV1Read,
Update: resourceFWRuleV1Update,
Delete: resourceFWRuleV1Delete,
Schema: map[string]*schema.Schema{
"region": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
DefaultFunc: envDefaultFunc("OS_REGION_NAME"),
},
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"protocol": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"action": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"ip_version": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 4,
},
"source_ip_address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"destination_ip_address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"source_port": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"destination_port": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"enabled": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"tenant_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
}
}
func resourceFWRuleV1Create(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
enabled := d.Get("enabled").(bool)
ruleConfiguration := rules.CreateOpts{
Name: d.Get("name").(string),
Description: d.Get("description").(string),
Protocol: d.Get("protocol").(string),
Action: d.Get("action").(string),
IPVersion: d.Get("ip_version").(int),
SourceIPAddress: d.Get("source_ip_address").(string),
DestinationIPAddress: d.Get("destination_ip_address").(string),
SourcePort: d.Get("source_port").(string),
DestinationPort: d.Get("destination_port").(string),
Enabled: &enabled,
TenantID: d.Get("tenant_id").(string),
}
log.Printf("[DEBUG] Create firewall rule: %#v", ruleConfiguration)
rule, err := rules.Create(networkingClient, ruleConfiguration).Extract()
if err != nil {
return err
}
log.Printf("[DEBUG] Firewall rule with id %s : %#v", rule.ID, rule)
d.SetId(rule.ID)
return resourceFWRuleV1Read(d, meta)
}
func resourceFWRuleV1Read(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG] Retrieve information about firewall rule: %s", d.Id())
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
rule, err := rules.Get(networkingClient, d.Id()).Extract()
if err != nil {
return CheckDeleted(d, err, "LB pool")
}
d.Set("protocol", rule.Protocol)
d.Set("action", rule.Action)
d.Set("name", rule.Name)
d.Set("description", rule.Description)
d.Set("ip_version", rule.IPVersion)
d.Set("source_ip_address", rule.SourceIPAddress)
d.Set("destination_ip_address", rule.DestinationIPAddress)
d.Set("source_port", rule.SourcePort)
d.Set("destination_port", rule.DestinationPort)
d.Set("enabled", rule.Enabled)
return nil
}
func resourceFWRuleV1Update(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
opts := rules.UpdateOpts{}
if d.HasChange("name") {
opts.Name = d.Get("name").(string)
}
if d.HasChange("description") {
opts.Description = d.Get("description").(string)
}
if d.HasChange("protocol") {
opts.Protocol = d.Get("protocol").(string)
}
if d.HasChange("action") {
opts.Action = d.Get("action").(string)
}
if d.HasChange("ip_version") {
opts.IPVersion = d.Get("ip_version").(int)
}
if d.HasChange("source_ip_address") {
sourceIPAddress := d.Get("source_ip_address").(string)
opts.SourceIPAddress = &sourceIPAddress
}
if d.HasChange("destination_ip_address") {
destinationIPAddress := d.Get("destination_ip_address").(string)
opts.DestinationIPAddress = &destinationIPAddress
}
if d.HasChange("source_port") {
sourcePort := d.Get("source_port").(string)
opts.SourcePort = &sourcePort
}
if d.HasChange("destination_port") {
destinationPort := d.Get("destination_port").(string)
opts.DestinationPort = &destinationPort
}
if d.HasChange("enabled") {
enabled := d.Get("enabled").(bool)
opts.Enabled = &enabled
}
log.Printf("[DEBUG] Updating firewall rules: %#v", opts)
err = rules.Update(networkingClient, d.Id(), opts).Err
if err != nil {
return err
}
return resourceFWRuleV1Read(d, meta)
}
func resourceFWRuleV1Delete(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG] Destroy firewall rule: %s", d.Id())
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
rule, err := rules.Get(networkingClient, d.Id()).Extract()
if err != nil {
return err
}
if rule.PolicyID != "" {
err := policies.RemoveRule(networkingClient, rule.PolicyID, rule.ID)
if err != nil {
return err
}
}
return rules.Delete(networkingClient, d.Id()).Err
}

View File

@ -0,0 +1,185 @@
package openstack
import (
"fmt"
"reflect"
"testing"
"time"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules"
)
func TestAccFWRuleV1(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckFWRuleV1Destroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testFirewallRuleMinimalConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckFWRuleV1Exists(
"openstack_fw_rule_v1.accept_test_minimal",
&rules.Rule{
Protocol: "udp",
Action: "deny",
IPVersion: 4,
Enabled: true,
}),
),
},
resource.TestStep{
Config: testFirewallRuleConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckFWRuleV1Exists(
"openstack_fw_rule_v1.accept_test",
&rules.Rule{
Name: "accept_test",
Protocol: "udp",
Action: "deny",
Description: "Terraform accept test",
IPVersion: 4,
SourceIPAddress: "1.2.3.4",
DestinationIPAddress: "4.3.2.0/24",
SourcePort: "444",
DestinationPort: "555",
Enabled: true,
}),
),
},
resource.TestStep{
Config: testFirewallRuleUpdateAllFieldsConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckFWRuleV1Exists(
"openstack_fw_rule_v1.accept_test",
&rules.Rule{
Name: "accept_test_updated_2",
Protocol: "tcp",
Action: "allow",
Description: "Terraform accept test updated",
IPVersion: 4,
SourceIPAddress: "1.2.3.0/24",
DestinationIPAddress: "4.3.2.8",
SourcePort: "666",
DestinationPort: "777",
Enabled: false,
}),
),
},
},
})
}
func testAccCheckFWRuleV1Destroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckOpenstackFirewallRuleDestroy) Error creating OpenStack networking client: %s", err)
}
for _, rs := range s.RootModule().Resources {
if rs.Type != "openstack_firewall_rule" {
continue
}
_, err = rules.Get(networkingClient, rs.Primary.ID).Extract()
if err == nil {
return fmt.Errorf("Firewall rule (%s) still exists.", rs.Primary.ID)
}
httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if !ok || httpError.Actual != 404 {
return httpError
}
}
return nil
}
func testAccCheckFWRuleV1Exists(n string, expected *rules.Rule) 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 ID is set")
}
config := testAccProvider.Meta().(*Config)
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
if err != nil {
return fmt.Errorf("(testAccCheckFirewallRuleExists) Error creating OpenStack networking client: %s", err)
}
var found *rules.Rule
for i := 0; i < 5; i++ {
// Firewall rule creation is asynchronous. Retry some times
// if we get a 404 error. Fail on any other error.
found, err = rules.Get(networkingClient, rs.Primary.ID).Extract()
if err != nil {
httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if !ok || httpError.Actual != 404 {
time.Sleep(time.Second)
continue
}
}
break
}
if err != nil {
return err
}
expected.ID = found.ID
// Erase the tenant id because we don't want to compare
// it as long it is not present in the expected
found.TenantID = ""
if !reflect.DeepEqual(expected, found) {
return fmt.Errorf("Expected:\n%#v\nFound:\n%#v", expected, found)
}
return nil
}
}
const testFirewallRuleMinimalConfig = `
resource "openstack_fw_rule_v1" "accept_test_minimal" {
protocol = "udp"
action = "deny"
}
`
const testFirewallRuleConfig = `
resource "openstack_fw_rule_v1" "accept_test" {
name = "accept_test"
description = "Terraform accept test"
protocol = "udp"
action = "deny"
ip_version = 4
source_ip_address = "1.2.3.4"
destination_ip_address = "4.3.2.0/24"
source_port = "444"
destination_port = "555"
enabled = true
}
`
const testFirewallRuleUpdateAllFieldsConfig = `
resource "openstack_fw_rule_v1" "accept_test" {
name = "accept_test_updated_2"
description = "Terraform accept test updated"
protocol = "tcp"
action = "allow"
ip_version = 4
source_ip_address = "1.2.3.0/24"
destination_ip_address = "4.3.2.8"
source_port = "666"
destination_port = "777"
enabled = false
}
`

View File

@ -0,0 +1,192 @@
package openstack
import (
"fmt"
"log"
"strconv"
"github.com/hashicorp/terraform/helper/schema"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors"
)
func resourceLBMonitorV1() *schema.Resource {
return &schema.Resource{
Create: resourceLBMonitorV1Create,
Read: resourceLBMonitorV1Read,
Update: resourceLBMonitorV1Update,
Delete: resourceLBMonitorV1Delete,
Schema: map[string]*schema.Schema{
"region": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
DefaultFunc: envDefaultFunc("OS_REGION_NAME"),
},
"tenant_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"type": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"delay": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: false,
},
"timeout": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: false,
},
"max_retries": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: false,
},
"url_path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
},
"http_method": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
},
"expected_codes": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
},
"admin_state_up": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
},
},
}
}
func resourceLBMonitorV1Create(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
createOpts := monitors.CreateOpts{
TenantID: d.Get("tenant_id").(string),
Type: d.Get("type").(string),
Delay: d.Get("delay").(int),
Timeout: d.Get("timeout").(int),
MaxRetries: d.Get("max_retries").(int),
URLPath: d.Get("url_path").(string),
ExpectedCodes: d.Get("expected_codes").(string),
HTTPMethod: d.Get("http_method").(string),
}
asuRaw := d.Get("admin_state_up").(string)
if asuRaw != "" {
asu, err := strconv.ParseBool(asuRaw)
if err != nil {
return fmt.Errorf("admin_state_up, if provided, must be either 'true' or 'false'")
}
createOpts.AdminStateUp = &asu
}
log.Printf("[DEBUG] Create Options: %#v", createOpts)
m, err := monitors.Create(networkingClient, createOpts).Extract()
if err != nil {
return fmt.Errorf("Error creating OpenStack LB Monitor: %s", err)
}
log.Printf("[INFO] LB Monitor ID: %s", m.ID)
d.SetId(m.ID)
return resourceLBMonitorV1Read(d, meta)
}
func resourceLBMonitorV1Read(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
m, err := monitors.Get(networkingClient, d.Id()).Extract()
if err != nil {
return CheckDeleted(d, err, "LB monitor")
}
log.Printf("[DEBUG] Retreived OpenStack LB Monitor %s: %+v", d.Id(), m)
d.Set("type", m.Type)
d.Set("delay", m.Delay)
d.Set("timeout", m.Timeout)
d.Set("max_retries", m.MaxRetries)
d.Set("tenant_id", m.TenantID)
d.Set("url_path", m.URLPath)
d.Set("http_method", m.HTTPMethod)
d.Set("expected_codes", m.ExpectedCodes)
d.Set("admin_state_up", strconv.FormatBool(m.AdminStateUp))
return nil
}
func resourceLBMonitorV1Update(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
updateOpts := monitors.UpdateOpts{
Delay: d.Get("delay").(int),
Timeout: d.Get("timeout").(int),
MaxRetries: d.Get("max_retries").(int),
URLPath: d.Get("url_path").(string),
HTTPMethod: d.Get("http_method").(string),
ExpectedCodes: d.Get("expected_codes").(string),
}
if d.HasChange("admin_state_up") {
asuRaw := d.Get("admin_state_up").(string)
if asuRaw != "" {
asu, err := strconv.ParseBool(asuRaw)
if err != nil {
return fmt.Errorf("admin_state_up, if provided, must be either 'true' or 'false'")
}
updateOpts.AdminStateUp = &asu
}
}
log.Printf("[DEBUG] Updating OpenStack LB Monitor %s with options: %+v", d.Id(), updateOpts)
_, err = monitors.Update(networkingClient, d.Id(), updateOpts).Extract()
if err != nil {
return fmt.Errorf("Error updating OpenStack LB Monitor: %s", err)
}
return resourceLBMonitorV1Read(d, meta)
}
func resourceLBMonitorV1Delete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack networking client: %s", err)
}
err = monitors.Delete(networkingClient, d.Id()).ExtractErr()
if err != nil {
return fmt.Errorf("Error deleting OpenStack LB Monitor: %s", err)
}
d.SetId("")
return nil
}

Some files were not shown because too many files have changed in this diff Show More