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:
commit
85c0910165
52
CHANGELOG.md
52
CHANGELOG.md
|
@ -6,20 +6,32 @@ BACKWARDS INCOMPATIBILITIES:
|
|||
the `remote` command: `terraform remote push` and `terraform remote pull`.
|
||||
The old `remote` functionality is now at `terraform remote config`. This
|
||||
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:
|
||||
|
||||
* **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
|
||||
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
|
||||
attributes within a provisioner. Ex. `${self.private_ip_address}` [GH-1033]
|
||||
* **Continous state** saving during `terraform apply`. The state file is
|
||||
continously updated as apply is running, meaning that the state is
|
||||
* **Continuous state** saving during `terraform apply`. The state file is
|
||||
continuously updated as apply is running, meaning that the state is
|
||||
less likely to become corrupt in a catastrophic case: terraform panic
|
||||
or system killing Terraform.
|
||||
* **Math operations** in interpolations. You can now do things like
|
||||
`${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:
|
||||
|
||||
|
@ -31,10 +43,23 @@ IMPROVEMENTS:
|
|||
* **New config function: `split`** - Split a value based on a delimiter.
|
||||
This is useful for faking lists as parameters to modules.
|
||||
* **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
|
||||
change. This will lower the amount of state changing on things
|
||||
like refresh.
|
||||
* 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:
|
||||
|
||||
|
@ -47,13 +72,31 @@ BUG FIXES:
|
|||
a computed attribute was used as part of a set parameter. [GH-1073]
|
||||
* core: Fix edge case where state containing both "resource" and
|
||||
"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: `source_dest_check` regression fixed (now works). [GH-1020]
|
||||
* providers/aws: Longer wait times for DB instances.
|
||||
* 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: 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/heroku: Fix panic when config vars block was empty. [GH-1211]
|
||||
|
||||
PLUGIN CHANGES:
|
||||
|
||||
|
@ -80,7 +123,7 @@ IMPROVEMENTS:
|
|||
* provider/aws: The `aws_db_instance` resource no longer requires both
|
||||
`final_snapshot_identifier` and `skip_final_snapshot`; the presence or
|
||||
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]
|
||||
* provider/aws: Add `apply_method` to `aws_db_parameter_group` [GH-897]
|
||||
* 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]
|
||||
* helper/hashcode: Update `hash.String()` to always return a positive 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: Instance should ignore root EBS devices. [GH-877]
|
||||
* provider/aws: Fix `aws_db_instance` to not recreate each time. [GH-874]
|
||||
|
@ -519,3 +562,4 @@ BUG FIXES:
|
|||
|
||||
* Initial release
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
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
|
||||
Go 1.2. On a Mac, you can `brew install go` to install Go 1.2.
|
||||
1. Install Go. Make sure the Go version is at least Go 1.4. Terraform will not work with anything less than
|
||||
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`.
|
||||
For example, you can add to your `.bash_profile`.
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package main
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/hashicorp/aws-sdk-go/gen/autoscaling"
|
||||
"github.com/hashicorp/aws-sdk-go/gen/ec2"
|
||||
"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/route53"
|
||||
"github.com/hashicorp/aws-sdk-go/gen/s3"
|
||||
|
@ -30,6 +31,7 @@ type AWSClient struct {
|
|||
r53conn *route53.Route53
|
||||
region string
|
||||
rdsconn *rds.RDS
|
||||
iamconn *iam.IAM
|
||||
}
|
||||
|
||||
// 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)
|
||||
log.Println("[INFO] Initializing EC2 Connection")
|
||||
client.ec2conn = ec2.New(creds, c.Region, nil)
|
||||
|
||||
client.iamconn = iam.New(creds, c.Region, nil)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
|
|
|
@ -58,6 +58,7 @@ func Provider() terraform.ResourceProvider {
|
|||
"aws_launch_configuration": resourceAwsLaunchConfiguration(),
|
||||
"aws_main_route_table_association": resourceAwsMainRouteTableAssociation(),
|
||||
"aws_network_acl": resourceAwsNetworkAcl(),
|
||||
"aws_network_interface": resourceAwsNetworkInterface(),
|
||||
"aws_route53_record": resourceAwsRoute53Record(),
|
||||
"aws_route53_zone": resourceAwsRoute53Zone(),
|
||||
"aws_route_table": resourceAwsRouteTable(),
|
||||
|
@ -67,6 +68,7 @@ func Provider() terraform.ResourceProvider {
|
|||
"aws_subnet": resourceAwsSubnet(),
|
||||
"aws_vpc": resourceAwsVpc(),
|
||||
"aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(),
|
||||
"aws_vpn_gateway": resourceAwsVpnGateway(),
|
||||
},
|
||||
|
||||
ConfigureFunc: providerConfigure,
|
||||
|
|
|
@ -118,6 +118,8 @@ func resourceAwsAutoscalingGroup() *schema.Resource {
|
|||
return hashcode.String(v.(string))
|
||||
},
|
||||
},
|
||||
|
||||
"tag": autoscalingTagsSchema(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -133,11 +135,16 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{})
|
|||
autoScalingGroupOpts.AvailabilityZones = expandStringList(
|
||||
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 {
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -186,15 +193,16 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e
|
|||
}
|
||||
|
||||
d.Set("availability_zones", g.AvailabilityZones)
|
||||
d.Set("default_cooldown", *g.DefaultCooldown)
|
||||
d.Set("desired_capacity", *g.DesiredCapacity)
|
||||
d.Set("health_check_grace_period", *g.HealthCheckGracePeriod)
|
||||
d.Set("health_check_type", *g.HealthCheckType)
|
||||
d.Set("launch_configuration", *g.LaunchConfigurationName)
|
||||
d.Set("default_cooldown", g.DefaultCooldown)
|
||||
d.Set("desired_capacity", g.DesiredCapacity)
|
||||
d.Set("health_check_grace_period", g.HealthCheckGracePeriod)
|
||||
d.Set("health_check_type", g.HealthCheckType)
|
||||
d.Set("launch_configuration", g.LaunchConfigurationName)
|
||||
d.Set("load_balancers", g.LoadBalancerNames)
|
||||
d.Set("min_size", *g.MinSize)
|
||||
d.Set("max_size", *g.MaxSize)
|
||||
d.Set("name", *g.AutoScalingGroupName)
|
||||
d.Set("min_size", g.MinSize)
|
||||
d.Set("max_size", g.MaxSize)
|
||||
d.Set("name", g.AutoScalingGroupName)
|
||||
d.Set("tag", g.Tags)
|
||||
d.Set("vpc_zone_identifier", strings.Split(*g.VPCZoneIdentifier, ","))
|
||||
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))
|
||||
}
|
||||
|
||||
if err := setAutoscalingTags(autoscalingconn, d); err != nil {
|
||||
return err
|
||||
} else {
|
||||
d.SetPartial("tag")
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] AutoScaling Group update configuration: %#v", opts)
|
||||
err := autoscalingconn.UpdateAutoScalingGroup(&opts)
|
||||
if err != nil {
|
||||
|
@ -273,7 +287,12 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{})
|
|||
return err
|
||||
}
|
||||
|
||||
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(
|
||||
|
|
|
@ -2,6 +2,7 @@ package aws
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/aws-sdk-go/aws"
|
||||
|
@ -53,6 +54,44 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) {
|
|||
resource.TestCheckResourceAttr(
|
||||
"aws_autoscaling_group.bar", "desired_capacity", "5"),
|
||||
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" {
|
||||
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 {
|
||||
|
@ -145,6 +184,21 @@ func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.AutoScalingGro
|
|||
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
|
||||
}
|
||||
}
|
||||
|
@ -226,6 +280,12 @@ resource "aws_autoscaling_group" "bar" {
|
|||
termination_policies = ["OldestInstance"]
|
||||
|
||||
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
|
||||
|
||||
launch_configuration = "${aws_launch_configuration.new.name}"
|
||||
|
||||
tag {
|
||||
key = "Bar"
|
||||
value = "bar-foo"
|
||||
propagate_at_launch = true
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
"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/terraform/helper/hashcode"
|
||||
|
@ -17,6 +18,7 @@ func resourceAwsDbInstance() *schema.Resource {
|
|||
return &schema.Resource{
|
||||
Create: resourceAwsDbInstanceCreate,
|
||||
Read: resourceAwsDbInstanceRead,
|
||||
Update: resourceAwsDbInstanceUpdate,
|
||||
Delete: resourceAwsDbInstanceDelete,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
|
@ -47,7 +49,6 @@ func resourceAwsDbInstance() *schema.Resource {
|
|||
"engine_version": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"storage_encrypted": &schema.Schema{
|
||||
|
@ -119,7 +120,6 @@ func resourceAwsDbInstance() *schema.Resource {
|
|||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"port": &schema.Schema{
|
||||
|
@ -138,6 +138,7 @@ func resourceAwsDbInstance() *schema.Resource {
|
|||
"vpc_security_group_ids": &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))
|
||||
|
@ -162,13 +163,13 @@ func resourceAwsDbInstance() *schema.Resource {
|
|||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
Computed: true,
|
||||
},
|
||||
|
||||
"parameter_group_name": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"address": &schema.Schema{
|
||||
|
@ -185,12 +186,24 @@ func resourceAwsDbInstance() *schema.Resource {
|
|||
Type: schema.TypeString,
|
||||
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 {
|
||||
conn := meta.(*AWSClient).rdsconn
|
||||
tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{}))
|
||||
opts := rds.CreateDBInstanceMessage{
|
||||
AllocatedStorage: aws.Integer(d.Get("allocated_storage").(int)),
|
||||
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)),
|
||||
EngineVersion: aws.String(d.Get("engine_version").(string)),
|
||||
StorageEncrypted: aws.Boolean(d.Get("storage_encrypted").(bool)),
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
if attr, ok := d.GetOk("storage_type"); ok {
|
||||
|
@ -304,7 +318,11 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if v.DBName != nil {
|
||||
d.Set("name", *v.DBName)
|
||||
} else {
|
||||
d.Set("name", "")
|
||||
}
|
||||
d.Set("username", *v.MasterUsername)
|
||||
d.Set("engine", *v.Engine)
|
||||
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("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
|
||||
ids := &schema.Set{
|
||||
F: func(v interface{}) int {
|
||||
|
@ -390,6 +430,56 @@ func resourceAwsDbInstanceDelete(d *schema.ResourceData, meta interface{}) error
|
|||
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(
|
||||
d *schema.ResourceData, meta interface{}) (*rds.DBInstance, error) {
|
||||
conn := meta.(*AWSClient).rdsconn
|
||||
|
@ -439,3 +529,16 @@ func resourceAwsDbInstanceStateRefreshFunc(
|
|||
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
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/hashcode"
|
||||
|
@ -220,7 +221,8 @@ func resourceAwsDbParameterHash(v interface{}) int {
|
|||
var buf bytes.Buffer
|
||||
m := v.(map[string]interface{})
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -79,6 +79,11 @@ func resourceAwsDbSubnetGroupRead(d *schema.ResourceData, meta interface{}) erro
|
|||
|
||||
describeResp, err := rdsconn.DescribeDBSubnetGroups(&describeOpts)
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -154,6 +154,8 @@ func resourceAwsElb() *schema.Resource {
|
|||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
|
||||
"tags": tagsSchema(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -167,11 +169,12 @@ func resourceAwsElbCreate(d *schema.ResourceData, meta interface{}) error {
|
|||
return err
|
||||
}
|
||||
|
||||
tags := tagsFromMapELB(d.Get("tags").(map[string]interface{}))
|
||||
// Provision the elb
|
||||
|
||||
elbOpts := &elb.CreateAccessPointInput{
|
||||
LoadBalancerName: aws.String(d.Get("name").(string)),
|
||||
Listeners: listeners,
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
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("subnets")
|
||||
|
||||
d.Set("tags", tagsToMapELB(tags))
|
||||
|
||||
if d.HasChange("health_check") {
|
||||
vs := d.Get("health_check").(*schema.Set).List()
|
||||
if len(vs) > 0 {
|
||||
|
@ -267,6 +272,15 @@ func resourceAwsElbRead(d *schema.ResourceData, meta interface{}) error {
|
|||
d.Set("security_groups", lb.SecurityGroups)
|
||||
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
|
||||
// currently can
|
||||
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)
|
||||
|
||||
return resourceAwsElbRead(d, meta)
|
||||
|
|
|
@ -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) {
|
||||
var conf elb.LoadBalancerDescription
|
||||
|
||||
|
@ -288,6 +343,31 @@ resource "aws_elb" "bar" {
|
|||
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
|
||||
}
|
||||
`
|
||||
|
|
|
@ -24,6 +24,9 @@ func resourceAwsInstance() *schema.Resource {
|
|||
Update: resourceAwsInstanceUpdate,
|
||||
Delete: resourceAwsInstanceDelete,
|
||||
|
||||
SchemaVersion: 1,
|
||||
MigrateState: resourceAwsInstanceMigrateState,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
"ami": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
|
@ -127,53 +130,28 @@ func resourceAwsInstance() *schema.Resource {
|
|||
ForceNew: true,
|
||||
Optional: true,
|
||||
},
|
||||
|
||||
"tenancy": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"tags": tagsSchema(),
|
||||
|
||||
"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,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Elem: &schema.Resource{
|
||||
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{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
|
@ -181,6 +159,12 @@ func resourceAwsInstance() *schema.Resource {
|
|||
ForceNew: true,
|
||||
},
|
||||
|
||||
"device_name": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"encrypted": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
|
@ -194,36 +178,12 @@ func resourceAwsInstance() *schema.Resource {
|
|||
Computed: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Set: resourceAwsInstanceBlockDevicesHash,
|
||||
},
|
||||
|
||||
"root_block_device": &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{
|
||||
"snapshot_id": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ForceNew: true,
|
||||
Default: "/dev/sda1",
|
||||
},
|
||||
|
||||
"volume_size": &schema.Schema{
|
||||
|
@ -239,6 +199,71 @@ func resourceAwsInstance() *schema.Resource {
|
|||
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)))
|
||||
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{
|
||||
Type: schema.TypeInt,
|
||||
|
@ -246,7 +271,31 @@ func resourceAwsInstance() *schema.Resource {
|
|||
Computed: 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)))
|
||||
}
|
||||
|
||||
// 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{
|
||||
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{
|
||||
|
@ -288,11 +349,6 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
|
|||
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
|
||||
if v := d.Get("security_groups"); v != nil {
|
||||
// Security group names.
|
||||
|
@ -347,44 +403,86 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
|
|||
runOpts.KeyName = aws.String(v.(string))
|
||||
}
|
||||
|
||||
blockDevices := make([]interface{}, 0)
|
||||
blockDevices := make([]ec2.BlockDeviceMapping, 0)
|
||||
|
||||
if v := d.Get("block_device"); v != nil {
|
||||
blockDevices = append(blockDevices, v.(*schema.Set).List()...)
|
||||
}
|
||||
|
||||
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 {
|
||||
if v, ok := d.GetOk("ebs_block_device"); ok {
|
||||
vL := v.(*schema.Set).List()
|
||||
for _, v := range vL {
|
||||
bd := v.(map[string]interface{})
|
||||
runOpts.BlockDeviceMappings[i].DeviceName = aws.String(bd["device_name"].(string))
|
||||
runOpts.BlockDeviceMappings[i].EBS = &ec2.EBSBlockDevice{
|
||||
VolumeType: aws.String(bd["volume_type"].(string)),
|
||||
VolumeSize: aws.Integer(bd["volume_size"].(int)),
|
||||
ebs := &ec2.EBSBlockDevice{
|
||||
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 != "" {
|
||||
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 {
|
||||
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
|
||||
|
@ -473,13 +571,18 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
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("public_dns", instance.PublicDNSName)
|
||||
d.Set("public_ip", instance.PublicIPAddress)
|
||||
d.Set("private_dns", instance.PrivateDNSName)
|
||||
d.Set("private_ip", instance.PrivateIPAddress)
|
||||
d.Set("subnet_id", instance.SubnetID)
|
||||
if len(instance.NetworkInterfaces) > 0 {
|
||||
d.Set("subnet_id", instance.NetworkInterfaces[0].SubnetID)
|
||||
} else {
|
||||
|
@ -487,14 +590,13 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
|||
}
|
||||
d.Set("ebs_optimized", instance.EBSOptimized)
|
||||
d.Set("tags", tagsToMap(instance.Tags))
|
||||
d.Set("tenancy", instance.Placement.Tenancy)
|
||||
|
||||
// Determine whether we're referring to security groups with
|
||||
// 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
|
||||
// all-name list of security groups, we use names. Or, if we had any
|
||||
// IDs, we use IDs.
|
||||
useID := *instance.SubnetID != ""
|
||||
useID := instance.SubnetID != nil && *instance.SubnetID != ""
|
||||
if v := d.Get("security_groups"); v != nil {
|
||||
match := false
|
||||
for _, v := range v.(*schema.Set).List() {
|
||||
|
@ -518,68 +620,29 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
|||
}
|
||||
d.Set("security_groups", sgs)
|
||||
|
||||
blockDevices := make(map[string]ec2.InstanceBlockDeviceMapping)
|
||||
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 {
|
||||
if err := readBlockDevices(d, instance, ec2conn); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||
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
|
||||
if d.Get("subnet_id").(string) != "" {
|
||||
log.Printf("[INFO] Modifying instance %s", d.Id())
|
||||
err := ec2conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeRequest{
|
||||
InstanceID: aws.String(d.Id()),
|
||||
SourceDestCheck: &ec2.AttributeBooleanValue{
|
||||
Value: aws.Boolean(d.Get("source_dest_check").(bool)),
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mitchellh): wait for the attributes we modified to
|
||||
// persist the change...
|
||||
|
@ -656,11 +719,111 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRe
|
|||
}
|
||||
}
|
||||
|
||||
func resourceAwsInstanceBlockDevicesHash(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)))
|
||||
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
|
||||
return hashcode.String(buf.String())
|
||||
func readBlockDevices(d *schema.ResourceData, instance *ec2.Instance, ec2conn *ec2.EC2) error {
|
||||
ibds, err := readBlockDevicesFromInstance(instance, ec2conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
func TestAccAWSInstance_normal(t *testing.T) {
|
||||
var v ec2.Instance
|
||||
var vol *ec2.Volume
|
||||
|
||||
testCheck := func(*terraform.State) error {
|
||||
if *v.Placement.AvailabilityZone != "us-west-2a" {
|
||||
|
@ -35,6 +36,21 @@ func TestAccAWSInstance_normal(t *testing.T) {
|
|||
Providers: testAccProviders,
|
||||
CheckDestroy: testAccCheckInstanceDestroy,
|
||||
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{
|
||||
Config: testAccInstanceConfig,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
|
@ -45,6 +61,8 @@ func TestAccAWSInstance_normal(t *testing.T) {
|
|||
"aws_instance.foo",
|
||||
"user_data",
|
||||
"3dc39dda39be1205215e776bad998da361a5955d"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "ebs_block_device.#", "0"),
|
||||
),
|
||||
},
|
||||
|
||||
|
@ -61,8 +79,19 @@ func TestAccAWSInstance_normal(t *testing.T) {
|
|||
"aws_instance.foo",
|
||||
"user_data",
|
||||
"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(
|
||||
"aws_instance.foo", "root_block_device.#", "1"),
|
||||
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(
|
||||
"aws_instance.foo", "root_block_device.0.volume_size", "11"),
|
||||
// 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
|
||||
"aws_instance.foo", "root_block_device.1023169747.volume_type", "gp2"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "root_block_device.0.volume_type", "gp2"),
|
||||
"aws_instance.foo", "ebs_block_device.#", "2"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.#", "2"),
|
||||
"aws_instance.foo", "ebs_block_device.2225977507.device_name", "/dev/sdb"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.172787947.device_name", "/dev/sdb"),
|
||||
"aws_instance.foo", "ebs_block_device.2225977507.volume_size", "9"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.172787947.volume_size", "9"),
|
||||
"aws_instance.foo", "ebs_block_device.2225977507.volume_type", "standard"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.172787947.iops", "0"),
|
||||
// Check provisioned SSD device
|
||||
"aws_instance.foo", "ebs_block_device.1977224956.device_name", "/dev/sdc"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.3336996981.volume_type", "io1"),
|
||||
"aws_instance.foo", "ebs_block_device.1977224956.volume_size", "10"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.3336996981.device_name", "/dev/sdc"),
|
||||
"aws_instance.foo", "ebs_block_device.1977224956.volume_type", "io1"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.3336996981.volume_size", "10"),
|
||||
"aws_instance.foo", "ebs_block_device.1977224956.iops", "100"),
|
||||
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(),
|
||||
),
|
||||
},
|
||||
|
@ -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 = `
|
||||
resource "aws_security_group" "tf_test_foo" {
|
||||
name = "tf_test_foo"
|
||||
|
@ -420,21 +463,25 @@ resource "aws_instance" "foo" {
|
|||
# us-west-2
|
||||
ami = "ami-55a7ea65"
|
||||
instance_type = "m1.small"
|
||||
|
||||
root_block_device {
|
||||
device_name = "/dev/sda1"
|
||||
volume_type = "gp2"
|
||||
volume_size = 11
|
||||
}
|
||||
block_device {
|
||||
ebs_block_device {
|
||||
device_name = "/dev/sdb"
|
||||
volume_size = 9
|
||||
}
|
||||
block_device {
|
||||
ebs_block_device {
|
||||
device_name = "/dev/sdc"
|
||||
volume_size = 10
|
||||
volume_type = "io1"
|
||||
iops = 100
|
||||
}
|
||||
ephemeral_block_device {
|
||||
device_name = "/dev/sde"
|
||||
virtual_name = "ephemeral0"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
@ -199,39 +199,14 @@ func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{})
|
|||
d.Id(),
|
||||
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
|
||||
log.Printf("[DEBUG] Waiting for internet gateway (%s) to detach", d.Id())
|
||||
stateConf := &resource.StateChangeConf{
|
||||
Pending: []string{"attached", "detaching", "available"},
|
||||
Pending: []string{"detaching"},
|
||||
Target: "detached",
|
||||
Refresh: IGAttachStateRefreshFunc(ec2conn, d.Id(), "detached"),
|
||||
Timeout: 1 * time.Minute,
|
||||
Refresh: detachIGStateRefreshFunc(ec2conn, d.Id(), vpcID.(string)),
|
||||
Timeout: 2 * time.Minute,
|
||||
Delay: 10 * time.Second,
|
||||
}
|
||||
if _, err := stateConf.WaitForState(); err != nil {
|
||||
return fmt.Errorf(
|
||||
|
@ -242,6 +217,32 @@ func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{})
|
|||
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
|
||||
// an internet gateway.
|
||||
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]
|
||||
|
||||
if time.Now().Sub(start) > 10*time.Second {
|
||||
return ig, expected, nil
|
||||
}
|
||||
|
||||
if len(ig.Attachments) == 0 {
|
||||
// No attachments, we're detached
|
||||
return ig, "detached", nil
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
`
|
|
@ -67,17 +67,8 @@ func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) er
|
|||
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
|
||||
rec, err := resourceAwsRoute53RecordBuildSet(d)
|
||||
rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name)
|
||||
if err != nil {
|
||||
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",
|
||||
zone, d.Get("name").(string))
|
||||
zone, *rec.Name)
|
||||
|
||||
wait := resource.StateChangeConf{
|
||||
Pending: []string{"rejected"},
|
||||
|
@ -111,11 +102,13 @@ func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) er
|
|||
Refresh: func() (interface{}, string, error) {
|
||||
resp, err := conn.ChangeResourceRecordSets(req)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "PriorRequestNotComplete") {
|
||||
if r53err, ok := err.(aws.APIError); ok {
|
||||
if r53err.Code == "PriorRequestNotComplete" {
|
||||
// There is some pending operation, so just retry
|
||||
// in a bit.
|
||||
return nil, "rejected", nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, "failure", err
|
||||
}
|
||||
|
@ -159,9 +152,17 @@ func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) erro
|
|||
conn := meta.(*AWSClient).r53conn
|
||||
|
||||
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{
|
||||
HostedZoneID: aws.String(cleanZoneID(zone)),
|
||||
StartRecordName: aws.String(d.Get("name").(string)),
|
||||
StartRecordName: aws.String(en),
|
||||
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)
|
||||
log.Printf("[DEBUG] Deleting resource records for zone: %s, name: %s",
|
||||
zone, d.Get("name").(string))
|
||||
|
||||
zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(zone)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Get the records
|
||||
rec, err := resourceAwsRoute53RecordBuildSet(d)
|
||||
rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -260,16 +264,30 @@ func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) er
|
|||
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()
|
||||
records := make([]route53.ResourceRecord, 0, len(recs))
|
||||
|
||||
typeStr := d.Get("type").(string)
|
||||
for _, r := range recs {
|
||||
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{
|
||||
Name: aws.String(d.Get("name").(string)),
|
||||
Name: aws.String(en),
|
||||
Type: aws.String(d.Get("type").(string)),
|
||||
TTL: aws.Long(int64(d.Get("ttl").(int))),
|
||||
ResourceRecords: records,
|
||||
|
@ -297,3 +315,15 @@ func cleanRecordName(name string) string {
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
resource.Test(t, resource.TestCase{
|
||||
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) {
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
|
@ -135,9 +172,11 @@ func testAccCheckRoute53RecordExists(n string) resource.TestCheckFunc {
|
|||
name := parts[1]
|
||||
rType := parts[2]
|
||||
|
||||
en := expandRecordName(name, "notexample.com")
|
||||
|
||||
lopts := &route53.ListResourceRecordSetsRequest{
|
||||
HostedZoneID: aws.String(cleanZoneID(zone)),
|
||||
StartRecordName: aws.String(name),
|
||||
StartRecordName: aws.String(en),
|
||||
StartRecordType: aws.String(rType),
|
||||
}
|
||||
|
||||
|
@ -151,7 +190,7 @@ func testAccCheckRoute53RecordExists(n string) resource.TestCheckFunc {
|
|||
// rec := resp.ResourceRecordSets[0]
|
||||
for _, rec := range resp.ResourceRecordSets {
|
||||
recName := cleanRecordName(*rec.Name)
|
||||
if FQDN(recName) == FQDN(name) && *rec.Type == rType {
|
||||
if FQDN(recName) == FQDN(en) && *rec.Type == rType {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -230,3 +269,16 @@ resource "aws_route53_record" "wildcard" {
|
|||
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"]
|
||||
}
|
||||
`
|
||||
|
|
|
@ -16,6 +16,7 @@ func resourceAwsRoute53Zone() *schema.Resource {
|
|||
return &schema.Resource{
|
||||
Create: resourceAwsRoute53ZoneCreate,
|
||||
Read: resourceAwsRoute53ZoneRead,
|
||||
Update: resourceAwsRoute53ZoneUpdate,
|
||||
Delete: resourceAwsRoute53ZoneDelete,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
|
@ -29,6 +30,8 @@ func resourceAwsRoute53Zone() *schema.Resource {
|
|||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
|
||||
"tags": tagsSchema(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +75,7 @@ func resourceAwsRoute53ZoneCreate(d *schema.ResourceData, meta interface{}) erro
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return resourceAwsRoute53ZoneUpdate(d, meta)
|
||||
}
|
||||
|
||||
func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error {
|
||||
|
@ -87,9 +90,41 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error
|
|||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
r53 := meta.(*AWSClient).r53conn
|
||||
|
||||
|
|
|
@ -63,6 +63,9 @@ func TestCleanChangeID(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAccRoute53Zone(t *testing.T) {
|
||||
var zone route53.HostedZone
|
||||
var td route53.ResourceTagSet
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
|
@ -71,7 +74,9 @@ func TestAccRoute53Zone(t *testing.T) {
|
|||
resource.TestStep{
|
||||
Config: testAccRoute53ZoneConfig,
|
||||
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
|
||||
}
|
||||
|
||||
func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc {
|
||||
func testAccCheckRoute53ZoneExists(n string, zone *route53.HostedZone) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
rs, ok := s.RootModule().Resources[n]
|
||||
if !ok {
|
||||
|
@ -105,10 +110,34 @@ func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc {
|
|||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -116,5 +145,10 @@ func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc {
|
|||
const testAccRoute53ZoneConfig = `
|
||||
resource "aws_route53_zone" "main" {
|
||||
name = "hashicorp.com"
|
||||
|
||||
tags {
|
||||
foo = "bar"
|
||||
Name = "tf-route53-tag-test"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
@ -107,6 +107,7 @@ func resourceAwsRouteTableRead(d *schema.ResourceData, meta interface{}) error {
|
|||
return err
|
||||
}
|
||||
if rtRaw == nil {
|
||||
d.SetId("")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ func resourceAwsS3Bucket() *schema.Resource {
|
|||
return &schema.Resource{
|
||||
Create: resourceAwsS3BucketCreate,
|
||||
Read: resourceAwsS3BucketRead,
|
||||
Update: resourceAwsS3BucketUpdate,
|
||||
Delete: resourceAwsS3BucketDelete,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
|
@ -29,6 +30,8 @@ func resourceAwsS3Bucket() *schema.Resource {
|
|||
Optional: 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
|
||||
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 {
|
||||
|
@ -76,6 +87,16 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
|
|||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -285,6 +285,7 @@ func resourceAwsSecurityGroupRuleHash(v interface{}) int {
|
|||
buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int)))
|
||||
buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int)))
|
||||
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
|
||||
// generate the same hash code no matter what is in the set.
|
||||
|
|
|
@ -30,15 +30,15 @@ func TestAccAWSSecurityGroup_normal(t *testing.T) {
|
|||
resource.TestCheckResourceAttr(
|
||||
"aws_security_group.web", "description", "Used in the terraform acceptance tests"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_security_group.web", "ingress.332851786.protocol", "tcp"),
|
||||
"aws_security_group.web", "ingress.3629188364.protocol", "tcp"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_security_group.web", "ingress.332851786.from_port", "80"),
|
||||
"aws_security_group.web", "ingress.3629188364.from_port", "80"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_security_group.web", "ingress.332851786.to_port", "8000"),
|
||||
"aws_security_group.web", "ingress.3629188364.to_port", "8000"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_security_group.web", "ingress.332851786.cidr_blocks.#", "1"),
|
||||
"aws_security_group.web", "ingress.3629188364.cidr_blocks.#", "1"),
|
||||
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(
|
||||
"aws_security_group.web", "description", "Used in the terraform acceptance tests"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_security_group.web", "ingress.332851786.protocol", "tcp"),
|
||||
"aws_security_group.web", "ingress.3629188364.protocol", "tcp"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_security_group.web", "ingress.332851786.from_port", "80"),
|
||||
"aws_security_group.web", "ingress.3629188364.from_port", "80"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_security_group.web", "ingress.332851786.to_port", "8000"),
|
||||
"aws_security_group.web", "ingress.3629188364.to_port", "8000"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_security_group.web", "ingress.332851786.cidr_blocks.#", "1"),
|
||||
"aws_security_group.web", "ingress.3629188364.cidr_blocks.#", "1"),
|
||||
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(
|
||||
"aws_security_group.web", "egress.332851786.protocol", "tcp"),
|
||||
"aws_security_group.web", "egress.3629188364.protocol", "tcp"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_security_group.web", "egress.332851786.from_port", "80"),
|
||||
"aws_security_group.web", "egress.3629188364.from_port", "80"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_security_group.web", "egress.332851786.to_port", "8000"),
|
||||
"aws_security_group.web", "egress.3629188364.to_port", "8000"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_security_group.web", "egress.332851786.cidr_blocks.#", "1"),
|
||||
"aws_security_group.web", "egress.3629188364.cidr_blocks.#", "1"),
|
||||
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,
|
||||
),
|
||||
},
|
||||
|
|
|
@ -159,17 +159,38 @@ func resourceAwsSubnetDelete(d *schema.ResourceData, meta interface{}) error {
|
|||
ec2conn := meta.(*AWSClient).ec2conn
|
||||
|
||||
log.Printf("[INFO] Deleting subnet: %s", d.Id())
|
||||
|
||||
err := ec2conn.DeleteSubnet(&ec2.DeleteSubnetRequest{
|
||||
req := &ec2.DeleteSubnetRequest{
|
||||
SubnetID: aws.String(d.Id()),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
ec2err, ok := err.(aws.APIError)
|
||||
if ok && ec2err.Code == "InvalidSubnetID.NotFound" {
|
||||
return nil
|
||||
}
|
||||
|
||||
wait := resource.StateChangeConf{
|
||||
Pending: []string{"pending"},
|
||||
Target: "destroyed",
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -185,29 +185,32 @@ func resourceAwsVpcUpdate(d *schema.ResourceData, meta interface{}) error {
|
|||
// Turn on partial mode
|
||||
d.Partial(true)
|
||||
vpcid := d.Id()
|
||||
modifyOpts := &ec2.ModifyVPCAttributeRequest{
|
||||
VPCID: &vpcid,
|
||||
}
|
||||
if d.HasChange("enable_dns_hostnames") {
|
||||
val := d.Get("enable_dns_hostnames").(bool)
|
||||
modifyOpts.EnableDNSHostnames = &ec2.AttributeBooleanValue{
|
||||
modifyOpts := &ec2.ModifyVPCAttributeRequest{
|
||||
VPCID: &vpcid,
|
||||
EnableDNSHostnames: &ec2.AttributeBooleanValue{
|
||||
Value: &val,
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
if err := ec2conn.ModifyVPCAttribute(modifyOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.SetPartial("enable_dns_hostnames")
|
||||
d.SetPartial("enable_dns_support")
|
||||
}
|
||||
|
||||
if d.HasChange("enable_dns_support") {
|
||||
val := d.Get("enable_dns_hostnames").(bool)
|
||||
modifyOpts.EnableDNSSupport = &ec2.AttributeBooleanValue{
|
||||
val := d.Get("enable_dns_support").(bool)
|
||||
modifyOpts := &ec2.ModifyVPCAttributeRequest{
|
||||
VPCID: &vpcid,
|
||||
EnableDNSSupport: &ec2.AttributeBooleanValue{
|
||||
Value: &val,
|
||||
},
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
|
@ -238,7 +241,7 @@ func resourceAwsVpcDelete(d *schema.ResourceData, meta interface{}) error {
|
|||
}
|
||||
log.Printf("[INFO] Deleting VPC: %s", d.Id())
|
||||
if err := ec2conn.DeleteVPC(DeleteVpcOpts); err != nil {
|
||||
ec2err, ok := err.(*aws.APIError)
|
||||
ec2err, ok := err.(aws.APIError)
|
||||
if ok && ec2err.Code == "InvalidVpcID.NotFound" {
|
||||
return nil
|
||||
}
|
||||
|
@ -258,7 +261,7 @@ func VPCStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc {
|
|||
}
|
||||
resp, err := conn.DescribeVPCs(DescribeVpcOpts)
|
||||
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
|
||||
} else {
|
||||
log.Printf("Error on VPCStateRefresh: %s", err)
|
||||
|
|
|
@ -2,11 +2,12 @@ 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"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAccVpc_basic(t *testing.T) {
|
||||
|
@ -132,7 +133,7 @@ func testAccCheckVpcDestroy(s *terraform.State) error {
|
|||
}
|
||||
|
||||
// Verify the error is what we want
|
||||
ec2err, ok := err.(*aws.APIError)
|
||||
ec2err, ok := err.(aws.APIError)
|
||||
if !ok {
|
||||
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 = `
|
||||
resource "aws_vpc" "foo" {
|
||||
cidr_block = "10.1.0.0/16"
|
||||
|
@ -223,3 +244,12 @@ resource "aws_vpc" "bar" {
|
|||
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
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
`
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -207,3 +207,47 @@ func expandStringList(configured []interface{}) []string {
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
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"])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -84,7 +84,7 @@ func resourceCloudStackDiskCreate(d *schema.ResourceData, meta interface{}) erro
|
|||
|
||||
if d.Get("size").(int) != 0 {
|
||||
// Set the volume size
|
||||
p.SetSize(d.Get("size").(int))
|
||||
p.SetSize(int64(d.Get("size").(int)))
|
||||
}
|
||||
|
||||
// Retrieve the zone UUID
|
||||
|
@ -141,7 +141,7 @@ func resourceCloudStackDiskRead(d *schema.ResourceData, meta interface{}) error
|
|||
d.Set("name", v.Name)
|
||||
d.Set("attach", v.Attached != "") // If attached this will contain a timestamp when attached
|
||||
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)
|
||||
|
||||
if v.Attached != "" {
|
||||
|
@ -196,7 +196,7 @@ func resourceCloudStackDiskUpdate(d *schema.ResourceData, meta interface{}) erro
|
|||
|
||||
if d.Get("size").(int) != 0 {
|
||||
// Set the size
|
||||
p.SetSize(d.Get("size").(int))
|
||||
p.SetSize(int64(d.Get("size").(int)))
|
||||
}
|
||||
|
||||
// Set the shrink bit
|
||||
|
@ -367,7 +367,7 @@ func isAttached(cs *cloudstack.CloudStackClient, id string) (bool, error) {
|
|||
return v.Attached != "", nil
|
||||
}
|
||||
|
||||
func retrieveDeviceID(device string) int {
|
||||
func retrieveDeviceID(device string) int64 {
|
||||
switch device {
|
||||
case "/dev/xvdb", "D:":
|
||||
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 {
|
||||
case 1:
|
||||
if os == "Windows" {
|
||||
|
|
|
@ -87,11 +87,11 @@ func resourceCloudStackVPNCustomerGatewayCreate(d *schema.ResourceData, meta int
|
|||
}
|
||||
|
||||
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 {
|
||||
p.SetIkelifetime(ikelifetime.(int))
|
||||
p.SetIkelifetime(int64(ikelifetime.(int)))
|
||||
}
|
||||
|
||||
// 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("ipsec_psk", v.Ipsecpsk)
|
||||
d.Set("dpd", v.Dpd)
|
||||
d.Set("esp_lifetime", v.Esplifetime)
|
||||
d.Set("ike_lifetime", v.Ikelifetime)
|
||||
d.Set("esp_lifetime", int(v.Esplifetime))
|
||||
d.Set("ike_lifetime", int(v.Ikelifetime))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -154,11 +154,11 @@ func resourceCloudStackVPNCustomerGatewayUpdate(d *schema.ResourceData, meta int
|
|||
}
|
||||
|
||||
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 {
|
||||
p.SetIkelifetime(ikelifetime.(int))
|
||||
p.SetIkelifetime(int64(ikelifetime.(int)))
|
||||
}
|
||||
|
||||
// Update the VPN Customer Gateway
|
||||
|
|
|
@ -91,8 +91,9 @@ func resourceDigitalOceanRecordCreate(d *schema.ResourceData, meta interface{})
|
|||
|
||||
func resourceDigitalOceanRecordRead(d *schema.ResourceData, meta interface{}) error {
|
||||
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 the record is somehow already destroyed, mark as
|
||||
// succesfully gone
|
||||
|
@ -104,6 +105,18 @@ func resourceDigitalOceanRecordRead(d *schema.ResourceData, meta interface{}) er
|
|||
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("type", rec.Type)
|
||||
d.Set("value", rec.Data)
|
||||
|
|
|
@ -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 {
|
||||
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 = `
|
||||
resource "digitalocean_domain" "foobar" {
|
||||
name = "foobar-test-terraform.com"
|
||||
|
@ -173,3 +265,45 @@ resource "digitalocean_record" "foobar" {
|
|||
value = "192.168.0.11"
|
||||
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"
|
||||
}`
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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}"
|
||||
}
|
||||
`
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
`
|
|
@ -7,11 +7,10 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
|
||||
"code.google.com/p/google-api-go-client/compute/v1"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"golang.org/x/oauth2/jwt"
|
||||
"google.golang.org/api/compute/v1"
|
||||
)
|
||||
|
||||
// Config is the configuration structure used to instantiate the Google
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package google
|
||||
|
||||
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.
|
||||
|
|
|
@ -4,7 +4,8 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"code.google.com/p/google-api-go-client/compute/v1"
|
||||
"google.golang.org/api/compute/v1"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
)
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"log"
|
||||
"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"
|
||||
"google.golang.org/api/compute/v1"
|
||||
"google.golang.org/api/googleapi"
|
||||
)
|
||||
|
||||
func resourceComputeAddress() *schema.Resource {
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.google.com/p/google-api-go-client/compute/v1"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"google.golang.org/api/compute/v1"
|
||||
)
|
||||
|
||||
func TestAccComputeAddress_basic(t *testing.T) {
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"log"
|
||||
"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"
|
||||
"google.golang.org/api/compute/v1"
|
||||
"google.golang.org/api/googleapi"
|
||||
)
|
||||
|
||||
func resourceComputeDisk() *schema.Resource {
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.google.com/p/google-api-go-client/compute/v1"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"google.golang.org/api/compute/v1"
|
||||
)
|
||||
|
||||
func TestAccComputeDisk_basic(t *testing.T) {
|
||||
|
|
|
@ -6,10 +6,10 @@ import (
|
|||
"sort"
|
||||
"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/schema"
|
||||
"google.golang.org/api/compute/v1"
|
||||
"google.golang.org/api/googleapi"
|
||||
)
|
||||
|
||||
func resourceComputeFirewall() *schema.Resource {
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.google.com/p/google-api-go-client/compute/v1"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"google.golang.org/api/compute/v1"
|
||||
)
|
||||
|
||||
func TestAccComputeFirewall_basic(t *testing.T) {
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"log"
|
||||
"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"
|
||||
"google.golang.org/api/compute/v1"
|
||||
"google.golang.org/api/googleapi"
|
||||
)
|
||||
|
||||
func resourceComputeForwardingRule() *schema.Resource {
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"log"
|
||||
"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"
|
||||
"google.golang.org/api/compute/v1"
|
||||
"google.golang.org/api/googleapi"
|
||||
)
|
||||
|
||||
func resourceComputeHttpHealthCheck() *schema.Resource {
|
||||
|
|
|
@ -5,10 +5,10 @@ import (
|
|||
"log"
|
||||
"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/schema"
|
||||
"google.golang.org/api/compute/v1"
|
||||
"google.golang.org/api/googleapi"
|
||||
)
|
||||
|
||||
func resourceComputeInstance() *schema.Resource {
|
||||
|
@ -72,6 +72,13 @@ func resourceComputeInstance() *schema.Resource {
|
|||
"auto_delete": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
Default: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"size": &schema.Schema{
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
},
|
||||
|
@ -283,11 +290,7 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err
|
|||
disk.Type = "PERSISTENT"
|
||||
disk.Mode = "READ_WRITE"
|
||||
disk.Boot = i == 0
|
||||
disk.AutoDelete = true
|
||||
|
||||
if v, ok := d.GetOk(prefix + ".auto_delete"); ok {
|
||||
disk.AutoDelete = v.(bool)
|
||||
}
|
||||
disk.AutoDelete = d.Get(prefix + ".auto_delete").(bool)
|
||||
|
||||
// Load up the disk for this disk if specified
|
||||
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
|
||||
}
|
||||
|
||||
if v, ok := d.GetOk(prefix + ".size"); ok {
|
||||
diskSizeGb := v.(int)
|
||||
disk.InitializeParams.DiskSizeGb = int64(diskSizeGb)
|
||||
}
|
||||
|
||||
disks = append(disks, &disk)
|
||||
}
|
||||
|
||||
|
@ -564,6 +572,7 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error
|
|||
networkInterfaces = append(networkInterfaces, map[string]interface{}{
|
||||
"name": iface.Name,
|
||||
"address": iface.NetworkIP,
|
||||
"network": iface.Network,
|
||||
"access_config": accessConfigs,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,10 +4,10 @@ import (
|
|||
"fmt"
|
||||
"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/schema"
|
||||
"google.golang.org/api/compute/v1"
|
||||
"google.golang.org/api/googleapi"
|
||||
)
|
||||
|
||||
func resourceComputeInstanceTemplate() *schema.Resource {
|
||||
|
@ -58,6 +58,7 @@ func resourceComputeInstanceTemplate() *schema.Resource {
|
|||
"auto_delete": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
Default: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
|
@ -235,11 +236,7 @@ func buildDisks(d *schema.ResourceData, meta interface{}) []*compute.AttachedDis
|
|||
disk.Mode = "READ_WRITE"
|
||||
disk.Interface = "SCSI"
|
||||
disk.Boot = i == 0
|
||||
disk.AutoDelete = true
|
||||
|
||||
if v, ok := d.GetOk(prefix + ".auto_delete"); ok {
|
||||
disk.AutoDelete = v.(bool)
|
||||
}
|
||||
disk.AutoDelete = d.Get(prefix + ".auto_delete").(bool)
|
||||
|
||||
if v, ok := d.GetOk(prefix + ".boot"); ok {
|
||||
disk.Boot = v.(bool)
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.google.com/p/google-api-go-client/compute/v1"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"google.golang.org/api/compute/v1"
|
||||
)
|
||||
|
||||
func TestAccComputeInstanceTemplate_basic(t *testing.T) {
|
||||
|
@ -65,7 +65,7 @@ func TestAccComputeInstanceTemplate_disks(t *testing.T) {
|
|||
testAccCheckComputeInstanceTemplateExists(
|
||||
"google_compute_instance_template.foobar", &instanceTemplate),
|
||||
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 = `
|
||||
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" {
|
||||
name = "terraform-test"
|
||||
machine_type = "n1-standard-1"
|
||||
|
@ -263,7 +271,7 @@ resource "google_compute_instance_template" "foobar" {
|
|||
}
|
||||
|
||||
disk {
|
||||
source = "foo_existing_disk"
|
||||
source = "terraform-test-foobar"
|
||||
auto_delete = false
|
||||
boot = false
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.google.com/p/google-api-go-client/compute/v1"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"google.golang.org/api/compute/v1"
|
||||
)
|
||||
|
||||
func TestAccComputeInstance_basic_deprecated_network(t *testing.T) {
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"log"
|
||||
"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"
|
||||
"google.golang.org/api/compute/v1"
|
||||
"google.golang.org/api/googleapi"
|
||||
)
|
||||
|
||||
func resourceComputeNetwork() *schema.Resource {
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.google.com/p/google-api-go-client/compute/v1"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"google.golang.org/api/compute/v1"
|
||||
)
|
||||
|
||||
func TestAccComputeNetwork_basic(t *testing.T) {
|
||||
|
|
|
@ -5,10 +5,10 @@ import (
|
|||
"log"
|
||||
"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/schema"
|
||||
"google.golang.org/api/compute/v1"
|
||||
"google.golang.org/api/googleapi"
|
||||
)
|
||||
|
||||
func resourceComputeRoute() *schema.Resource {
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.google.com/p/google-api-go-client/compute/v1"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"google.golang.org/api/compute/v1"
|
||||
)
|
||||
|
||||
func TestAccComputeRoute_basic(t *testing.T) {
|
||||
|
|
|
@ -6,9 +6,9 @@ import (
|
|||
"strings"
|
||||
"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"
|
||||
"google.golang.org/api/compute/v1"
|
||||
"google.golang.org/api/googleapi"
|
||||
)
|
||||
|
||||
func resourceComputeTargetPool() *schema.Resource {
|
||||
|
|
|
@ -358,16 +358,20 @@ func updateConfigVars(
|
|||
vars := make(map[string]*string)
|
||||
|
||||
for _, v := range o {
|
||||
if v != nil {
|
||||
for k, _ := range v.(map[string]interface{}) {
|
||||
vars[k] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, v := range n {
|
||||
if v != nil {
|
||||
for k, v := range v.(map[string]interface{}) {
|
||||
val := v.(string)
|
||||
vars[k] = &val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Updating config vars: *%#v", vars)
|
||||
if _, err := client.ConfigVarUpdate(id, vars); err != nil {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -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
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -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)
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
`
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
`
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
`
|
|
@ -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
Loading…
Reference in New Issue