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 `remote` command: `terraform remote push` and `terraform remote pull`.
|
||||||
The old `remote` functionality is now at `terraform remote config`. This
|
The old `remote` functionality is now at `terraform remote config`. This
|
||||||
consolidates all remote state management under one command.
|
consolidates all remote state management under one command.
|
||||||
|
* Period-prefixed configuration files are now ignored. This might break
|
||||||
|
existing Terraform configurations if you had period-prefixed files.
|
||||||
|
|
||||||
FEATURES:
|
FEATURES:
|
||||||
|
|
||||||
* **New provider: `dme` (DNSMadeEasy)** [GH-855]
|
* **New provider: `dme` (DNSMadeEasy)** [GH-855]
|
||||||
|
* **New provider: `docker` (Docker)** - Manage container lifecycle
|
||||||
|
using the standard Docker API. [GH-855]
|
||||||
|
* **New provider: `openstack` (OpenStack)** - Interact with the many resources
|
||||||
|
provided by OpenStack. [GH-924]
|
||||||
* **New command: `taint`** - Manually mark a resource as tainted, causing
|
* **New command: `taint`** - Manually mark a resource as tainted, causing
|
||||||
a destroy and recreate on the next plan/apply.
|
a destroy and recreate on the next plan/apply.
|
||||||
|
* **New resource: `aws_vpn_gateway`** [GH-1137]
|
||||||
|
* **New resource: `aws_elastic_network_interfaces`** [GH-1149]
|
||||||
* **Self-variables** can be used to reference the current resource's
|
* **Self-variables** can be used to reference the current resource's
|
||||||
attributes within a provisioner. Ex. `${self.private_ip_address}` [GH-1033]
|
attributes within a provisioner. Ex. `${self.private_ip_address}` [GH-1033]
|
||||||
* **Continous state** saving during `terraform apply`. The state file is
|
* **Continuous state** saving during `terraform apply`. The state file is
|
||||||
continously updated as apply is running, meaning that the state is
|
continuously updated as apply is running, meaning that the state is
|
||||||
less likely to become corrupt in a catastrophic case: terraform panic
|
less likely to become corrupt in a catastrophic case: terraform panic
|
||||||
or system killing Terraform.
|
or system killing Terraform.
|
||||||
* **Math operations** in interpolations. You can now do things like
|
* **Math operations** in interpolations. You can now do things like
|
||||||
`${count.index+1}`. [GH-1068]
|
`${count.index+1}`. [GH-1068]
|
||||||
|
* **New AWS SDK:** Move to `aws-sdk-go` (hashicorp/aws-sdk-go),
|
||||||
|
a fork of the offical `awslabs` repo. We forked for stability while
|
||||||
|
`awslabs` refactored the library, and will move back to the officially
|
||||||
|
supported version in the next release.
|
||||||
|
|
||||||
IMPROVEMENTS:
|
IMPROVEMENTS:
|
||||||
|
|
||||||
|
@ -31,10 +43,23 @@ IMPROVEMENTS:
|
||||||
* **New config function: `split`** - Split a value based on a delimiter.
|
* **New config function: `split`** - Split a value based on a delimiter.
|
||||||
This is useful for faking lists as parameters to modules.
|
This is useful for faking lists as parameters to modules.
|
||||||
* **New resource: `digitalocean_ssh_key`** [GH-1074]
|
* **New resource: `digitalocean_ssh_key`** [GH-1074]
|
||||||
|
* config: Expand `~` with homedir in `file()` paths [GH-1338]
|
||||||
* core: The serial of the state is only updated if there is an actual
|
* core: The serial of the state is only updated if there is an actual
|
||||||
change. This will lower the amount of state changing on things
|
change. This will lower the amount of state changing on things
|
||||||
like refresh.
|
like refresh.
|
||||||
* core: Autoload `terraform.tfvars.json` as well as `terraform.tfvars` [GH-1030]
|
* core: Autoload `terraform.tfvars.json` as well as `terraform.tfvars` [GH-1030]
|
||||||
|
* core: `.tf` files that start with a period are now ignored. [GH-1227]
|
||||||
|
* command/remote-config: After enabling remote state, a `pull` is
|
||||||
|
automatically done initially.
|
||||||
|
* providers/google: Add `size` option to disk blocks for instances. [GH-1284]
|
||||||
|
* providers/aws: Improve support for tagging resources.
|
||||||
|
* providers/aws: Add a short syntax for Route 53 Record names, e.g.
|
||||||
|
`www` instead of `www.example.com`.
|
||||||
|
* providers/aws: Improve dependency violation error handling, when deleting
|
||||||
|
Internet Gateways or Auto Scaling groups [GH-1325].
|
||||||
|
* provider/aws: Add non-destructive updates to AWS RDS. You can now upgrade
|
||||||
|
`egine_version`, `parameter_group_name`, and `multi_az` without forcing
|
||||||
|
a new database to be created.[GH-1341]
|
||||||
|
|
||||||
BUG FIXES:
|
BUG FIXES:
|
||||||
|
|
||||||
|
@ -47,13 +72,31 @@ BUG FIXES:
|
||||||
a computed attribute was used as part of a set parameter. [GH-1073]
|
a computed attribute was used as part of a set parameter. [GH-1073]
|
||||||
* core: Fix edge case where state containing both "resource" and
|
* core: Fix edge case where state containing both "resource" and
|
||||||
"resource.0" would ignore the latter completely. [GH-1086]
|
"resource.0" would ignore the latter completely. [GH-1086]
|
||||||
|
* core: Modules with a source of a relative file path moving up
|
||||||
|
directories work properly, i.e. "../a" [GH-1232]
|
||||||
* providers/aws: manually deleted VPC removes it from the state
|
* providers/aws: manually deleted VPC removes it from the state
|
||||||
* providers/aws: `source_dest_check` regression fixed (now works). [GH-1020]
|
* providers/aws: `source_dest_check` regression fixed (now works). [GH-1020]
|
||||||
* providers/aws: Longer wait times for DB instances.
|
* providers/aws: Longer wait times for DB instances.
|
||||||
* providers/aws: Longer wait times for route53 records (30 mins). [GH-1164]
|
* providers/aws: Longer wait times for route53 records (30 mins). [GH-1164]
|
||||||
|
* providers/aws: Fix support for TXT records in Route 53. [GH-1213]
|
||||||
|
* providers/aws: Fix support for wildcard records in Route 53. [GH-1222]
|
||||||
|
* providers/aws: Fix issue with ignoring the 'self' attribute of a
|
||||||
|
Security Group rule. [GH-1223]
|
||||||
|
* providers/aws: Fix issue with `sql_mode` in RDS parameter group always
|
||||||
|
causing an update. [GH-1225]
|
||||||
|
* providers/aws: Fix dependency violation with subnets and security groups
|
||||||
|
[GH-1252]
|
||||||
|
* providers/aws: Fix issue with refreshing `db_subnet_groups` causing an error
|
||||||
|
instead of updating state [GH-1254]
|
||||||
|
* providers/aws: Prevent empty string to be used as default
|
||||||
|
`health_check_type` [GH-1052]
|
||||||
|
* providers/aws: Add tags on AWS IG creation, not just on update [GH-1176]
|
||||||
* providers/digitalocean: Waits until droplet is ready to be destroyed [GH-1057]
|
* providers/digitalocean: Waits until droplet is ready to be destroyed [GH-1057]
|
||||||
* providers/digitalocean: More lenient about 404's while waiting [GH-1062]
|
* providers/digitalocean: More lenient about 404's while waiting [GH-1062]
|
||||||
|
* providers/digitalocean: FQDN for domain records in CNAME, MX, NS, etc.
|
||||||
|
Also fixes invalid updates in plans. [GH-863]
|
||||||
* providers/google: Network data in state was not being stored. [GH-1095]
|
* providers/google: Network data in state was not being stored. [GH-1095]
|
||||||
|
* providers/heroku: Fix panic when config vars block was empty. [GH-1211]
|
||||||
|
|
||||||
PLUGIN CHANGES:
|
PLUGIN CHANGES:
|
||||||
|
|
||||||
|
@ -80,7 +123,7 @@ IMPROVEMENTS:
|
||||||
* provider/aws: The `aws_db_instance` resource no longer requires both
|
* provider/aws: The `aws_db_instance` resource no longer requires both
|
||||||
`final_snapshot_identifier` and `skip_final_snapshot`; the presence or
|
`final_snapshot_identifier` and `skip_final_snapshot`; the presence or
|
||||||
absence of the former now implies the latter. [GH-874]
|
absence of the former now implies the latter. [GH-874]
|
||||||
* provider/aws: Avoid unecessary update of `aws_subnet` when
|
* provider/aws: Avoid unnecessary update of `aws_subnet` when
|
||||||
`map_public_ip_on_launch` is not specified in config. [GH-898]
|
`map_public_ip_on_launch` is not specified in config. [GH-898]
|
||||||
* provider/aws: Add `apply_method` to `aws_db_parameter_group` [GH-897]
|
* provider/aws: Add `apply_method` to `aws_db_parameter_group` [GH-897]
|
||||||
* provider/aws: Add `storage_type` to `aws_db_instance` [GH-896]
|
* provider/aws: Add `storage_type` to `aws_db_instance` [GH-896]
|
||||||
|
@ -113,7 +156,7 @@ BUG FIXES:
|
||||||
* command/apply: Fix regression where user variables weren't asked [GH-736]
|
* command/apply: Fix regression where user variables weren't asked [GH-736]
|
||||||
* helper/hashcode: Update `hash.String()` to always return a positive index.
|
* helper/hashcode: Update `hash.String()` to always return a positive index.
|
||||||
Fixes issue where specific strings would convert to a negative index
|
Fixes issue where specific strings would convert to a negative index
|
||||||
and be ommited when creating Route53 records. [GH-967]
|
and be omitted when creating Route53 records. [GH-967]
|
||||||
* provider/aws: Automatically suffix the Route53 zone name on record names. [GH-312]
|
* provider/aws: Automatically suffix the Route53 zone name on record names. [GH-312]
|
||||||
* provider/aws: Instance should ignore root EBS devices. [GH-877]
|
* provider/aws: Instance should ignore root EBS devices. [GH-877]
|
||||||
* provider/aws: Fix `aws_db_instance` to not recreate each time. [GH-874]
|
* provider/aws: Fix `aws_db_instance` to not recreate each time. [GH-874]
|
||||||
|
@ -519,3 +562,4 @@ BUG FIXES:
|
||||||
|
|
||||||
* Initial release
|
* Initial release
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -53,8 +53,8 @@ If you have never worked with Go before, you will have to complete the
|
||||||
following steps in order to be able to compile and test Terraform (or
|
following steps in order to be able to compile and test Terraform (or
|
||||||
use the Vagrantfile in this repo to stand up a dev VM).
|
use the Vagrantfile in this repo to stand up a dev VM).
|
||||||
|
|
||||||
1. Install Go. Make sure the Go version is at least Go 1.2. Terraform will not work with anything less than
|
1. Install Go. Make sure the Go version is at least Go 1.4. Terraform will not work with anything less than
|
||||||
Go 1.2. On a Mac, you can `brew install go` to install Go 1.2.
|
Go 1.4. On a Mac, you can `brew install go` to install Go 1.4.
|
||||||
|
|
||||||
2. Set and export the `GOPATH` environment variable and update your `PATH`.
|
2. Set and export the `GOPATH` environment variable and update your `PATH`.
|
||||||
For example, you can add to your `.bash_profile`.
|
For example, you can add to your `.bash_profile`.
|
||||||
|
|
|
@ -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/autoscaling"
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/ec2"
|
"github.com/hashicorp/aws-sdk-go/gen/ec2"
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/elb"
|
"github.com/hashicorp/aws-sdk-go/gen/elb"
|
||||||
|
"github.com/hashicorp/aws-sdk-go/gen/iam"
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/rds"
|
"github.com/hashicorp/aws-sdk-go/gen/rds"
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/route53"
|
"github.com/hashicorp/aws-sdk-go/gen/route53"
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/s3"
|
"github.com/hashicorp/aws-sdk-go/gen/s3"
|
||||||
|
@ -30,6 +31,7 @@ type AWSClient struct {
|
||||||
r53conn *route53.Route53
|
r53conn *route53.Route53
|
||||||
region string
|
region string
|
||||||
rdsconn *rds.RDS
|
rdsconn *rds.RDS
|
||||||
|
iamconn *iam.IAM
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client configures and returns a fully initailized AWSClient
|
// Client configures and returns a fully initailized AWSClient
|
||||||
|
@ -70,6 +72,8 @@ func (c *Config) Client() (interface{}, error) {
|
||||||
client.r53conn = route53.New(creds, "us-east-1", nil)
|
client.r53conn = route53.New(creds, "us-east-1", nil)
|
||||||
log.Println("[INFO] Initializing EC2 Connection")
|
log.Println("[INFO] Initializing EC2 Connection")
|
||||||
client.ec2conn = ec2.New(creds, c.Region, nil)
|
client.ec2conn = ec2.New(creds, c.Region, nil)
|
||||||
|
|
||||||
|
client.iamconn = iam.New(creds, c.Region, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
|
|
|
@ -58,6 +58,7 @@ func Provider() terraform.ResourceProvider {
|
||||||
"aws_launch_configuration": resourceAwsLaunchConfiguration(),
|
"aws_launch_configuration": resourceAwsLaunchConfiguration(),
|
||||||
"aws_main_route_table_association": resourceAwsMainRouteTableAssociation(),
|
"aws_main_route_table_association": resourceAwsMainRouteTableAssociation(),
|
||||||
"aws_network_acl": resourceAwsNetworkAcl(),
|
"aws_network_acl": resourceAwsNetworkAcl(),
|
||||||
|
"aws_network_interface": resourceAwsNetworkInterface(),
|
||||||
"aws_route53_record": resourceAwsRoute53Record(),
|
"aws_route53_record": resourceAwsRoute53Record(),
|
||||||
"aws_route53_zone": resourceAwsRoute53Zone(),
|
"aws_route53_zone": resourceAwsRoute53Zone(),
|
||||||
"aws_route_table": resourceAwsRouteTable(),
|
"aws_route_table": resourceAwsRouteTable(),
|
||||||
|
@ -67,6 +68,7 @@ func Provider() terraform.ResourceProvider {
|
||||||
"aws_subnet": resourceAwsSubnet(),
|
"aws_subnet": resourceAwsSubnet(),
|
||||||
"aws_vpc": resourceAwsVpc(),
|
"aws_vpc": resourceAwsVpc(),
|
||||||
"aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(),
|
"aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(),
|
||||||
|
"aws_vpn_gateway": resourceAwsVpnGateway(),
|
||||||
},
|
},
|
||||||
|
|
||||||
ConfigureFunc: providerConfigure,
|
ConfigureFunc: providerConfigure,
|
||||||
|
|
|
@ -118,6 +118,8 @@ func resourceAwsAutoscalingGroup() *schema.Resource {
|
||||||
return hashcode.String(v.(string))
|
return hashcode.String(v.(string))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"tag": autoscalingTagsSchema(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,11 +135,16 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{})
|
||||||
autoScalingGroupOpts.AvailabilityZones = expandStringList(
|
autoScalingGroupOpts.AvailabilityZones = expandStringList(
|
||||||
d.Get("availability_zones").(*schema.Set).List())
|
d.Get("availability_zones").(*schema.Set).List())
|
||||||
|
|
||||||
|
if v, ok := d.GetOk("tag"); ok {
|
||||||
|
autoScalingGroupOpts.Tags = autoscalingTagsFromMap(
|
||||||
|
setToMapByKey(v.(*schema.Set), "key"), d.Get("name").(string))
|
||||||
|
}
|
||||||
|
|
||||||
if v, ok := d.GetOk("default_cooldown"); ok {
|
if v, ok := d.GetOk("default_cooldown"); ok {
|
||||||
autoScalingGroupOpts.DefaultCooldown = aws.Integer(v.(int))
|
autoScalingGroupOpts.DefaultCooldown = aws.Integer(v.(int))
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, ok := d.GetOk("health_check"); ok && v.(string) != "" {
|
if v, ok := d.GetOk("health_check_type"); ok && v.(string) != "" {
|
||||||
autoScalingGroupOpts.HealthCheckType = aws.String(v.(string))
|
autoScalingGroupOpts.HealthCheckType = aws.String(v.(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,15 +193,16 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e
|
||||||
}
|
}
|
||||||
|
|
||||||
d.Set("availability_zones", g.AvailabilityZones)
|
d.Set("availability_zones", g.AvailabilityZones)
|
||||||
d.Set("default_cooldown", *g.DefaultCooldown)
|
d.Set("default_cooldown", g.DefaultCooldown)
|
||||||
d.Set("desired_capacity", *g.DesiredCapacity)
|
d.Set("desired_capacity", g.DesiredCapacity)
|
||||||
d.Set("health_check_grace_period", *g.HealthCheckGracePeriod)
|
d.Set("health_check_grace_period", g.HealthCheckGracePeriod)
|
||||||
d.Set("health_check_type", *g.HealthCheckType)
|
d.Set("health_check_type", g.HealthCheckType)
|
||||||
d.Set("launch_configuration", *g.LaunchConfigurationName)
|
d.Set("launch_configuration", g.LaunchConfigurationName)
|
||||||
d.Set("load_balancers", g.LoadBalancerNames)
|
d.Set("load_balancers", g.LoadBalancerNames)
|
||||||
d.Set("min_size", *g.MinSize)
|
d.Set("min_size", g.MinSize)
|
||||||
d.Set("max_size", *g.MaxSize)
|
d.Set("max_size", g.MaxSize)
|
||||||
d.Set("name", *g.AutoScalingGroupName)
|
d.Set("name", g.AutoScalingGroupName)
|
||||||
|
d.Set("tag", g.Tags)
|
||||||
d.Set("vpc_zone_identifier", strings.Split(*g.VPCZoneIdentifier, ","))
|
d.Set("vpc_zone_identifier", strings.Split(*g.VPCZoneIdentifier, ","))
|
||||||
d.Set("termination_policies", g.TerminationPolicies)
|
d.Set("termination_policies", g.TerminationPolicies)
|
||||||
|
|
||||||
|
@ -224,6 +232,12 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{})
|
||||||
opts.MaxSize = aws.Integer(d.Get("max_size").(int))
|
opts.MaxSize = aws.Integer(d.Get("max_size").(int))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := setAutoscalingTags(autoscalingconn, d); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
d.SetPartial("tag")
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("[DEBUG] AutoScaling Group update configuration: %#v", opts)
|
log.Printf("[DEBUG] AutoScaling Group update configuration: %#v", opts)
|
||||||
err := autoscalingconn.UpdateAutoScalingGroup(&opts)
|
err := autoscalingconn.UpdateAutoScalingGroup(&opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -273,7 +287,12 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return resource.Retry(5*time.Minute, func() error {
|
||||||
|
if g, _ = getAwsAutoscalingGroup(d, meta); g != nil {
|
||||||
|
return fmt.Errorf("Auto Scaling Group still exists")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAwsAutoscalingGroup(
|
func getAwsAutoscalingGroup(
|
||||||
|
|
|
@ -2,6 +2,7 @@ package aws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/aws-sdk-go/aws"
|
"github.com/hashicorp/aws-sdk-go/aws"
|
||||||
|
@ -53,6 +54,44 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) {
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_autoscaling_group.bar", "desired_capacity", "5"),
|
"aws_autoscaling_group.bar", "desired_capacity", "5"),
|
||||||
testLaunchConfigurationName("aws_autoscaling_group.bar", &lc),
|
testLaunchConfigurationName("aws_autoscaling_group.bar", &lc),
|
||||||
|
testAccCheckAutoscalingTags(&group.Tags, "Bar", map[string]interface{}{
|
||||||
|
"value": "bar-foo",
|
||||||
|
"propagate_at_launch": true,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccAWSAutoScalingGroup_tags(t *testing.T) {
|
||||||
|
var group autoscaling.AutoScalingGroup
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccAWSAutoScalingGroupConfig,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group),
|
||||||
|
testAccCheckAutoscalingTags(&group.Tags, "Foo", map[string]interface{}{
|
||||||
|
"value": "foo-bar",
|
||||||
|
"propagate_at_launch": true,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccAWSAutoScalingGroupConfigUpdate,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group),
|
||||||
|
testAccCheckAutoscalingTagNotExists(&group.Tags, "Foo"),
|
||||||
|
testAccCheckAutoscalingTags(&group.Tags, "Bar", map[string]interface{}{
|
||||||
|
"value": "bar-foo",
|
||||||
|
"propagate_at_launch": true,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -130,7 +169,7 @@ func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.AutoScalingGro
|
||||||
}
|
}
|
||||||
|
|
||||||
if *group.HealthCheckType != "ELB" {
|
if *group.HealthCheckType != "ELB" {
|
||||||
return fmt.Errorf("Bad health_check_type: %s", *group.HealthCheckType)
|
return fmt.Errorf("Bad health_check_type,\nexpected: %s\ngot: %s", "ELB", *group.HealthCheckType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *group.HealthCheckGracePeriod != 300 {
|
if *group.HealthCheckGracePeriod != 300 {
|
||||||
|
@ -145,6 +184,21 @@ func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.AutoScalingGro
|
||||||
return fmt.Errorf("Bad launch configuration name: %s", *group.LaunchConfigurationName)
|
return fmt.Errorf("Bad launch configuration name: %s", *group.LaunchConfigurationName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t := autoscaling.TagDescription{
|
||||||
|
Key: aws.String("Foo"),
|
||||||
|
Value: aws.String("foo-bar"),
|
||||||
|
PropagateAtLaunch: aws.Boolean(true),
|
||||||
|
ResourceType: aws.String("auto-scaling-group"),
|
||||||
|
ResourceID: group.AutoScalingGroupName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(group.Tags[0], t) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Got:\n\n%#v\n\nExpected:\n\n%#v\n",
|
||||||
|
group.Tags[0],
|
||||||
|
t)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,6 +280,12 @@ resource "aws_autoscaling_group" "bar" {
|
||||||
termination_policies = ["OldestInstance"]
|
termination_policies = ["OldestInstance"]
|
||||||
|
|
||||||
launch_configuration = "${aws_launch_configuration.foobar.name}"
|
launch_configuration = "${aws_launch_configuration.foobar.name}"
|
||||||
|
|
||||||
|
tag {
|
||||||
|
key = "Foo"
|
||||||
|
value = "foo-bar"
|
||||||
|
propagate_at_launch = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -253,6 +313,12 @@ resource "aws_autoscaling_group" "bar" {
|
||||||
force_delete = true
|
force_delete = true
|
||||||
|
|
||||||
launch_configuration = "${aws_launch_configuration.new.name}"
|
launch_configuration = "${aws_launch_configuration.new.name}"
|
||||||
|
|
||||||
|
tag {
|
||||||
|
key = "Bar"
|
||||||
|
value = "bar-foo"
|
||||||
|
propagate_at_launch = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/aws-sdk-go/aws"
|
"github.com/hashicorp/aws-sdk-go/aws"
|
||||||
|
"github.com/hashicorp/aws-sdk-go/gen/iam"
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/rds"
|
"github.com/hashicorp/aws-sdk-go/gen/rds"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/helper/hashcode"
|
"github.com/hashicorp/terraform/helper/hashcode"
|
||||||
|
@ -17,6 +18,7 @@ func resourceAwsDbInstance() *schema.Resource {
|
||||||
return &schema.Resource{
|
return &schema.Resource{
|
||||||
Create: resourceAwsDbInstanceCreate,
|
Create: resourceAwsDbInstanceCreate,
|
||||||
Read: resourceAwsDbInstanceRead,
|
Read: resourceAwsDbInstanceRead,
|
||||||
|
Update: resourceAwsDbInstanceUpdate,
|
||||||
Delete: resourceAwsDbInstanceDelete,
|
Delete: resourceAwsDbInstanceDelete,
|
||||||
|
|
||||||
Schema: map[string]*schema.Schema{
|
Schema: map[string]*schema.Schema{
|
||||||
|
@ -47,7 +49,6 @@ func resourceAwsDbInstance() *schema.Resource {
|
||||||
"engine_version": &schema.Schema{
|
"engine_version": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Required: true,
|
Required: true,
|
||||||
ForceNew: true,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"storage_encrypted": &schema.Schema{
|
"storage_encrypted": &schema.Schema{
|
||||||
|
@ -119,7 +120,6 @@ func resourceAwsDbInstance() *schema.Resource {
|
||||||
Type: schema.TypeBool,
|
Type: schema.TypeBool,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
ForceNew: true,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"port": &schema.Schema{
|
"port": &schema.Schema{
|
||||||
|
@ -138,6 +138,7 @@ func resourceAwsDbInstance() *schema.Resource {
|
||||||
"vpc_security_group_ids": &schema.Schema{
|
"vpc_security_group_ids": &schema.Schema{
|
||||||
Type: schema.TypeSet,
|
Type: schema.TypeSet,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
Elem: &schema.Schema{Type: schema.TypeString},
|
Elem: &schema.Schema{Type: schema.TypeString},
|
||||||
Set: func(v interface{}) int {
|
Set: func(v interface{}) int {
|
||||||
return hashcode.String(v.(string))
|
return hashcode.String(v.(string))
|
||||||
|
@ -162,13 +163,13 @@ func resourceAwsDbInstance() *schema.Resource {
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
|
Computed: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
"parameter_group_name": &schema.Schema{
|
"parameter_group_name": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
ForceNew: true,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"address": &schema.Schema{
|
"address": &schema.Schema{
|
||||||
|
@ -185,12 +186,24 @@ func resourceAwsDbInstance() *schema.Resource {
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// apply_immediately is used to determine when the update modifications
|
||||||
|
// take place.
|
||||||
|
// See http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html
|
||||||
|
"apply_immediately": &schema.Schema{
|
||||||
|
Type: schema.TypeBool,
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"tags": tagsSchema(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error {
|
func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
conn := meta.(*AWSClient).rdsconn
|
conn := meta.(*AWSClient).rdsconn
|
||||||
|
tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{}))
|
||||||
opts := rds.CreateDBInstanceMessage{
|
opts := rds.CreateDBInstanceMessage{
|
||||||
AllocatedStorage: aws.Integer(d.Get("allocated_storage").(int)),
|
AllocatedStorage: aws.Integer(d.Get("allocated_storage").(int)),
|
||||||
DBInstanceClass: aws.String(d.Get("instance_class").(string)),
|
DBInstanceClass: aws.String(d.Get("instance_class").(string)),
|
||||||
|
@ -201,6 +214,7 @@ func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error
|
||||||
Engine: aws.String(d.Get("engine").(string)),
|
Engine: aws.String(d.Get("engine").(string)),
|
||||||
EngineVersion: aws.String(d.Get("engine_version").(string)),
|
EngineVersion: aws.String(d.Get("engine_version").(string)),
|
||||||
StorageEncrypted: aws.Boolean(d.Get("storage_encrypted").(bool)),
|
StorageEncrypted: aws.Boolean(d.Get("storage_encrypted").(bool)),
|
||||||
|
Tags: tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
if attr, ok := d.GetOk("storage_type"); ok {
|
if attr, ok := d.GetOk("storage_type"); ok {
|
||||||
|
@ -304,7 +318,11 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
d.Set("name", *v.DBName)
|
if v.DBName != nil {
|
||||||
|
d.Set("name", *v.DBName)
|
||||||
|
} else {
|
||||||
|
d.Set("name", "")
|
||||||
|
}
|
||||||
d.Set("username", *v.MasterUsername)
|
d.Set("username", *v.MasterUsername)
|
||||||
d.Set("engine", *v.Engine)
|
d.Set("engine", *v.Engine)
|
||||||
d.Set("engine_version", *v.EngineVersion)
|
d.Set("engine_version", *v.EngineVersion)
|
||||||
|
@ -328,6 +346,28 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
d.Set("status", *v.DBInstanceStatus)
|
d.Set("status", *v.DBInstanceStatus)
|
||||||
d.Set("storage_encrypted", *v.StorageEncrypted)
|
d.Set("storage_encrypted", *v.StorageEncrypted)
|
||||||
|
|
||||||
|
// list tags for resource
|
||||||
|
// set tags
|
||||||
|
conn := meta.(*AWSClient).rdsconn
|
||||||
|
arn, err := buildRDSARN(d, meta)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] Error building ARN for DB Instance, not setting Tags for DB %s", *v.DBName)
|
||||||
|
} else {
|
||||||
|
resp, err := conn.ListTagsForResource(&rds.ListTagsForResourceMessage{
|
||||||
|
ResourceName: aws.String(arn),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] Error retreiving tags for ARN: %s", arn)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dt []rds.Tag
|
||||||
|
if len(resp.TagList) > 0 {
|
||||||
|
dt = resp.TagList
|
||||||
|
}
|
||||||
|
d.Set("tags", tagsToMapRDS(dt))
|
||||||
|
}
|
||||||
|
|
||||||
// Create an empty schema.Set to hold all vpc security group ids
|
// Create an empty schema.Set to hold all vpc security group ids
|
||||||
ids := &schema.Set{
|
ids := &schema.Set{
|
||||||
F: func(v interface{}) int {
|
F: func(v interface{}) int {
|
||||||
|
@ -390,6 +430,56 @@ func resourceAwsDbInstanceDelete(d *schema.ResourceData, meta interface{}) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resourceAwsDbInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
conn := meta.(*AWSClient).rdsconn
|
||||||
|
|
||||||
|
d.Partial(true)
|
||||||
|
// Change is used to determine if a ModifyDBInstanceMessage request actually
|
||||||
|
// gets sent.
|
||||||
|
change := false
|
||||||
|
|
||||||
|
req := &rds.ModifyDBInstanceMessage{
|
||||||
|
ApplyImmediately: aws.Boolean(d.Get("apply_immediately").(bool)),
|
||||||
|
DBInstanceIdentifier: aws.String(d.Id()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("engine_version") {
|
||||||
|
change = true
|
||||||
|
d.SetPartial("engine_version")
|
||||||
|
req.EngineVersion = aws.String(d.Get("engine_version").(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("multi_az") {
|
||||||
|
change = true
|
||||||
|
d.SetPartial("multi_az")
|
||||||
|
req.MultiAZ = aws.Boolean(d.Get("multi_az").(bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("parameter_group_name") {
|
||||||
|
change = true
|
||||||
|
d.SetPartial("parameter_group_name")
|
||||||
|
req.DBParameterGroupName = aws.String(d.Get("parameter_group_name").(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
if change {
|
||||||
|
log.Printf("[DEBUG] DB Instance Modification request: %#v", req)
|
||||||
|
_, err := conn.ModifyDBInstance(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error mofigying DB Instance %s: %s", d.Id(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if arn, err := buildRDSARN(d, meta); err == nil {
|
||||||
|
if err := setTagsRDS(conn, d, arn); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
d.SetPartial("tags")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.Partial(false)
|
||||||
|
return resourceAwsDbInstanceRead(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
func resourceAwsBbInstanceRetrieve(
|
func resourceAwsBbInstanceRetrieve(
|
||||||
d *schema.ResourceData, meta interface{}) (*rds.DBInstance, error) {
|
d *schema.ResourceData, meta interface{}) (*rds.DBInstance, error) {
|
||||||
conn := meta.(*AWSClient).rdsconn
|
conn := meta.(*AWSClient).rdsconn
|
||||||
|
@ -439,3 +529,16 @@ func resourceAwsDbInstanceStateRefreshFunc(
|
||||||
return v, *v.DBInstanceStatus, nil
|
return v, *v.DBInstanceStatus, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildRDSARN(d *schema.ResourceData, meta interface{}) (string, error) {
|
||||||
|
iamconn := meta.(*AWSClient).iamconn
|
||||||
|
region := meta.(*AWSClient).region
|
||||||
|
// An zero value GetUserRequest{} defers to the currently logged in user
|
||||||
|
resp, err := iamconn.GetUser(&iam.GetUserRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
user := resp.User
|
||||||
|
arn := fmt.Sprintf("arn:aws:rds:%s:%s:db:%s", region, *user.UserID, d.Id())
|
||||||
|
return arn, nil
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/helper/hashcode"
|
"github.com/hashicorp/terraform/helper/hashcode"
|
||||||
|
@ -220,7 +221,8 @@ func resourceAwsDbParameterHash(v interface{}) int {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
m := v.(map[string]interface{})
|
m := v.(map[string]interface{})
|
||||||
buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
|
buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
|
||||||
buf.WriteString(fmt.Sprintf("%s-", m["value"].(string)))
|
// Store the value as a lower case string, to match how we store them in flattenParameters
|
||||||
|
buf.WriteString(fmt.Sprintf("%s-", strings.ToLower(m["value"].(string))))
|
||||||
|
|
||||||
return hashcode.String(buf.String())
|
return hashcode.String(buf.String())
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,11 @@ func resourceAwsDbSubnetGroupRead(d *schema.ResourceData, meta interface{}) erro
|
||||||
|
|
||||||
describeResp, err := rdsconn.DescribeDBSubnetGroups(&describeOpts)
|
describeResp, err := rdsconn.DescribeDBSubnetGroups(&describeOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "DBSubnetGroupNotFoundFault" {
|
||||||
|
// Update state to indicate the db subnet no longer exists.
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,6 +154,8 @@ func resourceAwsElb() *schema.Resource {
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"tags": tagsSchema(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,11 +169,12 @@ func resourceAwsElbCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tags := tagsFromMapELB(d.Get("tags").(map[string]interface{}))
|
||||||
// Provision the elb
|
// Provision the elb
|
||||||
|
|
||||||
elbOpts := &elb.CreateAccessPointInput{
|
elbOpts := &elb.CreateAccessPointInput{
|
||||||
LoadBalancerName: aws.String(d.Get("name").(string)),
|
LoadBalancerName: aws.String(d.Get("name").(string)),
|
||||||
Listeners: listeners,
|
Listeners: listeners,
|
||||||
|
Tags: tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
if scheme, ok := d.GetOk("internal"); ok && scheme.(bool) {
|
if scheme, ok := d.GetOk("internal"); ok && scheme.(bool) {
|
||||||
|
@ -208,6 +211,8 @@ func resourceAwsElbCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
d.SetPartial("security_groups")
|
d.SetPartial("security_groups")
|
||||||
d.SetPartial("subnets")
|
d.SetPartial("subnets")
|
||||||
|
|
||||||
|
d.Set("tags", tagsToMapELB(tags))
|
||||||
|
|
||||||
if d.HasChange("health_check") {
|
if d.HasChange("health_check") {
|
||||||
vs := d.Get("health_check").(*schema.Set).List()
|
vs := d.Get("health_check").(*schema.Set).List()
|
||||||
if len(vs) > 0 {
|
if len(vs) > 0 {
|
||||||
|
@ -267,6 +272,15 @@ func resourceAwsElbRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
d.Set("security_groups", lb.SecurityGroups)
|
d.Set("security_groups", lb.SecurityGroups)
|
||||||
d.Set("subnets", lb.Subnets)
|
d.Set("subnets", lb.Subnets)
|
||||||
|
|
||||||
|
resp, err := elbconn.DescribeTags(&elb.DescribeTagsInput{
|
||||||
|
LoadBalancerNames: []string{*lb.LoadBalancerName},
|
||||||
|
})
|
||||||
|
|
||||||
|
var et []elb.Tag
|
||||||
|
if len(resp.TagDescriptions) > 0 {
|
||||||
|
et = resp.TagDescriptions[0].Tags
|
||||||
|
}
|
||||||
|
d.Set("tags", tagsToMapELB(et))
|
||||||
// There's only one health check, so save that to state as we
|
// There's only one health check, so save that to state as we
|
||||||
// currently can
|
// currently can
|
||||||
if *lb.HealthCheck.Target != "" {
|
if *lb.HealthCheck.Target != "" {
|
||||||
|
@ -357,6 +371,11 @@ func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := setTagsELB(elbconn, d); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
d.SetPartial("tags")
|
||||||
|
}
|
||||||
d.Partial(false)
|
d.Partial(false)
|
||||||
|
|
||||||
return resourceAwsElbRead(d, meta)
|
return resourceAwsElbRead(d, meta)
|
||||||
|
|
|
@ -53,6 +53,61 @@ func TestAccAWSELB_basic(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccAWSELB_tags(t *testing.T) {
|
||||||
|
var conf elb.LoadBalancerDescription
|
||||||
|
var td elb.TagDescription
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckAWSELBDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccAWSELBConfig,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckAWSELBExists("aws_elb.bar", &conf),
|
||||||
|
testAccCheckAWSELBAttributes(&conf),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"aws_elb.bar", "name", "foobar-terraform-test"),
|
||||||
|
testAccLoadTags(&conf, &td),
|
||||||
|
testAccCheckELBTags(&td.Tags, "bar", "baz"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccAWSELBConfig_TagUpdate,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckAWSELBExists("aws_elb.bar", &conf),
|
||||||
|
testAccCheckAWSELBAttributes(&conf),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"aws_elb.bar", "name", "foobar-terraform-test"),
|
||||||
|
testAccLoadTags(&conf, &td),
|
||||||
|
testAccCheckELBTags(&td.Tags, "foo", "bar"),
|
||||||
|
testAccCheckELBTags(&td.Tags, "new", "type"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccLoadTags(conf *elb.LoadBalancerDescription, td *elb.TagDescription) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
conn := testAccProvider.Meta().(*AWSClient).elbconn
|
||||||
|
|
||||||
|
describe, err := conn.DescribeTags(&elb.DescribeTagsInput{
|
||||||
|
LoadBalancerNames: []string{*conf.LoadBalancerName},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(describe.TagDescriptions) > 0 {
|
||||||
|
*td = describe.TagDescriptions[0]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAccAWSELB_InstanceAttaching(t *testing.T) {
|
func TestAccAWSELB_InstanceAttaching(t *testing.T) {
|
||||||
var conf elb.LoadBalancerDescription
|
var conf elb.LoadBalancerDescription
|
||||||
|
|
||||||
|
@ -288,6 +343,31 @@ resource "aws_elb" "bar" {
|
||||||
lb_protocol = "http"
|
lb_protocol = "http"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tags {
|
||||||
|
bar = "baz"
|
||||||
|
}
|
||||||
|
|
||||||
|
cross_zone_load_balancing = true
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const testAccAWSELBConfig_TagUpdate = `
|
||||||
|
resource "aws_elb" "bar" {
|
||||||
|
name = "foobar-terraform-test"
|
||||||
|
availability_zones = ["us-west-2a", "us-west-2b", "us-west-2c"]
|
||||||
|
|
||||||
|
listener {
|
||||||
|
instance_port = 8000
|
||||||
|
instance_protocol = "http"
|
||||||
|
lb_port = 80
|
||||||
|
lb_protocol = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
tags {
|
||||||
|
foo = "bar"
|
||||||
|
new = "type"
|
||||||
|
}
|
||||||
|
|
||||||
cross_zone_load_balancing = true
|
cross_zone_load_balancing = true
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -24,6 +24,9 @@ func resourceAwsInstance() *schema.Resource {
|
||||||
Update: resourceAwsInstanceUpdate,
|
Update: resourceAwsInstanceUpdate,
|
||||||
Delete: resourceAwsInstanceDelete,
|
Delete: resourceAwsInstanceDelete,
|
||||||
|
|
||||||
|
SchemaVersion: 1,
|
||||||
|
MigrateState: resourceAwsInstanceMigrateState,
|
||||||
|
|
||||||
Schema: map[string]*schema.Schema{
|
Schema: map[string]*schema.Schema{
|
||||||
"ami": &schema.Schema{
|
"ami": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
|
@ -127,53 +130,28 @@ func resourceAwsInstance() *schema.Resource {
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
"tenancy": &schema.Schema{
|
"tenancy": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
"tags": tagsSchema(),
|
"tags": tagsSchema(),
|
||||||
|
|
||||||
"block_device": &schema.Schema{
|
"block_device": &schema.Schema{
|
||||||
|
Type: schema.TypeMap,
|
||||||
|
Optional: true,
|
||||||
|
Removed: "Split out into three sub-types; see Changelog and Docs",
|
||||||
|
},
|
||||||
|
|
||||||
|
"ebs_block_device": &schema.Schema{
|
||||||
Type: schema.TypeSet,
|
Type: schema.TypeSet,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
Elem: &schema.Resource{
|
Elem: &schema.Resource{
|
||||||
Schema: map[string]*schema.Schema{
|
Schema: map[string]*schema.Schema{
|
||||||
"device_name": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Required: true,
|
|
||||||
ForceNew: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
"virtual_name": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
ForceNew: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
"snapshot_id": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
Computed: true,
|
|
||||||
ForceNew: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
"volume_type": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
Computed: true,
|
|
||||||
ForceNew: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
"volume_size": &schema.Schema{
|
|
||||||
Type: schema.TypeInt,
|
|
||||||
Optional: true,
|
|
||||||
Computed: true,
|
|
||||||
ForceNew: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
"delete_on_termination": &schema.Schema{
|
"delete_on_termination": &schema.Schema{
|
||||||
Type: schema.TypeBool,
|
Type: schema.TypeBool,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
|
@ -181,6 +159,12 @@ func resourceAwsInstance() *schema.Resource {
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"device_name": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
"encrypted": &schema.Schema{
|
"encrypted": &schema.Schema{
|
||||||
Type: schema.TypeBool,
|
Type: schema.TypeBool,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
|
@ -194,36 +178,12 @@ func resourceAwsInstance() *schema.Resource {
|
||||||
Computed: true,
|
Computed: true,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
Set: resourceAwsInstanceBlockDevicesHash,
|
|
||||||
},
|
|
||||||
|
|
||||||
"root_block_device": &schema.Schema{
|
"snapshot_id": &schema.Schema{
|
||||||
// TODO: This is a list because we don't support singleton
|
|
||||||
// sub-resources today. We'll enforce that the list only ever has
|
|
||||||
// length zero or one below. When TF gains support for
|
|
||||||
// sub-resources this can be converted.
|
|
||||||
Type: schema.TypeList,
|
|
||||||
Optional: true,
|
|
||||||
Computed: true,
|
|
||||||
Elem: &schema.Resource{
|
|
||||||
// "You can only modify the volume size, volume type, and Delete on
|
|
||||||
// Termination flag on the block device mapping entry for the root
|
|
||||||
// device volume." - bit.ly/ec2bdmap
|
|
||||||
Schema: map[string]*schema.Schema{
|
|
||||||
"delete_on_termination": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
Default: true,
|
|
||||||
ForceNew: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
"device_name": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
Default: "/dev/sda1",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"volume_size": &schema.Schema{
|
"volume_size": &schema.Schema{
|
||||||
|
@ -239,6 +199,71 @@ func resourceAwsInstance() *schema.Resource {
|
||||||
Computed: true,
|
Computed: true,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Set: func(v interface{}) int {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
m := v.(map[string]interface{})
|
||||||
|
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
|
||||||
|
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
|
||||||
|
buf.WriteString(fmt.Sprintf("%t-", m["encrypted"].(bool)))
|
||||||
|
// NOTE: Not considering IOPS in hash; when using gp2, IOPS can come
|
||||||
|
// back set to something like "33", which throws off the set
|
||||||
|
// calculation and generates an unresolvable diff.
|
||||||
|
// buf.WriteString(fmt.Sprintf("%d-", m["iops"].(int)))
|
||||||
|
buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string)))
|
||||||
|
buf.WriteString(fmt.Sprintf("%d-", m["volume_size"].(int)))
|
||||||
|
buf.WriteString(fmt.Sprintf("%s-", m["volume_type"].(string)))
|
||||||
|
return hashcode.String(buf.String())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"ephemeral_block_device": &schema.Schema{
|
||||||
|
Type: schema.TypeSet,
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
ForceNew: true,
|
||||||
|
Elem: &schema.Resource{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"device_name": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"virtual_name": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Set: func(v interface{}) int {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
m := v.(map[string]interface{})
|
||||||
|
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
|
||||||
|
buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string)))
|
||||||
|
return hashcode.String(buf.String())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"root_block_device": &schema.Schema{
|
||||||
|
// TODO: This is a set because we don't support singleton
|
||||||
|
// sub-resources today. We'll enforce that the set only ever has
|
||||||
|
// length zero or one below. When TF gains support for
|
||||||
|
// sub-resources this can be converted.
|
||||||
|
Type: schema.TypeSet,
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Elem: &schema.Resource{
|
||||||
|
// "You can only modify the volume size, volume type, and Delete on
|
||||||
|
// Termination flag on the block device mapping entry for the root
|
||||||
|
// device volume." - bit.ly/ec2bdmap
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"delete_on_termination": &schema.Schema{
|
||||||
|
Type: schema.TypeBool,
|
||||||
|
Optional: true,
|
||||||
|
Default: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
"iops": &schema.Schema{
|
"iops": &schema.Schema{
|
||||||
Type: schema.TypeInt,
|
Type: schema.TypeInt,
|
||||||
|
@ -246,8 +271,32 @@ func resourceAwsInstance() *schema.Resource {
|
||||||
Computed: true,
|
Computed: true,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"volume_size": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"volume_type": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Set: func(v interface{}) int {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
m := v.(map[string]interface{})
|
||||||
|
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
|
||||||
|
// See the NOTE in "ebs_block_device" for why we skip iops here.
|
||||||
|
// buf.WriteString(fmt.Sprintf("%d-", m["iops"].(int)))
|
||||||
|
buf.WriteString(fmt.Sprintf("%d-", m["volume_size"].(int)))
|
||||||
|
buf.WriteString(fmt.Sprintf("%s-", m["volume_type"].(string)))
|
||||||
|
return hashcode.String(buf.String())
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -262,9 +311,21 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
userData = base64.StdEncoding.EncodeToString([]byte(v.(string)))
|
userData = base64.StdEncoding.EncodeToString([]byte(v.(string)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check for non-default Subnet, and cast it to a String
|
||||||
|
var hasSubnet bool
|
||||||
|
subnet, hasSubnet := d.GetOk("subnet_id")
|
||||||
|
subnetID := subnet.(string)
|
||||||
|
|
||||||
placement := &ec2.Placement{
|
placement := &ec2.Placement{
|
||||||
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
|
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
|
||||||
Tenancy: aws.String(d.Get("tenancy").(string)),
|
}
|
||||||
|
|
||||||
|
if hasSubnet {
|
||||||
|
// Tenancy is only valid inside a VPC
|
||||||
|
// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Placement.html
|
||||||
|
if v := d.Get("tenancy").(string); v != "" {
|
||||||
|
placement.Tenancy = aws.String(v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
iam := &ec2.IAMInstanceProfileSpecification{
|
iam := &ec2.IAMInstanceProfileSpecification{
|
||||||
|
@ -288,11 +349,6 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
associatePublicIPAddress = v.(bool)
|
associatePublicIPAddress = v.(bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for non-default Subnet, and cast it to a String
|
|
||||||
var hasSubnet bool
|
|
||||||
subnet, hasSubnet := d.GetOk("subnet_id")
|
|
||||||
subnetID := subnet.(string)
|
|
||||||
|
|
||||||
var groups []string
|
var groups []string
|
||||||
if v := d.Get("security_groups"); v != nil {
|
if v := d.Get("security_groups"); v != nil {
|
||||||
// Security group names.
|
// Security group names.
|
||||||
|
@ -347,46 +403,88 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
runOpts.KeyName = aws.String(v.(string))
|
runOpts.KeyName = aws.String(v.(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
blockDevices := make([]interface{}, 0)
|
blockDevices := make([]ec2.BlockDeviceMapping, 0)
|
||||||
|
|
||||||
if v := d.Get("block_device"); v != nil {
|
if v, ok := d.GetOk("ebs_block_device"); ok {
|
||||||
blockDevices = append(blockDevices, v.(*schema.Set).List()...)
|
vL := v.(*schema.Set).List()
|
||||||
}
|
for _, v := range vL {
|
||||||
|
|
||||||
if v := d.Get("root_block_device"); v != nil {
|
|
||||||
rootBlockDevices := v.([]interface{})
|
|
||||||
if len(rootBlockDevices) > 1 {
|
|
||||||
return fmt.Errorf("Cannot specify more than one root_block_device.")
|
|
||||||
}
|
|
||||||
blockDevices = append(blockDevices, rootBlockDevices...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(blockDevices) > 0 {
|
|
||||||
runOpts.BlockDeviceMappings = make([]ec2.BlockDeviceMapping, len(blockDevices))
|
|
||||||
for i, v := range blockDevices {
|
|
||||||
bd := v.(map[string]interface{})
|
bd := v.(map[string]interface{})
|
||||||
runOpts.BlockDeviceMappings[i].DeviceName = aws.String(bd["device_name"].(string))
|
ebs := &ec2.EBSBlockDevice{
|
||||||
runOpts.BlockDeviceMappings[i].EBS = &ec2.EBSBlockDevice{
|
|
||||||
VolumeType: aws.String(bd["volume_type"].(string)),
|
|
||||||
VolumeSize: aws.Integer(bd["volume_size"].(int)),
|
|
||||||
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
|
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, ok := bd["virtual_name"].(string); ok {
|
|
||||||
runOpts.BlockDeviceMappings[i].VirtualName = aws.String(v)
|
|
||||||
}
|
|
||||||
if v, ok := bd["snapshot_id"].(string); ok && v != "" {
|
if v, ok := bd["snapshot_id"].(string); ok && v != "" {
|
||||||
runOpts.BlockDeviceMappings[i].EBS.SnapshotID = aws.String(v)
|
ebs.SnapshotID = aws.String(v)
|
||||||
}
|
}
|
||||||
if v, ok := bd["encrypted"].(bool); ok {
|
|
||||||
runOpts.BlockDeviceMappings[i].EBS.Encrypted = aws.Boolean(v)
|
if v, ok := bd["volume_size"].(int); ok && v != 0 {
|
||||||
|
ebs.VolumeSize = aws.Integer(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v, ok := bd["volume_type"].(string); ok && v != "" {
|
||||||
|
ebs.VolumeType = aws.String(v)
|
||||||
|
}
|
||||||
|
|
||||||
if v, ok := bd["iops"].(int); ok && v > 0 {
|
if v, ok := bd["iops"].(int); ok && v > 0 {
|
||||||
runOpts.BlockDeviceMappings[i].EBS.IOPS = aws.Integer(v)
|
ebs.IOPS = aws.Integer(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
blockDevices = append(blockDevices, ec2.BlockDeviceMapping{
|
||||||
|
DeviceName: aws.String(bd["device_name"].(string)),
|
||||||
|
EBS: ebs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := d.GetOk("ephemeral_block_device"); ok {
|
||||||
|
vL := v.(*schema.Set).List()
|
||||||
|
for _, v := range vL {
|
||||||
|
bd := v.(map[string]interface{})
|
||||||
|
blockDevices = append(blockDevices, ec2.BlockDeviceMapping{
|
||||||
|
DeviceName: aws.String(bd["device_name"].(string)),
|
||||||
|
VirtualName: aws.String(bd["virtual_name"].(string)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := d.GetOk("root_block_device"); ok {
|
||||||
|
vL := v.(*schema.Set).List()
|
||||||
|
if len(vL) > 1 {
|
||||||
|
return fmt.Errorf("Cannot specify more than one root_block_device.")
|
||||||
|
}
|
||||||
|
for _, v := range vL {
|
||||||
|
bd := v.(map[string]interface{})
|
||||||
|
ebs := &ec2.EBSBlockDevice{
|
||||||
|
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := bd["volume_size"].(int); ok && v != 0 {
|
||||||
|
ebs.VolumeSize = aws.Integer(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := bd["volume_type"].(string); ok && v != "" {
|
||||||
|
ebs.VolumeType = aws.String(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := bd["iops"].(int); ok && v > 0 {
|
||||||
|
ebs.IOPS = aws.Integer(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dn, err := fetchRootDeviceName(d.Get("ami").(string), ec2conn); err == nil {
|
||||||
|
blockDevices = append(blockDevices, ec2.BlockDeviceMapping{
|
||||||
|
DeviceName: dn,
|
||||||
|
EBS: ebs,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(blockDevices) > 0 {
|
||||||
|
runOpts.BlockDeviceMappings = blockDevices
|
||||||
|
}
|
||||||
|
|
||||||
// Create the instance
|
// Create the instance
|
||||||
log.Printf("[DEBUG] Run configuration: %#v", runOpts)
|
log.Printf("[DEBUG] Run configuration: %#v", runOpts)
|
||||||
runResp, err := ec2conn.RunInstances(runOpts)
|
runResp, err := ec2conn.RunInstances(runOpts)
|
||||||
|
@ -473,13 +571,18 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
d.Set("availability_zone", instance.Placement.AvailabilityZone)
|
if instance.Placement != nil {
|
||||||
|
d.Set("availability_zone", instance.Placement.AvailabilityZone)
|
||||||
|
}
|
||||||
|
if instance.Placement.Tenancy != nil {
|
||||||
|
d.Set("tenancy", instance.Placement.Tenancy)
|
||||||
|
}
|
||||||
|
|
||||||
d.Set("key_name", instance.KeyName)
|
d.Set("key_name", instance.KeyName)
|
||||||
d.Set("public_dns", instance.PublicDNSName)
|
d.Set("public_dns", instance.PublicDNSName)
|
||||||
d.Set("public_ip", instance.PublicIPAddress)
|
d.Set("public_ip", instance.PublicIPAddress)
|
||||||
d.Set("private_dns", instance.PrivateDNSName)
|
d.Set("private_dns", instance.PrivateDNSName)
|
||||||
d.Set("private_ip", instance.PrivateIPAddress)
|
d.Set("private_ip", instance.PrivateIPAddress)
|
||||||
d.Set("subnet_id", instance.SubnetID)
|
|
||||||
if len(instance.NetworkInterfaces) > 0 {
|
if len(instance.NetworkInterfaces) > 0 {
|
||||||
d.Set("subnet_id", instance.NetworkInterfaces[0].SubnetID)
|
d.Set("subnet_id", instance.NetworkInterfaces[0].SubnetID)
|
||||||
} else {
|
} else {
|
||||||
|
@ -487,14 +590,13 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
}
|
}
|
||||||
d.Set("ebs_optimized", instance.EBSOptimized)
|
d.Set("ebs_optimized", instance.EBSOptimized)
|
||||||
d.Set("tags", tagsToMap(instance.Tags))
|
d.Set("tags", tagsToMap(instance.Tags))
|
||||||
d.Set("tenancy", instance.Placement.Tenancy)
|
|
||||||
|
|
||||||
// Determine whether we're referring to security groups with
|
// Determine whether we're referring to security groups with
|
||||||
// IDs or names. We use a heuristic to figure this out. By default,
|
// IDs or names. We use a heuristic to figure this out. By default,
|
||||||
// we use IDs if we're in a VPC. However, if we previously had an
|
// we use IDs if we're in a VPC. However, if we previously had an
|
||||||
// all-name list of security groups, we use names. Or, if we had any
|
// all-name list of security groups, we use names. Or, if we had any
|
||||||
// IDs, we use IDs.
|
// IDs, we use IDs.
|
||||||
useID := *instance.SubnetID != ""
|
useID := instance.SubnetID != nil && *instance.SubnetID != ""
|
||||||
if v := d.Get("security_groups"); v != nil {
|
if v := d.Get("security_groups"); v != nil {
|
||||||
match := false
|
match := false
|
||||||
for _, v := range v.(*schema.Set).List() {
|
for _, v := range v.(*schema.Set).List() {
|
||||||
|
@ -518,67 +620,28 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
}
|
}
|
||||||
d.Set("security_groups", sgs)
|
d.Set("security_groups", sgs)
|
||||||
|
|
||||||
blockDevices := make(map[string]ec2.InstanceBlockDeviceMapping)
|
if err := readBlockDevices(d, instance, ec2conn); err != nil {
|
||||||
for _, bd := range instance.BlockDeviceMappings {
|
|
||||||
blockDevices[*bd.EBS.VolumeID] = bd
|
|
||||||
}
|
|
||||||
|
|
||||||
volIDs := make([]string, 0, len(blockDevices))
|
|
||||||
for _, vol := range blockDevices {
|
|
||||||
volIDs = append(volIDs, *vol.EBS.VolumeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
volResp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesRequest{
|
|
||||||
VolumeIDs: volIDs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
nonRootBlockDevices := make([]map[string]interface{}, 0)
|
|
||||||
rootBlockDevice := make([]interface{}, 0, 1)
|
|
||||||
for _, vol := range volResp.Volumes {
|
|
||||||
blockDevice := make(map[string]interface{})
|
|
||||||
blockDevice["device_name"] = *blockDevices[*vol.VolumeID].DeviceName
|
|
||||||
blockDevice["volume_type"] = *vol.VolumeType
|
|
||||||
blockDevice["volume_size"] = *vol.Size
|
|
||||||
if vol.IOPS != nil {
|
|
||||||
blockDevice["iops"] = *vol.IOPS
|
|
||||||
}
|
|
||||||
blockDevice["delete_on_termination"] =
|
|
||||||
*blockDevices[*vol.VolumeID].EBS.DeleteOnTermination
|
|
||||||
|
|
||||||
// If this is the root device, save it. We stop here since we
|
|
||||||
// can't put invalid keys into this map.
|
|
||||||
if blockDevice["device_name"] == *instance.RootDeviceName {
|
|
||||||
rootBlockDevice = []interface{}{blockDevice}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
blockDevice["snapshot_id"] = *vol.SnapshotID
|
|
||||||
blockDevice["encrypted"] = *vol.Encrypted
|
|
||||||
nonRootBlockDevices = append(nonRootBlockDevices, blockDevice)
|
|
||||||
}
|
|
||||||
d.Set("block_device", nonRootBlockDevices)
|
|
||||||
d.Set("root_block_device", rootBlockDevice)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
|
func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
ec2conn := meta.(*AWSClient).ec2conn
|
ec2conn := meta.(*AWSClient).ec2conn
|
||||||
opts := new(ec2.ModifyInstanceAttributeRequest)
|
|
||||||
|
|
||||||
log.Printf("[INFO] Modifying instance %s: %#v", d.Id(), opts)
|
// SourceDestCheck can only be set on VPC instances
|
||||||
err := ec2conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeRequest{
|
if d.Get("subnet_id").(string) != "" {
|
||||||
InstanceID: aws.String(d.Id()),
|
log.Printf("[INFO] Modifying instance %s", d.Id())
|
||||||
SourceDestCheck: &ec2.AttributeBooleanValue{
|
err := ec2conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeRequest{
|
||||||
Value: aws.Boolean(d.Get("source_dest_check").(bool)),
|
InstanceID: aws.String(d.Id()),
|
||||||
},
|
SourceDestCheck: &ec2.AttributeBooleanValue{
|
||||||
})
|
Value: aws.Boolean(d.Get("source_dest_check").(bool)),
|
||||||
|
},
|
||||||
if err != nil {
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(mitchellh): wait for the attributes we modified to
|
// TODO(mitchellh): wait for the attributes we modified to
|
||||||
|
@ -656,11 +719,111 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceAwsInstanceBlockDevicesHash(v interface{}) int {
|
func readBlockDevices(d *schema.ResourceData, instance *ec2.Instance, ec2conn *ec2.EC2) error {
|
||||||
var buf bytes.Buffer
|
ibds, err := readBlockDevicesFromInstance(instance, ec2conn)
|
||||||
m := v.(map[string]interface{})
|
if err != nil {
|
||||||
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
|
return err
|
||||||
buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string)))
|
}
|
||||||
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
|
|
||||||
return hashcode.String(buf.String())
|
if err := d.Set("ebs_block_device", ibds["ebs"]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ibds["root"] != nil {
|
||||||
|
if err := d.Set("root_block_device", []interface{}{ibds["root"]}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBlockDevicesFromInstance(instance *ec2.Instance, ec2conn *ec2.EC2) (map[string]interface{}, error) {
|
||||||
|
blockDevices := make(map[string]interface{})
|
||||||
|
blockDevices["ebs"] = make([]map[string]interface{}, 0)
|
||||||
|
blockDevices["root"] = nil
|
||||||
|
|
||||||
|
instanceBlockDevices := make(map[string]ec2.InstanceBlockDeviceMapping)
|
||||||
|
for _, bd := range instance.BlockDeviceMappings {
|
||||||
|
if bd.EBS != nil {
|
||||||
|
instanceBlockDevices[*(bd.EBS.VolumeID)] = bd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(instanceBlockDevices) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
volIDs := make([]string, 0, len(instanceBlockDevices))
|
||||||
|
for volID := range instanceBlockDevices {
|
||||||
|
volIDs = append(volIDs, volID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to call DescribeVolumes to get volume_size and volume_type for each
|
||||||
|
// EBS block device
|
||||||
|
volResp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesRequest{
|
||||||
|
VolumeIDs: volIDs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, vol := range volResp.Volumes {
|
||||||
|
instanceBd := instanceBlockDevices[*vol.VolumeID]
|
||||||
|
bd := make(map[string]interface{})
|
||||||
|
|
||||||
|
if instanceBd.EBS != nil && instanceBd.EBS.DeleteOnTermination != nil {
|
||||||
|
bd["delete_on_termination"] = *instanceBd.EBS.DeleteOnTermination
|
||||||
|
}
|
||||||
|
if vol.Size != nil {
|
||||||
|
bd["volume_size"] = *vol.Size
|
||||||
|
}
|
||||||
|
if vol.VolumeType != nil {
|
||||||
|
bd["volume_type"] = *vol.VolumeType
|
||||||
|
}
|
||||||
|
if vol.IOPS != nil {
|
||||||
|
bd["iops"] = *vol.IOPS
|
||||||
|
}
|
||||||
|
|
||||||
|
if blockDeviceIsRoot(instanceBd, instance) {
|
||||||
|
blockDevices["root"] = bd
|
||||||
|
} else {
|
||||||
|
if instanceBd.DeviceName != nil {
|
||||||
|
bd["device_name"] = *instanceBd.DeviceName
|
||||||
|
}
|
||||||
|
if vol.Encrypted != nil {
|
||||||
|
bd["encrypted"] = *vol.Encrypted
|
||||||
|
}
|
||||||
|
if vol.SnapshotID != nil {
|
||||||
|
bd["snapshot_id"] = *vol.SnapshotID
|
||||||
|
}
|
||||||
|
|
||||||
|
blockDevices["ebs"] = append(blockDevices["ebs"].([]map[string]interface{}), bd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockDevices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func blockDeviceIsRoot(bd ec2.InstanceBlockDeviceMapping, instance *ec2.Instance) bool {
|
||||||
|
return (bd.DeviceName != nil &&
|
||||||
|
instance.RootDeviceName != nil &&
|
||||||
|
*bd.DeviceName == *instance.RootDeviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchRootDeviceName(ami string, conn *ec2.EC2) (aws.StringValue, error) {
|
||||||
|
if ami == "" {
|
||||||
|
return nil, fmt.Errorf("Cannot fetch root device name for blank AMI ID.")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Describing AMI %q to get root block device name", ami)
|
||||||
|
req := &ec2.DescribeImagesRequest{ImageIDs: []string{ami}}
|
||||||
|
if res, err := conn.DescribeImages(req); err == nil {
|
||||||
|
if len(res.Images) == 1 {
|
||||||
|
return res.Images[0].RootDeviceName, nil
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("Expected 1 AMI for ID: %s, got: %#v", ami, res.Images)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
func TestAccAWSInstance_normal(t *testing.T) {
|
||||||
var v ec2.Instance
|
var v ec2.Instance
|
||||||
|
var vol *ec2.Volume
|
||||||
|
|
||||||
testCheck := func(*terraform.State) error {
|
testCheck := func(*terraform.State) error {
|
||||||
if *v.Placement.AvailabilityZone != "us-west-2a" {
|
if *v.Placement.AvailabilityZone != "us-west-2a" {
|
||||||
|
@ -35,6 +36,21 @@ func TestAccAWSInstance_normal(t *testing.T) {
|
||||||
Providers: testAccProviders,
|
Providers: testAccProviders,
|
||||||
CheckDestroy: testAccCheckInstanceDestroy,
|
CheckDestroy: testAccCheckInstanceDestroy,
|
||||||
Steps: []resource.TestStep{
|
Steps: []resource.TestStep{
|
||||||
|
// Create a volume to cover #1249
|
||||||
|
resource.TestStep{
|
||||||
|
// Need a resource in this config so the provisioner will be available
|
||||||
|
Config: testAccInstanceConfig_pre,
|
||||||
|
Check: func(*terraform.State) error {
|
||||||
|
conn := testAccProvider.Meta().(*AWSClient).ec2conn
|
||||||
|
var err error
|
||||||
|
vol, err = conn.CreateVolume(&ec2.CreateVolumeRequest{
|
||||||
|
AvailabilityZone: aws.String("us-west-2a"),
|
||||||
|
Size: aws.Integer(5),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
resource.TestStep{
|
resource.TestStep{
|
||||||
Config: testAccInstanceConfig,
|
Config: testAccInstanceConfig,
|
||||||
Check: resource.ComposeTestCheckFunc(
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
@ -45,6 +61,8 @@ func TestAccAWSInstance_normal(t *testing.T) {
|
||||||
"aws_instance.foo",
|
"aws_instance.foo",
|
||||||
"user_data",
|
"user_data",
|
||||||
"3dc39dda39be1205215e776bad998da361a5955d"),
|
"3dc39dda39be1205215e776bad998da361a5955d"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"aws_instance.foo", "ebs_block_device.#", "0"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -61,8 +79,19 @@ func TestAccAWSInstance_normal(t *testing.T) {
|
||||||
"aws_instance.foo",
|
"aws_instance.foo",
|
||||||
"user_data",
|
"user_data",
|
||||||
"3dc39dda39be1205215e776bad998da361a5955d"),
|
"3dc39dda39be1205215e776bad998da361a5955d"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"aws_instance.foo", "ebs_block_device.#", "0"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Clean up volume created above
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccInstanceConfig,
|
||||||
|
Check: func(*terraform.State) error {
|
||||||
|
conn := testAccProvider.Meta().(*AWSClient).ec2conn
|
||||||
|
return conn.DeleteVolume(&ec2.DeleteVolumeRequest{VolumeID: vol.VolumeID})
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -111,31 +140,31 @@ func TestAccAWSInstance_blockDevices(t *testing.T) {
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_instance.foo", "root_block_device.#", "1"),
|
"aws_instance.foo", "root_block_device.#", "1"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_instance.foo", "root_block_device.0.device_name", "/dev/sda1"),
|
"aws_instance.foo", "root_block_device.1023169747.volume_size", "11"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_instance.foo", "root_block_device.0.volume_size", "11"),
|
"aws_instance.foo", "root_block_device.1023169747.volume_type", "gp2"),
|
||||||
// this one is important because it's the only root_block_device
|
|
||||||
// attribute that comes back from the API. so checking it verifies
|
|
||||||
// that we set state properly
|
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_instance.foo", "root_block_device.0.volume_type", "gp2"),
|
"aws_instance.foo", "ebs_block_device.#", "2"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_instance.foo", "block_device.#", "2"),
|
"aws_instance.foo", "ebs_block_device.2225977507.device_name", "/dev/sdb"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_instance.foo", "block_device.172787947.device_name", "/dev/sdb"),
|
"aws_instance.foo", "ebs_block_device.2225977507.volume_size", "9"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_instance.foo", "block_device.172787947.volume_size", "9"),
|
"aws_instance.foo", "ebs_block_device.2225977507.volume_type", "standard"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_instance.foo", "block_device.172787947.iops", "0"),
|
"aws_instance.foo", "ebs_block_device.1977224956.device_name", "/dev/sdc"),
|
||||||
// Check provisioned SSD device
|
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_instance.foo", "block_device.3336996981.volume_type", "io1"),
|
"aws_instance.foo", "ebs_block_device.1977224956.volume_size", "10"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_instance.foo", "block_device.3336996981.device_name", "/dev/sdc"),
|
"aws_instance.foo", "ebs_block_device.1977224956.volume_type", "io1"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_instance.foo", "block_device.3336996981.volume_size", "10"),
|
"aws_instance.foo", "ebs_block_device.1977224956.iops", "100"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_instance.foo", "block_device.3336996981.iops", "100"),
|
"aws_instance.foo", "ephemeral_block_device.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"aws_instance.foo", "ephemeral_block_device.1692014856.device_name", "/dev/sde"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"aws_instance.foo", "ephemeral_block_device.1692014856.virtual_name", "ephemeral0"),
|
||||||
testCheck(),
|
testCheck(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -391,6 +420,20 @@ func TestInstanceTenancySchema(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const testAccInstanceConfig_pre = `
|
||||||
|
resource "aws_security_group" "tf_test_foo" {
|
||||||
|
name = "tf_test_foo"
|
||||||
|
description = "foo"
|
||||||
|
|
||||||
|
ingress {
|
||||||
|
protocol = "icmp"
|
||||||
|
from_port = -1
|
||||||
|
to_port = -1
|
||||||
|
cidr_blocks = ["0.0.0.0/0"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const testAccInstanceConfig = `
|
const testAccInstanceConfig = `
|
||||||
resource "aws_security_group" "tf_test_foo" {
|
resource "aws_security_group" "tf_test_foo" {
|
||||||
name = "tf_test_foo"
|
name = "tf_test_foo"
|
||||||
|
@ -420,21 +463,25 @@ resource "aws_instance" "foo" {
|
||||||
# us-west-2
|
# us-west-2
|
||||||
ami = "ami-55a7ea65"
|
ami = "ami-55a7ea65"
|
||||||
instance_type = "m1.small"
|
instance_type = "m1.small"
|
||||||
|
|
||||||
root_block_device {
|
root_block_device {
|
||||||
device_name = "/dev/sda1"
|
|
||||||
volume_type = "gp2"
|
volume_type = "gp2"
|
||||||
volume_size = 11
|
volume_size = 11
|
||||||
}
|
}
|
||||||
block_device {
|
ebs_block_device {
|
||||||
device_name = "/dev/sdb"
|
device_name = "/dev/sdb"
|
||||||
volume_size = 9
|
volume_size = 9
|
||||||
}
|
}
|
||||||
block_device {
|
ebs_block_device {
|
||||||
device_name = "/dev/sdc"
|
device_name = "/dev/sdc"
|
||||||
volume_size = 10
|
volume_size = 10
|
||||||
volume_type = "io1"
|
volume_type = "io1"
|
||||||
iops = 100
|
iops = 100
|
||||||
}
|
}
|
||||||
|
ephemeral_block_device {
|
||||||
|
device_name = "/dev/sde"
|
||||||
|
virtual_name = "ephemeral0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
@ -199,39 +199,14 @@ func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{})
|
||||||
d.Id(),
|
d.Id(),
|
||||||
vpcID.(string))
|
vpcID.(string))
|
||||||
|
|
||||||
wait := true
|
|
||||||
err := ec2conn.DetachInternetGateway(&ec2.DetachInternetGatewayRequest{
|
|
||||||
InternetGatewayID: aws.String(d.Id()),
|
|
||||||
VPCID: aws.String(vpcID.(string)),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ec2err, ok := err.(aws.APIError)
|
|
||||||
if ok {
|
|
||||||
if ec2err.Code == "InvalidInternetGatewayID.NotFound" {
|
|
||||||
err = nil
|
|
||||||
wait = false
|
|
||||||
} else if ec2err.Code == "Gateway.NotAttached" {
|
|
||||||
err = nil
|
|
||||||
wait = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !wait {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for it to be fully detached before continuing
|
// Wait for it to be fully detached before continuing
|
||||||
log.Printf("[DEBUG] Waiting for internet gateway (%s) to detach", d.Id())
|
log.Printf("[DEBUG] Waiting for internet gateway (%s) to detach", d.Id())
|
||||||
stateConf := &resource.StateChangeConf{
|
stateConf := &resource.StateChangeConf{
|
||||||
Pending: []string{"attached", "detaching", "available"},
|
Pending: []string{"detaching"},
|
||||||
Target: "detached",
|
Target: "detached",
|
||||||
Refresh: IGAttachStateRefreshFunc(ec2conn, d.Id(), "detached"),
|
Refresh: detachIGStateRefreshFunc(ec2conn, d.Id(), vpcID.(string)),
|
||||||
Timeout: 1 * time.Minute,
|
Timeout: 2 * time.Minute,
|
||||||
|
Delay: 10 * time.Second,
|
||||||
}
|
}
|
||||||
if _, err := stateConf.WaitForState(); err != nil {
|
if _, err := stateConf.WaitForState(); err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
|
@ -242,6 +217,32 @@ func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
|
||||||
|
// an EC2 instance.
|
||||||
|
func detachIGStateRefreshFunc(conn *ec2.EC2, instanceID, vpcID string) resource.StateRefreshFunc {
|
||||||
|
return func() (interface{}, string, error) {
|
||||||
|
err := conn.DetachInternetGateway(&ec2.DetachInternetGatewayRequest{
|
||||||
|
InternetGatewayID: aws.String(instanceID),
|
||||||
|
VPCID: aws.String(vpcID),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec2err, ok := err.(aws.APIError)
|
||||||
|
if ok {
|
||||||
|
if ec2err.Code == "InvalidInternetGatewayID.NotFound" {
|
||||||
|
return nil, "Not Found", err
|
||||||
|
} else if ec2err.Code == "Gateway.NotAttached" {
|
||||||
|
return "detached", "detached", nil
|
||||||
|
} else if ec2err.Code == "DependencyViolation" {
|
||||||
|
return nil, "detaching", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// DetachInternetGateway only returns an error, so if it's nil, assume we're
|
||||||
|
// detached
|
||||||
|
return "detached", "detached", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// IGStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
|
// IGStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
|
||||||
// an internet gateway.
|
// an internet gateway.
|
||||||
func IGStateRefreshFunc(ec2conn *ec2.EC2, id string) resource.StateRefreshFunc {
|
func IGStateRefreshFunc(ec2conn *ec2.EC2, id string) resource.StateRefreshFunc {
|
||||||
|
@ -300,10 +301,6 @@ func IGAttachStateRefreshFunc(ec2conn *ec2.EC2, id string, expected string) reso
|
||||||
|
|
||||||
ig := &resp.InternetGateways[0]
|
ig := &resp.InternetGateways[0]
|
||||||
|
|
||||||
if time.Now().Sub(start) > 10*time.Second {
|
|
||||||
return ig, expected, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ig.Attachments) == 0 {
|
if len(ig.Attachments) == 0 {
|
||||||
// No attachments, we're detached
|
// No attachments, we're detached
|
||||||
return ig, "detached", nil
|
return ig, "detached", nil
|
||||||
|
|
|
@ -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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the current record name contains the zone suffix.
|
|
||||||
// If it does not, add the zone name to form a fully qualified name
|
|
||||||
// and keep AWS happy.
|
|
||||||
recordName := d.Get("name").(string)
|
|
||||||
zoneName := strings.Trim(*zoneRecord.HostedZone.Name, ".")
|
|
||||||
if !strings.HasSuffix(recordName, zoneName) {
|
|
||||||
d.Set("name", strings.Join([]string{recordName, zoneName}, "."))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the record
|
// Get the record
|
||||||
rec, err := resourceAwsRoute53RecordBuildSet(d)
|
rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -101,7 +92,7 @@ func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) er
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[DEBUG] Creating resource records for zone: %s, name: %s",
|
log.Printf("[DEBUG] Creating resource records for zone: %s, name: %s",
|
||||||
zone, d.Get("name").(string))
|
zone, *rec.Name)
|
||||||
|
|
||||||
wait := resource.StateChangeConf{
|
wait := resource.StateChangeConf{
|
||||||
Pending: []string{"rejected"},
|
Pending: []string{"rejected"},
|
||||||
|
@ -111,10 +102,12 @@ func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) er
|
||||||
Refresh: func() (interface{}, string, error) {
|
Refresh: func() (interface{}, string, error) {
|
||||||
resp, err := conn.ChangeResourceRecordSets(req)
|
resp, err := conn.ChangeResourceRecordSets(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "PriorRequestNotComplete") {
|
if r53err, ok := err.(aws.APIError); ok {
|
||||||
// There is some pending operation, so just retry
|
if r53err.Code == "PriorRequestNotComplete" {
|
||||||
// in a bit.
|
// There is some pending operation, so just retry
|
||||||
return nil, "rejected", nil
|
// in a bit.
|
||||||
|
return nil, "rejected", nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, "failure", err
|
return nil, "failure", err
|
||||||
|
@ -159,9 +152,17 @@ func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) erro
|
||||||
conn := meta.(*AWSClient).r53conn
|
conn := meta.(*AWSClient).r53conn
|
||||||
|
|
||||||
zone := d.Get("zone_id").(string)
|
zone := d.Get("zone_id").(string)
|
||||||
|
|
||||||
|
// get expanded name
|
||||||
|
zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(zone)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name)
|
||||||
|
|
||||||
lopts := &route53.ListResourceRecordSetsRequest{
|
lopts := &route53.ListResourceRecordSetsRequest{
|
||||||
HostedZoneID: aws.String(cleanZoneID(zone)),
|
HostedZoneID: aws.String(cleanZoneID(zone)),
|
||||||
StartRecordName: aws.String(d.Get("name").(string)),
|
StartRecordName: aws.String(en),
|
||||||
StartRecordType: aws.String(d.Get("type").(string)),
|
StartRecordType: aws.String(d.Get("type").(string)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,9 +203,12 @@ func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) er
|
||||||
zone := d.Get("zone_id").(string)
|
zone := d.Get("zone_id").(string)
|
||||||
log.Printf("[DEBUG] Deleting resource records for zone: %s, name: %s",
|
log.Printf("[DEBUG] Deleting resource records for zone: %s, name: %s",
|
||||||
zone, d.Get("name").(string))
|
zone, d.Get("name").(string))
|
||||||
|
zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(zone)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// Get the records
|
// Get the records
|
||||||
rec, err := resourceAwsRoute53RecordBuildSet(d)
|
rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -260,16 +264,30 @@ func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) er
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData) (*route53.ResourceRecordSet, error) {
|
func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) (*route53.ResourceRecordSet, error) {
|
||||||
recs := d.Get("records").(*schema.Set).List()
|
recs := d.Get("records").(*schema.Set).List()
|
||||||
records := make([]route53.ResourceRecord, 0, len(recs))
|
records := make([]route53.ResourceRecord, 0, len(recs))
|
||||||
|
|
||||||
|
typeStr := d.Get("type").(string)
|
||||||
for _, r := range recs {
|
for _, r := range recs {
|
||||||
records = append(records, route53.ResourceRecord{Value: aws.String(r.(string))})
|
switch typeStr {
|
||||||
|
case "TXT":
|
||||||
|
str := fmt.Sprintf("\"%s\"", r.(string))
|
||||||
|
records = append(records, route53.ResourceRecord{Value: aws.String(str)})
|
||||||
|
default:
|
||||||
|
records = append(records, route53.ResourceRecord{Value: aws.String(r.(string))})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get expanded name
|
||||||
|
en := expandRecordName(d.Get("name").(string), zoneName)
|
||||||
|
|
||||||
|
// Create the RecordSet request with the fully expanded name, e.g.
|
||||||
|
// sub.domain.com. Route 53 requires a fully qualified domain name, but does
|
||||||
|
// not require the trailing ".", which it will itself, so we don't call FQDN
|
||||||
|
// here.
|
||||||
rec := &route53.ResourceRecordSet{
|
rec := &route53.ResourceRecordSet{
|
||||||
Name: aws.String(d.Get("name").(string)),
|
Name: aws.String(en),
|
||||||
Type: aws.String(d.Get("type").(string)),
|
Type: aws.String(d.Get("type").(string)),
|
||||||
TTL: aws.Long(int64(d.Get("ttl").(int))),
|
TTL: aws.Long(int64(d.Get("ttl").(int))),
|
||||||
ResourceRecords: records,
|
ResourceRecords: records,
|
||||||
|
@ -297,3 +315,15 @@ func cleanRecordName(name string) string {
|
||||||
}
|
}
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the current record name contains the zone suffix.
|
||||||
|
// If it does not, add the zone name to form a fully qualified name
|
||||||
|
// and keep AWS happy.
|
||||||
|
func expandRecordName(name, zone string) string {
|
||||||
|
rn := strings.TrimSuffix(name, ".")
|
||||||
|
zone = strings.TrimSuffix(zone, ".")
|
||||||
|
if !strings.HasSuffix(rn, zone) {
|
||||||
|
rn = strings.Join([]string{name, zone}, ".")
|
||||||
|
}
|
||||||
|
return rn
|
||||||
|
}
|
||||||
|
|
|
@ -29,6 +29,27 @@ func TestCleanRecordName(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExpandRecordName(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
Input, Output string
|
||||||
|
}{
|
||||||
|
{"www", "www.nonexample.com"},
|
||||||
|
{"dev.www", "dev.www.nonexample.com"},
|
||||||
|
{"*", "*.nonexample.com"},
|
||||||
|
{"nonexample.com", "nonexample.com"},
|
||||||
|
{"test.nonexample.com", "test.nonexample.com"},
|
||||||
|
{"test.nonexample.com.", "test.nonexample.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
zone_name := "nonexample.com"
|
||||||
|
for _, tc := range cases {
|
||||||
|
actual := expandRecordName(tc.Input, zone_name)
|
||||||
|
if actual != tc.Output {
|
||||||
|
t.Fatalf("input: %s\noutput: %s", tc.Input, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAccRoute53Record(t *testing.T) {
|
func TestAccRoute53Record(t *testing.T) {
|
||||||
resource.Test(t, resource.TestCase{
|
resource.Test(t, resource.TestCase{
|
||||||
PreCheck: func() { testAccPreCheck(t) },
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
@ -45,6 +66,22 @@ func TestAccRoute53Record(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccRoute53Record_txtSupport(t *testing.T) {
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckRoute53RecordDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccRoute53RecordConfigTXT,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckRoute53RecordExists("aws_route53_record.default"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestAccRoute53Record_generatesSuffix(t *testing.T) {
|
func TestAccRoute53Record_generatesSuffix(t *testing.T) {
|
||||||
resource.Test(t, resource.TestCase{
|
resource.Test(t, resource.TestCase{
|
||||||
PreCheck: func() { testAccPreCheck(t) },
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
@ -135,9 +172,11 @@ func testAccCheckRoute53RecordExists(n string) resource.TestCheckFunc {
|
||||||
name := parts[1]
|
name := parts[1]
|
||||||
rType := parts[2]
|
rType := parts[2]
|
||||||
|
|
||||||
|
en := expandRecordName(name, "notexample.com")
|
||||||
|
|
||||||
lopts := &route53.ListResourceRecordSetsRequest{
|
lopts := &route53.ListResourceRecordSetsRequest{
|
||||||
HostedZoneID: aws.String(cleanZoneID(zone)),
|
HostedZoneID: aws.String(cleanZoneID(zone)),
|
||||||
StartRecordName: aws.String(name),
|
StartRecordName: aws.String(en),
|
||||||
StartRecordType: aws.String(rType),
|
StartRecordType: aws.String(rType),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +190,7 @@ func testAccCheckRoute53RecordExists(n string) resource.TestCheckFunc {
|
||||||
// rec := resp.ResourceRecordSets[0]
|
// rec := resp.ResourceRecordSets[0]
|
||||||
for _, rec := range resp.ResourceRecordSets {
|
for _, rec := range resp.ResourceRecordSets {
|
||||||
recName := cleanRecordName(*rec.Name)
|
recName := cleanRecordName(*rec.Name)
|
||||||
if FQDN(recName) == FQDN(name) && *rec.Type == rType {
|
if FQDN(recName) == FQDN(en) && *rec.Type == rType {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,3 +269,16 @@ resource "aws_route53_record" "wildcard" {
|
||||||
records = ["127.0.0.1"]
|
records = ["127.0.0.1"]
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
const testAccRoute53RecordConfigTXT = `
|
||||||
|
resource "aws_route53_zone" "main" {
|
||||||
|
name = "notexample.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_route53_record" "default" {
|
||||||
|
zone_id = "${aws_route53_zone.main.zone_id}"
|
||||||
|
name = "subdomain"
|
||||||
|
type = "TXT"
|
||||||
|
ttl = "30"
|
||||||
|
records = ["lalalala"]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
|
@ -16,6 +16,7 @@ func resourceAwsRoute53Zone() *schema.Resource {
|
||||||
return &schema.Resource{
|
return &schema.Resource{
|
||||||
Create: resourceAwsRoute53ZoneCreate,
|
Create: resourceAwsRoute53ZoneCreate,
|
||||||
Read: resourceAwsRoute53ZoneRead,
|
Read: resourceAwsRoute53ZoneRead,
|
||||||
|
Update: resourceAwsRoute53ZoneUpdate,
|
||||||
Delete: resourceAwsRoute53ZoneDelete,
|
Delete: resourceAwsRoute53ZoneDelete,
|
||||||
|
|
||||||
Schema: map[string]*schema.Schema{
|
Schema: map[string]*schema.Schema{
|
||||||
|
@ -29,6 +30,8 @@ func resourceAwsRoute53Zone() *schema.Resource {
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"tags": tagsSchema(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +75,7 @@ func resourceAwsRoute53ZoneCreate(d *schema.ResourceData, meta interface{}) erro
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return resourceAwsRoute53ZoneUpdate(d, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error {
|
func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
@ -87,9 +90,41 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get tags
|
||||||
|
req := &route53.ListTagsForResourceRequest{
|
||||||
|
ResourceID: aws.String(d.Id()),
|
||||||
|
ResourceType: aws.String("hostedzone"),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := r53.ListTagsForResource(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tags []route53.Tag
|
||||||
|
if resp.ResourceTagSet != nil {
|
||||||
|
tags = resp.ResourceTagSet.Tags
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.Set("tags", tagsToMapR53(tags)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resourceAwsRoute53ZoneUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
conn := meta.(*AWSClient).r53conn
|
||||||
|
|
||||||
|
if err := setTagsR53(conn, d); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
d.SetPartial("tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceAwsRoute53ZoneRead(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
func resourceAwsRoute53ZoneDelete(d *schema.ResourceData, meta interface{}) error {
|
func resourceAwsRoute53ZoneDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
r53 := meta.(*AWSClient).r53conn
|
r53 := meta.(*AWSClient).r53conn
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,9 @@ func TestCleanChangeID(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccRoute53Zone(t *testing.T) {
|
func TestAccRoute53Zone(t *testing.T) {
|
||||||
|
var zone route53.HostedZone
|
||||||
|
var td route53.ResourceTagSet
|
||||||
|
|
||||||
resource.Test(t, resource.TestCase{
|
resource.Test(t, resource.TestCase{
|
||||||
PreCheck: func() { testAccPreCheck(t) },
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
Providers: testAccProviders,
|
Providers: testAccProviders,
|
||||||
|
@ -71,7 +74,9 @@ func TestAccRoute53Zone(t *testing.T) {
|
||||||
resource.TestStep{
|
resource.TestStep{
|
||||||
Config: testAccRoute53ZoneConfig,
|
Config: testAccRoute53ZoneConfig,
|
||||||
Check: resource.ComposeTestCheckFunc(
|
Check: resource.ComposeTestCheckFunc(
|
||||||
testAccCheckRoute53ZoneExists("aws_route53_zone.main"),
|
testAccCheckRoute53ZoneExists("aws_route53_zone.main", &zone),
|
||||||
|
testAccLoadTagsR53(&zone, &td),
|
||||||
|
testAccCheckTagsR53(&td.Tags, "foo", "bar"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -93,7 +98,7 @@ func testAccCheckRoute53ZoneDestroy(s *terraform.State) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc {
|
func testAccCheckRoute53ZoneExists(n string, zone *route53.HostedZone) resource.TestCheckFunc {
|
||||||
return func(s *terraform.State) error {
|
return func(s *terraform.State) error {
|
||||||
rs, ok := s.RootModule().Resources[n]
|
rs, ok := s.RootModule().Resources[n]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -105,10 +110,34 @@ func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
conn := testAccProvider.Meta().(*AWSClient).r53conn
|
conn := testAccProvider.Meta().(*AWSClient).r53conn
|
||||||
_, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(rs.Primary.ID)})
|
resp, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(rs.Primary.ID)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Hosted zone err: %v", err)
|
return fmt.Errorf("Hosted zone err: %v", err)
|
||||||
}
|
}
|
||||||
|
*zone = *resp.HostedZone
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccLoadTagsR53(zone *route53.HostedZone, td *route53.ResourceTagSet) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
conn := testAccProvider.Meta().(*AWSClient).r53conn
|
||||||
|
|
||||||
|
zone := cleanZoneID(*zone.ID)
|
||||||
|
req := &route53.ListTagsForResourceRequest{
|
||||||
|
ResourceID: aws.String(zone),
|
||||||
|
ResourceType: aws.String("hostedzone"),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := conn.ListTagsForResource(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.ResourceTagSet != nil {
|
||||||
|
*td = *resp.ResourceTagSet
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,5 +145,10 @@ func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc {
|
||||||
const testAccRoute53ZoneConfig = `
|
const testAccRoute53ZoneConfig = `
|
||||||
resource "aws_route53_zone" "main" {
|
resource "aws_route53_zone" "main" {
|
||||||
name = "hashicorp.com"
|
name = "hashicorp.com"
|
||||||
|
|
||||||
|
tags {
|
||||||
|
foo = "bar"
|
||||||
|
Name = "tf-route53-tag-test"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -107,6 +107,7 @@ func resourceAwsRouteTableRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if rtRaw == nil {
|
if rtRaw == nil {
|
||||||
|
d.SetId("")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ func resourceAwsS3Bucket() *schema.Resource {
|
||||||
return &schema.Resource{
|
return &schema.Resource{
|
||||||
Create: resourceAwsS3BucketCreate,
|
Create: resourceAwsS3BucketCreate,
|
||||||
Read: resourceAwsS3BucketRead,
|
Read: resourceAwsS3BucketRead,
|
||||||
|
Update: resourceAwsS3BucketUpdate,
|
||||||
Delete: resourceAwsS3BucketDelete,
|
Delete: resourceAwsS3BucketDelete,
|
||||||
|
|
||||||
Schema: map[string]*schema.Schema{
|
Schema: map[string]*schema.Schema{
|
||||||
|
@ -29,6 +30,8 @@ func resourceAwsS3Bucket() *schema.Resource {
|
||||||
Optional: true,
|
Optional: true,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"tags": tagsSchema(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +67,15 @@ func resourceAwsS3BucketCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
// Assign the bucket name as the resource ID
|
// Assign the bucket name as the resource ID
|
||||||
d.SetId(bucket)
|
d.SetId(bucket)
|
||||||
|
|
||||||
return nil
|
return resourceAwsS3BucketUpdate(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceAwsS3BucketUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
s3conn := meta.(*AWSClient).s3conn
|
||||||
|
if err := setTagsS3(s3conn, d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return resourceAwsS3BucketRead(d, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
|
func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
@ -76,6 +87,16 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tagSet, err := getTagSetS3(s3conn, d.Id())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.Set("tags", tagsToMapS3(tagSet)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -285,6 +285,7 @@ func resourceAwsSecurityGroupRuleHash(v interface{}) int {
|
||||||
buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int)))
|
buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int)))
|
||||||
buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int)))
|
buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int)))
|
||||||
buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string)))
|
buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string)))
|
||||||
|
buf.WriteString(fmt.Sprintf("%t-", m["self"].(bool)))
|
||||||
|
|
||||||
// We need to make sure to sort the strings below so that we always
|
// We need to make sure to sort the strings below so that we always
|
||||||
// generate the same hash code no matter what is in the set.
|
// generate the same hash code no matter what is in the set.
|
||||||
|
|
|
@ -30,15 +30,15 @@ func TestAccAWSSecurityGroup_normal(t *testing.T) {
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "description", "Used in the terraform acceptance tests"),
|
"aws_security_group.web", "description", "Used in the terraform acceptance tests"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "ingress.332851786.protocol", "tcp"),
|
"aws_security_group.web", "ingress.3629188364.protocol", "tcp"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "ingress.332851786.from_port", "80"),
|
"aws_security_group.web", "ingress.3629188364.from_port", "80"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "ingress.332851786.to_port", "8000"),
|
"aws_security_group.web", "ingress.3629188364.to_port", "8000"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "ingress.332851786.cidr_blocks.#", "1"),
|
"aws_security_group.web", "ingress.3629188364.cidr_blocks.#", "1"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "ingress.332851786.cidr_blocks.0", "10.0.0.0/8"),
|
"aws_security_group.web", "ingress.3629188364.cidr_blocks.0", "10.0.0.0/8"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -116,25 +116,25 @@ func TestAccAWSSecurityGroup_vpc(t *testing.T) {
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "description", "Used in the terraform acceptance tests"),
|
"aws_security_group.web", "description", "Used in the terraform acceptance tests"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "ingress.332851786.protocol", "tcp"),
|
"aws_security_group.web", "ingress.3629188364.protocol", "tcp"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "ingress.332851786.from_port", "80"),
|
"aws_security_group.web", "ingress.3629188364.from_port", "80"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "ingress.332851786.to_port", "8000"),
|
"aws_security_group.web", "ingress.3629188364.to_port", "8000"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "ingress.332851786.cidr_blocks.#", "1"),
|
"aws_security_group.web", "ingress.3629188364.cidr_blocks.#", "1"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "ingress.332851786.cidr_blocks.0", "10.0.0.0/8"),
|
"aws_security_group.web", "ingress.3629188364.cidr_blocks.0", "10.0.0.0/8"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "egress.332851786.protocol", "tcp"),
|
"aws_security_group.web", "egress.3629188364.protocol", "tcp"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "egress.332851786.from_port", "80"),
|
"aws_security_group.web", "egress.3629188364.from_port", "80"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "egress.332851786.to_port", "8000"),
|
"aws_security_group.web", "egress.3629188364.to_port", "8000"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "egress.332851786.cidr_blocks.#", "1"),
|
"aws_security_group.web", "egress.3629188364.cidr_blocks.#", "1"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_security_group.web", "egress.332851786.cidr_blocks.0", "10.0.0.0/8"),
|
"aws_security_group.web", "egress.3629188364.cidr_blocks.0", "10.0.0.0/8"),
|
||||||
testCheck,
|
testCheck,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
@ -159,17 +159,38 @@ func resourceAwsSubnetDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
ec2conn := meta.(*AWSClient).ec2conn
|
ec2conn := meta.(*AWSClient).ec2conn
|
||||||
|
|
||||||
log.Printf("[INFO] Deleting subnet: %s", d.Id())
|
log.Printf("[INFO] Deleting subnet: %s", d.Id())
|
||||||
|
req := &ec2.DeleteSubnetRequest{
|
||||||
err := ec2conn.DeleteSubnet(&ec2.DeleteSubnetRequest{
|
|
||||||
SubnetID: aws.String(d.Id()),
|
SubnetID: aws.String(d.Id()),
|
||||||
})
|
}
|
||||||
|
|
||||||
if err != nil {
|
wait := resource.StateChangeConf{
|
||||||
ec2err, ok := err.(aws.APIError)
|
Pending: []string{"pending"},
|
||||||
if ok && ec2err.Code == "InvalidSubnetID.NotFound" {
|
Target: "destroyed",
|
||||||
return nil
|
Timeout: 5 * time.Minute,
|
||||||
}
|
MinTimeout: 1 * time.Second,
|
||||||
|
Refresh: func() (interface{}, string, error) {
|
||||||
|
err := ec2conn.DeleteSubnet(req)
|
||||||
|
if err != nil {
|
||||||
|
if apiErr, ok := err.(aws.APIError); ok {
|
||||||
|
if apiErr.Code == "DependencyViolation" {
|
||||||
|
// There is some pending operation, so just retry
|
||||||
|
// in a bit.
|
||||||
|
return 42, "pending", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiErr.Code == "InvalidSubnetID.NotFound" {
|
||||||
|
return 42, "destroyed", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 42, "failure", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return 42, "destroyed", nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := wait.WaitForState(); err != nil {
|
||||||
return fmt.Errorf("Error deleting subnet: %s", err)
|
return fmt.Errorf("Error deleting subnet: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -185,29 +185,32 @@ func resourceAwsVpcUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
// Turn on partial mode
|
// Turn on partial mode
|
||||||
d.Partial(true)
|
d.Partial(true)
|
||||||
vpcid := d.Id()
|
vpcid := d.Id()
|
||||||
modifyOpts := &ec2.ModifyVPCAttributeRequest{
|
|
||||||
VPCID: &vpcid,
|
|
||||||
}
|
|
||||||
if d.HasChange("enable_dns_hostnames") {
|
if d.HasChange("enable_dns_hostnames") {
|
||||||
val := d.Get("enable_dns_hostnames").(bool)
|
val := d.Get("enable_dns_hostnames").(bool)
|
||||||
modifyOpts.EnableDNSHostnames = &ec2.AttributeBooleanValue{
|
modifyOpts := &ec2.ModifyVPCAttributeRequest{
|
||||||
Value: &val,
|
VPCID: &vpcid,
|
||||||
|
EnableDNSHostnames: &ec2.AttributeBooleanValue{
|
||||||
|
Value: &val,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"[INFO] Modifying enable_dns_hostnames vpc attribute for %s: %#v",
|
"[INFO] Modifying enable_dns_support vpc attribute for %s: %#v",
|
||||||
d.Id(), modifyOpts)
|
d.Id(), modifyOpts)
|
||||||
if err := ec2conn.ModifyVPCAttribute(modifyOpts); err != nil {
|
if err := ec2conn.ModifyVPCAttribute(modifyOpts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
d.SetPartial("enable_dns_hostnames")
|
d.SetPartial("enable_dns_support")
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.HasChange("enable_dns_support") {
|
if d.HasChange("enable_dns_support") {
|
||||||
val := d.Get("enable_dns_hostnames").(bool)
|
val := d.Get("enable_dns_support").(bool)
|
||||||
modifyOpts.EnableDNSSupport = &ec2.AttributeBooleanValue{
|
modifyOpts := &ec2.ModifyVPCAttributeRequest{
|
||||||
Value: &val,
|
VPCID: &vpcid,
|
||||||
|
EnableDNSSupport: &ec2.AttributeBooleanValue{
|
||||||
|
Value: &val,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf(
|
log.Printf(
|
||||||
|
@ -238,7 +241,7 @@ func resourceAwsVpcDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
}
|
}
|
||||||
log.Printf("[INFO] Deleting VPC: %s", d.Id())
|
log.Printf("[INFO] Deleting VPC: %s", d.Id())
|
||||||
if err := ec2conn.DeleteVPC(DeleteVpcOpts); err != nil {
|
if err := ec2conn.DeleteVPC(DeleteVpcOpts); err != nil {
|
||||||
ec2err, ok := err.(*aws.APIError)
|
ec2err, ok := err.(aws.APIError)
|
||||||
if ok && ec2err.Code == "InvalidVpcID.NotFound" {
|
if ok && ec2err.Code == "InvalidVpcID.NotFound" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -258,7 +261,7 @@ func VPCStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc {
|
||||||
}
|
}
|
||||||
resp, err := conn.DescribeVPCs(DescribeVpcOpts)
|
resp, err := conn.DescribeVPCs(DescribeVpcOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ec2err, ok := err.(*aws.APIError); ok && ec2err.Code == "InvalidVpcID.NotFound" {
|
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidVpcID.NotFound" {
|
||||||
resp = nil
|
resp = nil
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Error on VPCStateRefresh: %s", err)
|
log.Printf("Error on VPCStateRefresh: %s", err)
|
||||||
|
|
|
@ -2,11 +2,12 @@ package aws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/aws-sdk-go/aws"
|
"github.com/hashicorp/aws-sdk-go/aws"
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/ec2"
|
"github.com/hashicorp/aws-sdk-go/gen/ec2"
|
||||||
"github.com/hashicorp/terraform/helper/resource"
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAccVpc_basic(t *testing.T) {
|
func TestAccVpc_basic(t *testing.T) {
|
||||||
|
@ -132,7 +133,7 @@ func testAccCheckVpcDestroy(s *terraform.State) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the error is what we want
|
// Verify the error is what we want
|
||||||
ec2err, ok := err.(*aws.APIError)
|
ec2err, ok := err.(aws.APIError)
|
||||||
if !ok {
|
if !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -184,6 +185,26 @@ func testAccCheckVpcExists(n string, vpc *ec2.VPC) resource.TestCheckFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/hashicorp/terraform/issues/1301
|
||||||
|
func TestAccVpc_bothDnsOptionsSet(t *testing.T) {
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckVpcDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccVpcConfig_BothDnsOptions,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"aws_vpc.bar", "enable_dns_hostnames", "true"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"aws_vpc.bar", "enable_dns_support", "true"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const testAccVpcConfig = `
|
const testAccVpcConfig = `
|
||||||
resource "aws_vpc" "foo" {
|
resource "aws_vpc" "foo" {
|
||||||
cidr_block = "10.1.0.0/16"
|
cidr_block = "10.1.0.0/16"
|
||||||
|
@ -223,3 +244,12 @@ resource "aws_vpc" "bar" {
|
||||||
cidr_block = "10.2.0.0/16"
|
cidr_block = "10.2.0.0/16"
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const testAccVpcConfig_BothDnsOptions = `
|
||||||
|
resource "aws_vpc" "bar" {
|
||||||
|
cidr_block = "10.2.0.0/16"
|
||||||
|
|
||||||
|
enable_dns_hostnames = true
|
||||||
|
enable_dns_support = true
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
|
@ -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
|
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)
|
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 {
|
if d.Get("size").(int) != 0 {
|
||||||
// Set the volume size
|
// Set the volume size
|
||||||
p.SetSize(d.Get("size").(int))
|
p.SetSize(int64(d.Get("size").(int)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the zone UUID
|
// Retrieve the zone UUID
|
||||||
|
@ -141,7 +141,7 @@ func resourceCloudStackDiskRead(d *schema.ResourceData, meta interface{}) error
|
||||||
d.Set("name", v.Name)
|
d.Set("name", v.Name)
|
||||||
d.Set("attach", v.Attached != "") // If attached this will contain a timestamp when attached
|
d.Set("attach", v.Attached != "") // If attached this will contain a timestamp when attached
|
||||||
d.Set("disk_offering", v.Diskofferingname)
|
d.Set("disk_offering", v.Diskofferingname)
|
||||||
d.Set("size", v.Size/(1024*1024*1024)) // Needed to get GB's again
|
d.Set("size", int(v.Size/(1024*1024*1024))) // Needed to get GB's again
|
||||||
d.Set("zone", v.Zonename)
|
d.Set("zone", v.Zonename)
|
||||||
|
|
||||||
if v.Attached != "" {
|
if v.Attached != "" {
|
||||||
|
@ -196,7 +196,7 @@ func resourceCloudStackDiskUpdate(d *schema.ResourceData, meta interface{}) erro
|
||||||
|
|
||||||
if d.Get("size").(int) != 0 {
|
if d.Get("size").(int) != 0 {
|
||||||
// Set the size
|
// Set the size
|
||||||
p.SetSize(d.Get("size").(int))
|
p.SetSize(int64(d.Get("size").(int)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the shrink bit
|
// Set the shrink bit
|
||||||
|
@ -367,7 +367,7 @@ func isAttached(cs *cloudstack.CloudStackClient, id string) (bool, error) {
|
||||||
return v.Attached != "", nil
|
return v.Attached != "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveDeviceID(device string) int {
|
func retrieveDeviceID(device string) int64 {
|
||||||
switch device {
|
switch device {
|
||||||
case "/dev/xvdb", "D:":
|
case "/dev/xvdb", "D:":
|
||||||
return 1
|
return 1
|
||||||
|
@ -402,7 +402,7 @@ func retrieveDeviceID(device string) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveDeviceName(device int, os string) string {
|
func retrieveDeviceName(device int64, os string) string {
|
||||||
switch device {
|
switch device {
|
||||||
case 1:
|
case 1:
|
||||||
if os == "Windows" {
|
if os == "Windows" {
|
||||||
|
|
|
@ -87,11 +87,11 @@ func resourceCloudStackVPNCustomerGatewayCreate(d *schema.ResourceData, meta int
|
||||||
}
|
}
|
||||||
|
|
||||||
if esplifetime, ok := d.GetOk("esp_lifetime"); ok {
|
if esplifetime, ok := d.GetOk("esp_lifetime"); ok {
|
||||||
p.SetEsplifetime(esplifetime.(int))
|
p.SetEsplifetime(int64(esplifetime.(int)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if ikelifetime, ok := d.GetOk("ike_lifetime"); ok {
|
if ikelifetime, ok := d.GetOk("ike_lifetime"); ok {
|
||||||
p.SetIkelifetime(ikelifetime.(int))
|
p.SetIkelifetime(int64(ikelifetime.(int)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the new VPN Customer Gateway
|
// Create the new VPN Customer Gateway
|
||||||
|
@ -128,8 +128,8 @@ func resourceCloudStackVPNCustomerGatewayRead(d *schema.ResourceData, meta inter
|
||||||
d.Set("ike_policy", v.Ikepolicy)
|
d.Set("ike_policy", v.Ikepolicy)
|
||||||
d.Set("ipsec_psk", v.Ipsecpsk)
|
d.Set("ipsec_psk", v.Ipsecpsk)
|
||||||
d.Set("dpd", v.Dpd)
|
d.Set("dpd", v.Dpd)
|
||||||
d.Set("esp_lifetime", v.Esplifetime)
|
d.Set("esp_lifetime", int(v.Esplifetime))
|
||||||
d.Set("ike_lifetime", v.Ikelifetime)
|
d.Set("ike_lifetime", int(v.Ikelifetime))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -154,11 +154,11 @@ func resourceCloudStackVPNCustomerGatewayUpdate(d *schema.ResourceData, meta int
|
||||||
}
|
}
|
||||||
|
|
||||||
if esplifetime, ok := d.GetOk("esp_lifetime"); ok {
|
if esplifetime, ok := d.GetOk("esp_lifetime"); ok {
|
||||||
p.SetEsplifetime(esplifetime.(int))
|
p.SetEsplifetime(int64(esplifetime.(int)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if ikelifetime, ok := d.GetOk("ike_lifetime"); ok {
|
if ikelifetime, ok := d.GetOk("ike_lifetime"); ok {
|
||||||
p.SetIkelifetime(ikelifetime.(int))
|
p.SetIkelifetime(int64(ikelifetime.(int)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the VPN Customer Gateway
|
// Update the VPN Customer Gateway
|
||||||
|
|
|
@ -91,8 +91,9 @@ func resourceDigitalOceanRecordCreate(d *schema.ResourceData, meta interface{})
|
||||||
|
|
||||||
func resourceDigitalOceanRecordRead(d *schema.ResourceData, meta interface{}) error {
|
func resourceDigitalOceanRecordRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
client := meta.(*digitalocean.Client)
|
client := meta.(*digitalocean.Client)
|
||||||
|
domain := d.Get("domain").(string)
|
||||||
|
|
||||||
rec, err := client.RetrieveRecord(d.Get("domain").(string), d.Id())
|
rec, err := client.RetrieveRecord(domain, d.Id())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If the record is somehow already destroyed, mark as
|
// If the record is somehow already destroyed, mark as
|
||||||
// succesfully gone
|
// succesfully gone
|
||||||
|
@ -104,6 +105,18 @@ func resourceDigitalOceanRecordRead(d *schema.ResourceData, meta interface{}) er
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update response data for records with domain value
|
||||||
|
if t := rec.Type; t == "CNAME" || t == "MX" || t == "NS" || t == "SRV" {
|
||||||
|
// Append dot to response if resource value is absolute
|
||||||
|
if value := d.Get("value").(string); strings.HasSuffix(value, ".") {
|
||||||
|
rec.Data += "."
|
||||||
|
// If resource value ends with current domain, make response data absolute
|
||||||
|
if strings.HasSuffix(value, domain+".") {
|
||||||
|
rec.Data += domain + "."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
d.Set("name", rec.Name)
|
d.Set("name", rec.Name)
|
||||||
d.Set("type", rec.Type)
|
d.Set("type", rec.Type)
|
||||||
d.Set("value", rec.Data)
|
d.Set("value", rec.Data)
|
||||||
|
|
|
@ -76,6 +76,87 @@ func TestAccDigitalOceanRecord_Updated(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccDigitalOceanRecord_HostnameValue(t *testing.T) {
|
||||||
|
var record digitalocean.Record
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckDigitalOceanRecordDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckDigitalOceanRecordConfig_cname,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckDigitalOceanRecordExists("digitalocean_record.foobar", &record),
|
||||||
|
testAccCheckDigitalOceanRecordAttributesHostname("a", &record),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"digitalocean_record.foobar", "name", "terraform"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"digitalocean_record.foobar", "domain", "foobar-test-terraform.com"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"digitalocean_record.foobar", "value", "a.foobar-test-terraform.com."),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"digitalocean_record.foobar", "type", "CNAME"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccDigitalOceanRecord_RelativeHostnameValue(t *testing.T) {
|
||||||
|
var record digitalocean.Record
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckDigitalOceanRecordDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckDigitalOceanRecordConfig_relative_cname,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckDigitalOceanRecordExists("digitalocean_record.foobar", &record),
|
||||||
|
testAccCheckDigitalOceanRecordAttributesHostname("a.b", &record),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"digitalocean_record.foobar", "name", "terraform"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"digitalocean_record.foobar", "domain", "foobar-test-terraform.com"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"digitalocean_record.foobar", "value", "a.b"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"digitalocean_record.foobar", "type", "CNAME"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccDigitalOceanRecord_ExternalHostnameValue(t *testing.T) {
|
||||||
|
var record digitalocean.Record
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckDigitalOceanRecordDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckDigitalOceanRecordConfig_external_cname,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckDigitalOceanRecordExists("digitalocean_record.foobar", &record),
|
||||||
|
testAccCheckDigitalOceanRecordAttributesHostname("a.foobar-test-terraform.net", &record),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"digitalocean_record.foobar", "name", "terraform"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"digitalocean_record.foobar", "domain", "foobar-test-terraform.com"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"digitalocean_record.foobar", "value", "a.foobar-test-terraform.net."),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"digitalocean_record.foobar", "type", "CNAME"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func testAccCheckDigitalOceanRecordDestroy(s *terraform.State) error {
|
func testAccCheckDigitalOceanRecordDestroy(s *terraform.State) error {
|
||||||
client := testAccProvider.Meta().(*digitalocean.Client)
|
client := testAccProvider.Meta().(*digitalocean.Client)
|
||||||
|
|
||||||
|
@ -146,6 +227,17 @@ func testAccCheckDigitalOceanRecordExists(n string, record *digitalocean.Record)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAccCheckDigitalOceanRecordAttributesHostname(data string, record *digitalocean.Record) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
|
||||||
|
if record.Data != data {
|
||||||
|
return fmt.Errorf("Bad value: expected %s, got %s", data, record.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const testAccCheckDigitalOceanRecordConfig_basic = `
|
const testAccCheckDigitalOceanRecordConfig_basic = `
|
||||||
resource "digitalocean_domain" "foobar" {
|
resource "digitalocean_domain" "foobar" {
|
||||||
name = "foobar-test-terraform.com"
|
name = "foobar-test-terraform.com"
|
||||||
|
@ -173,3 +265,45 @@ resource "digitalocean_record" "foobar" {
|
||||||
value = "192.168.0.11"
|
value = "192.168.0.11"
|
||||||
type = "A"
|
type = "A"
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
const testAccCheckDigitalOceanRecordConfig_cname = `
|
||||||
|
resource "digitalocean_domain" "foobar" {
|
||||||
|
name = "foobar-test-terraform.com"
|
||||||
|
ip_address = "192.168.0.10"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "digitalocean_record" "foobar" {
|
||||||
|
domain = "${digitalocean_domain.foobar.name}"
|
||||||
|
|
||||||
|
name = "terraform"
|
||||||
|
value = "a.foobar-test-terraform.com."
|
||||||
|
type = "CNAME"
|
||||||
|
}`
|
||||||
|
|
||||||
|
const testAccCheckDigitalOceanRecordConfig_relative_cname = `
|
||||||
|
resource "digitalocean_domain" "foobar" {
|
||||||
|
name = "foobar-test-terraform.com"
|
||||||
|
ip_address = "192.168.0.10"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "digitalocean_record" "foobar" {
|
||||||
|
domain = "${digitalocean_domain.foobar.name}"
|
||||||
|
|
||||||
|
name = "terraform"
|
||||||
|
value = "a.b"
|
||||||
|
type = "CNAME"
|
||||||
|
}`
|
||||||
|
|
||||||
|
const testAccCheckDigitalOceanRecordConfig_external_cname = `
|
||||||
|
resource "digitalocean_domain" "foobar" {
|
||||||
|
name = "foobar-test-terraform.com"
|
||||||
|
ip_address = "192.168.0.10"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "digitalocean_record" "foobar" {
|
||||||
|
domain = "${digitalocean_domain.foobar.name}"
|
||||||
|
|
||||||
|
name = "terraform"
|
||||||
|
value = "a.foobar-test-terraform.net."
|
||||||
|
type = "CNAME"
|
||||||
|
}`
|
||||||
|
|
|
@ -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"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/google"
|
"golang.org/x/oauth2/google"
|
||||||
"golang.org/x/oauth2/jwt"
|
"golang.org/x/oauth2/jwt"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the configuration structure used to instantiate the Google
|
// Config is the configuration structure used to instantiate the Google
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package google
|
package google
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
"google.golang.org/api/compute/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// readDiskType finds the disk type with the given name.
|
// readDiskType finds the disk type with the given name.
|
||||||
|
|
|
@ -4,7 +4,8 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
"google.golang.org/api/compute/v1"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/helper/resource"
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"code.google.com/p/google-api-go-client/googleapi"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
|
"google.golang.org/api/googleapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resourceComputeAddress() *schema.Resource {
|
func resourceComputeAddress() *schema.Resource {
|
||||||
|
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"github.com/hashicorp/terraform/helper/resource"
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAccComputeAddress_basic(t *testing.T) {
|
func TestAccComputeAddress_basic(t *testing.T) {
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"code.google.com/p/google-api-go-client/googleapi"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
|
"google.golang.org/api/googleapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resourceComputeDisk() *schema.Resource {
|
func resourceComputeDisk() *schema.Resource {
|
||||||
|
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"github.com/hashicorp/terraform/helper/resource"
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAccComputeDisk_basic(t *testing.T) {
|
func TestAccComputeDisk_basic(t *testing.T) {
|
||||||
|
|
|
@ -6,10 +6,10 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"code.google.com/p/google-api-go-client/googleapi"
|
|
||||||
"github.com/hashicorp/terraform/helper/hashcode"
|
"github.com/hashicorp/terraform/helper/hashcode"
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
|
"google.golang.org/api/googleapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resourceComputeFirewall() *schema.Resource {
|
func resourceComputeFirewall() *schema.Resource {
|
||||||
|
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"github.com/hashicorp/terraform/helper/resource"
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAccComputeFirewall_basic(t *testing.T) {
|
func TestAccComputeFirewall_basic(t *testing.T) {
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"code.google.com/p/google-api-go-client/googleapi"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
|
"google.golang.org/api/googleapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resourceComputeForwardingRule() *schema.Resource {
|
func resourceComputeForwardingRule() *schema.Resource {
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"code.google.com/p/google-api-go-client/googleapi"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
|
"google.golang.org/api/googleapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resourceComputeHttpHealthCheck() *schema.Resource {
|
func resourceComputeHttpHealthCheck() *schema.Resource {
|
||||||
|
|
|
@ -5,10 +5,10 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"code.google.com/p/google-api-go-client/googleapi"
|
|
||||||
"github.com/hashicorp/terraform/helper/hashcode"
|
"github.com/hashicorp/terraform/helper/hashcode"
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
|
"google.golang.org/api/googleapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resourceComputeInstance() *schema.Resource {
|
func resourceComputeInstance() *schema.Resource {
|
||||||
|
@ -72,6 +72,13 @@ func resourceComputeInstance() *schema.Resource {
|
||||||
"auto_delete": &schema.Schema{
|
"auto_delete": &schema.Schema{
|
||||||
Type: schema.TypeBool,
|
Type: schema.TypeBool,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
|
Default: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"size": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Optional: true,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -283,11 +290,7 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err
|
||||||
disk.Type = "PERSISTENT"
|
disk.Type = "PERSISTENT"
|
||||||
disk.Mode = "READ_WRITE"
|
disk.Mode = "READ_WRITE"
|
||||||
disk.Boot = i == 0
|
disk.Boot = i == 0
|
||||||
disk.AutoDelete = true
|
disk.AutoDelete = d.Get(prefix + ".auto_delete").(bool)
|
||||||
|
|
||||||
if v, ok := d.GetOk(prefix + ".auto_delete"); ok {
|
|
||||||
disk.AutoDelete = v.(bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load up the disk for this disk if specified
|
// Load up the disk for this disk if specified
|
||||||
if v, ok := d.GetOk(prefix + ".disk"); ok {
|
if v, ok := d.GetOk(prefix + ".disk"); ok {
|
||||||
|
@ -331,6 +334,11 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err
|
||||||
disk.InitializeParams.DiskType = diskType.SelfLink
|
disk.InitializeParams.DiskType = diskType.SelfLink
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v, ok := d.GetOk(prefix + ".size"); ok {
|
||||||
|
diskSizeGb := v.(int)
|
||||||
|
disk.InitializeParams.DiskSizeGb = int64(diskSizeGb)
|
||||||
|
}
|
||||||
|
|
||||||
disks = append(disks, &disk)
|
disks = append(disks, &disk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -564,6 +572,7 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error
|
||||||
networkInterfaces = append(networkInterfaces, map[string]interface{}{
|
networkInterfaces = append(networkInterfaces, map[string]interface{}{
|
||||||
"name": iface.Name,
|
"name": iface.Name,
|
||||||
"address": iface.NetworkIP,
|
"address": iface.NetworkIP,
|
||||||
|
"network": iface.Network,
|
||||||
"access_config": accessConfigs,
|
"access_config": accessConfigs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"code.google.com/p/google-api-go-client/googleapi"
|
|
||||||
"github.com/hashicorp/terraform/helper/hashcode"
|
"github.com/hashicorp/terraform/helper/hashcode"
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
|
"google.golang.org/api/googleapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resourceComputeInstanceTemplate() *schema.Resource {
|
func resourceComputeInstanceTemplate() *schema.Resource {
|
||||||
|
@ -58,6 +58,7 @@ func resourceComputeInstanceTemplate() *schema.Resource {
|
||||||
"auto_delete": &schema.Schema{
|
"auto_delete": &schema.Schema{
|
||||||
Type: schema.TypeBool,
|
Type: schema.TypeBool,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
|
Default: true,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -235,11 +236,7 @@ func buildDisks(d *schema.ResourceData, meta interface{}) []*compute.AttachedDis
|
||||||
disk.Mode = "READ_WRITE"
|
disk.Mode = "READ_WRITE"
|
||||||
disk.Interface = "SCSI"
|
disk.Interface = "SCSI"
|
||||||
disk.Boot = i == 0
|
disk.Boot = i == 0
|
||||||
disk.AutoDelete = true
|
disk.AutoDelete = d.Get(prefix + ".auto_delete").(bool)
|
||||||
|
|
||||||
if v, ok := d.GetOk(prefix + ".auto_delete"); ok {
|
|
||||||
disk.AutoDelete = v.(bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := d.GetOk(prefix + ".boot"); ok {
|
if v, ok := d.GetOk(prefix + ".boot"); ok {
|
||||||
disk.Boot = v.(bool)
|
disk.Boot = v.(bool)
|
||||||
|
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"github.com/hashicorp/terraform/helper/resource"
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAccComputeInstanceTemplate_basic(t *testing.T) {
|
func TestAccComputeInstanceTemplate_basic(t *testing.T) {
|
||||||
|
@ -65,7 +65,7 @@ func TestAccComputeInstanceTemplate_disks(t *testing.T) {
|
||||||
testAccCheckComputeInstanceTemplateExists(
|
testAccCheckComputeInstanceTemplateExists(
|
||||||
"google_compute_instance_template.foobar", &instanceTemplate),
|
"google_compute_instance_template.foobar", &instanceTemplate),
|
||||||
testAccCheckComputeInstanceTemplateDisk(&instanceTemplate, "debian-7-wheezy-v20140814", true, true),
|
testAccCheckComputeInstanceTemplateDisk(&instanceTemplate, "debian-7-wheezy-v20140814", true, true),
|
||||||
testAccCheckComputeInstanceTemplateDisk(&instanceTemplate, "foo_existing_disk", false, false),
|
testAccCheckComputeInstanceTemplateDisk(&instanceTemplate, "terraform-test-foobar", false, false),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -252,6 +252,14 @@ resource "google_compute_instance_template" "foobar" {
|
||||||
}`
|
}`
|
||||||
|
|
||||||
const testAccComputeInstanceTemplate_disks = `
|
const testAccComputeInstanceTemplate_disks = `
|
||||||
|
resource "google_compute_disk" "foobar" {
|
||||||
|
name = "terraform-test-foobar"
|
||||||
|
image = "debian-7-wheezy-v20140814"
|
||||||
|
size = 10
|
||||||
|
type = "pd-ssd"
|
||||||
|
zone = "us-central1-a"
|
||||||
|
}
|
||||||
|
|
||||||
resource "google_compute_instance_template" "foobar" {
|
resource "google_compute_instance_template" "foobar" {
|
||||||
name = "terraform-test"
|
name = "terraform-test"
|
||||||
machine_type = "n1-standard-1"
|
machine_type = "n1-standard-1"
|
||||||
|
@ -263,7 +271,7 @@ resource "google_compute_instance_template" "foobar" {
|
||||||
}
|
}
|
||||||
|
|
||||||
disk {
|
disk {
|
||||||
source = "foo_existing_disk"
|
source = "terraform-test-foobar"
|
||||||
auto_delete = false
|
auto_delete = false
|
||||||
boot = false
|
boot = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"github.com/hashicorp/terraform/helper/resource"
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAccComputeInstance_basic_deprecated_network(t *testing.T) {
|
func TestAccComputeInstance_basic_deprecated_network(t *testing.T) {
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"code.google.com/p/google-api-go-client/googleapi"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
|
"google.golang.org/api/googleapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resourceComputeNetwork() *schema.Resource {
|
func resourceComputeNetwork() *schema.Resource {
|
||||||
|
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"github.com/hashicorp/terraform/helper/resource"
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAccComputeNetwork_basic(t *testing.T) {
|
func TestAccComputeNetwork_basic(t *testing.T) {
|
||||||
|
|
|
@ -5,10 +5,10 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"code.google.com/p/google-api-go-client/googleapi"
|
|
||||||
"github.com/hashicorp/terraform/helper/hashcode"
|
"github.com/hashicorp/terraform/helper/hashcode"
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
|
"google.golang.org/api/googleapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resourceComputeRoute() *schema.Resource {
|
func resourceComputeRoute() *schema.Resource {
|
||||||
|
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"github.com/hashicorp/terraform/helper/resource"
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAccComputeRoute_basic(t *testing.T) {
|
func TestAccComputeRoute_basic(t *testing.T) {
|
||||||
|
|
|
@ -6,9 +6,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.google.com/p/google-api-go-client/compute/v1"
|
|
||||||
"code.google.com/p/google-api-go-client/googleapi"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"google.golang.org/api/compute/v1"
|
||||||
|
"google.golang.org/api/googleapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resourceComputeTargetPool() *schema.Resource {
|
func resourceComputeTargetPool() *schema.Resource {
|
||||||
|
|
|
@ -358,14 +358,18 @@ func updateConfigVars(
|
||||||
vars := make(map[string]*string)
|
vars := make(map[string]*string)
|
||||||
|
|
||||||
for _, v := range o {
|
for _, v := range o {
|
||||||
for k, _ := range v.(map[string]interface{}) {
|
if v != nil {
|
||||||
vars[k] = nil
|
for k, _ := range v.(map[string]interface{}) {
|
||||||
|
vars[k] = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, v := range n {
|
for _, v := range n {
|
||||||
for k, v := range v.(map[string]interface{}) {
|
if v != nil {
|
||||||
val := v.(string)
|
for k, v := range v.(map[string]interface{}) {
|
||||||
vars[k] = &val
|
val := v.(string)
|
||||||
|
vars[k] = &val
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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