Merge branch 'master' into f-aws-rds-tags
* master: (172 commits) 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 core: docs for targeted operations core: targeted operations user_data support ...
This commit is contained in:
commit
d16492a962
41
CHANGELOG.md
41
CHANGELOG.md
|
@ -12,17 +12,26 @@ BACKWARDS INCOMPATIBILITIES:
|
||||||
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_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:
|
||||||
|
|
||||||
|
@ -34,7 +43,7 @@ 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]
|
||||||
* **New resource: `aws_elastic_network_interfaces`** [GH-1149]
|
* 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.
|
||||||
|
@ -43,6 +52,11 @@ IMPROVEMENTS:
|
||||||
* command/remote-config: After enabling remote state, a `pull` is
|
* command/remote-config: After enabling remote state, a `pull` is
|
||||||
automatically done initially.
|
automatically done initially.
|
||||||
* providers/google: Add `size` option to disk blocks for instances. [GH-1284]
|
* 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].
|
||||||
|
|
||||||
BUG FIXES:
|
BUG FIXES:
|
||||||
|
|
||||||
|
@ -55,15 +69,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.
|
* providers/digitalocean: FQDN for domain records in CNAME, MX, NS, etc.
|
||||||
Also fixes invalid updates in plans. [GH-863]
|
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:
|
||||||
|
|
||||||
|
@ -90,7 +120,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]
|
||||||
|
@ -123,7 +153,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]
|
||||||
|
@ -529,3 +559,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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -287,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(
|
||||||
|
|
|
@ -26,7 +26,7 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) {
|
||||||
testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group),
|
testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group),
|
||||||
testAccCheckAWSAutoScalingGroupAttributes(&group),
|
testAccCheckAWSAutoScalingGroupAttributes(&group),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_autoscaling_group.bar", "availability_zones.1807834199", "us-west-2a"),
|
"aws_autoscaling_group.bar", "availability_zones.2487133097", "us-west-2a"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_autoscaling_group.bar", "name", "foobar3-terraform-test"),
|
"aws_autoscaling_group.bar", "name", "foobar3-terraform-test"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
|
|
|
@ -311,7 +311,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)
|
||||||
|
|
|
@ -24,7 +24,13 @@ func resourceAwsInstanceMigrateState(
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) {
|
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)
|
log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes)
|
||||||
|
|
||||||
// Delete old count
|
// Delete old count
|
||||||
delete(is.Attributes, "block_device.#")
|
delete(is.Attributes, "block_device.#")
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,7 @@ func TestAWSInstanceMigrateState(t *testing.T) {
|
||||||
|
|
||||||
for tn, tc := range cases {
|
for tn, tc := range cases {
|
||||||
is := &terraform.InstanceState{
|
is := &terraform.InstanceState{
|
||||||
|
ID: "i-abc123",
|
||||||
Attributes: tc.Attributes,
|
Attributes: tc.Attributes,
|
||||||
}
|
}
|
||||||
is, err := resourceAwsInstanceMigrateState(
|
is, err := resourceAwsInstanceMigrateState(
|
||||||
|
@ -133,3 +134,26 @@ func TestAWSInstanceMigrateState(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -88,14 +88,12 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := s3conn.GetBucketTagging(&s3.GetBucketTaggingRequest{
|
tagSet, err := getTagSetS3(s3conn, d.Id())
|
||||||
Bucket: aws.String(d.Id()),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.Set("tags", tagsToMapS3(resp.TagSet)); err != nil {
|
if err := d.Set("tags", tagsToMapS3(tagSet)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
@ -110,3 +110,22 @@ func tagsToMapS3(ts []s3.Tag) map[string]string {
|
||||||
|
|
||||||
return result
|
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,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
|
||||||
|
}
|
||||||
|
`
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccLBV1Monitor_basic(t *testing.T) {
|
||||||
|
var monitor monitors.Monitor
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckLBV1MonitorDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccLBV1Monitor_basic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckLBV1MonitorExists(t, "openstack_lb_monitor_v1.monitor_1", &monitor),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccLBV1Monitor_update,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("openstack_lb_monitor_v1.monitor_1", "delay", "20"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckLBV1MonitorDestroy(s *terraform.State) error {
|
||||||
|
config := testAccProvider.Meta().(*Config)
|
||||||
|
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(testAccCheckLBV1MonitorDestroy) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "openstack_lb_monitor_v1" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := monitors.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("LB monitor still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckLBV1MonitorExists(t *testing.T, n string, monitor *monitors.Monitor) 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("(testAccCheckLBV1MonitorExists) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
found, err := monitors.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if found.ID != rs.Primary.ID {
|
||||||
|
return fmt.Errorf("Monitor not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
*monitor = *found
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testAccLBV1Monitor_basic = fmt.Sprintf(`
|
||||||
|
resource "openstack_lb_monitor_v1" "monitor_1" {
|
||||||
|
region = "%s"
|
||||||
|
type = "PING"
|
||||||
|
delay = 30
|
||||||
|
timeout = 5
|
||||||
|
max_retries = 3
|
||||||
|
admin_state_up = "true"
|
||||||
|
}`,
|
||||||
|
OS_REGION_NAME)
|
||||||
|
|
||||||
|
var testAccLBV1Monitor_update = fmt.Sprintf(`
|
||||||
|
resource "openstack_lb_monitor_v1" "monitor_1" {
|
||||||
|
region = "%s"
|
||||||
|
type = "PING"
|
||||||
|
delay = 20
|
||||||
|
timeout = 5
|
||||||
|
max_retries = 3
|
||||||
|
admin_state_up = "true"
|
||||||
|
}`,
|
||||||
|
OS_REGION_NAME)
|
|
@ -0,0 +1,327 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/hashcode"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
|
||||||
|
"github.com/rackspace/gophercloud/pagination"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceLBPoolV1() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceLBPoolV1Create,
|
||||||
|
Read: resourceLBPoolV1Read,
|
||||||
|
Update: resourceLBPoolV1Update,
|
||||||
|
Delete: resourceLBPoolV1Delete,
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
"protocol": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"subnet_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"lb_method": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"tenant_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"member": &schema.Schema{
|
||||||
|
Type: schema.TypeSet,
|
||||||
|
Optional: true,
|
||||||
|
Elem: &schema.Resource{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
"address": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"port": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"admin_state_up": &schema.Schema{
|
||||||
|
Type: schema.TypeBool,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Set: resourceLBMemberV1Hash,
|
||||||
|
},
|
||||||
|
"monitor_ids": &schema.Schema{
|
||||||
|
Type: schema.TypeSet,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeString},
|
||||||
|
Set: func(v interface{}) int {
|
||||||
|
return hashcode.String(v.(string))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceLBPoolV1Create(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 := pools.CreateOpts{
|
||||||
|
Name: d.Get("name").(string),
|
||||||
|
Protocol: d.Get("protocol").(string),
|
||||||
|
SubnetID: d.Get("subnet_id").(string),
|
||||||
|
LBMethod: d.Get("lb_method").(string),
|
||||||
|
TenantID: d.Get("tenant_id").(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Create Options: %#v", createOpts)
|
||||||
|
p, err := pools.Create(networkingClient, createOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack LB pool: %s", err)
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] LB Pool ID: %s", p.ID)
|
||||||
|
|
||||||
|
d.SetId(p.ID)
|
||||||
|
|
||||||
|
if mIDs := resourcePoolMonitorIDsV1(d); mIDs != nil {
|
||||||
|
for _, mID := range mIDs {
|
||||||
|
_, err := pools.AssociateMonitor(networkingClient, p.ID, mID).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error associating monitor (%s) with OpenStack LB pool (%s): %s", mID, p.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if memberOpts := resourcePoolMembersV1(d); memberOpts != nil {
|
||||||
|
for _, memberOpt := range memberOpts {
|
||||||
|
_, err := members.Create(networkingClient, memberOpt).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack LB member: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceLBPoolV1Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceLBPoolV1Read(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := pools.Get(networkingClient, d.Id()).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return CheckDeleted(d, err, "LB pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Retreived OpenStack LB Pool %s: %+v", d.Id(), p)
|
||||||
|
|
||||||
|
d.Set("name", p.Name)
|
||||||
|
d.Set("protocol", p.Protocol)
|
||||||
|
d.Set("subnet_id", p.SubnetID)
|
||||||
|
d.Set("lb_method", p.LBMethod)
|
||||||
|
d.Set("tenant_id", p.TenantID)
|
||||||
|
d.Set("monitor_ids", p.MonitorIDs)
|
||||||
|
d.Set("member_ids", p.MemberIDs)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceLBPoolV1Update(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateOpts pools.UpdateOpts
|
||||||
|
if d.HasChange("name") {
|
||||||
|
updateOpts.Name = d.Get("name").(string)
|
||||||
|
}
|
||||||
|
if d.HasChange("lb_method") {
|
||||||
|
updateOpts.LBMethod = d.Get("lb_method").(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Updating OpenStack LB Pool %s with options: %+v", d.Id(), updateOpts)
|
||||||
|
|
||||||
|
_, err = pools.Update(networkingClient, d.Id(), updateOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error updating OpenStack LB Pool: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("monitor_ids") {
|
||||||
|
oldMIDsRaw, newMIDsRaw := d.GetChange("security_groups")
|
||||||
|
oldMIDsSet, newMIDsSet := oldMIDsRaw.(*schema.Set), newMIDsRaw.(*schema.Set)
|
||||||
|
monitorsToAdd := newMIDsSet.Difference(oldMIDsSet)
|
||||||
|
monitorsToRemove := oldMIDsSet.Difference(newMIDsSet)
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Monitors to add: %v", monitorsToAdd)
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Monitors to remove: %v", monitorsToRemove)
|
||||||
|
|
||||||
|
for _, m := range monitorsToAdd.List() {
|
||||||
|
_, err := pools.AssociateMonitor(networkingClient, d.Id(), m.(string)).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error associating monitor (%s) with OpenStack server (%s): %s", m.(string), d.Id(), err)
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] Associated monitor (%s) with pool (%s)", m.(string), d.Id())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range monitorsToRemove.List() {
|
||||||
|
_, err := pools.DisassociateMonitor(networkingClient, d.Id(), m.(string)).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error disassociating monitor (%s) from OpenStack server (%s): %s", m.(string), d.Id(), err)
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] Disassociated monitor (%s) from pool (%s)", m.(string), d.Id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("member") {
|
||||||
|
oldMembersRaw, newMembersRaw := d.GetChange("member")
|
||||||
|
oldMembersSet, newMembersSet := oldMembersRaw.(*schema.Set), newMembersRaw.(*schema.Set)
|
||||||
|
membersToAdd := newMembersSet.Difference(oldMembersSet)
|
||||||
|
membersToRemove := oldMembersSet.Difference(newMembersSet)
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Members to add: %v", membersToAdd)
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Members to remove: %v", membersToRemove)
|
||||||
|
|
||||||
|
for _, m := range membersToRemove.List() {
|
||||||
|
oldMember := resourcePoolMemberV1(d, m)
|
||||||
|
listOpts := members.ListOpts{
|
||||||
|
PoolID: d.Id(),
|
||||||
|
Address: oldMember.Address,
|
||||||
|
ProtocolPort: oldMember.ProtocolPort,
|
||||||
|
}
|
||||||
|
err = members.List(networkingClient, listOpts).EachPage(func(page pagination.Page) (bool, error) {
|
||||||
|
extractedMembers, err := members.ExtractMembers(page)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, member := range extractedMembers {
|
||||||
|
err := members.Delete(networkingClient, member.ID).ExtractErr()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("Error deleting member (%s) from OpenStack LB pool (%s): %s", member.ID, d.Id(), err)
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] Deleted member (%s) from pool (%s)", member.ID, d.Id())
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range membersToAdd.List() {
|
||||||
|
createOpts := resourcePoolMemberV1(d, m)
|
||||||
|
newMember, err := members.Create(networkingClient, createOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating LB member: %s", err)
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] Created member (%s) in OpenStack LB pool (%s)", newMember.ID, d.Id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceLBPoolV1Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceLBPoolV1Delete(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 = pools.Delete(networkingClient, d.Id()).ExtractErr()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error deleting OpenStack LB Pool: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourcePoolMonitorIDsV1(d *schema.ResourceData) []string {
|
||||||
|
mIDsRaw := d.Get("monitor_ids").(*schema.Set)
|
||||||
|
mIDs := make([]string, mIDsRaw.Len())
|
||||||
|
for i, raw := range mIDsRaw.List() {
|
||||||
|
mIDs[i] = raw.(string)
|
||||||
|
}
|
||||||
|
return mIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourcePoolMembersV1(d *schema.ResourceData) []members.CreateOpts {
|
||||||
|
memberOptsRaw := (d.Get("member")).(*schema.Set)
|
||||||
|
memberOpts := make([]members.CreateOpts, memberOptsRaw.Len())
|
||||||
|
for i, raw := range memberOptsRaw.List() {
|
||||||
|
rawMap := raw.(map[string]interface{})
|
||||||
|
memberOpts[i] = members.CreateOpts{
|
||||||
|
TenantID: rawMap["tenant_id"].(string),
|
||||||
|
Address: rawMap["address"].(string),
|
||||||
|
ProtocolPort: rawMap["port"].(int),
|
||||||
|
PoolID: d.Id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return memberOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourcePoolMemberV1(d *schema.ResourceData, raw interface{}) members.CreateOpts {
|
||||||
|
rawMap := raw.(map[string]interface{})
|
||||||
|
return members.CreateOpts{
|
||||||
|
TenantID: rawMap["tenant_id"].(string),
|
||||||
|
Address: rawMap["address"].(string),
|
||||||
|
ProtocolPort: rawMap["port"].(int),
|
||||||
|
PoolID: d.Id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceLBMemberV1Hash(v interface{}) int {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
m := v.(map[string]interface{})
|
||||||
|
buf.WriteString(fmt.Sprintf("%s-", m["region"].(string)))
|
||||||
|
buf.WriteString(fmt.Sprintf("%s-", m["tenant_id"].(string)))
|
||||||
|
buf.WriteString(fmt.Sprintf("%s-", m["address"].(string)))
|
||||||
|
buf.WriteString(fmt.Sprintf("%d-", m["port"].(int)))
|
||||||
|
|
||||||
|
return hashcode.String(buf.String())
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccLBV1Pool_basic(t *testing.T) {
|
||||||
|
var pool pools.Pool
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckLBV1PoolDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccLBV1Pool_basic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckLBV1PoolExists(t, "openstack_lb_pool_v1.pool_1", &pool),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccLBV1Pool_update,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("openstack_lb_pool_v1.pool_1", "name", "tf_test_lb_pool_updated"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckLBV1PoolDestroy(s *terraform.State) error {
|
||||||
|
config := testAccProvider.Meta().(*Config)
|
||||||
|
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(testAccCheckLBV1PoolDestroy) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "openstack_lb_pool_v1" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := pools.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("LB Pool still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckLBV1PoolExists(t *testing.T, n string, pool *pools.Pool) 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("(testAccCheckLBV1PoolExists) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
found, err := pools.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if found.ID != rs.Primary.ID {
|
||||||
|
return fmt.Errorf("Pool not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
*pool = *found
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testAccLBV1Pool_basic = fmt.Sprintf(`
|
||||||
|
resource "openstack_networking_network_v2" "network_1" {
|
||||||
|
region = "%s"
|
||||||
|
name = "network_1"
|
||||||
|
admin_state_up = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_networking_subnet_v2" "subnet_1" {
|
||||||
|
region = "%s"
|
||||||
|
network_id = "${openstack_networking_network_v2.network_1.id}"
|
||||||
|
cidr = "192.168.199.0/24"
|
||||||
|
ip_version = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_lb_pool_v1" "pool_1" {
|
||||||
|
region = "%s"
|
||||||
|
name = "tf_test_lb_pool"
|
||||||
|
protocol = "HTTP"
|
||||||
|
subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}"
|
||||||
|
lb_method = "ROUND_ROBIN"
|
||||||
|
}`,
|
||||||
|
OS_REGION_NAME, OS_REGION_NAME, OS_REGION_NAME)
|
||||||
|
|
||||||
|
var testAccLBV1Pool_update = fmt.Sprintf(`
|
||||||
|
resource "openstack_networking_network_v2" "network_1" {
|
||||||
|
region = "%s"
|
||||||
|
name = "network_1"
|
||||||
|
admin_state_up = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_networking_subnet_v2" "subnet_1" {
|
||||||
|
region = "%s"
|
||||||
|
network_id = "${openstack_networking_network_v2.network_1.id}"
|
||||||
|
cidr = "192.168.199.0/24"
|
||||||
|
ip_version = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_lb_pool_v1" "pool_1" {
|
||||||
|
region = "%s"
|
||||||
|
name = "tf_test_lb_pool_updated"
|
||||||
|
protocol = "HTTP"
|
||||||
|
subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}"
|
||||||
|
lb_method = "ROUND_ROBIN"
|
||||||
|
}`,
|
||||||
|
OS_REGION_NAME, OS_REGION_NAME, OS_REGION_NAME)
|
|
@ -0,0 +1,258 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/rackspace/gophercloud"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceLBVipV1() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceLBVipV1Create,
|
||||||
|
Read: resourceLBVipV1Read,
|
||||||
|
Update: resourceLBVipV1Update,
|
||||||
|
Delete: resourceLBVipV1Delete,
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
"subnet_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"protocol": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"port": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"pool_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"tenant_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"address": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"description": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"persistence": &schema.Schema{
|
||||||
|
Type: schema.TypeMap,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"conn_limit": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"admin_state_up": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceLBVipV1Create(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 := vips.CreateOpts{
|
||||||
|
Name: d.Get("name").(string),
|
||||||
|
SubnetID: d.Get("subnet_id").(string),
|
||||||
|
Protocol: d.Get("protocol").(string),
|
||||||
|
ProtocolPort: d.Get("port").(int),
|
||||||
|
PoolID: d.Get("pool_id").(string),
|
||||||
|
TenantID: d.Get("tenant_id").(string),
|
||||||
|
Address: d.Get("address").(string),
|
||||||
|
Description: d.Get("description").(string),
|
||||||
|
Persistence: resourceVipPersistenceV1(d),
|
||||||
|
ConnLimit: gophercloud.MaybeInt(d.Get("conn_limit").(int)),
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
p, err := vips.Create(networkingClient, createOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack LB VIP: %s", err)
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] LB VIP ID: %s", p.ID)
|
||||||
|
|
||||||
|
d.SetId(p.ID)
|
||||||
|
|
||||||
|
return resourceLBVipV1Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceLBVipV1Read(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := vips.Get(networkingClient, d.Id()).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return CheckDeleted(d, err, "LB VIP")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Retreived OpenStack LB VIP %s: %+v", d.Id(), p)
|
||||||
|
|
||||||
|
d.Set("name", p.Name)
|
||||||
|
d.Set("subnet_id", p.SubnetID)
|
||||||
|
d.Set("protocol", p.Protocol)
|
||||||
|
d.Set("port", p.ProtocolPort)
|
||||||
|
d.Set("pool_id", p.PoolID)
|
||||||
|
|
||||||
|
if t, exists := d.GetOk("tenant_id"); exists && t != "" {
|
||||||
|
d.Set("tenant_id", p.TenantID)
|
||||||
|
} else {
|
||||||
|
d.Set("tenant_id", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, exists := d.GetOk("address"); exists && t != "" {
|
||||||
|
d.Set("address", p.Address)
|
||||||
|
} else {
|
||||||
|
d.Set("address", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, exists := d.GetOk("description"); exists && t != "" {
|
||||||
|
d.Set("description", p.Description)
|
||||||
|
} else {
|
||||||
|
d.Set("description", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, exists := d.GetOk("persistence"); exists && t != "" {
|
||||||
|
d.Set("persistence", p.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, exists := d.GetOk("conn_limit"); exists && t != "" {
|
||||||
|
d.Set("conn_limit", p.ConnLimit)
|
||||||
|
} else {
|
||||||
|
d.Set("conn_limit", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, exists := d.GetOk("admin_state_up"); exists && t != "" {
|
||||||
|
d.Set("admin_state_up", strconv.FormatBool(p.AdminStateUp))
|
||||||
|
} else {
|
||||||
|
d.Set("admin_state_up", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceLBVipV1Update(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateOpts vips.UpdateOpts
|
||||||
|
if d.HasChange("name") {
|
||||||
|
updateOpts.Name = d.Get("name").(string)
|
||||||
|
}
|
||||||
|
if d.HasChange("pool_id") {
|
||||||
|
updateOpts.PoolID = d.Get("pool_id").(string)
|
||||||
|
}
|
||||||
|
if d.HasChange("description") {
|
||||||
|
updateOpts.Description = d.Get("description").(string)
|
||||||
|
}
|
||||||
|
if d.HasChange("persistence") {
|
||||||
|
updateOpts.Persistence = resourceVipPersistenceV1(d)
|
||||||
|
}
|
||||||
|
if d.HasChange("conn_limit") {
|
||||||
|
updateOpts.ConnLimit = gophercloud.MaybeInt(d.Get("conn_limit").(int))
|
||||||
|
}
|
||||||
|
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 VIP %s with options: %+v", d.Id(), updateOpts)
|
||||||
|
|
||||||
|
_, err = vips.Update(networkingClient, d.Id(), updateOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error updating OpenStack LB VIP: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceLBVipV1Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceLBVipV1Delete(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 = vips.Delete(networkingClient, d.Id()).ExtractErr()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error deleting OpenStack LB VIP: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceVipPersistenceV1(d *schema.ResourceData) *vips.SessionPersistence {
|
||||||
|
rawP := d.Get("persistence").(interface{})
|
||||||
|
rawMap := rawP.(map[string]interface{})
|
||||||
|
if len(rawMap) != 0 {
|
||||||
|
p := vips.SessionPersistence{}
|
||||||
|
if t, ok := rawMap["type"]; ok {
|
||||||
|
p.Type = t.(string)
|
||||||
|
}
|
||||||
|
if c, ok := rawMap["cookie_name"]; ok {
|
||||||
|
p.CookieName = c.(string)
|
||||||
|
}
|
||||||
|
return &p
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccLBV1VIP_basic(t *testing.T) {
|
||||||
|
var vip vips.VirtualIP
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckLBV1VIPDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccLBV1VIP_basic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckLBV1VIPExists(t, "openstack_lb_vip_v1.vip_1", &vip),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccLBV1VIP_update,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("openstack_lb_vip_v1.vip_1", "name", "tf_test_lb_vip_updated"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckLBV1VIPDestroy(s *terraform.State) error {
|
||||||
|
config := testAccProvider.Meta().(*Config)
|
||||||
|
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(testAccCheckLBV1VIPDestroy) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "openstack_lb_vip_v1" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := vips.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("LB VIP still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckLBV1VIPExists(t *testing.T, n string, vip *vips.VirtualIP) 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("(testAccCheckLBV1VIPExists) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
found, err := vips.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if found.ID != rs.Primary.ID {
|
||||||
|
return fmt.Errorf("VIP not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
*vip = *found
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testAccLBV1VIP_basic = fmt.Sprintf(`
|
||||||
|
resource "openstack_networking_network_v2" "network_1" {
|
||||||
|
region = "%s"
|
||||||
|
name = "network_1"
|
||||||
|
admin_state_up = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_networking_subnet_v2" "subnet_1" {
|
||||||
|
region = "%s"
|
||||||
|
network_id = "${openstack_networking_network_v2.network_1.id}"
|
||||||
|
cidr = "192.168.199.0/24"
|
||||||
|
ip_version = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_lb_pool_v1" "pool_1" {
|
||||||
|
region = "%s"
|
||||||
|
name = "tf_test_lb_pool"
|
||||||
|
protocol = "HTTP"
|
||||||
|
subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}"
|
||||||
|
lb_method = "ROUND_ROBIN"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_lb_vip_v1" "vip_1" {
|
||||||
|
region = "RegionOne"
|
||||||
|
name = "tf_test_lb_vip"
|
||||||
|
subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}"
|
||||||
|
protocol = "HTTP"
|
||||||
|
port = 80
|
||||||
|
pool_id = "${openstack_lb_pool_v1.pool_1.id}"
|
||||||
|
}`,
|
||||||
|
OS_REGION_NAME, OS_REGION_NAME, OS_REGION_NAME)
|
||||||
|
|
||||||
|
var testAccLBV1VIP_update = fmt.Sprintf(`
|
||||||
|
resource "openstack_networking_network_v2" "network_1" {
|
||||||
|
region = "%s"
|
||||||
|
name = "network_1"
|
||||||
|
admin_state_up = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_networking_subnet_v2" "subnet_1" {
|
||||||
|
region = "%s"
|
||||||
|
network_id = "${openstack_networking_network_v2.network_1.id}"
|
||||||
|
cidr = "192.168.199.0/24"
|
||||||
|
ip_version = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_lb_pool_v1" "pool_1" {
|
||||||
|
region = "%s"
|
||||||
|
name = "tf_test_lb_pool"
|
||||||
|
protocol = "HTTP"
|
||||||
|
subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}"
|
||||||
|
lb_method = "ROUND_ROBIN"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_lb_vip_v1" "vip_1" {
|
||||||
|
region = "RegionOne"
|
||||||
|
name = "tf_test_lb_vip_updated"
|
||||||
|
subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}"
|
||||||
|
protocol = "HTTP"
|
||||||
|
port = 80
|
||||||
|
pool_id = "${openstack_lb_pool_v1.pool_1.id}"
|
||||||
|
}`,
|
||||||
|
OS_REGION_NAME, OS_REGION_NAME, OS_REGION_NAME)
|
|
@ -0,0 +1,163 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
|
||||||
|
"github.com/rackspace/gophercloud/pagination"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceNetworkingFloatingIPV2() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceNetworkFloatingIPV2Create,
|
||||||
|
Read: resourceNetworkFloatingIPV2Read,
|
||||||
|
Delete: resourceNetworkFloatingIPV2Delete,
|
||||||
|
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"region": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
DefaultFunc: envDefaultFunc("OS_REGION_NAME"),
|
||||||
|
},
|
||||||
|
"address": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"pool": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
DefaultFunc: envDefaultFunc("OS_POOL_NAME"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkFloatingIPV2Create(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
config := meta.(*Config)
|
||||||
|
networkClient, err := config.networkingV2Client(d.Get("region").(string))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack network client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
poolID, err := getNetworkID(d, meta, d.Get("pool").(string))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error retrieving floating IP pool name: %s", err)
|
||||||
|
}
|
||||||
|
if len(poolID) == 0 {
|
||||||
|
return fmt.Errorf("No network found with name: %s", d.Get("pool").(string))
|
||||||
|
}
|
||||||
|
createOpts := floatingips.CreateOpts{
|
||||||
|
FloatingNetworkID: poolID,
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] Create Options: %#v", createOpts)
|
||||||
|
floatingIP, err := floatingips.Create(networkClient, createOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error allocating floating IP: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId(floatingIP.ID)
|
||||||
|
|
||||||
|
return resourceNetworkFloatingIPV2Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkFloatingIPV2Read(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
config := meta.(*Config)
|
||||||
|
networkClient, err := config.networkingV2Client(d.Get("region").(string))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack network client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
floatingIP, err := floatingips.Get(networkClient, d.Id()).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return CheckDeleted(d, err, "floating IP")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Set("address", floatingIP.FloatingIP)
|
||||||
|
poolName, err := getNetworkName(d, meta, floatingIP.FloatingNetworkID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error retrieving floating IP pool name: %s", err)
|
||||||
|
}
|
||||||
|
d.Set("pool", poolName)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkFloatingIPV2Delete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
config := meta.(*Config)
|
||||||
|
networkClient, err := config.networkingV2Client(d.Get("region").(string))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack network client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = floatingips.Delete(networkClient, d.Id()).ExtractErr()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error deleting floating IP: %s", err)
|
||||||
|
}
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNetworkID(d *schema.ResourceData, meta interface{}, networkName string) (string, error) {
|
||||||
|
config := meta.(*Config)
|
||||||
|
networkClient, err := config.networkingV2Client(d.Get("region").(string))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error creating OpenStack network client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := networks.ListOpts{Name: networkName}
|
||||||
|
pager := networks.List(networkClient, opts)
|
||||||
|
networkID := ""
|
||||||
|
|
||||||
|
err = pager.EachPage(func(page pagination.Page) (bool, error) {
|
||||||
|
networkList, err := networks.ExtractNetworks(page)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range networkList {
|
||||||
|
if n.Name == networkName {
|
||||||
|
networkID = n.ID
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return networkID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNetworkName(d *schema.ResourceData, meta interface{}, networkID string) (string, error) {
|
||||||
|
config := meta.(*Config)
|
||||||
|
networkClient, err := config.networkingV2Client(d.Get("region").(string))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error creating OpenStack network client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := networks.ListOpts{ID: networkID}
|
||||||
|
pager := networks.List(networkClient, opts)
|
||||||
|
networkName := ""
|
||||||
|
|
||||||
|
err = pager.EachPage(func(page pagination.Page) (bool, error) {
|
||||||
|
networkList, err := networks.ExtractNetworks(page)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range networkList {
|
||||||
|
if n.ID == networkID {
|
||||||
|
networkName = n.Name
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return networkName, err
|
||||||
|
}
|
|
@ -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/networking/v2/extensions/layer3/floatingips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccNetworkingV2FloatingIP_basic(t *testing.T) {
|
||||||
|
var floatingIP floatingips.FloatingIP
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckNetworkingV2FloatingIPDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccNetworkingV2FloatingIP_basic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckNetworkingV2FloatingIPExists(t, "openstack_networking_floatingip_v2.foo", &floatingIP),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNetworkingV2FloatingIPDestroy(s *terraform.State) error {
|
||||||
|
config := testAccProvider.Meta().(*Config)
|
||||||
|
networkClient, err := config.networkingV2Client(OS_REGION_NAME)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(testAccCheckNetworkingV2FloatingIPDestroy) Error creating OpenStack floating IP: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "openstack_networking_floatingip_v2" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := floatingips.Get(networkClient, rs.Primary.ID).Extract()
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("FloatingIP still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNetworkingV2FloatingIPExists(t *testing.T, n string, kp *floatingips.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)
|
||||||
|
networkClient, err := config.networkingV2Client(OS_REGION_NAME)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(testAccCheckNetworkingV2FloatingIPExists) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
found, err := floatingips.Get(networkClient, 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 testAccNetworkingV2FloatingIP_basic = `
|
||||||
|
resource "openstack_networking_floatingip_v2" "foo" {
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_compute_instance_v2" "bar" {
|
||||||
|
name = "terraform-acc-floating-ip-test"
|
||||||
|
floating_ip = "${openstack_networking_floatingip_v2.foo.address}"
|
||||||
|
}`
|
|
@ -0,0 +1,170 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceNetworkingNetworkV2() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceNetworkingNetworkV2Create,
|
||||||
|
Read: resourceNetworkingNetworkV2Read,
|
||||||
|
Update: resourceNetworkingNetworkV2Update,
|
||||||
|
Delete: resourceNetworkingNetworkV2Delete,
|
||||||
|
|
||||||
|
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,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"admin_state_up": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"shared": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"tenant_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingNetworkV2Create(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 := networks.CreateOpts{
|
||||||
|
Name: d.Get("name").(string),
|
||||||
|
TenantID: d.Get("tenant_id").(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
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedRaw := d.Get("shared").(string)
|
||||||
|
if sharedRaw != "" {
|
||||||
|
shared, err := strconv.ParseBool(sharedRaw)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("shared, if provided, must be either 'true' or 'false': %v", err)
|
||||||
|
}
|
||||||
|
createOpts.Shared = &shared
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Create Options: %#v", createOpts)
|
||||||
|
n, err := networks.Create(networkingClient, createOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack Neutron network: %s", err)
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] Network ID: %s", n.ID)
|
||||||
|
|
||||||
|
d.SetId(n.ID)
|
||||||
|
|
||||||
|
return resourceNetworkingNetworkV2Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingNetworkV2Read(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := networks.Get(networkingClient, d.Id()).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return CheckDeleted(d, err, "network")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Retreived Network %s: %+v", d.Id(), n)
|
||||||
|
|
||||||
|
d.Set("name", n.Name)
|
||||||
|
d.Set("admin_state_up", strconv.FormatBool(n.AdminStateUp))
|
||||||
|
d.Set("shared", strconv.FormatBool(n.Shared))
|
||||||
|
d.Set("tenant_id", n.TenantID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingNetworkV2Update(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateOpts networks.UpdateOpts
|
||||||
|
if d.HasChange("name") {
|
||||||
|
updateOpts.Name = d.Get("name").(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d.HasChange("shared") {
|
||||||
|
sharedRaw := d.Get("shared").(string)
|
||||||
|
if sharedRaw != "" {
|
||||||
|
shared, err := strconv.ParseBool(sharedRaw)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("shared, if provided, must be either 'true' or 'false': %v", err)
|
||||||
|
}
|
||||||
|
updateOpts.Shared = &shared
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Updating Network %s with options: %+v", d.Id(), updateOpts)
|
||||||
|
|
||||||
|
_, err = networks.Update(networkingClient, d.Id(), updateOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error updating OpenStack Neutron Network: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceNetworkingNetworkV2Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingNetworkV2Delete(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 = networks.Delete(networkingClient, d.Id()).ExtractErr()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error deleting OpenStack Neutron Network: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccNetworkingV2Network_basic(t *testing.T) {
|
||||||
|
var network networks.Network
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckNetworkingV2NetworkDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccNetworkingV2Network_basic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckNetworkingV2NetworkExists(t, "openstack_networking_network_v2.foo", &network),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccNetworkingV2Network_update,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("openstack_networking_network_v2.foo", "name", "network_2"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNetworkingV2NetworkDestroy(s *terraform.State) error {
|
||||||
|
config := testAccProvider.Meta().(*Config)
|
||||||
|
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(testAccCheckNetworkingV2NetworkDestroy) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "openstack_networking_network_v2" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := networks.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("Network still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNetworkingV2NetworkExists(t *testing.T, n string, network *networks.Network) 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("(testAccCheckNetworkingV2NetworkExists) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
found, err := networks.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if found.ID != rs.Primary.ID {
|
||||||
|
return fmt.Errorf("Network not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
*network = *found
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testAccNetworkingV2Network_basic = fmt.Sprintf(`
|
||||||
|
resource "openstack_networking_network_v2" "foo" {
|
||||||
|
region = "%s"
|
||||||
|
name = "network_1"
|
||||||
|
admin_state_up = "true"
|
||||||
|
}`,
|
||||||
|
OS_REGION_NAME)
|
||||||
|
|
||||||
|
var testAccNetworkingV2Network_update = fmt.Sprintf(`
|
||||||
|
resource "openstack_networking_network_v2" "foo" {
|
||||||
|
region = "%s"
|
||||||
|
name = "network_2"
|
||||||
|
admin_state_up = "true"
|
||||||
|
}`,
|
||||||
|
OS_REGION_NAME)
|
|
@ -0,0 +1,107 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/rackspace/gophercloud"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/ports"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceNetworkingRouterInterfaceV2() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceNetworkingRouterInterfaceV2Create,
|
||||||
|
Read: resourceNetworkingRouterInterfaceV2Read,
|
||||||
|
Delete: resourceNetworkingRouterInterfaceV2Delete,
|
||||||
|
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"region": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
DefaultFunc: envDefaultFunc("OS_REGION_NAME"),
|
||||||
|
},
|
||||||
|
"router_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"subnet_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingRouterInterfaceV2Create(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 := routers.InterfaceOpts{
|
||||||
|
SubnetID: d.Get("subnet_id").(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Create Options: %#v", createOpts)
|
||||||
|
n, err := routers.AddInterface(networkingClient, d.Get("router_id").(string), createOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack Neutron router interface: %s", err)
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] Router interface Port ID: %s", n.PortID)
|
||||||
|
|
||||||
|
d.SetId(n.PortID)
|
||||||
|
|
||||||
|
return resourceNetworkingRouterInterfaceV2Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingRouterInterfaceV2Read(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := ports.Get(networkingClient, d.Id()).Extract()
|
||||||
|
if err != nil {
|
||||||
|
httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Error retrieving OpenStack Neutron Router Interface: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if httpError.Actual == 404 {
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Error retrieving OpenStack Neutron Router Interface: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Retreived Router Interface %s: %+v", d.Id(), n)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingRouterInterfaceV2Delete(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeOpts := routers.InterfaceOpts{
|
||||||
|
SubnetID: d.Get("subnet_id").(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = routers.RemoveInterface(networkingClient, d.Get("router_id").(string), removeOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error deleting OpenStack Neutron Router Interface: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/ports"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccNetworkingV2RouterInterface_basic(t *testing.T) {
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckNetworkingV2RouterInterfaceDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccNetworkingV2RouterInterface_basic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckNetworkingV2RouterInterfaceExists(t, "openstack_networking_router_interface_v2.int_1"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNetworkingV2RouterInterfaceDestroy(s *terraform.State) error {
|
||||||
|
config := testAccProvider.Meta().(*Config)
|
||||||
|
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(testAccCheckNetworkingV2RouterInterfaceDestroy) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "openstack_networking_router_interface_v2" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ports.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("Router interface still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNetworkingV2RouterInterfaceExists(t *testing.T, 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := testAccProvider.Meta().(*Config)
|
||||||
|
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(testAccCheckNetworkingV2RouterInterfaceExists) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
found, err := ports.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if found.ID != rs.Primary.ID {
|
||||||
|
return fmt.Errorf("Router interface not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testAccNetworkingV2RouterInterface_basic = fmt.Sprintf(`
|
||||||
|
resource "openstack_networking_router_v2" "router_1" {
|
||||||
|
name = "router_1"
|
||||||
|
admin_state_up = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_networking_router_interface_v2" "int_1" {
|
||||||
|
subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}"
|
||||||
|
router_id = "${openstack_networking_router_v2.router_1.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_networking_network_v2" "network_1" {
|
||||||
|
name = "network_1"
|
||||||
|
admin_state_up = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_networking_subnet_v2" "subnet_1" {
|
||||||
|
network_id = "${openstack_networking_network_v2.network_1.id}"
|
||||||
|
cidr = "192.168.199.0/24"
|
||||||
|
ip_version = 4
|
||||||
|
}`)
|
|
@ -0,0 +1,169 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/rackspace/gophercloud"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceNetworkingRouterV2() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceNetworkingRouterV2Create,
|
||||||
|
Read: resourceNetworkingRouterV2Read,
|
||||||
|
Update: resourceNetworkingRouterV2Update,
|
||||||
|
Delete: resourceNetworkingRouterV2Delete,
|
||||||
|
|
||||||
|
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,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"admin_state_up": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"external_gateway": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"tenant_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingRouterV2Create(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 := routers.CreateOpts{
|
||||||
|
Name: d.Get("name").(string),
|
||||||
|
TenantID: d.Get("tenant_id").(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
|
||||||
|
}
|
||||||
|
|
||||||
|
externalGateway := d.Get("external_gateway").(string)
|
||||||
|
if externalGateway != "" {
|
||||||
|
gatewayInfo := routers.GatewayInfo{
|
||||||
|
NetworkID: externalGateway,
|
||||||
|
}
|
||||||
|
createOpts.GatewayInfo = &gatewayInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Create Options: %#v", createOpts)
|
||||||
|
n, err := routers.Create(networkingClient, createOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack Neutron router: %s", err)
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] Router ID: %s", n.ID)
|
||||||
|
|
||||||
|
d.SetId(n.ID)
|
||||||
|
|
||||||
|
return resourceNetworkingRouterV2Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingRouterV2Read(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := routers.Get(networkingClient, d.Id()).Extract()
|
||||||
|
if err != nil {
|
||||||
|
httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Error retrieving OpenStack Neutron Router: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if httpError.Actual == 404 {
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Error retrieving OpenStack Neutron Router: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Retreived Router %s: %+v", d.Id(), n)
|
||||||
|
|
||||||
|
d.Set("name", n.Name)
|
||||||
|
d.Set("admin_state_up", strconv.FormatBool(n.AdminStateUp))
|
||||||
|
d.Set("tenant_id", n.TenantID)
|
||||||
|
d.Set("external_gateway", n.GatewayInfo.NetworkID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingRouterV2Update(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateOpts routers.UpdateOpts
|
||||||
|
if d.HasChange("name") {
|
||||||
|
updateOpts.Name = d.Get("name").(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 Router %s with options: %+v", d.Id(), updateOpts)
|
||||||
|
|
||||||
|
_, err = routers.Update(networkingClient, d.Id(), updateOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error updating OpenStack Neutron Router: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceNetworkingRouterV2Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingRouterV2Delete(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 = routers.Delete(networkingClient, d.Id()).ExtractErr()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error deleting OpenStack Neutron Router: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccNetworkingV2Router_basic(t *testing.T) {
|
||||||
|
var router routers.Router
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckNetworkingV2RouterDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccNetworkingV2Router_basic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckNetworkingV2RouterExists(t, "openstack_networking_router_v2.foo", &router),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccNetworkingV2Router_update,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("openstack_networking_router_v2.foo", "name", "router_2"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNetworkingV2RouterDestroy(s *terraform.State) error {
|
||||||
|
config := testAccProvider.Meta().(*Config)
|
||||||
|
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(testAccCheckNetworkingV2RouterDestroy) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "openstack_networking_router_v2" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := routers.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("Router still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNetworkingV2RouterExists(t *testing.T, n string, router *routers.Router) 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("(testAccCheckNetworkingV2RouterExists) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
found, err := routers.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if found.ID != rs.Primary.ID {
|
||||||
|
return fmt.Errorf("Router not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
*router = *found
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testAccNetworkingV2Router_basic = fmt.Sprintf(`
|
||||||
|
resource "openstack_networking_router_v2" "foo" {
|
||||||
|
name = "router"
|
||||||
|
admin_state_up = "true"
|
||||||
|
}`)
|
||||||
|
|
||||||
|
var testAccNetworkingV2Router_update = fmt.Sprintf(`
|
||||||
|
resource "openstack_networking_router_v2" "foo" {
|
||||||
|
name = "router_2"
|
||||||
|
admin_state_up = "true"
|
||||||
|
}`)
|
|
@ -0,0 +1,272 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/hashcode"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceNetworkingSubnetV2() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceNetworkingSubnetV2Create,
|
||||||
|
Read: resourceNetworkingSubnetV2Read,
|
||||||
|
Update: resourceNetworkingSubnetV2Update,
|
||||||
|
Delete: resourceNetworkingSubnetV2Delete,
|
||||||
|
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"region": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
DefaultFunc: envDefaultFunc("OS_REGION_NAME"),
|
||||||
|
},
|
||||||
|
"network_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"cidr": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"name": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"tenant_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"allocation_pools": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
Elem: &schema.Resource{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"start": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"end": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"gateway_ip": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"ip_version": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"enable_dhcp": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"dns_nameservers": &schema.Schema{
|
||||||
|
Type: schema.TypeSet,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeString},
|
||||||
|
Set: func(v interface{}) int {
|
||||||
|
return hashcode.String(v.(string))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"host_routes": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
Elem: &schema.Resource{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"destination_cidr": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"next_hop": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingSubnetV2Create(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 := subnets.CreateOpts{
|
||||||
|
NetworkID: d.Get("network_id").(string),
|
||||||
|
CIDR: d.Get("cidr").(string),
|
||||||
|
Name: d.Get("name").(string),
|
||||||
|
TenantID: d.Get("tenant_id").(string),
|
||||||
|
AllocationPools: resourceSubnetAllocationPoolsV2(d),
|
||||||
|
GatewayIP: d.Get("gateway_ip").(string),
|
||||||
|
IPVersion: d.Get("ip_version").(int),
|
||||||
|
DNSNameservers: resourceSubnetDNSNameserversV2(d),
|
||||||
|
HostRoutes: resourceSubnetHostRoutesV2(d),
|
||||||
|
}
|
||||||
|
|
||||||
|
edRaw := d.Get("enable_dhcp").(string)
|
||||||
|
if edRaw != "" {
|
||||||
|
ed, err := strconv.ParseBool(edRaw)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("enable_dhcp, if provided, must be either 'true' or 'false'")
|
||||||
|
}
|
||||||
|
createOpts.EnableDHCP = &ed
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Create Options: %#v", createOpts)
|
||||||
|
s, err := subnets.Create(networkingClient, createOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack Neutron subnet: %s", err)
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] Subnet ID: %s", s.ID)
|
||||||
|
|
||||||
|
d.SetId(s.ID)
|
||||||
|
|
||||||
|
return resourceNetworkingSubnetV2Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingSubnetV2Read(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := subnets.Get(networkingClient, d.Id()).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return CheckDeleted(d, err, "subnet")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Retreived Subnet %s: %+v", d.Id(), s)
|
||||||
|
|
||||||
|
d.Set("newtork_id", s.NetworkID)
|
||||||
|
d.Set("cidr", s.CIDR)
|
||||||
|
d.Set("ip_version", s.IPVersion)
|
||||||
|
d.Set("name", s.Name)
|
||||||
|
d.Set("tenant_id", s.TenantID)
|
||||||
|
d.Set("allocation_pools", s.AllocationPools)
|
||||||
|
d.Set("gateway_ip", s.GatewayIP)
|
||||||
|
d.Set("enable_dhcp", strconv.FormatBool(s.EnableDHCP))
|
||||||
|
d.Set("dns_nameservers", s.DNSNameservers)
|
||||||
|
d.Set("host_routes", s.HostRoutes)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingSubnetV2Update(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateOpts subnets.UpdateOpts
|
||||||
|
|
||||||
|
if d.HasChange("name") {
|
||||||
|
updateOpts.Name = d.Get("name").(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("gateway_ip") {
|
||||||
|
updateOpts.GatewayIP = d.Get("gateway_ip").(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("dns_nameservers") {
|
||||||
|
updateOpts.DNSNameservers = resourceSubnetDNSNameserversV2(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("host_routes") {
|
||||||
|
updateOpts.HostRoutes = resourceSubnetHostRoutesV2(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("enable_dhcp") {
|
||||||
|
edRaw := d.Get("enable_dhcp").(string)
|
||||||
|
if edRaw != "" {
|
||||||
|
ed, err := strconv.ParseBool(edRaw)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("enable_dhcp, if provided, must be either 'true' or 'false'")
|
||||||
|
}
|
||||||
|
updateOpts.EnableDHCP = &ed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Updating Subnet %s with options: %+v", d.Id(), updateOpts)
|
||||||
|
|
||||||
|
_, err = subnets.Update(networkingClient, d.Id(), updateOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error updating OpenStack Neutron Subnet: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceNetworkingSubnetV2Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNetworkingSubnetV2Delete(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 = subnets.Delete(networkingClient, d.Id()).ExtractErr()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error deleting OpenStack Neutron Subnet: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceSubnetAllocationPoolsV2(d *schema.ResourceData) []subnets.AllocationPool {
|
||||||
|
rawAPs := d.Get("allocation_pools").([]interface{})
|
||||||
|
aps := make([]subnets.AllocationPool, len(rawAPs))
|
||||||
|
for i, raw := range rawAPs {
|
||||||
|
rawMap := raw.(map[string]interface{})
|
||||||
|
aps[i] = subnets.AllocationPool{
|
||||||
|
Start: rawMap["start"].(string),
|
||||||
|
End: rawMap["end"].(string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aps
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceSubnetDNSNameserversV2(d *schema.ResourceData) []string {
|
||||||
|
rawDNSN := d.Get("dns_nameservers").(*schema.Set)
|
||||||
|
dnsn := make([]string, rawDNSN.Len())
|
||||||
|
for i, raw := range rawDNSN.List() {
|
||||||
|
dnsn[i] = raw.(string)
|
||||||
|
}
|
||||||
|
return dnsn
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceSubnetHostRoutesV2(d *schema.ResourceData) []subnets.HostRoute {
|
||||||
|
rawHR := d.Get("host_routes").([]interface{})
|
||||||
|
hr := make([]subnets.HostRoute, len(rawHR))
|
||||||
|
for i, raw := range rawHR {
|
||||||
|
rawMap := raw.(map[string]interface{})
|
||||||
|
hr[i] = subnets.HostRoute{
|
||||||
|
DestinationCIDR: rawMap["destination_cidr"].(string),
|
||||||
|
NextHop: rawMap["next_hop"].(string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hr
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
|
||||||
|
"github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccNetworkingV2Subnet_basic(t *testing.T) {
|
||||||
|
var subnet subnets.Subnet
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckNetworkingV2SubnetDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccNetworkingV2Subnet_basic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckNetworkingV2SubnetExists(t, "openstack_networking_subnet_v2.subnet_1", &subnet),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccNetworkingV2Subnet_update,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("openstack_networking_subnet_v2.subnet_1", "name", "tf-test-subnet"),
|
||||||
|
resource.TestCheckResourceAttr("openstack_networking_subnet_v2.subnet_1", "gateway_ip", "192.68.0.1"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNetworkingV2SubnetDestroy(s *terraform.State) error {
|
||||||
|
config := testAccProvider.Meta().(*Config)
|
||||||
|
networkingClient, err := config.networkingV2Client(OS_REGION_NAME)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(testAccCheckNetworkingV2SubnetDestroy) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "openstack_networking_subnet_v2" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := subnets.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("Subnet still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNetworkingV2SubnetExists(t *testing.T, n string, subnet *subnets.Subnet) 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("(testAccCheckNetworkingV2SubnetExists) Error creating OpenStack networking client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
found, err := subnets.Get(networkingClient, rs.Primary.ID).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if found.ID != rs.Primary.ID {
|
||||||
|
return fmt.Errorf("Subnet not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
*subnet = *found
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testAccNetworkingV2Subnet_basic = fmt.Sprintf(`
|
||||||
|
resource "openstack_networking_network_v2" "network_1" {
|
||||||
|
region = "%s"
|
||||||
|
name = "network_1"
|
||||||
|
admin_state_up = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_networking_subnet_v2" "subnet_1" {
|
||||||
|
region = "%s"
|
||||||
|
network_id = "${openstack_networking_network_v2.network_1.id}"
|
||||||
|
cidr = "192.168.199.0/24"
|
||||||
|
ip_version = 4
|
||||||
|
}`, OS_REGION_NAME, OS_REGION_NAME)
|
||||||
|
|
||||||
|
var testAccNetworkingV2Subnet_update = fmt.Sprintf(`
|
||||||
|
resource "openstack_networking_network_v2" "network_1" {
|
||||||
|
region = "%s"
|
||||||
|
name = "network_1"
|
||||||
|
admin_state_up = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "openstack_networking_subnet_v2" "subnet_1" {
|
||||||
|
region = "%s"
|
||||||
|
name = "tf-test-subnet"
|
||||||
|
network_id = "${openstack_networking_network_v2.network_1.id}"
|
||||||
|
cidr = "192.168.199.0/24"
|
||||||
|
ip_version = 4
|
||||||
|
gateway_ip = "192.68.0.1"
|
||||||
|
}`, OS_REGION_NAME, OS_REGION_NAME)
|
|
@ -0,0 +1,148 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceObjectStorageContainerV1() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceObjectStorageContainerV1Create,
|
||||||
|
Read: resourceObjectStorageContainerV1Read,
|
||||||
|
Update: resourceObjectStorageContainerV1Update,
|
||||||
|
Delete: resourceObjectStorageContainerV1Delete,
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
"container_read": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"container_sync_to": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"container_sync_key": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"container_write": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"content_type": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
"metadata": &schema.Schema{
|
||||||
|
Type: schema.TypeMap,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceObjectStorageContainerV1Create(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
config := meta.(*Config)
|
||||||
|
objectStorageClient, err := config.objectStorageV1Client(d.Get("region").(string))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack object storage client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cn := d.Get("name").(string)
|
||||||
|
|
||||||
|
createOpts := &containers.CreateOpts{
|
||||||
|
ContainerRead: d.Get("container_read").(string),
|
||||||
|
ContainerSyncTo: d.Get("container_sync_to").(string),
|
||||||
|
ContainerSyncKey: d.Get("container_sync_key").(string),
|
||||||
|
ContainerWrite: d.Get("container_write").(string),
|
||||||
|
ContentType: d.Get("content_type").(string),
|
||||||
|
Metadata: resourceContainerMetadataV2(d),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Create Options: %#v", createOpts)
|
||||||
|
_, err = containers.Create(objectStorageClient, cn, createOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack container: %s", err)
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] Container ID: %s", cn)
|
||||||
|
|
||||||
|
// Store the ID now
|
||||||
|
d.SetId(cn)
|
||||||
|
|
||||||
|
return resourceObjectStorageContainerV1Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceObjectStorageContainerV1Read(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceObjectStorageContainerV1Update(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
config := meta.(*Config)
|
||||||
|
objectStorageClient, err := config.objectStorageV1Client(d.Get("region").(string))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack object storage client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOpts := containers.UpdateOpts{
|
||||||
|
ContainerRead: d.Get("container_read").(string),
|
||||||
|
ContainerSyncTo: d.Get("container_sync_to").(string),
|
||||||
|
ContainerSyncKey: d.Get("container_sync_key").(string),
|
||||||
|
ContainerWrite: d.Get("container_write").(string),
|
||||||
|
ContentType: d.Get("content_type").(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("metadata") {
|
||||||
|
updateOpts.Metadata = resourceContainerMetadataV2(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = containers.Update(objectStorageClient, d.Id(), updateOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error updating OpenStack container: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceObjectStorageContainerV1Read(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceObjectStorageContainerV1Delete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
config := meta.(*Config)
|
||||||
|
objectStorageClient, err := config.objectStorageV1Client(d.Get("region").(string))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack object storage client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = containers.Delete(objectStorageClient, d.Id()).Extract()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error deleting OpenStack container: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceContainerMetadataV2(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
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccObjectStorageV1Container_basic(t *testing.T) {
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckObjectStorageV1ContainerDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccObjectStorageV1Container_basic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("openstack_objectstorage_container_v1.container_1", "name", "tf-test-container"),
|
||||||
|
resource.TestCheckResourceAttr("openstack_objectstorage_container_v1.container_1", "content_type", "application/json"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccObjectStorageV1Container_update,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("openstack_objectstorage_container_v1.container_1", "content_type", "text/plain"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckObjectStorageV1ContainerDestroy(s *terraform.State) error {
|
||||||
|
config := testAccProvider.Meta().(*Config)
|
||||||
|
objectStorageClient, err := config.objectStorageV1Client(OS_REGION_NAME)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating OpenStack object storage client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "openstack_objectstorage_container_v1" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := containers.Get(objectStorageClient, rs.Primary.ID).Extract()
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("Container still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var testAccObjectStorageV1Container_basic = fmt.Sprintf(`
|
||||||
|
resource "openstack_objectstorage_container_v1" "container_1" {
|
||||||
|
region = "%s"
|
||||||
|
name = "tf-test-container"
|
||||||
|
metadata {
|
||||||
|
test = "true"
|
||||||
|
}
|
||||||
|
content_type = "application/json"
|
||||||
|
}`,
|
||||||
|
OS_REGION_NAME)
|
||||||
|
|
||||||
|
var testAccObjectStorageV1Container_update = fmt.Sprintf(`
|
||||||
|
resource "openstack_objectstorage_container_v1" "container_1" {
|
||||||
|
region = "%s"
|
||||||
|
name = "tf-test-container"
|
||||||
|
metadata {
|
||||||
|
test = "true"
|
||||||
|
}
|
||||||
|
content_type = "text/plain"
|
||||||
|
}`,
|
||||||
|
OS_REGION_NAME)
|
|
@ -0,0 +1,22 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/rackspace/gophercloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckDeleted checks the error to see if it's a 404 (Not Found) and, if so,
|
||||||
|
// sets the resource ID to the empty string instead of throwing an error.
|
||||||
|
func CheckDeleted(d *schema.ResourceData, err error, msg string) error {
|
||||||
|
errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%s: %s", msg, err)
|
||||||
|
}
|
||||||
|
if errCode.Actual == 404 {
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s: %s", msg, err)
|
||||||
|
}
|
|
@ -93,6 +93,7 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
|
|
||||||
// Build the context based on the arguments given
|
// Build the context based on the arguments given
|
||||||
ctx, planned, err := c.Context(contextOpts{
|
ctx, planned, err := c.Context(contextOpts{
|
||||||
|
Destroy: c.Destroy,
|
||||||
Path: configPath,
|
Path: configPath,
|
||||||
StatePath: c.Meta.statePath,
|
StatePath: c.Meta.statePath,
|
||||||
})
|
})
|
||||||
|
@ -140,12 +141,7 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var opts terraform.PlanOpts
|
if _, err := ctx.Plan(); err != nil {
|
||||||
if c.Destroy {
|
|
||||||
opts.Destroy = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := ctx.Plan(&opts); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
"Error creating plan: %s", err))
|
"Error creating plan: %s", err))
|
||||||
return 1
|
return 1
|
||||||
|
@ -319,6 +315,10 @@ Options:
|
||||||
"-state". This can be used to preserve the old
|
"-state". This can be used to preserve the old
|
||||||
state.
|
state.
|
||||||
|
|
||||||
|
-target=resource Resource to target. Operation will be limited to this
|
||||||
|
resource and its dependencies. This flag can be used
|
||||||
|
multiple times.
|
||||||
|
|
||||||
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
||||||
flag can be set multiple times.
|
flag can be set multiple times.
|
||||||
|
|
||||||
|
@ -357,6 +357,10 @@ Options:
|
||||||
"-state". This can be used to preserve the old
|
"-state". This can be used to preserve the old
|
||||||
state.
|
state.
|
||||||
|
|
||||||
|
-target=resource Resource to target. Operation will be limited to this
|
||||||
|
resource and its dependencies. This flag can be used
|
||||||
|
multiple times.
|
||||||
|
|
||||||
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
||||||
flag can be set multiple times.
|
flag can be set multiple times.
|
||||||
|
|
||||||
|
|
|
@ -116,6 +116,96 @@ func TestApply_destroyPlan(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApply_destroyTargeted(t *testing.T) {
|
||||||
|
originalState := &terraform.State{
|
||||||
|
Modules: []*terraform.ModuleState{
|
||||||
|
&terraform.ModuleState{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*terraform.ResourceState{
|
||||||
|
"test_instance.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_instance",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "i-ab123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"test_load_balancer.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_load_balancer",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "lb-abc123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
statePath := testStateFile(t, originalState)
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &ApplyCommand{
|
||||||
|
Destroy: true,
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the apply command pointing to our existing state
|
||||||
|
args := []string{
|
||||||
|
"-force",
|
||||||
|
"-target", "test_instance.foo",
|
||||||
|
"-state", statePath,
|
||||||
|
testFixturePath("apply-destroy-targeted"),
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify a new state exists
|
||||||
|
if _, err := os.Stat(statePath); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(statePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
state, err := terraform.ReadState(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
if state == nil {
|
||||||
|
t.Fatal("state should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
actualStr := strings.TrimSpace(state.String())
|
||||||
|
expectedStr := strings.TrimSpace(testApplyDestroyStr)
|
||||||
|
if actualStr != expectedStr {
|
||||||
|
t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have a backup file
|
||||||
|
f, err = os.Open(statePath + DefaultBackupExtention)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
backupState, err := terraform.ReadState(f)
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actualStr = strings.TrimSpace(backupState.String())
|
||||||
|
expectedStr = strings.TrimSpace(originalState.String())
|
||||||
|
if actualStr != expectedStr {
|
||||||
|
t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", actualStr, expectedStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const testApplyDestroyStr = `
|
const testApplyDestroyStr = `
|
||||||
<no state>
|
<no state>
|
||||||
`
|
`
|
||||||
|
|
|
@ -85,3 +85,17 @@ func loadKVFile(rawPath string) (map[string]string, error) {
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FlagStringSlice is a flag.Value implementation for parsing targets from the
|
||||||
|
// command line, e.g. -target=aws_instance.foo -target=aws_vpc.bar
|
||||||
|
|
||||||
|
type FlagStringSlice []string
|
||||||
|
|
||||||
|
func (v *FlagStringSlice) String() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
func (v *FlagStringSlice) Set(raw string) error {
|
||||||
|
*v = append(*v, raw)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -38,6 +38,9 @@ type Meta struct {
|
||||||
input bool
|
input bool
|
||||||
variables map[string]string
|
variables map[string]string
|
||||||
|
|
||||||
|
// Targets for this context (private)
|
||||||
|
targets []string
|
||||||
|
|
||||||
color bool
|
color bool
|
||||||
oldUi cli.Ui
|
oldUi cli.Ui
|
||||||
|
|
||||||
|
@ -126,6 +129,9 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
|
||||||
m.statePath = copts.StatePath
|
m.statePath = copts.StatePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tell the context if we're in a destroy plan / apply
|
||||||
|
opts.Destroy = copts.Destroy
|
||||||
|
|
||||||
// Store the loaded state
|
// Store the loaded state
|
||||||
state, err := m.State()
|
state, err := m.State()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -267,6 +273,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
|
||||||
vs[k] = v
|
vs[k] = v
|
||||||
}
|
}
|
||||||
opts.Variables = vs
|
opts.Variables = vs
|
||||||
|
opts.Targets = m.targets
|
||||||
opts.UIInput = m.UIInput()
|
opts.UIInput = m.UIInput()
|
||||||
|
|
||||||
return &opts
|
return &opts
|
||||||
|
@ -278,6 +285,7 @@ func (m *Meta) flagSet(n string) *flag.FlagSet {
|
||||||
f.BoolVar(&m.input, "input", true, "input")
|
f.BoolVar(&m.input, "input", true, "input")
|
||||||
f.Var((*FlagKV)(&m.variables), "var", "variables")
|
f.Var((*FlagKV)(&m.variables), "var", "variables")
|
||||||
f.Var((*FlagKVFile)(&m.variables), "var-file", "variable file")
|
f.Var((*FlagKVFile)(&m.variables), "var-file", "variable file")
|
||||||
|
f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target")
|
||||||
|
|
||||||
if m.autoKey != "" {
|
if m.autoKey != "" {
|
||||||
f.Var((*FlagKVFile)(&m.autoVariables), m.autoKey, "variable file")
|
f.Var((*FlagKVFile)(&m.autoVariables), m.autoKey, "variable file")
|
||||||
|
@ -388,4 +396,7 @@ type contextOpts struct {
|
||||||
|
|
||||||
// GetMode is the module.GetMode to use when loading the module tree.
|
// GetMode is the module.GetMode to use when loading the module tree.
|
||||||
GetMode module.GetMode
|
GetMode module.GetMode
|
||||||
|
|
||||||
|
// Set to true when running a destroy plan/apply.
|
||||||
|
Destroy bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ func (c *PlanCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, _, err := c.Context(contextOpts{
|
ctx, _, err := c.Context(contextOpts{
|
||||||
|
Destroy: destroy,
|
||||||
Path: path,
|
Path: path,
|
||||||
StatePath: c.Meta.statePath,
|
StatePath: c.Meta.statePath,
|
||||||
})
|
})
|
||||||
|
@ -86,7 +87,7 @@ func (c *PlanCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plan, err := ctx.Plan(&terraform.PlanOpts{Destroy: destroy})
|
plan, err := ctx.Plan()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error running plan: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error running plan: %s", err))
|
||||||
return 1
|
return 1
|
||||||
|
@ -168,6 +169,10 @@ Options:
|
||||||
up Terraform-managed resources. By default it will
|
up Terraform-managed resources. By default it will
|
||||||
use the state "terraform.tfstate" if it exists.
|
use the state "terraform.tfstate" if it exists.
|
||||||
|
|
||||||
|
-target=resource Resource to target. Operation will be limited to this
|
||||||
|
resource and its dependencies. This flag can be used
|
||||||
|
multiple times.
|
||||||
|
|
||||||
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
||||||
flag can be set multiple times.
|
flag can be set multiple times.
|
||||||
|
|
||||||
|
|
|
@ -135,6 +135,10 @@ Options:
|
||||||
-state-out=path Path to write updated state file. By default, the
|
-state-out=path Path to write updated state file. By default, the
|
||||||
"-state" path will be used.
|
"-state" path will be used.
|
||||||
|
|
||||||
|
-target=resource Resource to target. Operation will be limited to this
|
||||||
|
resource and its dependencies. This flag can be used
|
||||||
|
multiple times.
|
||||||
|
|
||||||
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
||||||
flag can be set multiple times.
|
flag can be set multiple times.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "test_instance" "foo" {
|
||||||
|
count = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_load_balancer" "foo" {
|
||||||
|
instances = ["${test_instance.foo.*.id}"]
|
||||||
|
}
|
|
@ -179,7 +179,7 @@ func (c *Config) discoverSingle(glob string, m *map[string]string) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[DEBUG] Discoverd plugin: %s = %s", parts[2], match)
|
log.Printf("[DEBUG] Discovered plugin: %s = %s", parts[2], match)
|
||||||
(*m)[parts[2]] = match
|
(*m)[parts[2]] = match
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config/lang/ast"
|
"github.com/hashicorp/terraform/config/lang/ast"
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Funcs is the mapping of built-in functions for configuration.
|
// Funcs is the mapping of built-in functions for configuration.
|
||||||
|
@ -57,7 +58,11 @@ func interpolationFuncFile() ast.Function {
|
||||||
ArgTypes: []ast.Type{ast.TypeString},
|
ArgTypes: []ast.Type{ast.TypeString},
|
||||||
ReturnType: ast.TypeString,
|
ReturnType: ast.TypeString,
|
||||||
Callback: func(args []interface{}) (interface{}, error) {
|
Callback: func(args []interface{}) (interface{}, error) {
|
||||||
data, err := ioutil.ReadFile(args[0].(string))
|
path, err := homedir.Expand(args[0].(string))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package module
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
@ -20,8 +21,27 @@ func (d *FileDetector) Detect(src, pwd string) (string, bool, error) {
|
||||||
"relative paths require a module with a pwd")
|
"relative paths require a module with a pwd")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stat the pwd to determine if its a symbolic link. If it is,
|
||||||
|
// then the pwd becomes the original directory. Otherwise,
|
||||||
|
// `filepath.Join` below does some weird stuff.
|
||||||
|
//
|
||||||
|
// We just ignore if the pwd doesn't exist. That error will be
|
||||||
|
// caught later when we try to use the URL.
|
||||||
|
if fi, err := os.Lstat(pwd); !os.IsNotExist(err) {
|
||||||
|
if err != nil {
|
||||||
|
return "", true, err
|
||||||
|
}
|
||||||
|
if fi.Mode()&os.ModeSymlink != 0 {
|
||||||
|
pwd, err = os.Readlink(pwd)
|
||||||
|
if err != nil {
|
||||||
|
return "", true, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
src = filepath.Join(pwd, src)
|
src = filepath.Join(pwd, src)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmtFileURL(src), true, nil
|
return fmtFileURL(src), true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
module "b" {
|
||||||
|
source = "../c"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
# Hello
|
|
@ -0,0 +1,3 @@
|
||||||
|
module "a" {
|
||||||
|
source = "./a"
|
||||||
|
}
|
|
@ -94,6 +94,44 @@ func TestTreeLoad_duplicate(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTreeLoad_parentRef(t *testing.T) {
|
||||||
|
storage := testStorage(t)
|
||||||
|
tree := NewTree("", testConfig(t, "basic-parent"))
|
||||||
|
|
||||||
|
if tree.Loaded() {
|
||||||
|
t.Fatal("should not be loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should error because we haven't gotten things yet
|
||||||
|
if err := tree.Load(storage, GetModeNone); err == nil {
|
||||||
|
t.Fatal("should error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tree.Loaded() {
|
||||||
|
t.Fatal("should not be loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should get things
|
||||||
|
if err := tree.Load(storage, GetModeGet); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tree.Loaded() {
|
||||||
|
t.Fatal("should be loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should no longer error
|
||||||
|
if err := tree.Load(storage, GetModeNone); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := strings.TrimSpace(tree.String())
|
||||||
|
expected := strings.TrimSpace(treeLoadParentStr)
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad: \n\n%s", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTreeLoad_subdir(t *testing.T) {
|
func TestTreeLoad_subdir(t *testing.T) {
|
||||||
storage := testStorage(t)
|
storage := testStorage(t)
|
||||||
tree := NewTree("", testConfig(t, "basic-subdir"))
|
tree := NewTree("", testConfig(t, "basic-subdir"))
|
||||||
|
@ -239,6 +277,11 @@ root
|
||||||
foo
|
foo
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const treeLoadParentStr = `
|
||||||
|
root
|
||||||
|
a
|
||||||
|
b
|
||||||
|
`
|
||||||
const treeLoadSubdirStr = `
|
const treeLoadSubdirStr = `
|
||||||
root
|
root
|
||||||
foo
|
foo
|
||||||
|
|
93
dag/dag.go
93
dag/dag.go
|
@ -17,6 +17,40 @@ type AcyclicGraph struct {
|
||||||
// WalkFunc is the callback used for walking the graph.
|
// WalkFunc is the callback used for walking the graph.
|
||||||
type WalkFunc func(Vertex) error
|
type WalkFunc func(Vertex) error
|
||||||
|
|
||||||
|
// Returns a Set that includes every Vertex yielded by walking down from the
|
||||||
|
// provided starting Vertex v.
|
||||||
|
func (g *AcyclicGraph) Ancestors(v Vertex) (*Set, error) {
|
||||||
|
s := new(Set)
|
||||||
|
start := asVertexList(g.DownEdges(v))
|
||||||
|
memoFunc := func(v Vertex) error {
|
||||||
|
s.Add(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.depthFirstWalk(start, memoFunc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a Set that includes every Vertex yielded by walking up from the
|
||||||
|
// provided starting Vertex v.
|
||||||
|
func (g *AcyclicGraph) Descendents(v Vertex) (*Set, error) {
|
||||||
|
s := new(Set)
|
||||||
|
start := asVertexList(g.UpEdges(v))
|
||||||
|
memoFunc := func(v Vertex) error {
|
||||||
|
s.Add(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.reverseDepthFirstWalk(start, memoFunc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Root returns the root of the DAG, or an error.
|
// Root returns the root of the DAG, or an error.
|
||||||
//
|
//
|
||||||
// Complexity: O(V)
|
// Complexity: O(V)
|
||||||
|
@ -61,15 +95,11 @@ func (g *AcyclicGraph) TransitiveReduction() {
|
||||||
|
|
||||||
for _, u := range g.Vertices() {
|
for _, u := range g.Vertices() {
|
||||||
uTargets := g.DownEdges(u)
|
uTargets := g.DownEdges(u)
|
||||||
vs := make([]Vertex, uTargets.Len())
|
vs := asVertexList(g.DownEdges(u))
|
||||||
for i, vRaw := range uTargets.List() {
|
|
||||||
vs[i] = vRaw.(Vertex)
|
|
||||||
}
|
|
||||||
|
|
||||||
g.depthFirstWalk(vs, func(v Vertex) error {
|
g.depthFirstWalk(vs, func(v Vertex) error {
|
||||||
shared := uTargets.Intersection(g.DownEdges(v))
|
shared := uTargets.Intersection(g.DownEdges(v))
|
||||||
for _, raw := range shared.List() {
|
for _, vPrime := range asVertexList(shared) {
|
||||||
vPrime := raw.(Vertex)
|
|
||||||
g.RemoveEdge(BasicEdge(u, vPrime))
|
g.RemoveEdge(BasicEdge(u, vPrime))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,12 +175,10 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error {
|
||||||
for _, v := range vertices {
|
for _, v := range vertices {
|
||||||
// Build our list of dependencies and the list of channels to
|
// Build our list of dependencies and the list of channels to
|
||||||
// wait on until we start executing for this vertex.
|
// wait on until we start executing for this vertex.
|
||||||
depsRaw := g.DownEdges(v).List()
|
deps := asVertexList(g.DownEdges(v))
|
||||||
deps := make([]Vertex, len(depsRaw))
|
|
||||||
depChs := make([]<-chan struct{}, len(deps))
|
depChs := make([]<-chan struct{}, len(deps))
|
||||||
for i, raw := range depsRaw {
|
for i, dep := range deps {
|
||||||
deps[i] = raw.(Vertex)
|
depChs[i] = vertMap[dep]
|
||||||
depChs[i] = vertMap[deps[i]]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get our channel so that we can close it when we're done
|
// Get our channel so that we can close it when we're done
|
||||||
|
@ -200,6 +228,16 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error {
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// simple convenience helper for converting a dag.Set to a []Vertex
|
||||||
|
func asVertexList(s *Set) []Vertex {
|
||||||
|
rawList := s.List()
|
||||||
|
vertexList := make([]Vertex, len(rawList))
|
||||||
|
for i, raw := range rawList {
|
||||||
|
vertexList[i] = raw.(Vertex)
|
||||||
|
}
|
||||||
|
return vertexList
|
||||||
|
}
|
||||||
|
|
||||||
// depthFirstWalk does a depth-first walk of the graph starting from
|
// depthFirstWalk does a depth-first walk of the graph starting from
|
||||||
// the vertices in start. This is not exported now but it would make sense
|
// the vertices in start. This is not exported now but it would make sense
|
||||||
// to export this publicly at some point.
|
// to export this publicly at some point.
|
||||||
|
@ -233,3 +271,36 @@ func (g *AcyclicGraph) depthFirstWalk(start []Vertex, cb WalkFunc) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reverseDepthFirstWalk does a depth-first walk _up_ the graph starting from
|
||||||
|
// the vertices in start.
|
||||||
|
func (g *AcyclicGraph) reverseDepthFirstWalk(start []Vertex, cb WalkFunc) error {
|
||||||
|
seen := make(map[Vertex]struct{})
|
||||||
|
frontier := make([]Vertex, len(start))
|
||||||
|
copy(frontier, start)
|
||||||
|
for len(frontier) > 0 {
|
||||||
|
// Pop the current vertex
|
||||||
|
n := len(frontier)
|
||||||
|
current := frontier[n-1]
|
||||||
|
frontier = frontier[:n-1]
|
||||||
|
|
||||||
|
// Check if we've seen this already and return...
|
||||||
|
if _, ok := seen[current]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[current] = struct{}{}
|
||||||
|
|
||||||
|
// Visit the current node
|
||||||
|
if err := cb(current); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit targets of this in reverse order.
|
||||||
|
targets := g.UpEdges(current).List()
|
||||||
|
for i := len(targets) - 1; i >= 0; i-- {
|
||||||
|
frontier = append(frontier, targets[i].(Vertex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -126,6 +126,68 @@ func TestAcyclicGraphValidate_cycleSelf(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAcyclicGraphAncestors(t *testing.T) {
|
||||||
|
var g AcyclicGraph
|
||||||
|
g.Add(1)
|
||||||
|
g.Add(2)
|
||||||
|
g.Add(3)
|
||||||
|
g.Add(4)
|
||||||
|
g.Add(5)
|
||||||
|
g.Connect(BasicEdge(0, 1))
|
||||||
|
g.Connect(BasicEdge(1, 2))
|
||||||
|
g.Connect(BasicEdge(2, 3))
|
||||||
|
g.Connect(BasicEdge(3, 4))
|
||||||
|
g.Connect(BasicEdge(4, 5))
|
||||||
|
|
||||||
|
actual, err := g.Ancestors(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []Vertex{3, 4, 5}
|
||||||
|
|
||||||
|
if actual.Len() != len(expected) {
|
||||||
|
t.Fatalf("bad length! expected %#v to have len %d", actual, len(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range expected {
|
||||||
|
if !actual.Include(e) {
|
||||||
|
t.Fatalf("expected: %#v to include: %#v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAcyclicGraphDescendents(t *testing.T) {
|
||||||
|
var g AcyclicGraph
|
||||||
|
g.Add(1)
|
||||||
|
g.Add(2)
|
||||||
|
g.Add(3)
|
||||||
|
g.Add(4)
|
||||||
|
g.Add(5)
|
||||||
|
g.Connect(BasicEdge(0, 1))
|
||||||
|
g.Connect(BasicEdge(1, 2))
|
||||||
|
g.Connect(BasicEdge(2, 3))
|
||||||
|
g.Connect(BasicEdge(3, 4))
|
||||||
|
g.Connect(BasicEdge(4, 5))
|
||||||
|
|
||||||
|
actual, err := g.Descendents(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []Vertex{0, 1}
|
||||||
|
|
||||||
|
if actual.Len() != len(expected) {
|
||||||
|
t.Fatalf("bad length! expected %#v to have len %d", actual, len(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range expected {
|
||||||
|
if !actual.Include(e) {
|
||||||
|
t.Fatalf("expected: %#v to include: %#v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAcyclicGraphWalk(t *testing.T) {
|
func TestAcyclicGraphWalk(t *testing.T) {
|
||||||
var g AcyclicGraph
|
var g AcyclicGraph
|
||||||
g.Add(1)
|
g.Add(1)
|
||||||
|
|
|
@ -190,6 +190,7 @@ func testStep(
|
||||||
// Build the context
|
// Build the context
|
||||||
opts.Module = mod
|
opts.Module = mod
|
||||||
opts.State = state
|
opts.State = state
|
||||||
|
opts.Destroy = step.Destroy
|
||||||
ctx := terraform.NewContext(&opts)
|
ctx := terraform.NewContext(&opts)
|
||||||
if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 {
|
if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 {
|
||||||
estrs := make([]string, len(es))
|
estrs := make([]string, len(es))
|
||||||
|
@ -209,7 +210,7 @@ func testStep(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plan!
|
// Plan!
|
||||||
if p, err := ctx.Plan(&terraform.PlanOpts{Destroy: step.Destroy}); err != nil {
|
if p, err := ctx.Plan(); err != nil {
|
||||||
return state, fmt.Errorf(
|
return state, fmt.Errorf(
|
||||||
"Error planning: %s", err)
|
"Error planning: %s", err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -33,6 +33,7 @@ const (
|
||||||
// ContextOpts are the user-configurable options to create a context with
|
// ContextOpts are the user-configurable options to create a context with
|
||||||
// NewContext.
|
// NewContext.
|
||||||
type ContextOpts struct {
|
type ContextOpts struct {
|
||||||
|
Destroy bool
|
||||||
Diff *Diff
|
Diff *Diff
|
||||||
Hooks []Hook
|
Hooks []Hook
|
||||||
Module *module.Tree
|
Module *module.Tree
|
||||||
|
@ -40,6 +41,7 @@ type ContextOpts struct {
|
||||||
State *State
|
State *State
|
||||||
Providers map[string]ResourceProviderFactory
|
Providers map[string]ResourceProviderFactory
|
||||||
Provisioners map[string]ResourceProvisionerFactory
|
Provisioners map[string]ResourceProvisionerFactory
|
||||||
|
Targets []string
|
||||||
Variables map[string]string
|
Variables map[string]string
|
||||||
|
|
||||||
UIInput UIInput
|
UIInput UIInput
|
||||||
|
@ -49,6 +51,7 @@ type ContextOpts struct {
|
||||||
// perform operations on infrastructure. This structure is built using
|
// perform operations on infrastructure. This structure is built using
|
||||||
// NewContext. See the documentation for that.
|
// NewContext. See the documentation for that.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
|
destroy bool
|
||||||
diff *Diff
|
diff *Diff
|
||||||
diffLock sync.RWMutex
|
diffLock sync.RWMutex
|
||||||
hooks []Hook
|
hooks []Hook
|
||||||
|
@ -58,6 +61,7 @@ type Context struct {
|
||||||
sh *stopHook
|
sh *stopHook
|
||||||
state *State
|
state *State
|
||||||
stateLock sync.RWMutex
|
stateLock sync.RWMutex
|
||||||
|
targets []string
|
||||||
uiInput UIInput
|
uiInput UIInput
|
||||||
variables map[string]string
|
variables map[string]string
|
||||||
|
|
||||||
|
@ -95,12 +99,14 @@ func NewContext(opts *ContextOpts) *Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Context{
|
return &Context{
|
||||||
|
destroy: opts.Destroy,
|
||||||
diff: opts.Diff,
|
diff: opts.Diff,
|
||||||
hooks: hooks,
|
hooks: hooks,
|
||||||
module: opts.Module,
|
module: opts.Module,
|
||||||
providers: opts.Providers,
|
providers: opts.Providers,
|
||||||
provisioners: opts.Provisioners,
|
provisioners: opts.Provisioners,
|
||||||
state: state,
|
state: state,
|
||||||
|
targets: opts.Targets,
|
||||||
uiInput: opts.UIInput,
|
uiInput: opts.UIInput,
|
||||||
variables: opts.Variables,
|
variables: opts.Variables,
|
||||||
|
|
||||||
|
@ -135,6 +141,8 @@ func (c *Context) GraphBuilder() GraphBuilder {
|
||||||
Providers: providers,
|
Providers: providers,
|
||||||
Provisioners: provisioners,
|
Provisioners: provisioners,
|
||||||
State: c.state,
|
State: c.state,
|
||||||
|
Targets: c.targets,
|
||||||
|
Destroy: c.destroy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +261,7 @@ func (c *Context) Apply() (*State, error) {
|
||||||
//
|
//
|
||||||
// Plan also updates the diff of this context to be the diff generated
|
// Plan also updates the diff of this context to be the diff generated
|
||||||
// by the plan, so Apply can be called after.
|
// by the plan, so Apply can be called after.
|
||||||
func (c *Context) Plan(opts *PlanOpts) (*Plan, error) {
|
func (c *Context) Plan() (*Plan, error) {
|
||||||
v := c.acquireRun()
|
v := c.acquireRun()
|
||||||
defer c.releaseRun(v)
|
defer c.releaseRun(v)
|
||||||
|
|
||||||
|
@ -264,7 +272,7 @@ func (c *Context) Plan(opts *PlanOpts) (*Plan, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var operation walkOperation
|
var operation walkOperation
|
||||||
if opts != nil && opts.Destroy {
|
if c.destroy {
|
||||||
operation = walkPlanDestroy
|
operation = walkPlanDestroy
|
||||||
} else {
|
} else {
|
||||||
// Set our state to be something temporary. We do this so that
|
// Set our state to be something temporary. We do this so that
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -65,6 +65,13 @@ type BuiltinGraphBuilder struct {
|
||||||
|
|
||||||
// Provisioners is the list of provisioners supported.
|
// Provisioners is the list of provisioners supported.
|
||||||
Provisioners []string
|
Provisioners []string
|
||||||
|
|
||||||
|
// Targets is the user-specified list of resources to target.
|
||||||
|
Targets []string
|
||||||
|
|
||||||
|
// Destroy is set to true when we're in a `terraform destroy` or a
|
||||||
|
// `terraform plan -destroy`
|
||||||
|
Destroy bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build builds the graph according to the steps returned by Steps.
|
// Build builds the graph according to the steps returned by Steps.
|
||||||
|
@ -82,7 +89,11 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer {
|
||||||
return []GraphTransformer{
|
return []GraphTransformer{
|
||||||
// Create all our resources from the configuration and state
|
// Create all our resources from the configuration and state
|
||||||
&ConfigTransformer{Module: b.Root},
|
&ConfigTransformer{Module: b.Root},
|
||||||
&OrphanTransformer{State: b.State, Module: b.Root},
|
&OrphanTransformer{
|
||||||
|
State: b.State,
|
||||||
|
Module: b.Root,
|
||||||
|
Targeting: (len(b.Targets) > 0),
|
||||||
|
},
|
||||||
|
|
||||||
// Provider-related transformations
|
// Provider-related transformations
|
||||||
&MissingProviderTransformer{Providers: b.Providers},
|
&MissingProviderTransformer{Providers: b.Providers},
|
||||||
|
@ -104,6 +115,10 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Optionally reduces the graph to a user-specified list of targets and
|
||||||
|
// their dependencies.
|
||||||
|
&TargetsTransformer{Targets: b.Targets, Destroy: b.Destroy},
|
||||||
|
|
||||||
// Create the destruction nodes
|
// Create the destruction nodes
|
||||||
&DestroyTransformer{},
|
&DestroyTransformer{},
|
||||||
&CreateBeforeDestroyTransformer{},
|
&CreateBeforeDestroyTransformer{},
|
||||||
|
|
|
@ -21,6 +21,26 @@ type graphNodeConfig interface {
|
||||||
GraphNodeDependent
|
GraphNodeDependent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphNodeAddressable is an interface that all graph nodes for the
|
||||||
|
// configuration graph need to implement in order to be be addressed / targeted
|
||||||
|
// properly.
|
||||||
|
type GraphNodeAddressable interface {
|
||||||
|
graphNodeConfig
|
||||||
|
|
||||||
|
ResourceAddress() *ResourceAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphNodeTargetable is an interface for graph nodes to implement when they
|
||||||
|
// need to be told about incoming targets. This is useful for nodes that need
|
||||||
|
// to respect targets as they dynamically expand. Note that the list of targets
|
||||||
|
// provided will contain every target provided, and each implementing graph
|
||||||
|
// node must filter this list to targets considered relevant.
|
||||||
|
type GraphNodeTargetable interface {
|
||||||
|
GraphNodeAddressable
|
||||||
|
|
||||||
|
SetTargets([]ResourceAddress)
|
||||||
|
}
|
||||||
|
|
||||||
// GraphNodeConfigModule represents a module within the configuration graph.
|
// GraphNodeConfigModule represents a module within the configuration graph.
|
||||||
type GraphNodeConfigModule struct {
|
type GraphNodeConfigModule struct {
|
||||||
Path []string
|
Path []string
|
||||||
|
@ -191,6 +211,9 @@ type GraphNodeConfigResource struct {
|
||||||
// If this is set to anything other than destroyModeNone, then this
|
// If this is set to anything other than destroyModeNone, then this
|
||||||
// resource represents a resource that will be destroyed in some way.
|
// resource represents a resource that will be destroyed in some way.
|
||||||
DestroyMode GraphNodeDestroyMode
|
DestroyMode GraphNodeDestroyMode
|
||||||
|
|
||||||
|
// Used during DynamicExpand to target indexes
|
||||||
|
Targets []ResourceAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *GraphNodeConfigResource) DependableName() []string {
|
func (n *GraphNodeConfigResource) DependableName() []string {
|
||||||
|
@ -279,6 +302,7 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
|
||||||
steps = append(steps, &ResourceCountTransformer{
|
steps = append(steps, &ResourceCountTransformer{
|
||||||
Resource: n.Resource,
|
Resource: n.Resource,
|
||||||
Destroy: n.DestroyMode != DestroyNone,
|
Destroy: n.DestroyMode != DestroyNone,
|
||||||
|
Targets: n.Targets,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,8 +313,9 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
|
||||||
// expand orphans, which have all the same semantics in a destroy
|
// expand orphans, which have all the same semantics in a destroy
|
||||||
// as a primary.
|
// as a primary.
|
||||||
steps = append(steps, &OrphanTransformer{
|
steps = append(steps, &OrphanTransformer{
|
||||||
State: state,
|
State: state,
|
||||||
View: n.Resource.Id(),
|
View: n.Resource.Id(),
|
||||||
|
Targeting: (len(n.Targets) > 0),
|
||||||
})
|
})
|
||||||
|
|
||||||
steps = append(steps, &DeposedTransformer{
|
steps = append(steps, &DeposedTransformer{
|
||||||
|
@ -314,6 +339,22 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
|
||||||
return b.Build(ctx.Path())
|
return b.Build(ctx.Path())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphNodeAddressable impl.
|
||||||
|
func (n *GraphNodeConfigResource) ResourceAddress() *ResourceAddress {
|
||||||
|
return &ResourceAddress{
|
||||||
|
// Indicates no specific index; will match on other three fields
|
||||||
|
Index: -1,
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Name: n.Resource.Name,
|
||||||
|
Type: n.Resource.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphNodeTargetable impl.
|
||||||
|
func (n *GraphNodeConfigResource) SetTargets(targets []ResourceAddress) {
|
||||||
|
n.Targets = targets
|
||||||
|
}
|
||||||
|
|
||||||
// GraphNodeEvalable impl.
|
// GraphNodeEvalable impl.
|
||||||
func (n *GraphNodeConfigResource) EvalTree() EvalNode {
|
func (n *GraphNodeConfigResource) EvalTree() EvalNode {
|
||||||
return &EvalSequence{
|
return &EvalSequence{
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
//go:generate stringer -type=InstanceType instancetype.go
|
||||||
|
|
||||||
|
// InstanceType is an enum of the various types of instances store in the State
|
||||||
|
type InstanceType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeInvalid InstanceType = iota
|
||||||
|
TypePrimary
|
||||||
|
TypeTainted
|
||||||
|
TypeDeposed
|
||||||
|
)
|
|
@ -0,0 +1,16 @@
|
||||||
|
// generated by stringer -type=InstanceType instancetype.go; DO NOT EDIT
|
||||||
|
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const _InstanceType_name = "TypeInvalidTypePrimaryTypeTaintedTypeDeposed"
|
||||||
|
|
||||||
|
var _InstanceType_index = [...]uint8{0, 11, 22, 33, 44}
|
||||||
|
|
||||||
|
func (i InstanceType) String() string {
|
||||||
|
if i < 0 || i+1 >= InstanceType(len(_InstanceType_index)) {
|
||||||
|
return fmt.Sprintf("InstanceType(%d)", i)
|
||||||
|
}
|
||||||
|
return _InstanceType_name[_InstanceType_index[i]:_InstanceType_index[i+1]]
|
||||||
|
}
|
|
@ -18,15 +18,6 @@ func init() {
|
||||||
gob.Register(make(map[string]string))
|
gob.Register(make(map[string]string))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlanOpts are the options used to generate an execution plan for
|
|
||||||
// Terraform.
|
|
||||||
type PlanOpts struct {
|
|
||||||
// If set to true, then the generated plan will destroy all resources
|
|
||||||
// that are created. Otherwise, it will move towards the desired state
|
|
||||||
// specified in the configuration.
|
|
||||||
Destroy bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plan represents a single Terraform execution plan, which contains
|
// Plan represents a single Terraform execution plan, which contains
|
||||||
// all the information necessary to make an infrastructure change.
|
// all the information necessary to make an infrastructure change.
|
||||||
type Plan struct {
|
type Plan struct {
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourceAddress is a way of identifying an individual resource (or,
|
||||||
|
// eventually, a subset of resources) within the state. It is used for Targets.
|
||||||
|
type ResourceAddress struct {
|
||||||
|
Index int
|
||||||
|
InstanceType InstanceType
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseResourceAddress(s string) (*ResourceAddress, error) {
|
||||||
|
matches, err := tokenizeResourceAddress(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resourceIndex := -1
|
||||||
|
if matches["index"] != "" {
|
||||||
|
var err error
|
||||||
|
if resourceIndex, err = strconv.Atoi(matches["index"]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instanceType := TypePrimary
|
||||||
|
if matches["instance_type"] != "" {
|
||||||
|
var err error
|
||||||
|
if instanceType, err = ParseInstanceType(matches["instance_type"]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ResourceAddress{
|
||||||
|
Index: resourceIndex,
|
||||||
|
InstanceType: instanceType,
|
||||||
|
Name: matches["name"],
|
||||||
|
Type: matches["type"],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (addr *ResourceAddress) Equals(raw interface{}) bool {
|
||||||
|
other, ok := raw.(*ResourceAddress)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
indexMatch := (addr.Index == -1 ||
|
||||||
|
other.Index == -1 ||
|
||||||
|
addr.Index == other.Index)
|
||||||
|
|
||||||
|
return (indexMatch &&
|
||||||
|
addr.InstanceType == other.InstanceType &&
|
||||||
|
addr.Name == other.Name &&
|
||||||
|
addr.Type == other.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseInstanceType(s string) (InstanceType, error) {
|
||||||
|
switch s {
|
||||||
|
case "primary":
|
||||||
|
return TypePrimary, nil
|
||||||
|
case "deposed":
|
||||||
|
return TypeDeposed, nil
|
||||||
|
case "tainted":
|
||||||
|
return TypeTainted, nil
|
||||||
|
default:
|
||||||
|
return TypeInvalid, fmt.Errorf("Unexpected value for InstanceType field: %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenizeResourceAddress(s string) (map[string]string, error) {
|
||||||
|
// Example of portions of the regexp below using the
|
||||||
|
// string "aws_instance.web.tainted[1]"
|
||||||
|
re := regexp.MustCompile(`\A` +
|
||||||
|
// "aws_instance"
|
||||||
|
`(?P<type>\w+)\.` +
|
||||||
|
// "web"
|
||||||
|
`(?P<name>\w+)` +
|
||||||
|
// "tainted" (optional, omission implies: "primary")
|
||||||
|
`(?:\.(?P<instance_type>\w+))?` +
|
||||||
|
// "1" (optional, omission implies: "0")
|
||||||
|
`(?:\[(?P<index>\d+)\])?` +
|
||||||
|
`\z`)
|
||||||
|
groupNames := re.SubexpNames()
|
||||||
|
rawMatches := re.FindAllStringSubmatch(s, -1)
|
||||||
|
if len(rawMatches) != 1 {
|
||||||
|
return nil, fmt.Errorf("Problem parsing address: %q", s)
|
||||||
|
}
|
||||||
|
matches := make(map[string]string)
|
||||||
|
for i, m := range rawMatches[0] {
|
||||||
|
matches[groupNames[i]] = m
|
||||||
|
}
|
||||||
|
return matches, nil
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseResourceAddress(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Input string
|
||||||
|
Expected *ResourceAddress
|
||||||
|
}{
|
||||||
|
"implicit primary, no specific index": {
|
||||||
|
Input: "aws_instance.foo",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"implicit primary, explicit index": {
|
||||||
|
Input: "aws_instance.foo[2]",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"explicit primary, explicit index": {
|
||||||
|
Input: "aws_instance.foo.primary[2]",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tainted": {
|
||||||
|
Input: "aws_instance.foo.tainted",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypeTainted,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"deposed": {
|
||||||
|
Input: "aws_instance.foo.deposed",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypeDeposed,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for tn, tc := range cases {
|
||||||
|
out, err := ParseResourceAddress(tc.Input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected err: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(out, tc.Expected) {
|
||||||
|
t.Fatalf("bad: %q\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Expected, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceAddressEquals(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Address *ResourceAddress
|
||||||
|
Other interface{}
|
||||||
|
Expect bool
|
||||||
|
}{
|
||||||
|
"basic match": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
"address does not set index": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 3,
|
||||||
|
},
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
"other does not set index": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 3,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
"neither sets index": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
"different type": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_vpc",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
"different name": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "bar",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
"different instance type": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypeTainted,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
"different index": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 1,
|
||||||
|
},
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for tn, tc := range cases {
|
||||||
|
actual := tc.Address.Equals(tc.Other)
|
||||||
|
if actual != tc.Expect {
|
||||||
|
t.Fatalf("%q: expected equals: %t, got %t for:\n%#v\n%#v",
|
||||||
|
tn, tc.Expect, actual, tc.Address, tc.Other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -889,6 +889,10 @@ func (i *InstanceState) deepcopy() *InstanceState {
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InstanceState) Empty() bool {
|
||||||
|
return s == nil || s.ID == ""
|
||||||
|
}
|
||||||
|
|
||||||
func (s *InstanceState) Equal(other *InstanceState) bool {
|
func (s *InstanceState) Equal(other *InstanceState) bool {
|
||||||
// Short circuit some nil checks
|
// Short circuit some nil checks
|
||||||
if s == nil || other == nil {
|
if s == nil || other == nil {
|
||||||
|
|
|
@ -366,6 +366,34 @@ func TestResourceStateTaint(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInstanceStateEmpty(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
In *InstanceState
|
||||||
|
Result bool
|
||||||
|
}{
|
||||||
|
"nil is empty": {
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
"non-nil but without ID is empty": {
|
||||||
|
&InstanceState{},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
"with ID is not empty": {
|
||||||
|
&InstanceState{
|
||||||
|
ID: "i-abc123",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for tn, tc := range cases {
|
||||||
|
if tc.In.Empty() != tc.Result {
|
||||||
|
t.Fatalf("%q expected %#v to be empty: %#v", tn, tc.In, tc.Result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInstanceStateEqual(t *testing.T) {
|
func TestInstanceStateEqual(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
Result bool
|
Result bool
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
count = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "bar" {
|
||||||
|
count = 3
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
num = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "bar" {
|
||||||
|
foo = "bar"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
num = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "bar" {
|
||||||
|
foo = "${aws_instance.foo.num}"
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
resource "aws_vpc" "metoo" {}
|
||||||
|
resource "aws_instance" "notme" { }
|
||||||
|
resource "aws_instance" "me" {
|
||||||
|
vpc_id = "${aws_vpc.metoo.id}"
|
||||||
|
count = 3
|
||||||
|
}
|
||||||
|
resource "aws_elb" "meneither" {
|
||||||
|
instances = ["${aws_instance.me.*.id}"]
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
resource "aws_vpc" "metoo" {}
|
||||||
|
resource "aws_instance" "notme" { }
|
||||||
|
resource "aws_instance" "me" {
|
||||||
|
vpc_id = "${aws_vpc.metoo.id}"
|
||||||
|
}
|
||||||
|
resource "aws_elb" "meneither" {
|
||||||
|
instances = ["${aws_instance.me.*.id}"]
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
resource "aws_vpc" "me" {}
|
||||||
|
|
||||||
|
resource "aws_subnet" "me" {
|
||||||
|
vpc_id = "${aws_vpc.me.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "me" {
|
||||||
|
subnet_id = "${aws_subnet.me.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_vpc" "notme" {}
|
||||||
|
resource "aws_subnet" "notme" {}
|
||||||
|
resource "aws_instance" "notme" {}
|
||||||
|
resource "aws_instance" "notmeeither" {
|
||||||
|
name = "${aws_instance.me.id}"
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
resource "aws_vpc" "notme" {}
|
||||||
|
|
||||||
|
resource "aws_subnet" "notme" {
|
||||||
|
vpc_id = "${aws_vpc.notme.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "me" {
|
||||||
|
subnet_id = "${aws_subnet.notme.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "notme" {}
|
||||||
|
resource "aws_instance" "metoo" {
|
||||||
|
name = "${aws_instance.me.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_elb" "me" {
|
||||||
|
instances = "${aws_instance.me.*.id}"
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package terraform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/hashicorp/terraform/config/module"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
|
@ -25,6 +26,11 @@ type OrphanTransformer struct {
|
||||||
// using the graph path.
|
// using the graph path.
|
||||||
Module *module.Tree
|
Module *module.Tree
|
||||||
|
|
||||||
|
// Targets are user-specified resources to target. We need to be aware of
|
||||||
|
// these so we don't improperly identify orphans when they've just been
|
||||||
|
// filtered out of the graph via targeting.
|
||||||
|
Targeting bool
|
||||||
|
|
||||||
// View, if non-nil will set a view on the module state.
|
// View, if non-nil will set a view on the module state.
|
||||||
View string
|
View string
|
||||||
}
|
}
|
||||||
|
@ -35,6 +41,13 @@ func (t *OrphanTransformer) Transform(g *Graph) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.Targeting {
|
||||||
|
log.Printf("Skipping orphan transformer because we have targets.")
|
||||||
|
// If we are in a run where we are targeting nodes, we won't process
|
||||||
|
// orphans for this run.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Build up all our state representatives
|
// Build up all our state representatives
|
||||||
resourceRep := make(map[string]struct{})
|
resourceRep := make(map[string]struct{})
|
||||||
for _, v := range g.Vertices() {
|
for _, v := range g.Vertices() {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
type ResourceCountTransformer struct {
|
type ResourceCountTransformer struct {
|
||||||
Resource *config.Resource
|
Resource *config.Resource
|
||||||
Destroy bool
|
Destroy bool
|
||||||
|
Targets []ResourceAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||||
|
@ -27,7 +28,7 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each count, build and add the node
|
// For each count, build and add the node
|
||||||
nodes := make([]dag.Vertex, count)
|
nodes := make([]dag.Vertex, 0, count)
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
// Set the index. If our count is 1 we special case it so that
|
// Set the index. If our count is 1 we special case it so that
|
||||||
// we handle the "resource.0" and "resource" boundary properly.
|
// we handle the "resource.0" and "resource" boundary properly.
|
||||||
|
@ -49,9 +50,14 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip nodes if targeting excludes them
|
||||||
|
if !t.nodeIsTargeted(node) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Add the node now
|
// Add the node now
|
||||||
nodes[i] = node
|
nodes = append(nodes, node)
|
||||||
g.Add(nodes[i])
|
g.Add(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the dependency connections
|
// Make the dependency connections
|
||||||
|
@ -64,6 +70,25 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *ResourceCountTransformer) nodeIsTargeted(node dag.Vertex) bool {
|
||||||
|
// no targets specified, everything stays in the graph
|
||||||
|
if len(t.Targets) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
addressable, ok := node.(GraphNodeAddressable)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := addressable.ResourceAddress()
|
||||||
|
for _, targetAddr := range t.Targets {
|
||||||
|
if targetAddr.Equals(addr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type graphNodeExpandedResource struct {
|
type graphNodeExpandedResource struct {
|
||||||
Index int
|
Index int
|
||||||
Resource *config.Resource
|
Resource *config.Resource
|
||||||
|
@ -77,6 +102,23 @@ func (n *graphNodeExpandedResource) Name() string {
|
||||||
return fmt.Sprintf("%s #%d", n.Resource.Id(), n.Index)
|
return fmt.Sprintf("%s #%d", n.Resource.Id(), n.Index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphNodeAddressable impl.
|
||||||
|
func (n *graphNodeExpandedResource) ResourceAddress() *ResourceAddress {
|
||||||
|
// We want this to report the logical index properly, so we must undo the
|
||||||
|
// special case from the expand
|
||||||
|
index := n.Index
|
||||||
|
if index == -1 {
|
||||||
|
index = 0
|
||||||
|
}
|
||||||
|
return &ResourceAddress{
|
||||||
|
Index: index,
|
||||||
|
// TODO: kjkjkj
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Name: n.Resource.Name,
|
||||||
|
Type: n.Resource.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GraphNodeDependable impl.
|
// GraphNodeDependable impl.
|
||||||
func (n *graphNodeExpandedResource) DependableName() []string {
|
func (n *graphNodeExpandedResource) DependableName() []string {
|
||||||
return []string{
|
return []string{
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import "github.com/hashicorp/terraform/dag"
|
||||||
|
|
||||||
|
// TargetsTransformer is a GraphTransformer that, when the user specifies a
|
||||||
|
// list of resources to target, limits the graph to only those resources and
|
||||||
|
// their dependencies.
|
||||||
|
type TargetsTransformer struct {
|
||||||
|
// List of targeted resource names specified by the user
|
||||||
|
Targets []string
|
||||||
|
|
||||||
|
// Set to true when we're in a `terraform destroy` or a
|
||||||
|
// `terraform plan -destroy`
|
||||||
|
Destroy bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TargetsTransformer) Transform(g *Graph) error {
|
||||||
|
if len(t.Targets) > 0 {
|
||||||
|
// TODO: duplicated in OrphanTransformer; pull up parsing earlier
|
||||||
|
addrs, err := t.parseTargetAddresses()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetedNodes, err := t.selectTargetedNodes(g, addrs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range g.Vertices() {
|
||||||
|
if targetedNodes.Include(v) {
|
||||||
|
} else {
|
||||||
|
g.Remove(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TargetsTransformer) parseTargetAddresses() ([]ResourceAddress, error) {
|
||||||
|
addrs := make([]ResourceAddress, len(t.Targets))
|
||||||
|
for i, target := range t.Targets {
|
||||||
|
ta, err := ParseResourceAddress(target)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
addrs[i] = *ta
|
||||||
|
}
|
||||||
|
return addrs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TargetsTransformer) selectTargetedNodes(
|
||||||
|
g *Graph, addrs []ResourceAddress) (*dag.Set, error) {
|
||||||
|
targetedNodes := new(dag.Set)
|
||||||
|
for _, v := range g.Vertices() {
|
||||||
|
// Keep all providers; they'll be pruned later if necessary
|
||||||
|
if r, ok := v.(GraphNodeProvider); ok {
|
||||||
|
targetedNodes.Add(r)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the remaining filter, we only care about addressable nodes
|
||||||
|
r, ok := v.(GraphNodeAddressable)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.nodeIsTarget(r, addrs) {
|
||||||
|
targetedNodes.Add(r)
|
||||||
|
// If the node would like to know about targets, tell it.
|
||||||
|
if n, ok := r.(GraphNodeTargetable); ok {
|
||||||
|
n.SetTargets(addrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
var deps *dag.Set
|
||||||
|
var err error
|
||||||
|
if t.Destroy {
|
||||||
|
deps, err = g.Descendents(r)
|
||||||
|
} else {
|
||||||
|
deps, err = g.Ancestors(r)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range deps.List() {
|
||||||
|
targetedNodes.Add(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return targetedNodes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TargetsTransformer) nodeIsTarget(
|
||||||
|
r GraphNodeAddressable, addrs []ResourceAddress) bool {
|
||||||
|
addr := r.ResourceAddress()
|
||||||
|
for _, targetAddr := range addrs {
|
||||||
|
if targetAddr.Equals(addr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTargetsTransformer(t *testing.T) {
|
||||||
|
mod := testModule(t, "transform-targets-basic")
|
||||||
|
|
||||||
|
g := Graph{Path: RootModulePath}
|
||||||
|
{
|
||||||
|
tf := &ConfigTransformer{Module: mod}
|
||||||
|
if err := tf.Transform(&g); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
transform := &TargetsTransformer{Targets: []string{"aws_instance.me"}}
|
||||||
|
if err := transform.Transform(&g); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := strings.TrimSpace(g.String())
|
||||||
|
expected := strings.TrimSpace(`
|
||||||
|
aws_instance.me
|
||||||
|
aws_subnet.me
|
||||||
|
aws_subnet.me
|
||||||
|
aws_vpc.me
|
||||||
|
aws_vpc.me
|
||||||
|
`)
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetsTransformer_destroy(t *testing.T) {
|
||||||
|
mod := testModule(t, "transform-targets-destroy")
|
||||||
|
|
||||||
|
g := Graph{Path: RootModulePath}
|
||||||
|
{
|
||||||
|
tf := &ConfigTransformer{Module: mod}
|
||||||
|
if err := tf.Transform(&g); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
transform := &TargetsTransformer{
|
||||||
|
Targets: []string{"aws_instance.me"},
|
||||||
|
Destroy: true,
|
||||||
|
}
|
||||||
|
if err := transform.Transform(&g); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := strings.TrimSpace(g.String())
|
||||||
|
expected := strings.TrimSpace(`
|
||||||
|
aws_elb.me
|
||||||
|
aws_instance.me
|
||||||
|
aws_instance.me
|
||||||
|
aws_instance.metoo
|
||||||
|
aws_instance.me
|
||||||
|
`)
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue