Merge branch 'master' into gce_autoscaling

This commit is contained in:
Dave Cunningham 2015-07-20 14:25:26 -04:00
commit c6f0bf479b
577 changed files with 41121 additions and 8832 deletions

View File

@ -1,20 +1,266 @@
## 0.5.1 (unreleased) ## 0.6.1 (Unreleased)
FEATURES:
* **New resource: `google_container_cluster`** [GH-2357]
* **New resource: `aws_vpc_endpoint`** [GH-2695]
BUG FIXES:
* core: don't prompt for variables with defaults [GH-2613]
* core: Return correct number of planned updates [GH-2620]
* core: Fix "provider not found" error that can occur while running
a destroy plan with grandchildren modules [GH-2755]
* connection/ssh: fix issue on machines with an SSH Agent available
preventing `key_file` from being read without explicitly
setting `agent = false` [GH-2615]
* provider/aws: Allow uppercase characters in `aws_elb.name` [GH-2580]
* provider/aws: Allow underscores in `aws_db_subnet_group.name` (undocumented by AWS) [GH-2604]
* provider/aws: Allow dots in `aws_db_subnet_group.name` (undocumented by AWS) [GH-2665]
* provider/aws: Fix issue with pending Spot Instance requests [GH-2640]
* provider/aws: Fix issue in AWS Classic environment with referencing external
Security Groups [GH-2644]
* provider/aws: Bump internet gateway detach timeout [GH-2669]
* provider/aws: `ecs_cluster` rename (recreation) and deletion is handled correctly [GH-2698]
* provider/aws: `aws_route_table` ignores routes generated for VPC endpoints [GH-2695]
* provider/aws: Fix issue with Launch Configurations and enable_monitoring [GH-2735]
* provider/openstack: allow empty api_key and endpoint_type [GH-2626]
IMPROVEMENTS: IMPROVEMENTS:
* connection/ssh: Print SSH bastion host details to output [GH-2684]
* provider/aws: Create RDS databases from snapshots [GH-2062]
* provider/aws: Add support for restoring from Redis backup stored in S3 [GH-2634]
* provider/aws: Add `maintenance_window` to ElastiCache cluster [GH-2642]
* provider/aws: Availability Zones are optional when specifying VPC Zone Identifiers in
Auto Scaling Groups updates [GH-2724]
* provider/google: Add metadata_startup_script to google_compute_instance [GH-2375]
## 0.6.0 (June 30, 2015)
BACKWARDS INCOMPATIBILITIES:
* command/push: If a variable is already set within Atlas, it won't be
updated unless the `-overwrite` flag is present [GH-2373]
* connection/ssh: The `agent` field now defaults to `true` if
the `SSH_AGENT_SOCK` environment variable is present. In other words,
`ssh-agent` support is now opt-out instead of opt-in functionality. [GH-2408]
* provider/aws: If you were setting access and secret key to blank ("")
to force Terraform to load credentials from another source such as the
EC2 role, this will now error. Remove the blank lines and Terraform
will load from other sources.
* `concat()` has been repurposed to combine lists instead of strings (old behavior
of joining strings is maintained in this version but is deprecated, strings
should be combined using interpolation syntax, like "${var.foo}{var.bar}")
[GH-1790]
FEATURES:
* **New provider: `azure`** [GH-2052, GH-2053, GH-2372, GH-2380, GH-2394, GH-2515, GH-2530, GH-2562]
* **New resource: `aws_autoscaling_notification`** [GH-2197]
* **New resource: `aws_autoscaling_policy`** [GH-2201]
* **New resource: `aws_cloudwatch_metric_alarm`** [GH-2201]
* **New resource: `aws_dynamodb_table`** [GH-2121]
* **New resource: `aws_ecs_cluster`** [GH-1803]
* **New resource: `aws_ecs_service`** [GH-1803]
* **New resource: `aws_ecs_task_definition`** [GH-1803, GH-2402]
* **New resource: `aws_elasticache_parameter_group`** [GH-2276]
* **New resource: `aws_flow_log`** [GH-2384]
* **New resource: `aws_iam_group_association`** [GH-2273]
* **New resource: `aws_iam_policy_attachment`** [GH-2395]
* **New resource: `aws_lambda_function`** [GH-2170]
* **New resource: `aws_route53_delegation_set`** [GH-1999]
* **New resource: `aws_route53_health_check`** [GH-2226]
* **New resource: `aws_spot_instance_request`** [GH-2263]
* **New resource: `cloudstack_ssh_keypair`** [GH-2004]
* **New remote state backend: `swift`**: You can now store remote state in
a OpenStack Swift. [GH-2254]
* command/output: support display of module outputs [GH-2102]
* core: `keys()` and `values()` funcs for map variables [GH-2198]
* connection/ssh: SSH bastion host support and ssh-agent forwarding [GH-2425]
IMPROVEMENTS:
* core: HTTP remote state now accepts `skip_cert_verification`
option to ignore TLS cert verification. [GH-2214]
* core: S3 remote state now accepts the 'encrypt' option for SSE [GH-2405]
* core: `plan` now reports sum of resources to be changed/created/destroyed [GH-2458]
* core: Change string list representation so we can distinguish empty, single
element lists [GH-2504]
* core: Properly close provider and provisioner plugin connections [GH-2406, GH-2527]
* provider/aws: AutoScaling groups now support updating Load Balancers without
recreation [GH-2472]
* provider/aws: Allow more in-place updates for ElastiCache cluster without recreating
[GH-2469]
* provider/aws: ElastiCache Subnet Groups can be updated
without destroying first [GH-2191]
* provider/aws: Normalize `certificate_chain` in `aws_iam_server_certificate` to
prevent unnecessary replacement. [GH-2411]
* provider/aws: `aws_instance` supports `monitoring' [GH-2489]
* provider/aws: `aws_launch_configuration` now supports `enable_monitoring` [GH-2410]
* provider/aws: Show outputs after `terraform refresh` [GH-2347]
* provider/aws: Add backoff/throttling during DynamoDB creation [GH-2462]
* provider/aws: Add validation for aws_vpc.cidr_block [GH-2514]
* provider/aws: Add validation for aws_db_subnet_group.name [GH-2513]
* provider/aws: Add validation for aws_db_instance.identifier [GH-2516]
* provider/aws: Add validation for aws_elb.name [GH-2517]
* provider/aws: Add validation for aws_security_group (name+description) [GH-2518]
* provider/aws: Add validation for aws_launch_configuration [GH-2519]
* provider/aws: Add validation for aws_autoscaling_group.name [GH-2520]
* provider/aws: Add validation for aws_iam_role.name [GH-2521]
* provider/aws: Add validation for aws_iam_role_policy.name [GH-2552]
* provider/aws: Add validation for aws_iam_instance_profile.name [GH-2553]
* provider/aws: aws_auto_scaling_group.default_cooldown no longer requires
resource replacement [GH-2510]
* provider/aws: add AH and ESP protocol integers [GH-2321]
* provider/docker: `docker_container` has the `privileged`
option. [GH-2227]
* provider/openstack: allow `OS_AUTH_TOKEN` environment variable
to set the openstack `api_key` field [GH-2234]
* provider/openstack: Can now configure endpoint type (public, admin,
internal) [GH-2262]
* provider/cloudstack: `cloudstack_instance` now supports projects [GH-2115]
* provisioner/chef: Added a `os_type` to specifically specify the target OS [GH-2483]
* provisioner/chef: Added a `ohai_hints` option to upload hint files [GH-2487]
BUG FIXES:
* core: lifecycle `prevent_destroy` can be any value that can be
coerced into a bool [GH-2268]
* core: matching provider types in sibling modules won't override
each other's config. [GH-2464]
* core: computed provider configurations now properly validate [GH-2457]
* core: orphan (commented out) resource dependencies are destroyed in
the correct order [GH-2453]
* core: validate object types in plugins are actually objects [GH-2450]
* core: fix `-no-color` flag in subcommands [GH-2414]
* core: Fix error of 'attribute not found for variable' when a computed
resource attribute is used as a parameter to a module [GH-2477]
* core: moduled orphans will properly inherit provider configs [GH-2476]
* core: modules with provider aliases work properly if the parent
doesn't implement those aliases [GH-2475]
* core: unknown resource attributes passed in as parameters to modules
now error [GH-2478]
* core: better error messages for missing variables [GH-2479]
* core: removed set items now properly appear in diffs and applies [GH-2507]
* core: '*' will not be added as part of the variable name when you
attempt multiplication without a space [GH-2505]
* core: fix target dependency calculation across module boundaries [GH-2555]
* command/*: fixed bug where variable input was not asked for unset
vars if terraform.tfvars existed [GH-2502]
* command/apply: prevent output duplication when reporting errors [GH-2267]
* command/apply: destroyed orphan resources are properly counted [GH-2506]
* provider/aws: loading credentials from the environment (vars, EC2 role,
etc.) is more robust and will not ask for credentials from stdin [GH-1841]
* provider/aws: fix panic when route has no `cidr_block` [GH-2215]
* provider/aws: fix issue preventing destruction of IAM Roles [GH-2177]
* provider/aws: fix issue where Security Group Rules could collide and fail
to save to the state file correctly [GH-2376]
* provider/aws: fix issue preventing destruction self referencing Securtity
Group Rules [GH-2305]
* provider/aws: fix issue causing perpetual diff on ELB listeners
when non-lowercase protocol strings were used [GH-2246]
* provider/aws: corrected frankfurt S3 website region [GH-2259]
* provider/aws: `aws_elasticache_cluster` port is required [GH-2160]
* provider/aws: Handle AMIs where RootBlockDevice does not appear in the
BlockDeviceMapping, preventing root_block_device from working [GH-2271]
* provider/aws: fix `terraform show` with remote state [GH-2371]
* provider/aws: detect `instance_type` drift on `aws_instance` [GH-2374]
* provider/aws: fix crash when `security_group_rule` referenced non-existent
security group [GH-2434]
* provider/aws: `aws_launch_configuration` retries if IAM instance
profile is not ready yet. [GH-2452]
* provider/aws: `fqdn` is populated during creation for `aws_route53_record` [GH-2528]
* provider/aws: retry VPC delete on DependencyViolation due to eventual
consistency [GH-2532]
* provider/aws: VPC peering connections in "failed" state are deleted [GH-2544]
* provider/aws: EIP deletion works if it was manually disassociated [GH-2543]
* provider/aws: `elasticache_subnet_group.subnet_ids` is now a required argument [GH-2534]
* provider/aws: handle nil response from VPN connection describes [GH-2533]
* provider/cloudflare: manual record deletion doesn't cause error [GH-2545]
* provider/digitalocean: handle case where droplet is deleted outside of
terraform [GH-2497]
* provider/dme: No longer an error if record deleted manually [GH-2546]
* provider/docker: Fix issues when using containers with links [GH-2327]
* provider/openstack: fix panic case if API returns nil network [GH-2448]
* provider/template: fix issue causing "unknown variable" rendering errors
when an existing set of template variables is changed [GH-2386]
* provisioner/chef: improve the decoding logic to prevent parameter not found errors [GH-2206]
## 0.5.3 (June 1, 2015)
IMPROVEMENTS:
* **New resource: `aws_kinesis_stream`** [GH-2110]
* **New resource: `aws_iam_server_certificate`** [GH-2086]
* **New resource: `aws_sqs_queue`** [GH-1939]
* **New resource: `aws_sns_topic`** [GH-1974]
* **New resource: `aws_sns_topic_subscription`** [GH-1974]
* **New resource: `aws_volume_attachment`** [GH-2050]
* **New resource: `google_storage_bucket`** [GH-2060]
* provider/aws: support ec2 termination protection [GH-1988]
* provider/aws: support for RDS Read Replicas [GH-1946]
* provider/aws: `aws_s3_bucket` add support for `policy` [GH-1992]
* provider/aws: `aws_ebs_volume` add support for `tags` [GH-2135]
* provider/aws: `aws_elasticache_cluster` Confirm node status before reporting
available
* provider/aws: `aws_network_acl` Add support for ICMP Protocol [GH-2148]
* provider/aws: New `force_destroy` parameter for S3 buckets, to destroy
Buckets that contain objects [GH-2007]
* provider/aws: switching `health_check_type` on ASGs no longer requires
resource refresh [GH-2147]
* provider/aws: ignore empty `vpc_security_group_ids` on `aws_instance` [GH-2311]
BUG FIXES:
* provider/aws: Correctly handle AWS keypairs which no longer exist [GH-2032]
* provider/aws: Fix issue with restoring an Instance from snapshot ID [GH-2120]
* provider/template: store relative path in the state [GH-2038]
* provisioner/chef: fix interpolation in the Chef provisioner [GH-2168]
* provisioner/remote-exec: Don't prepend shebang on scripts that already
have one [GH-2041]
## 0.5.2 (May 15, 2015)
FEATURES:
* **Chef provisioning**: You can now provision new hosts (both Linux and
Windows) with [Chef](https://chef.io) using a native provisioner [GH-1868]
IMPROVEMENTS:
* **New config function: `formatlist`** - Format lists in a similar way to `format`.
Useful for creating URLs from a list of IPs. [GH-1829]
* **New resource: `aws_route53_zone_association`**
* provider/aws: `aws_autoscaling_group` can wait for capacity in ELB
via `min_elb_capacity` [GH-1970]
* provider/aws: `aws_db_instances` supports `license_model` [GH-1966]
* provider/aws: `aws_elasticache_cluster` add support for Tags [GH-1965]
* provider/aws: `aws_network_acl` Network ACLs can be applied to multiple subnets [GH-1931]
* provider/aws: `aws_s3_bucket` exports `hosted_zone_id` and `region` [GH-1865] * provider/aws: `aws_s3_bucket` exports `hosted_zone_id` and `region` [GH-1865]
* provider/aws: `aws_s3_bucket` add support for website `redirect_all_requests_to` [GH-1909]
* provider/aws: `aws_route53_record` exports `fqdn` [GH-1847] * provider/aws: `aws_route53_record` exports `fqdn` [GH-1847]
* provider/aws: `aws_route53_zone` can create private hosted zones [GH-1526]
* provider/google: `google_compute_instance` `scratch` attribute added [GH-1920] * provider/google: `google_compute_instance` `scratch` attribute added [GH-1920]
BUG FIXES: BUG FIXES:
* core: fix "resource not found" for interpolation issues with modules * core: fix "resource not found" for interpolation issues with modules
* core: fix unflattenable error for orphans [GH-1922] * core: fix unflattenable error for orphans [GH-1922]
* core: fix deadlock with create-before-destroy + modules [GH-1949]
* core: fix "no roots found" error with create-before-destroy [GH-1953]
* core: variables set with environment variables won't validate as
not set without a default [GH-1930]
* core: resources with a blank ID in the state are now assumed to not exist [GH-1905]
* command/push: local vars override remote ones [GH-1881] * command/push: local vars override remote ones [GH-1881]
* provider/aws: Mark `aws_security_group` description as `ForceNew` [GH-1871] * provider/aws: Mark `aws_security_group` description as `ForceNew` [GH-1871]
* provider/aws: `aws_db_instance` ARN value is correct [GH-1910] * provider/aws: `aws_db_instance` ARN value is correct [GH-1910]
* provider/aws: `aws_db_instance` only submit modify request if there * provider/aws: `aws_db_instance` only submit modify request if there
is a change. [GH-1906] is a change. [GH-1906]
* provider/aws: `aws_elasticache_cluster` export missing information on cluster nodes [GH-1965]
* provider/aws: bad AMI on a launch configuration won't block refresh [GH-1901]
* provider/aws: `aws_security_group` + `aws_subnet` - destroy timeout increased
to prevent DependencyViolation errors. [GH-1886]
* provider/google: `google_compute_instance` Local SSDs no-longer cause crash * provider/google: `google_compute_instance` Local SSDs no-longer cause crash
[GH-1088] [GH-1088]
* provider/google: `google_http_health_check` Defaults now driven from Terraform, * provider/google: `google_http_health_check` Defaults now driven from Terraform,
@ -23,6 +269,10 @@ BUG FIXES:
definition to match changes to Instance [GH-980] definition to match changes to Instance [GH-980]
* provider/template: Fix infinite diff [GH-1898] * provider/template: Fix infinite diff [GH-1898]
## 0.5.1 (never released)
This version was never released since we accidentally skipped it!
## 0.5.0 (May 7, 2015) ## 0.5.0 (May 7, 2015)
BACKWARDS INCOMPATIBILITIES: BACKWARDS INCOMPATIBILITIES:
@ -77,7 +327,7 @@ IMPROVEMENTS:
* **New resource: `google_dns_record_set`** * **New resource: `google_dns_record_set`**
* **Migrate to upstream AWS SDK:** Migrate the AWS provider to * **Migrate to upstream AWS SDK:** Migrate the AWS provider to
[awslabs/aws-sdk-go](https://github.com/awslabs/aws-sdk-go), [awslabs/aws-sdk-go](https://github.com/awslabs/aws-sdk-go),
the offical `awslabs` library. Previously we had forked the library for the official `awslabs` library. Previously we had forked the library for
stability while `awslabs` refactored. Now that work has completed, and we've stability while `awslabs` refactored. Now that work has completed, and we've
migrated back to the upstream version. migrated back to the upstream version.
* core: Improve error message on diff mismatch [GH-1501] * core: Improve error message on diff mismatch [GH-1501]
@ -105,7 +355,7 @@ IMPROVEMENTS:
* provider/aws: `aws_network_acl` improved validation for network ACL ports * provider/aws: `aws_network_acl` improved validation for network ACL ports
and protocols [GH-1798] [GH-1808] and protocols [GH-1798] [GH-1808]
* provider/aws: `aws_route_table` can target network interfaces [GH-968] * provider/aws: `aws_route_table` can target network interfaces [GH-968]
* provider/aws: `aws_route_table` can specify propogating VGWs [GH-1516] * provider/aws: `aws_route_table` can specify propagating VGWs [GH-1516]
* provider/aws: `aws_route53_record` supports weighted sets [GH-1578] * provider/aws: `aws_route53_record` supports weighted sets [GH-1578]
* provider/aws: `aws_route53_zone` exports nameservers [GH-1525] * provider/aws: `aws_route53_zone` exports nameservers [GH-1525]
* provider/aws: `aws_s3_bucket` website support [GH-1738] * provider/aws: `aws_s3_bucket` website support [GH-1738]
@ -262,7 +512,7 @@ FEATURES:
* **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), * **New AWS SDK:** Move to `aws-sdk-go` (hashicorp/aws-sdk-go),
a fork of the offical `awslabs` repo. We forked for stability while a fork of the official `awslabs` repo. We forked for stability while
`awslabs` refactored the library, and will move back to the officially `awslabs` refactored the library, and will move back to the officially
supported version in the next release. supported version in the next release.
@ -291,7 +541,7 @@ IMPROVEMENTS:
* providers/aws: Improve dependency violation error handling, when deleting * providers/aws: Improve dependency violation error handling, when deleting
Internet Gateways or Auto Scaling groups [GH-1325]. Internet Gateways or Auto Scaling groups [GH-1325].
* provider/aws: Add non-destructive updates to AWS RDS. You can now upgrade * provider/aws: Add non-destructive updates to AWS RDS. You can now upgrade
`egine_version`, `parameter_group_name`, and `multi_az` without forcing `engine_version`, `parameter_group_name`, and `multi_az` without forcing
a new database to be created.[GH-1341] a new database to be created.[GH-1341]
* providers/aws: Full support for block device mappings on instances and * providers/aws: Full support for block device mappings on instances and
launch configurations [GH-1045, GH-1364] launch configurations [GH-1045, GH-1364]
@ -797,5 +1047,3 @@ BUG FIXES:
## 0.1.0 (July 28, 2014) ## 0.1.0 (July 28, 2014)
* Initial release * Initial release

View File

@ -15,6 +15,10 @@ dev: generate
quickdev: generate quickdev: generate
@TF_QUICKDEV=1 TF_DEV=1 sh -c "'$(CURDIR)/scripts/build.sh'" @TF_QUICKDEV=1 TF_DEV=1 sh -c "'$(CURDIR)/scripts/build.sh'"
release: updatedeps
gox -build-toolchain
@$(MAKE) bin
# test runs the unit tests and vets the code # test runs the unit tests and vets the code
test: generate test: generate
TF_ACC= go test $(TEST) $(TESTARGS) -timeout=30s -parallel=4 TF_ACC= go test $(TEST) $(TESTARGS) -timeout=30s -parallel=4
@ -23,10 +27,11 @@ test: generate
# testacc runs acceptance tests # testacc runs acceptance tests
testacc: generate testacc: generate
@if [ "$(TEST)" = "./..." ]; then \ @if [ "$(TEST)" = "./..." ]; then \
echo "ERROR: Set TEST to a specific package"; \ echo "ERROR: Set TEST to a specific package. For example,"; \
echo " make testacc TEST=./builtin/providers/aws"; \
exit 1; \ exit 1; \
fi fi
TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 45m TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 90m
# testrace runs the race checker # testrace runs the race checker
testrace: generate testrace: generate
@ -61,7 +66,8 @@ vet:
@go tool vet $(VETARGS) . ; if [ $$? -eq 1 ]; then \ @go tool vet $(VETARGS) . ; if [ $$? -eq 1 ]; then \
echo ""; \ echo ""; \
echo "Vet found suspicious constructs. Please check the reported constructs"; \ echo "Vet found suspicious constructs. Please check the reported constructs"; \
echo "and fix them if necessary before submitting the code for reviewal."; \ echo "and fix them if necessary before submitting the code for review."; \
exit 1; \
fi fi
# generate runs `go generate` to build the dynamically generated # generate runs `go generate` to build the dynamically generated

View File

@ -31,7 +31,7 @@ Developing Terraform
If you wish to work on Terraform itself or any of its built-in providers, you'll first need [Go](http://www.golang.org) installed on your machine (version 1.4+ is *required*). Alternatively, you can use the Vagrantfile in the root of this repo to stand up a virtual machine with the appropriate dev tooling already set up for you. If you wish to work on Terraform itself or any of its built-in providers, you'll first need [Go](http://www.golang.org) installed on your machine (version 1.4+ is *required*). Alternatively, you can use the Vagrantfile in the root of this repo to stand up a virtual machine with the appropriate dev tooling already set up for you.
For local dev first make sure Go is properly installed, including setting up a [GOPATH](http://golang.org/doc/code.html#GOPATH). Next, install the following software packages, which are needed for some dependencies: For local dev first make sure Go is properly installed, including setting up a [GOPATH](http://golang.org/doc/code.html#GOPATH). You will also need to add `$GOPATH/bin` to your `$PATH`. Next, install the following software packages, which are needed for some dependencies:
- [Git](http://git-scm.com/) - [Git](http://git-scm.com/)
- [Mercurial](http://mercurial.selenic.com/) - [Mercurial](http://mercurial.selenic.com/)
@ -74,7 +74,7 @@ To run the acceptance tests, invoke `make testacc`:
```sh ```sh
$ make testacc TEST=./builtin/providers/aws TESTARGS='-run=Vpc' $ make testacc TEST=./builtin/providers/aws TESTARGS='-run=Vpc'
go generate ./... go generate ./...
TF_ACC=1 go test ./builtin/providers/aws -v -run=Vpc -timeout 45m TF_ACC=1 go test ./builtin/providers/aws -v -run=Vpc -timeout 90m
=== RUN TestAccVpc_basic === RUN TestAccVpc_basic
2015/02/10 14:11:17 [INFO] Test: Using us-west-2 as test region 2015/02/10 14:11:17 [INFO] Test: Using us-west-2 as test region
[...] [...]

84
RELEASING.md Normal file
View File

@ -0,0 +1,84 @@
# Releasing Terraform
This document contains details about the Terraform release process.
## Schedule
Terraform currently has no fixed release schedule, the HashiCorp maintainers
can usually give a feel for roughly when the next release is planned.
## Versioning
As a pre-1.0 project, we use the MINOR and PATCH versions as follows:
* a `MINOR` version increment indicates a release that may contain backwards
incompatible changes
* a `PATCH` version increment indicates a release that may contain bugfixes as
well as additive (backwards compatible) features and enhancements
## Process
For maintainer documentation purposes, here is the current release process:
```sh
# Spin up a fresh build VM
vagrant destroy -f
vagrant up
vagrant ssh
cd /opt/gopath/src/github.com/hashicorp/terraform/
# Fetch dependencies
make updatedeps
# Verify unit tests pass
make test
# Prep release commit
export VERSION="vX.Y.Z"
# Edit CHANGELOG.md, adding current date to unreleased version header
# Edit version.go, setting VersionPrelease to empty string
# Snapshot dependency information
go get github.com/tools/godep
godep save ./...
cp Godeps/Godeps.json deps/$(echo $VERSION | sed 's/\./-/g').json
# Make and tag release commit (skipping Godeps dir)
git add CHANGELOG.md terraform/version.go deps/
git commit -a -m "${VERSION}"
git tag -m "${VERSION}" "${VERSION}"
# Build the release
make release
# Make an archive with vendored dependencies
stashName=$(git stash)
git archive -o terraform-$VERSION-src.tar.gz $stashName
# Zip and push release to bintray
export BINTRAY_API_KEY="..."
./scripts/dist "X.Y.Z" # no `v` prefix here
# -- "Point of no return" --
# -- Process can be aborted safely at any point before this --
# Push the release commit and tag
git push origin master
git push origin vX.Y.Z
# Click "publish" on the release from the Bintray Web UI
# Upload terraform-$VERSION-src.tar.gz as a file to the GitHub release.
# -- Release is complete! --
# Start release branch (to be used for reproducible builds and docs updates)
git checkout -b release/$VERSION
git push origin release/$VERSION
# Clean up master
git checkout master
# Set VersionPrerelease to "dev"
# Add new CHANGELOG section for next release
git add -A
git commit -m "release: clean up after ${VERSION}"
```

56
Vagrantfile vendored
View File

@ -5,34 +5,52 @@
VAGRANTFILE_API_VERSION = "2" VAGRANTFILE_API_VERSION = "2"
$script = <<SCRIPT $script = <<SCRIPT
# Install Go and prerequisites SRCROOT="/opt/go"
apt-get -qq update SRCPATH="/opt/gopath"
apt-get -qq install build-essential curl git-core libpcre3-dev mercurial pkg-config zip
hg clone -u release https://code.google.com/p/go /opt/go
cd /opt/go/src && ./all.bash
# Setup the GOPATH # Get the ARCH
mkdir -p /opt/gopath ARCH=`uname -m | sed 's|i686|386|' | sed 's|x86_64|amd64|'`
cat <<EOF >/etc/profile.d/gopath.sh
export GOPATH="/opt/gopath" # Install Prereq Packages
export PATH="/opt/go/bin:\$GOPATH/bin:\$PATH" sudo apt-get update
sudo apt-get install -y build-essential curl git-core libpcre3-dev mercurial pkg-config zip
# Install Go
cd /tmp
wget -q https://storage.googleapis.com/golang/go1.4.2.linux-${ARCH}.tar.gz
tar -xf go1.4.2.linux-${ARCH}.tar.gz
sudo mv go $SRCROOT
sudo chmod 775 $SRCROOT
sudo chown vagrant:vagrant $SRCROOT
# Setup the GOPATH; even though the shared folder spec gives the working
# directory the right user/group, we need to set it properly on the
# parent path to allow subsequent "go get" commands to work.
sudo mkdir -p $SRCPATH
sudo chown -R vagrant:vagrant $SRCPATH 2>/dev/null || true
# ^^ silencing errors here because we expect this to fail for the shared folder
cat <<EOF >/tmp/gopath.sh
export GOPATH="$SRCPATH"
export GOROOT="$SRCROOT"
export PATH="$SRCROOT/bin:$SRCPATH/bin:\$PATH"
EOF EOF
sudo mv /tmp/gopath.sh /etc/profile.d/gopath.sh
# Make sure the GOPATH is usable by vagrant sudo chmod 0755 /etc/profile.d/gopath.sh
chown -R vagrant:vagrant /opt/go source /etc/profile.d/gopath.sh
chown -R vagrant:vagrant /opt/gopath
SCRIPT SCRIPT
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "chef/ubuntu-12.04" config.vm.box = "chef/ubuntu-12.04"
config.vm.provision "shell", inline: $script config.vm.provision "shell", inline: $script, privileged: false
config.vm.synced_folder '.', '/opt/gopath/src/github.com/hashicorp/terraform'
["vmware_fusion", "vmware_workstation"].each do |p| ["vmware_fusion", "vmware_workstation"].each do |p|
config.vm.provider "p" do |v| config.vm.provider p do |v|
v.vmx["memsize"] = "2048" v.vmx["memsize"] = "4096"
v.vmx["numvcpus"] = "2" v.vmx["numvcpus"] = "4"
v.vmx["cpuid.coresPerSocket"] = "1" v.vmx['cpuid.coresPerSocket'] = '2'
end end
end end
end end

View File

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

View File

@ -0,0 +1 @@
package main

View File

@ -0,0 +1,15 @@
package main
import (
"github.com/hashicorp/terraform/builtin/provisioners/chef"
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: func() terraform.ResourceProvisioner {
return new(chef.ResourceProvisioner)
},
})
}

View File

@ -0,0 +1 @@
package main

View File

@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"log" "log"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )

View File

@ -5,7 +5,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )

View File

@ -7,16 +7,23 @@ import (
"github.com/hashicorp/terraform/helper/multierror" "github.com/hashicorp/terraform/helper/multierror"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/awslabs/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/awslabs/aws-sdk-go/service/elasticache" "github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/awslabs/aws-sdk-go/service/elb" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/ecs"
"github.com/awslabs/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/service/elasticache"
"github.com/awslabs/aws-sdk-go/service/route53" "github.com/aws/aws-sdk-go/service/elb"
"github.com/awslabs/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/kinesis"
"github.com/aws/aws-sdk-go/service/lambda"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/sns"
"github.com/aws/aws-sdk-go/service/sqs"
) )
type Config struct { type Config struct {
@ -31,15 +38,22 @@ type Config struct {
} }
type AWSClient struct { type AWSClient struct {
cloudwatchconn *cloudwatch.CloudWatch
dynamodbconn *dynamodb.DynamoDB
ec2conn *ec2.EC2 ec2conn *ec2.EC2
ecsconn *ecs.ECS
elbconn *elb.ELB elbconn *elb.ELB
autoscalingconn *autoscaling.AutoScaling autoscalingconn *autoscaling.AutoScaling
s3conn *s3.S3 s3conn *s3.S3
sqsconn *sqs.SQS
snsconn *sns.SNS
r53conn *route53.Route53 r53conn *route53.Route53
region string region string
rdsconn *rds.RDS rdsconn *rds.RDS
iamconn *iam.IAM iamconn *iam.IAM
kinesisconn *kinesis.Kinesis
elasticacheconn *elasticache.ElastiCache elasticacheconn *elasticache.ElastiCache
lambdaconn *lambda.Lambda
} }
// Client configures and returns a fully initailized AWSClient // Client configures and returns a fully initailized AWSClient
@ -62,34 +76,39 @@ func (c *Config) Client() (interface{}, error) {
client.region = c.Region client.region = c.Region
log.Println("[INFO] Building AWS auth structure") log.Println("[INFO] Building AWS auth structure")
creds := credentials.NewChainCredentials([]credentials.Provider{ // We fetched all credential sources in Provider. If they are
&credentials.StaticProvider{Value: credentials.Value{ // available, they'll already be in c. See Provider definition.
AccessKeyID: c.AccessKey, creds := credentials.NewStaticCredentials(c.AccessKey, c.SecretKey, c.Token)
SecretAccessKey: c.SecretKey,
SessionToken: c.Token,
}},
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{Filename: "", Profile: ""},
&credentials.EC2RoleProvider{},
})
awsConfig := &aws.Config{ awsConfig := &aws.Config{
Credentials: creds, Credentials: creds,
Region: c.Region, Region: c.Region,
MaxRetries: c.MaxRetries, MaxRetries: c.MaxRetries,
} }
log.Println("[INFO] Initializing DynamoDB connection")
client.dynamodbconn = dynamodb.New(awsConfig)
log.Println("[INFO] Initializing ELB connection") log.Println("[INFO] Initializing ELB connection")
client.elbconn = elb.New(awsConfig) client.elbconn = elb.New(awsConfig)
log.Println("[INFO] Initializing S3 connection") log.Println("[INFO] Initializing S3 connection")
client.s3conn = s3.New(awsConfig) client.s3conn = s3.New(awsConfig)
log.Println("[INFO] Initializing SQS connection")
client.sqsconn = sqs.New(awsConfig)
log.Println("[INFO] Initializing SNS connection")
client.snsconn = sns.New(awsConfig)
log.Println("[INFO] Initializing RDS Connection") log.Println("[INFO] Initializing RDS Connection")
client.rdsconn = rds.New(awsConfig) client.rdsconn = rds.New(awsConfig)
log.Println("[INFO] Initializing IAM Connection") log.Println("[INFO] Initializing IAM Connection")
client.iamconn = iam.New(awsConfig) client.iamconn = iam.New(awsConfig)
log.Println("[INFO] Initializing Kinesis Connection")
client.kinesisconn = kinesis.New(awsConfig)
err := c.ValidateAccountId(client.iamconn) err := c.ValidateAccountId(client.iamconn)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
@ -101,6 +120,9 @@ func (c *Config) Client() (interface{}, error) {
log.Println("[INFO] Initializing EC2 Connection") log.Println("[INFO] Initializing EC2 Connection")
client.ec2conn = ec2.New(awsConfig) client.ec2conn = ec2.New(awsConfig)
log.Println("[INFO] Initializing ECS Connection")
client.ecsconn = ecs.New(awsConfig)
// aws-sdk-go uses v4 for signing requests, which requires all global // aws-sdk-go uses v4 for signing requests, which requires all global
// endpoints to use 'us-east-1'. // endpoints to use 'us-east-1'.
// See http://docs.aws.amazon.com/general/latest/gr/sigv4_changes.html // See http://docs.aws.amazon.com/general/latest/gr/sigv4_changes.html
@ -113,6 +135,12 @@ func (c *Config) Client() (interface{}, error) {
log.Println("[INFO] Initializing Elasticache Connection") log.Println("[INFO] Initializing Elasticache Connection")
client.elasticacheconn = elasticache.New(awsConfig) client.elasticacheconn = elasticache.New(awsConfig)
log.Println("[INFO] Initializing Lambda Connection")
client.lambdaconn = lambda.New(awsConfig)
log.Println("[INFO] Initializing CloudWatch SDK connection")
client.cloudwatchconn = cloudwatch.New(awsConfig)
} }
if len(errs) > 0 { if len(errs) > 0 {

View File

@ -5,8 +5,8 @@ import (
"net" "net"
"strconv" "strconv"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
) )
func expandNetworkAclEntries(configured []interface{}, entryType string) ([]*ec2.NetworkACLEntry, error) { func expandNetworkAclEntries(configured []interface{}, entryType string) ([]*ec2.NetworkACLEntry, error) {
@ -34,6 +34,18 @@ func expandNetworkAclEntries(configured []interface{}, entryType string) ([]*ec2
RuleNumber: aws.Long(int64(data["rule_no"].(int))), RuleNumber: aws.Long(int64(data["rule_no"].(int))),
CIDRBlock: aws.String(data["cidr_block"].(string)), CIDRBlock: aws.String(data["cidr_block"].(string)),
} }
// Specify additional required fields for ICMP
if p == 1 {
e.ICMPTypeCode = &ec2.ICMPTypeCode{}
if v, ok := data["icmp_code"]; ok {
e.ICMPTypeCode.Code = aws.Long(int64(v.(int)))
}
if v, ok := data["icmp_type"]; ok {
e.ICMPTypeCode.Type = aws.Long(int64(v.(int)))
}
}
entries = append(entries, e) entries = append(entries, e)
} }
return entries, nil return entries, nil
@ -60,6 +72,9 @@ func flattenNetworkAclEntries(list []*ec2.NetworkACLEntry) []map[string]interfac
func protocolIntegers() map[string]int { func protocolIntegers() map[string]int {
var protocolIntegers = make(map[string]int) var protocolIntegers = make(map[string]int)
protocolIntegers = map[string]int{ protocolIntegers = map[string]int{
// defined at https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
"ah": 51,
"esp": 50,
"udp": 17, "udp": 17,
"tcp": 6, "tcp": 6,
"icmp": 1, "icmp": 1,

View File

@ -4,8 +4,8 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
) )
func Test_expandNetworkACLEntry(t *testing.T) { func Test_expandNetworkACLEntry(t *testing.T) {

View File

@ -1,6 +1,11 @@
package aws package aws
import ( import (
"net"
"sync"
"time"
"github.com/awslabs/aws-sdk-go/aws/credentials"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
@ -11,35 +16,95 @@ func Provider() terraform.ResourceProvider {
// TODO: Move the validation to this, requires conditional schemas // TODO: Move the validation to this, requires conditional schemas
// TODO: Move the configuration to this, requires validation // TODO: Move the configuration to this, requires validation
// These variables are closed within the `getCreds` function below.
// This function is responsible for reading credentials from the
// environment in the case that they're not explicitly specified
// in the Terraform configuration.
//
// By using the getCreds function here instead of making the default
// empty, we avoid asking for input on credentials if they're available
// in the environment.
var credVal credentials.Value
var credErr error
var once sync.Once
getCreds := func() {
// Build the list of providers to look for creds in
providers := []credentials.Provider{
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{},
}
// We only look in the EC2 metadata API if we can connect
// to the metadata service within a reasonable amount of time
conn, err := net.DialTimeout("tcp", "169.254.169.254:80", 100*time.Millisecond)
if err == nil {
conn.Close()
providers = append(providers, &credentials.EC2RoleProvider{})
}
credVal, credErr = credentials.NewChainCredentials(providers).Get()
// If we didn't successfully find any credentials, just
// set the error to nil.
if credErr == credentials.ErrNoValidProvidersFoundInChain {
credErr = nil
}
}
// getCredDefault is a function used by DefaultFunc below to
// get the default value for various parts of the credentials.
// This function properly handles loading the credentials, checking
// for errors, etc.
getCredDefault := func(def interface{}, f func() string) (interface{}, error) {
once.Do(getCreds)
// If there was an error, that is always first
if credErr != nil {
return nil, credErr
}
// If the value is empty string, return nil (not set)
val := f()
if val == "" {
return def, nil
}
return val, nil
}
// The actual provider
return &schema.Provider{ return &schema.Provider{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"access_key": &schema.Schema{ "access_key": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{ DefaultFunc: func() (interface{}, error) {
"AWS_ACCESS_KEY", return getCredDefault(nil, func() string {
"AWS_ACCESS_KEY_ID", return credVal.AccessKeyID
}, nil), })
},
Description: descriptions["access_key"], Description: descriptions["access_key"],
}, },
"secret_key": &schema.Schema{ "secret_key": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{ DefaultFunc: func() (interface{}, error) {
"AWS_SECRET_KEY", return getCredDefault(nil, func() string {
"AWS_SECRET_ACCESS_KEY", return credVal.SecretAccessKey
}, nil), })
},
Description: descriptions["secret_key"], Description: descriptions["secret_key"],
}, },
"token": &schema.Schema{ "token": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{ DefaultFunc: func() (interface{}, error) {
"AWS_SESSION_TOKEN", return getCredDefault("", func() string {
"AWS_SECURITY_TOKEN", return credVal.SessionToken
}, ""), })
},
Description: descriptions["token"], Description: descriptions["token"],
}, },
@ -85,47 +150,70 @@ func Provider() terraform.ResourceProvider {
ResourcesMap: map[string]*schema.Resource{ ResourcesMap: map[string]*schema.Resource{
"aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(), "aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(),
"aws_autoscaling_group": resourceAwsAutoscalingGroup(), "aws_autoscaling_group": resourceAwsAutoscalingGroup(),
"aws_autoscaling_notification": resourceAwsAutoscalingNotification(),
"aws_autoscaling_policy": resourceAwsAutoscalingPolicy(),
"aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(),
"aws_customer_gateway": resourceAwsCustomerGateway(), "aws_customer_gateway": resourceAwsCustomerGateway(),
"aws_db_instance": resourceAwsDbInstance(), "aws_db_instance": resourceAwsDbInstance(),
"aws_db_parameter_group": resourceAwsDbParameterGroup(), "aws_db_parameter_group": resourceAwsDbParameterGroup(),
"aws_db_security_group": resourceAwsDbSecurityGroup(), "aws_db_security_group": resourceAwsDbSecurityGroup(),
"aws_db_subnet_group": resourceAwsDbSubnetGroup(), "aws_db_subnet_group": resourceAwsDbSubnetGroup(),
"aws_dynamodb_table": resourceAwsDynamoDbTable(),
"aws_ebs_volume": resourceAwsEbsVolume(), "aws_ebs_volume": resourceAwsEbsVolume(),
"aws_ecs_cluster": resourceAwsEcsCluster(),
"aws_ecs_service": resourceAwsEcsService(),
"aws_ecs_task_definition": resourceAwsEcsTaskDefinition(),
"aws_eip": resourceAwsEip(), "aws_eip": resourceAwsEip(),
"aws_elasticache_cluster": resourceAwsElasticacheCluster(), "aws_elasticache_cluster": resourceAwsElasticacheCluster(),
"aws_elasticache_parameter_group": resourceAwsElasticacheParameterGroup(),
"aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(), "aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(),
"aws_elasticache_subnet_group": resourceAwsElasticacheSubnetGroup(), "aws_elasticache_subnet_group": resourceAwsElasticacheSubnetGroup(),
"aws_elb": resourceAwsElb(), "aws_elb": resourceAwsElb(),
"aws_flow_log": resourceAwsFlowLog(),
"aws_iam_access_key": resourceAwsIamAccessKey(), "aws_iam_access_key": resourceAwsIamAccessKey(),
"aws_iam_group_policy": resourceAwsIamGroupPolicy(), "aws_iam_group_policy": resourceAwsIamGroupPolicy(),
"aws_iam_group": resourceAwsIamGroup(), "aws_iam_group": resourceAwsIamGroup(),
"aws_iam_group_membership": resourceAwsIamGroupMembership(),
"aws_iam_instance_profile": resourceAwsIamInstanceProfile(), "aws_iam_instance_profile": resourceAwsIamInstanceProfile(),
"aws_iam_policy": resourceAwsIamPolicy(), "aws_iam_policy": resourceAwsIamPolicy(),
"aws_iam_policy_attachment": resourceAwsIamPolicyAttachment(),
"aws_iam_role_policy": resourceAwsIamRolePolicy(), "aws_iam_role_policy": resourceAwsIamRolePolicy(),
"aws_iam_role": resourceAwsIamRole(), "aws_iam_role": resourceAwsIamRole(),
"aws_iam_server_certificate": resourceAwsIAMServerCertificate(),
"aws_iam_user_policy": resourceAwsIamUserPolicy(), "aws_iam_user_policy": resourceAwsIamUserPolicy(),
"aws_iam_user": resourceAwsIamUser(), "aws_iam_user": resourceAwsIamUser(),
"aws_instance": resourceAwsInstance(), "aws_instance": resourceAwsInstance(),
"aws_internet_gateway": resourceAwsInternetGateway(), "aws_internet_gateway": resourceAwsInternetGateway(),
"aws_key_pair": resourceAwsKeyPair(), "aws_key_pair": resourceAwsKeyPair(),
"aws_kinesis_stream": resourceAwsKinesisStream(),
"aws_lambda_function": resourceAwsLambdaFunction(),
"aws_launch_configuration": resourceAwsLaunchConfiguration(), "aws_launch_configuration": resourceAwsLaunchConfiguration(),
"aws_lb_cookie_stickiness_policy": resourceAwsLBCookieStickinessPolicy(), "aws_lb_cookie_stickiness_policy": resourceAwsLBCookieStickinessPolicy(),
"aws_main_route_table_association": resourceAwsMainRouteTableAssociation(), "aws_main_route_table_association": resourceAwsMainRouteTableAssociation(),
"aws_network_acl": resourceAwsNetworkAcl(), "aws_network_acl": resourceAwsNetworkAcl(),
"aws_network_interface": resourceAwsNetworkInterface(), "aws_network_interface": resourceAwsNetworkInterface(),
"aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(), "aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(),
"aws_route53_delegation_set": resourceAwsRoute53DelegationSet(),
"aws_route53_record": resourceAwsRoute53Record(), "aws_route53_record": resourceAwsRoute53Record(),
"aws_route53_zone_association": resourceAwsRoute53ZoneAssociation(),
"aws_route53_zone": resourceAwsRoute53Zone(), "aws_route53_zone": resourceAwsRoute53Zone(),
"aws_route_table_association": resourceAwsRouteTableAssociation(), "aws_route53_health_check": resourceAwsRoute53HealthCheck(),
"aws_route_table": resourceAwsRouteTable(), "aws_route_table": resourceAwsRouteTable(),
"aws_route_table_association": resourceAwsRouteTableAssociation(),
"aws_s3_bucket": resourceAwsS3Bucket(), "aws_s3_bucket": resourceAwsS3Bucket(),
"aws_security_group": resourceAwsSecurityGroup(), "aws_security_group": resourceAwsSecurityGroup(),
"aws_security_group_rule": resourceAwsSecurityGroupRule(), "aws_security_group_rule": resourceAwsSecurityGroupRule(),
"aws_spot_instance_request": resourceAwsSpotInstanceRequest(),
"aws_sqs_queue": resourceAwsSqsQueue(),
"aws_sns_topic": resourceAwsSnsTopic(),
"aws_sns_topic_subscription": resourceAwsSnsTopicSubscription(),
"aws_subnet": resourceAwsSubnet(), "aws_subnet": resourceAwsSubnet(),
"aws_volume_attachment": resourceAwsVolumeAttachment(),
"aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(), "aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(),
"aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(), "aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(),
"aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(), "aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(),
"aws_vpc": resourceAwsVpc(), "aws_vpc": resourceAwsVpc(),
"aws_vpc_endpoint": resourceAwsVpcEndpoint(),
"aws_vpn_connection": resourceAwsVpnConnection(), "aws_vpn_connection": resourceAwsVpnConnection(),
"aws_vpn_connection_route": resourceAwsVpnConnectionRoute(), "aws_vpn_connection_route": resourceAwsVpnConnectionRoute(),
"aws_vpn_gateway": resourceAwsVpnGateway(), "aws_vpn_gateway": resourceAwsVpnGateway(),

View File

@ -4,8 +4,9 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/elb" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elb"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -90,7 +91,7 @@ func resourceAwsAppCookieStickinessPolicyRead(d *schema.ResourceData, meta inter
getResp, err := elbconn.DescribeLoadBalancerPolicies(request) getResp, err := elbconn.DescribeLoadBalancerPolicies(request)
if err != nil { if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "PolicyNotFound" { if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "PolicyNotFound" {
// The policy is gone. // The policy is gone.
d.SetId("") d.SetId("")
return nil return nil

View File

@ -4,14 +4,14 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/elb" "github.com/aws/aws-sdk-go/service/elb"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSAppCookieStickinessPolicy(t *testing.T) { func TestAccAWSAppCookieStickinessPolicy_basic(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,

View File

@ -9,8 +9,10 @@ import (
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/aws/aws-sdk-go/service/elb"
) )
func resourceAwsAutoscalingGroup() *schema.Resource { func resourceAwsAutoscalingGroup() *schema.Resource {
@ -25,6 +27,15 @@ func resourceAwsAutoscalingGroup() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
// https://github.com/boto/botocore/blob/9f322b1/botocore/data/autoscaling/2011-01-01/service-2.json#L1862-L1873
value := v.(string)
if len(value) > 255 {
errors = append(errors, fmt.Errorf(
"%q cannot be longer than 255 characters", k))
}
return
},
}, },
"launch_configuration": &schema.Schema{ "launch_configuration": &schema.Schema{
@ -38,6 +49,11 @@ func resourceAwsAutoscalingGroup() *schema.Resource {
Computed: true, Computed: true,
}, },
"min_elb_capacity": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"min_size": &schema.Schema{ "min_size": &schema.Schema{
Type: schema.TypeInt, Type: schema.TypeInt,
Required: true, Required: true,
@ -52,7 +68,6 @@ func resourceAwsAutoscalingGroup() *schema.Resource {
Type: schema.TypeInt, Type: schema.TypeInt,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true,
}, },
"force_delete": &schema.Schema{ "force_delete": &schema.Schema{
@ -72,13 +87,11 @@ func resourceAwsAutoscalingGroup() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true,
}, },
"availability_zones": &schema.Schema{ "availability_zones": &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
Required: true, Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString}, Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString, Set: schema.HashString,
}, },
@ -86,7 +99,6 @@ func resourceAwsAutoscalingGroup() *schema.Resource {
"load_balancers": &schema.Schema{ "load_balancers": &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString}, Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString, Set: schema.HashString,
}, },
@ -95,7 +107,6 @@ func resourceAwsAutoscalingGroup() *schema.Resource {
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString}, Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString, Set: schema.HashString,
}, },
@ -122,8 +133,11 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{})
autoScalingGroupOpts.LaunchConfigurationName = aws.String(d.Get("launch_configuration").(string)) autoScalingGroupOpts.LaunchConfigurationName = aws.String(d.Get("launch_configuration").(string))
autoScalingGroupOpts.MinSize = aws.Long(int64(d.Get("min_size").(int))) autoScalingGroupOpts.MinSize = aws.Long(int64(d.Get("min_size").(int)))
autoScalingGroupOpts.MaxSize = aws.Long(int64(d.Get("max_size").(int))) autoScalingGroupOpts.MaxSize = aws.Long(int64(d.Get("max_size").(int)))
autoScalingGroupOpts.AvailabilityZones = expandStringList(
d.Get("availability_zones").(*schema.Set).List()) // Availability Zones are optional if VPC Zone Identifer(s) are specified
if v, ok := d.GetOk("availability_zones"); ok && v.(*schema.Set).Len() > 0 {
autoScalingGroupOpts.AvailabilityZones = expandStringList(v.(*schema.Set).List())
}
if v, ok := d.GetOk("tag"); ok { if v, ok := d.GetOk("tag"); ok {
autoScalingGroupOpts.Tags = autoscalingTagsFromMap( autoScalingGroupOpts.Tags = autoscalingTagsFromMap(
@ -152,12 +166,7 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{})
} }
if v, ok := d.GetOk("vpc_zone_identifier"); ok && v.(*schema.Set).Len() > 0 { if v, ok := d.GetOk("vpc_zone_identifier"); ok && v.(*schema.Set).Len() > 0 {
exp := expandStringList(v.(*schema.Set).List()) autoScalingGroupOpts.VPCZoneIdentifier = expandVpcZoneIdentifiers(v.(*schema.Set).List())
strs := make([]string, len(exp))
for _, s := range exp {
strs = append(strs, *s)
}
autoScalingGroupOpts.VPCZoneIdentifier = aws.String(strings.Join(strs, ","))
} }
if v, ok := d.GetOk("termination_policies"); ok && v.(*schema.Set).Len() > 0 { if v, ok := d.GetOk("termination_policies"); ok && v.(*schema.Set).Len() > 0 {
@ -214,6 +223,10 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{})
AutoScalingGroupName: aws.String(d.Id()), AutoScalingGroupName: aws.String(d.Id()),
} }
if d.HasChange("default_cooldown") {
opts.DefaultCooldown = aws.Long(int64(d.Get("default_cooldown").(int)))
}
if d.HasChange("desired_capacity") { if d.HasChange("desired_capacity") {
opts.DesiredCapacity = aws.Long(int64(d.Get("desired_capacity").(int))) opts.DesiredCapacity = aws.Long(int64(d.Get("desired_capacity").(int)))
} }
@ -234,6 +247,21 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{})
opts.HealthCheckGracePeriod = aws.Long(int64(d.Get("health_check_grace_period").(int))) opts.HealthCheckGracePeriod = aws.Long(int64(d.Get("health_check_grace_period").(int)))
} }
if d.HasChange("health_check_type") {
opts.HealthCheckGracePeriod = aws.Long(int64(d.Get("health_check_grace_period").(int)))
opts.HealthCheckType = aws.String(d.Get("health_check_type").(string))
}
if d.HasChange("vpc_zone_identifier") {
opts.VPCZoneIdentifier = expandVpcZoneIdentifiers(d.Get("vpc_zone_identifier").(*schema.Set).List())
}
if d.HasChange("availability_zones") {
if v, ok := d.GetOk("availability_zones"); ok && v.(*schema.Set).Len() > 0 {
opts.AvailabilityZones = expandStringList(d.Get("availability_zones").(*schema.Set).List())
}
}
if err := setAutoscalingTags(conn, d); err != nil { if err := setAutoscalingTags(conn, d); err != nil {
return err return err
} else { } else {
@ -247,6 +275,42 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{})
return fmt.Errorf("Error updating Autoscaling group: %s", err) return fmt.Errorf("Error updating Autoscaling group: %s", err)
} }
if d.HasChange("load_balancers") {
o, n := d.GetChange("load_balancers")
if o == nil {
o = new(schema.Set)
}
if n == nil {
n = new(schema.Set)
}
os := o.(*schema.Set)
ns := n.(*schema.Set)
remove := expandStringList(os.Difference(ns).List())
add := expandStringList(ns.Difference(os).List())
if len(remove) > 0 {
_, err := conn.DetachLoadBalancers(&autoscaling.DetachLoadBalancersInput{
AutoScalingGroupName: aws.String(d.Id()),
LoadBalancerNames: remove,
})
if err != nil {
return fmt.Errorf("[WARN] Error updating Load Balancers for AutoScaling Group (%s), error: %s", d.Id(), err)
}
}
if len(add) > 0 {
_, err := conn.AttachLoadBalancers(&autoscaling.AttachLoadBalancersInput{
AutoScalingGroupName: aws.String(d.Id()),
LoadBalancerNames: add,
})
if err != nil {
return fmt.Errorf("[WARN] Error updating Load Balancers for AutoScaling Group (%s), error: %s", d.Id(), err)
}
}
}
return resourceAwsAutoscalingGroupRead(d, meta) return resourceAwsAutoscalingGroupRead(d, meta)
} }
@ -286,8 +350,8 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{})
// scaling operations within 5m. // scaling operations within 5m.
err = resource.Retry(5*time.Minute, func() error { err = resource.Retry(5*time.Minute, func() error {
if _, err := conn.DeleteAutoScalingGroup(&deleteopts); err != nil { if _, err := conn.DeleteAutoScalingGroup(&deleteopts); err != nil {
if awserr, ok := err.(aws.APIError); ok { if awserr, ok := err.(awserr.Error); ok {
switch awserr.Code { switch awserr.Code() {
case "InvalidGroup.NotFound": case "InvalidGroup.NotFound":
// Already gone? Sure! // Already gone? Sure!
return nil return nil
@ -316,7 +380,7 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{})
func getAwsAutoscalingGroup( func getAwsAutoscalingGroup(
d *schema.ResourceData, d *schema.ResourceData,
meta interface{}) (*autoscaling.AutoScalingGroup, error) { meta interface{}) (*autoscaling.Group, error) {
conn := meta.(*AWSClient).autoscalingconn conn := meta.(*AWSClient).autoscalingconn
describeOpts := autoscaling.DescribeAutoScalingGroupsInput{ describeOpts := autoscaling.DescribeAutoScalingGroupsInput{
@ -326,8 +390,8 @@ func getAwsAutoscalingGroup(
log.Printf("[DEBUG] AutoScaling Group describe configuration: %#v", describeOpts) log.Printf("[DEBUG] AutoScaling Group describe configuration: %#v", describeOpts)
describeGroups, err := conn.DescribeAutoScalingGroups(&describeOpts) describeGroups, err := conn.DescribeAutoScalingGroups(&describeOpts)
if err != nil { if err != nil {
autoscalingerr, ok := err.(aws.APIError) autoscalingerr, ok := err.(awserr.Error)
if ok && autoscalingerr.Code == "InvalidGroup.NotFound" { if ok && autoscalingerr.Code() == "InvalidGroup.NotFound" {
d.SetId("") d.SetId("")
return nil, nil return nil, nil
} }
@ -386,13 +450,19 @@ var waitForASGCapacityTimeout = 10 * time.Minute
// Waits for a minimum number of healthy instances to show up as healthy in the // Waits for a minimum number of healthy instances to show up as healthy in the
// ASG before continuing. Waits up to `waitForASGCapacityTimeout` for // ASG before continuing. Waits up to `waitForASGCapacityTimeout` for
// "desired_capacity", or "min_size" if desired capacity is not specified. // "desired_capacity", or "min_size" if desired capacity is not specified.
//
// If "min_elb_capacity" is specified, will also wait for that number of
// instances to show up InService in all attached ELBs. See "Waiting for
// Capacity" in docs for more discussion of the feature.
func waitForASGCapacity(d *schema.ResourceData, meta interface{}) error { func waitForASGCapacity(d *schema.ResourceData, meta interface{}) error {
waitFor := d.Get("min_size").(int) wantASG := d.Get("min_size").(int)
if v := d.Get("desired_capacity").(int); v > 0 { if v := d.Get("desired_capacity").(int); v > 0 {
waitFor = v wantASG = v
} }
wantELB := d.Get("min_elb_capacity").(int)
log.Printf("[DEBUG] Waiting for capacity: %d ASG, %d ELB", wantASG, wantELB)
log.Printf("[DEBUG] Waiting for group to have %d healthy instances", waitFor)
return resource.Retry(waitForASGCapacityTimeout, func() error { return resource.Retry(waitForASGCapacityTimeout, func() error {
g, err := getAwsAutoscalingGroup(d, meta) g, err := getAwsAutoscalingGroup(d, meta)
if err != nil { if err != nil {
@ -401,24 +471,84 @@ func waitForASGCapacity(d *schema.ResourceData, meta interface{}) error {
if g == nil { if g == nil {
return nil return nil
} }
lbis, err := getLBInstanceStates(g, meta)
if err != nil {
return resource.RetryError{Err: err}
}
haveASG := 0
haveELB := 0
healthy := 0
for _, i := range g.Instances { for _, i := range g.Instances {
if i.HealthStatus == nil { if i.HealthStatus == nil || i.InstanceID == nil || i.LifecycleState == nil {
continue continue
} }
if strings.EqualFold(*i.HealthStatus, "Healthy") {
healthy++ if !strings.EqualFold(*i.HealthStatus, "Healthy") {
continue
}
if !strings.EqualFold(*i.LifecycleState, "InService") {
continue
}
haveASG++
if wantELB > 0 {
inAllLbs := true
for _, states := range lbis {
state, ok := states[*i.InstanceID]
if !ok || !strings.EqualFold(state, "InService") {
inAllLbs = false
}
}
if inAllLbs {
haveELB++
}
} }
} }
log.Printf( log.Printf("[DEBUG] %q Capacity: %d/%d ASG, %d/%d ELB",
"[DEBUG] %q has %d/%d healthy instances", d.Id(), healthy, waitFor) d.Id(), haveASG, wantASG, haveELB, wantELB)
if healthy >= waitFor { if haveASG >= wantASG && haveELB >= wantELB {
return nil return nil
} }
return fmt.Errorf("Waiting for healthy instances: %d/%d", healthy, waitFor) return fmt.Errorf("Still need to wait for more healthy instances. This could mean instances failed to launch. See Scaling History for more information.")
}) })
} }
// Returns a mapping of the instance states of all the ELBs attached to the
// provided ASG.
//
// Nested like: lbName -> instanceId -> instanceState
func getLBInstanceStates(g *autoscaling.Group, meta interface{}) (map[string]map[string]string, error) {
lbInstanceStates := make(map[string]map[string]string)
elbconn := meta.(*AWSClient).elbconn
for _, lbName := range g.LoadBalancerNames {
lbInstanceStates[*lbName] = make(map[string]string)
opts := &elb.DescribeInstanceHealthInput{LoadBalancerName: lbName}
r, err := elbconn.DescribeInstanceHealth(opts)
if err != nil {
return nil, err
}
for _, is := range r.InstanceStates {
if is.InstanceID == nil || is.State == nil {
continue
}
lbInstanceStates[*lbName][*is.InstanceID] = *is.State
}
}
return lbInstanceStates, nil
}
func expandVpcZoneIdentifiers(list []interface{}) *string {
strs := make([]string, len(list))
for _, s := range list {
strs = append(strs, s.(string))
}
return aws.String(strings.Join(strs, ","))
}

View File

@ -6,14 +6,15 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSAutoScalingGroup_basic(t *testing.T) { func TestAccAWSAutoScalingGroup_basic(t *testing.T) {
var group autoscaling.AutoScalingGroup var group autoscaling.Group
var lc autoscaling.LaunchConfiguration var lc autoscaling.LaunchConfiguration
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
@ -67,7 +68,7 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) {
} }
func TestAccAWSAutoScalingGroup_tags(t *testing.T) { func TestAccAWSAutoScalingGroup_tags(t *testing.T) {
var group autoscaling.AutoScalingGroup var group autoscaling.Group
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
@ -100,8 +101,34 @@ func TestAccAWSAutoScalingGroup_tags(t *testing.T) {
}) })
} }
func TestAccAWSAutoScalingGroup_VpcUpdates(t *testing.T) {
var group autoscaling.Group
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSAutoScalingGroupConfigWithAZ,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group),
),
},
resource.TestStep{
Config: testAccAWSAutoScalingGroupConfigWithVPCIdent,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group),
testAccCheckAWSAutoScalingGroupAttributesVPCZoneIdentifer(&group),
),
},
},
})
}
func TestAccAWSAutoScalingGroup_WithLoadBalancer(t *testing.T) { func TestAccAWSAutoScalingGroup_WithLoadBalancer(t *testing.T) {
var group autoscaling.AutoScalingGroup var group autoscaling.Group
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
@ -141,11 +168,11 @@ func testAccCheckAWSAutoScalingGroupDestroy(s *terraform.State) error {
} }
// Verify the error // Verify the error
ec2err, ok := err.(aws.APIError) ec2err, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if ec2err.Code != "InvalidGroup.NotFound" { if ec2err.Code() != "InvalidGroup.NotFound" {
return err return err
} }
} }
@ -153,7 +180,7 @@ func testAccCheckAWSAutoScalingGroupDestroy(s *terraform.State) error {
return nil return nil
} }
func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.AutoScalingGroup) resource.TestCheckFunc { func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.Group) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
if *group.AvailabilityZones[0] != "us-west-2a" { if *group.AvailabilityZones[0] != "us-west-2a" {
return fmt.Errorf("Bad availability_zones: %#v", group.AvailabilityZones[0]) return fmt.Errorf("Bad availability_zones: %#v", group.AvailabilityZones[0])
@ -206,7 +233,7 @@ func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.AutoScalingGro
} }
} }
func testAccCheckAWSAutoScalingGroupAttributesLoadBalancer(group *autoscaling.AutoScalingGroup) resource.TestCheckFunc { func testAccCheckAWSAutoScalingGroupAttributesLoadBalancer(group *autoscaling.Group) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
if *group.LoadBalancerNames[0] != "foobar-terraform-test" { if *group.LoadBalancerNames[0] != "foobar-terraform-test" {
return fmt.Errorf("Bad load_balancers: %#v", group.LoadBalancerNames[0]) return fmt.Errorf("Bad load_balancers: %#v", group.LoadBalancerNames[0])
@ -216,7 +243,7 @@ func testAccCheckAWSAutoScalingGroupAttributesLoadBalancer(group *autoscaling.Au
} }
} }
func testAccCheckAWSAutoScalingGroupExists(n string, group *autoscaling.AutoScalingGroup) resource.TestCheckFunc { func testAccCheckAWSAutoScalingGroupExists(n string, group *autoscaling.Group) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n] rs, ok := s.RootModule().Resources[n]
if !ok { if !ok {
@ -265,7 +292,7 @@ func testLaunchConfigurationName(n string, lc *autoscaling.LaunchConfiguration)
} }
func testAccCheckAWSAutoScalingGroupHealthyCapacity( func testAccCheckAWSAutoScalingGroupHealthyCapacity(
g *autoscaling.AutoScalingGroup, exp int) resource.TestCheckFunc { g *autoscaling.Group, exp int) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
healthy := 0 healthy := 0
for _, i := range g.Instances { for _, i := range g.Instances {
@ -283,6 +310,40 @@ func testAccCheckAWSAutoScalingGroupHealthyCapacity(
} }
} }
func testAccCheckAWSAutoScalingGroupAttributesVPCZoneIdentifer(group *autoscaling.Group) resource.TestCheckFunc {
return func(s *terraform.State) error {
// Grab Subnet Ids
var subnets []string
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_subnet" {
continue
}
subnets = append(subnets, rs.Primary.Attributes["id"])
}
if group.VPCZoneIdentifier == nil {
return fmt.Errorf("Bad VPC Zone Identifier\nexpected: %s\ngot nil", subnets)
}
zones := strings.Split(*group.VPCZoneIdentifier, ",")
remaining := len(zones)
for _, z := range zones {
for _, s := range subnets {
if z == s {
remaining--
}
}
}
if remaining != 0 {
return fmt.Errorf("Bad VPC Zone Identifier match\nexpected: %s\ngot:%s", zones, subnets)
}
return nil
}
}
const testAccAWSAutoScalingGroupConfig = ` const testAccAWSAutoScalingGroupConfig = `
resource "aws_launch_configuration" "foobar" { resource "aws_launch_configuration" "foobar" {
image_id = "ami-21f78e11" image_id = "ami-21f78e11"
@ -342,34 +403,176 @@ resource "aws_autoscaling_group" "bar" {
` `
const testAccAWSAutoScalingGroupConfigWithLoadBalancer = ` const testAccAWSAutoScalingGroupConfigWithLoadBalancer = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
tags { Name = "tf-asg-test" }
}
resource "aws_internet_gateway" "gw" {
vpc_id = "${aws_vpc.foo.id}"
}
resource "aws_subnet" "foo" {
cidr_block = "10.1.1.0/24"
vpc_id = "${aws_vpc.foo.id}"
}
resource "aws_security_group" "foo" {
vpc_id="${aws_vpc.foo.id}"
ingress {
protocol = "-1"
from_port = 0
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
egress {
protocol = "-1"
from_port = 0
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_elb" "bar" { resource "aws_elb" "bar" {
name = "foobar-terraform-test" name = "foobar-terraform-test"
availability_zones = ["us-west-2a"] subnets = ["${aws_subnet.foo.id}"]
security_groups = ["${aws_security_group.foo.id}"]
listener { listener {
instance_port = 8000 instance_port = 80
instance_protocol = "http" instance_protocol = "http"
lb_port = 80 lb_port = 80
lb_protocol = "http" lb_protocol = "http"
} }
health_check {
healthy_threshold = 2
unhealthy_threshold = 2
target = "HTTP:80/"
interval = 5
timeout = 2
}
depends_on = ["aws_internet_gateway.gw"]
} }
resource "aws_launch_configuration" "foobar" { resource "aws_launch_configuration" "foobar" {
image_id = "ami-21f78e11" // need an AMI that listens on :80 at boot, this is:
instance_type = "t1.micro" // bitnami-nginxstack-1.6.1-0-linux-ubuntu-14.04.1-x86_64-hvm-ebs-ami-99f5b1a9-3
image_id = "ami-b5b3fc85"
instance_type = "t2.micro"
security_groups = ["${aws_security_group.foo.id}"]
} }
resource "aws_autoscaling_group" "bar" { resource "aws_autoscaling_group" "bar" {
availability_zones = ["us-west-2a"] availability_zones = ["${aws_subnet.foo.availability_zone}"]
vpc_zone_identifier = ["${aws_subnet.foo.id}"]
name = "foobar3-terraform-test" name = "foobar3-terraform-test"
max_size = 5 max_size = 2
min_size = 2 min_size = 2
health_check_grace_period = 300 health_check_grace_period = 300
health_check_type = "ELB" health_check_type = "ELB"
desired_capacity = 4 min_elb_capacity = 2
force_delete = true force_delete = true
launch_configuration = "${aws_launch_configuration.foobar.name}" launch_configuration = "${aws_launch_configuration.foobar.name}"
load_balancers = ["${aws_elb.bar.name}"] load_balancers = ["${aws_elb.bar.name}"]
} }
` `
const testAccAWSAutoScalingGroupConfigWithAZ = `
resource "aws_vpc" "default" {
cidr_block = "10.0.0.0/16"
tags {
Name = "terraform-test"
}
}
resource "aws_subnet" "main" {
vpc_id = "${aws_vpc.default.id}"
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-2a"
tags {
Name = "terraform-test"
}
}
resource "aws_subnet" "alt" {
vpc_id = "${aws_vpc.default.id}"
cidr_block = "10.0.2.0/24"
availability_zone = "us-west-2b"
tags {
Name = "asg-vpc-thing"
}
}
resource "aws_launch_configuration" "foobar" {
name = "vpc-asg-test"
image_id = "ami-b5b3fc85"
instance_type = "t2.micro"
}
resource "aws_autoscaling_group" "bar" {
availability_zones = ["us-west-2a"]
name = "vpc-asg-test"
max_size = 2
min_size = 1
health_check_grace_period = 300
health_check_type = "ELB"
desired_capacity = 1
force_delete = true
termination_policies = ["OldestInstance"]
launch_configuration = "${aws_launch_configuration.foobar.name}"
}
`
const testAccAWSAutoScalingGroupConfigWithVPCIdent = `
resource "aws_vpc" "default" {
cidr_block = "10.0.0.0/16"
tags {
Name = "terraform-test"
}
}
resource "aws_subnet" "main" {
vpc_id = "${aws_vpc.default.id}"
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-2a"
tags {
Name = "terraform-test"
}
}
resource "aws_subnet" "alt" {
vpc_id = "${aws_vpc.default.id}"
cidr_block = "10.0.2.0/24"
availability_zone = "us-west-2b"
tags {
Name = "asg-vpc-thing"
}
}
resource "aws_launch_configuration" "foobar" {
name = "vpc-asg-test"
image_id = "ami-b5b3fc85"
instance_type = "t2.micro"
}
resource "aws_autoscaling_group" "bar" {
vpc_zone_identifier = [
"${aws_subnet.main.id}",
"${aws_subnet.alt.id}",
]
name = "vpc-asg-test"
max_size = 2
min_size = 1
health_check_grace_period = 300
health_check_type = "ELB"
desired_capacity = 1
force_delete = true
termination_policies = ["OldestInstance"]
launch_configuration = "${aws_launch_configuration.foobar.name}"
}
`

View File

@ -0,0 +1,200 @@
package aws
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsAutoscalingNotification() *schema.Resource {
return &schema.Resource{
Create: resourceAwsAutoscalingNotificationCreate,
Read: resourceAwsAutoscalingNotificationRead,
Update: resourceAwsAutoscalingNotificationUpdate,
Delete: resourceAwsAutoscalingNotificationDelete,
Schema: map[string]*schema.Schema{
"topic_arn": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"group_names": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"notifications": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
},
}
}
func resourceAwsAutoscalingNotificationCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).autoscalingconn
gl := convertSetToList(d.Get("group_names").(*schema.Set))
nl := convertSetToList(d.Get("notifications").(*schema.Set))
topic := d.Get("topic_arn").(string)
if err := addNotificationConfigToGroupsWithTopic(conn, gl, nl, topic); err != nil {
return err
}
// ARNs are unique, and these notifications are per ARN, so we re-use the ARN
// here as the ID
d.SetId(topic)
return resourceAwsAutoscalingNotificationRead(d, meta)
}
func resourceAwsAutoscalingNotificationRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).autoscalingconn
gl := convertSetToList(d.Get("group_names").(*schema.Set))
opts := &autoscaling.DescribeNotificationConfigurationsInput{
AutoScalingGroupNames: gl,
}
resp, err := conn.DescribeNotificationConfigurations(opts)
if err != nil {
return fmt.Errorf("Error describing notifications")
}
topic := d.Get("topic_arn").(string)
// Grab all applicable notifcation configurations for this Topic.
// Each NotificationType will have a record, so 1 Group with 3 Types results
// in 3 records, all with the same Group name
gRaw := make(map[string]bool)
nRaw := make(map[string]bool)
for _, n := range resp.NotificationConfigurations {
if *n.TopicARN == topic {
gRaw[*n.AutoScalingGroupName] = true
nRaw[*n.NotificationType] = true
}
}
// Grab the keys here as the list of Groups
var gList []string
for k, _ := range gRaw {
gList = append(gList, k)
}
// Grab the keys here as the list of Types
var nList []string
for k, _ := range nRaw {
nList = append(nList, k)
}
if err := d.Set("group_names", gList); err != nil {
return err
}
if err := d.Set("notifications", nList); err != nil {
return err
}
return nil
}
func resourceAwsAutoscalingNotificationUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).autoscalingconn
// Notifications API call is a PUT, so we don't need to diff the list, just
// push whatever it is and AWS sorts it out
nl := convertSetToList(d.Get("notifications").(*schema.Set))
o, n := d.GetChange("group_names")
if o == nil {
o = new(schema.Set)
}
if n == nil {
n = new(schema.Set)
}
os := o.(*schema.Set)
ns := n.(*schema.Set)
remove := convertSetToList(os.Difference(ns))
add := convertSetToList(ns.Difference(os))
topic := d.Get("topic_arn").(string)
if err := removeNotificationConfigToGroupsWithTopic(conn, remove, topic); err != nil {
return err
}
var update []*string
if d.HasChange("notifications") {
update = convertSetToList(d.Get("group_names").(*schema.Set))
} else {
update = add
}
if err := addNotificationConfigToGroupsWithTopic(conn, update, nl, topic); err != nil {
return err
}
return resourceAwsAutoscalingNotificationRead(d, meta)
}
func addNotificationConfigToGroupsWithTopic(conn *autoscaling.AutoScaling, groups []*string, nl []*string, topic string) error {
for _, a := range groups {
opts := &autoscaling.PutNotificationConfigurationInput{
AutoScalingGroupName: a,
NotificationTypes: nl,
TopicARN: aws.String(topic),
}
_, err := conn.PutNotificationConfiguration(opts)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
return fmt.Errorf("[WARN] Error creating Autoscaling Group Notification for Group %s, error: \"%s\", code: \"%s\"", *a, awsErr.Message(), awsErr.Code())
}
return err
}
}
return nil
}
func removeNotificationConfigToGroupsWithTopic(conn *autoscaling.AutoScaling, groups []*string, topic string) error {
for _, r := range groups {
opts := &autoscaling.DeleteNotificationConfigurationInput{
AutoScalingGroupName: r,
TopicARN: aws.String(topic),
}
_, err := conn.DeleteNotificationConfiguration(opts)
if err != nil {
return fmt.Errorf("[WARN] Error deleting notification configuration for ASG \"%s\", Topic ARN \"%s\"", *r, topic)
}
}
return nil
}
func resourceAwsAutoscalingNotificationDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).autoscalingconn
gl := convertSetToList(d.Get("group_names").(*schema.Set))
topic := d.Get("topic_arn").(string)
if err := removeNotificationConfigToGroupsWithTopic(conn, gl, topic); err != nil {
return err
}
return nil
}
func convertSetToList(s *schema.Set) (nl []*string) {
l := s.List()
for _, n := range l {
nl = append(nl, aws.String(n.(string)))
}
return nl
}

View File

@ -0,0 +1,252 @@
package aws
import (
"fmt"
"strconv"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSASGNotification_basic(t *testing.T) {
var asgn autoscaling.DescribeNotificationConfigurationsOutput
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckASGNDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccASGNotificationConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckASGNotificationExists("aws_autoscaling_notification.example", []string{"foobar1-terraform-test"}, &asgn),
testAccCheckAWSASGNotificationAttributes("aws_autoscaling_notification.example", &asgn),
),
},
},
})
}
func TestAccAWSASGNotification_update(t *testing.T) {
var asgn autoscaling.DescribeNotificationConfigurationsOutput
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckASGNDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccASGNotificationConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckASGNotificationExists("aws_autoscaling_notification.example", []string{"foobar1-terraform-test"}, &asgn),
testAccCheckAWSASGNotificationAttributes("aws_autoscaling_notification.example", &asgn),
),
},
resource.TestStep{
Config: testAccASGNotificationConfig_update,
Check: resource.ComposeTestCheckFunc(
testAccCheckASGNotificationExists("aws_autoscaling_notification.example", []string{"foobar1-terraform-test", "barfoo-terraform-test"}, &asgn),
testAccCheckAWSASGNotificationAttributes("aws_autoscaling_notification.example", &asgn),
),
},
},
})
}
func testAccCheckASGNotificationExists(n string, groups []string, asgn *autoscaling.DescribeNotificationConfigurationsOutput) 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 ASG Notification ID is set")
}
var gl []*string
for _, g := range groups {
gl = append(gl, aws.String(g))
}
conn := testAccProvider.Meta().(*AWSClient).autoscalingconn
opts := &autoscaling.DescribeNotificationConfigurationsInput{
AutoScalingGroupNames: gl,
}
resp, err := conn.DescribeNotificationConfigurations(opts)
if err != nil {
return fmt.Errorf("Error describing notifications")
}
*asgn = *resp
return nil
}
}
func testAccCheckASGNDestroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_autoscaling_notification" {
continue
}
groups := []*string{aws.String("foobar1-terraform-test")}
conn := testAccProvider.Meta().(*AWSClient).autoscalingconn
opts := &autoscaling.DescribeNotificationConfigurationsInput{
AutoScalingGroupNames: groups,
}
resp, err := conn.DescribeNotificationConfigurations(opts)
if err != nil {
return fmt.Errorf("Error describing notifications")
}
if len(resp.NotificationConfigurations) != 0 {
fmt.Errorf("Error finding notification descriptions")
}
}
return nil
}
func testAccCheckAWSASGNotificationAttributes(n string, asgn *autoscaling.DescribeNotificationConfigurationsOutput) 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 ASG Notification ID is set")
}
if len(asgn.NotificationConfigurations) == 0 {
return fmt.Errorf("Error: no ASG Notifications found")
}
// build a unique list of groups, notification types
gRaw := make(map[string]bool)
nRaw := make(map[string]bool)
for _, n := range asgn.NotificationConfigurations {
if *n.TopicARN == rs.Primary.Attributes["topic_arn"] {
gRaw[*n.AutoScalingGroupName] = true
nRaw[*n.NotificationType] = true
}
}
// Grab the keys here as the list of Groups
var gList []string
for k, _ := range gRaw {
gList = append(gList, k)
}
// Grab the keys here as the list of Types
var nList []string
for k, _ := range nRaw {
nList = append(nList, k)
}
typeCount, _ := strconv.Atoi(rs.Primary.Attributes["notifications.#"])
if len(nList) != typeCount {
return fmt.Errorf("Error: Bad ASG Notification count, expected (%d), got (%d)", typeCount, len(nList))
}
groupCount, _ := strconv.Atoi(rs.Primary.Attributes["group_names.#"])
if len(gList) != groupCount {
return fmt.Errorf("Error: Bad ASG Group count, expected (%d), got (%d)", typeCount, len(gList))
}
return nil
}
}
const testAccASGNotificationConfig_basic = `
resource "aws_sns_topic" "topic_example" {
name = "user-updates-topic"
}
resource "aws_launch_configuration" "foobar" {
name = "foobarautoscaling-terraform-test"
image_id = "ami-21f78e11"
instance_type = "t1.micro"
}
resource "aws_autoscaling_group" "bar" {
availability_zones = ["us-west-2a"]
name = "foobar1-terraform-test"
max_size = 1
min_size = 1
health_check_grace_period = 100
health_check_type = "ELB"
desired_capacity = 1
force_delete = true
termination_policies = ["OldestInstance"]
launch_configuration = "${aws_launch_configuration.foobar.name}"
}
resource "aws_autoscaling_notification" "example" {
group_names = ["${aws_autoscaling_group.bar.name}"]
notifications = [
"autoscaling:EC2_INSTANCE_LAUNCH",
"autoscaling:EC2_INSTANCE_TERMINATE",
]
topic_arn = "${aws_sns_topic.topic_example.arn}"
}
`
const testAccASGNotificationConfig_update = `
resource "aws_sns_topic" "user_updates" {
name = "user-updates-topic"
}
resource "aws_launch_configuration" "foobar" {
name = "foobarautoscaling-terraform-test"
image_id = "ami-21f78e11"
instance_type = "t1.micro"
}
resource "aws_autoscaling_group" "bar" {
availability_zones = ["us-west-2a"]
name = "foobar1-terraform-test"
max_size = 1
min_size = 1
health_check_grace_period = 100
health_check_type = "ELB"
desired_capacity = 1
force_delete = true
termination_policies = ["OldestInstance"]
launch_configuration = "${aws_launch_configuration.foobar.name}"
}
resource "aws_autoscaling_group" "foo" {
availability_zones = ["us-west-2b"]
name = "barfoo-terraform-test"
max_size = 1
min_size = 1
health_check_grace_period = 200
health_check_type = "ELB"
desired_capacity = 1
force_delete = true
termination_policies = ["OldestInstance"]
launch_configuration = "${aws_launch_configuration.foobar.name}"
}
resource "aws_autoscaling_notification" "example" {
group_names = [
"${aws_autoscaling_group.bar.name}",
"${aws_autoscaling_group.foo.name}",
]
notifications = [
"autoscaling:EC2_INSTANCE_LAUNCH",
"autoscaling:EC2_INSTANCE_TERMINATE",
"autoscaling:EC2_INSTANCE_LAUNCH_ERROR"
]
topic_arn = "${aws_sns_topic.user_updates.arn}"
}`

View File

@ -0,0 +1,181 @@
package aws
import (
"fmt"
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsAutoscalingPolicy() *schema.Resource {
return &schema.Resource{
Create: resourceAwsAutoscalingPolicyCreate,
Read: resourceAwsAutoscalingPolicyRead,
Update: resourceAwsAutoscalingPolicyUpdate,
Delete: resourceAwsAutoscalingPolicyDelete,
Schema: map[string]*schema.Schema{
"arn": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"adjustment_type": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"autoscaling_group_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"cooldown": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"min_adjustment_step": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"scaling_adjustment": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
},
}
}
func resourceAwsAutoscalingPolicyCreate(d *schema.ResourceData, meta interface{}) error {
autoscalingconn := meta.(*AWSClient).autoscalingconn
params := getAwsAutoscalingPutScalingPolicyInput(d)
log.Printf("[DEBUG] AutoScaling PutScalingPolicy: %#v", params)
resp, err := autoscalingconn.PutScalingPolicy(&params)
if err != nil {
return fmt.Errorf("Error putting scaling policy: %s", err)
}
d.Set("arn", resp.PolicyARN)
d.SetId(d.Get("name").(string))
log.Printf("[INFO] AutoScaling Scaling PolicyARN: %s", d.Get("arn").(string))
return resourceAwsAutoscalingPolicyRead(d, meta)
}
func resourceAwsAutoscalingPolicyRead(d *schema.ResourceData, meta interface{}) error {
p, err := getAwsAutoscalingPolicy(d, meta)
if err != nil {
return err
}
if p == nil {
d.SetId("")
return nil
}
log.Printf("[DEBUG] Read Scaling Policy: ASG: %s, SP: %s, Obj: %#v", d.Get("autoscaling_group_name"), d.Get("name"), p)
d.Set("adjustment_type", p.AdjustmentType)
d.Set("autoscaling_group_name", p.AutoScalingGroupName)
d.Set("cooldown", p.Cooldown)
d.Set("min_adjustment_step", p.MinAdjustmentStep)
d.Set("arn", p.PolicyARN)
d.Set("name", p.PolicyName)
d.Set("scaling_adjustment", p.ScalingAdjustment)
return nil
}
func resourceAwsAutoscalingPolicyUpdate(d *schema.ResourceData, meta interface{}) error {
autoscalingconn := meta.(*AWSClient).autoscalingconn
params := getAwsAutoscalingPutScalingPolicyInput(d)
log.Printf("[DEBUG] Autoscaling Update Scaling Policy: %#v", params)
_, err := autoscalingconn.PutScalingPolicy(&params)
if err != nil {
return err
}
return resourceAwsAutoscalingPolicyRead(d, meta)
}
func resourceAwsAutoscalingPolicyDelete(d *schema.ResourceData, meta interface{}) error {
autoscalingconn := meta.(*AWSClient).autoscalingconn
p, err := getAwsAutoscalingPolicy(d, meta)
if err != nil {
return err
}
if p == nil {
return nil
}
params := autoscaling.DeletePolicyInput{
AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)),
PolicyName: aws.String(d.Get("name").(string)),
}
if _, err := autoscalingconn.DeletePolicy(&params); err != nil {
return fmt.Errorf("Autoscaling Scaling Policy: %s ", err)
}
d.SetId("")
return nil
}
// PutScalingPolicy seems to require all params to be resent, so create and update can share this common function
func getAwsAutoscalingPutScalingPolicyInput(d *schema.ResourceData) autoscaling.PutScalingPolicyInput {
var params = autoscaling.PutScalingPolicyInput{
AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)),
PolicyName: aws.String(d.Get("name").(string)),
}
if v, ok := d.GetOk("adjustment_type"); ok {
params.AdjustmentType = aws.String(v.(string))
}
if v, ok := d.GetOk("cooldown"); ok {
params.Cooldown = aws.Long(int64(v.(int)))
}
if v, ok := d.GetOk("scaling_adjustment"); ok {
params.ScalingAdjustment = aws.Long(int64(v.(int)))
}
if v, ok := d.GetOk("min_adjustment_step"); ok {
params.MinAdjustmentStep = aws.Long(int64(v.(int)))
}
return params
}
func getAwsAutoscalingPolicy(d *schema.ResourceData, meta interface{}) (*autoscaling.ScalingPolicy, error) {
autoscalingconn := meta.(*AWSClient).autoscalingconn
params := autoscaling.DescribePoliciesInput{
AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)),
PolicyNames: []*string{aws.String(d.Get("name").(string))},
}
log.Printf("[DEBUG] AutoScaling Scaling Policy Describe Params: %#v", params)
resp, err := autoscalingconn.DescribePolicies(&params)
if err != nil {
return nil, fmt.Errorf("Error retrieving scaling policies: %s", err)
}
// find scaling policy
name := d.Get("name")
for idx, sp := range resp.ScalingPolicies {
if *sp.PolicyName == name {
return resp.ScalingPolicies[idx], nil
}
}
// policy not found
return nil, nil
}

View File

@ -0,0 +1,115 @@
package aws
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSAutoscalingPolicy_basic(t *testing.T) {
var policy autoscaling.ScalingPolicy
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSAutoscalingPolicyDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSAutoscalingPolicyConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckScalingPolicyExists("aws_autoscaling_policy.foobar", &policy),
resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar", "adjustment_type", "ChangeInCapacity"),
resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar", "cooldown", "300"),
),
},
},
})
}
func testAccCheckScalingPolicyExists(n string, policy *autoscaling.ScalingPolicy) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
rs = rs
return fmt.Errorf("Not found: %s", n)
}
conn := testAccProvider.Meta().(*AWSClient).autoscalingconn
params := &autoscaling.DescribePoliciesInput{
AutoScalingGroupName: aws.String(rs.Primary.Attributes["autoscaling_group_name"]),
PolicyNames: []*string{aws.String(rs.Primary.ID)},
}
resp, err := conn.DescribePolicies(params)
if err != nil {
return err
}
if len(resp.ScalingPolicies) == 0 {
return fmt.Errorf("ScalingPolicy not found")
}
return nil
}
}
func testAccCheckAWSAutoscalingPolicyDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).autoscalingconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_autoscaling_group" {
continue
}
params := autoscaling.DescribePoliciesInput{
AutoScalingGroupName: aws.String(rs.Primary.Attributes["autoscaling_group_name"]),
PolicyNames: []*string{aws.String(rs.Primary.ID)},
}
resp, err := conn.DescribePolicies(&params)
if err == nil {
if len(resp.ScalingPolicies) != 0 &&
*resp.ScalingPolicies[0].PolicyName == rs.Primary.ID {
return fmt.Errorf("Scaling Policy Still Exists: %s", rs.Primary.ID)
}
}
}
return nil
}
var testAccAWSAutoscalingPolicyConfig = fmt.Sprintf(`
resource "aws_launch_configuration" "foobar" {
name = "terraform-test-foobar5"
image_id = "ami-21f78e11"
instance_type = "t1.micro"
}
resource "aws_autoscaling_group" "foobar" {
availability_zones = ["us-west-2a"]
name = "terraform-test-foobar5"
max_size = 5
min_size = 2
health_check_grace_period = 300
health_check_type = "ELB"
force_delete = true
termination_policies = ["OldestInstance"]
launch_configuration = "${aws_launch_configuration.foobar.name}"
tag {
key = "Foo"
value = "foo-bar"
propagate_at_launch = true
}
}
resource "aws_autoscaling_policy" "foobar" {
name = "foobar"
scaling_adjustment = 4
adjustment_type = "ChangeInCapacity"
cooldown = 300
autoscaling_group_name = "${aws_autoscaling_group.foobar.name}"
}
`)

View File

@ -0,0 +1,288 @@
package aws
import (
"fmt"
"log"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
)
func resourceAwsCloudWatchMetricAlarm() *schema.Resource {
return &schema.Resource{
Create: resourceAwsCloudWatchMetricAlarmCreate,
Read: resourceAwsCloudWatchMetricAlarmRead,
Update: resourceAwsCloudWatchMetricAlarmUpdate,
Delete: resourceAwsCloudWatchMetricAlarmDelete,
Schema: map[string]*schema.Schema{
"alarm_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"comparison_operator": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"evaluation_periods": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"metric_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"namespace": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"period": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"statistic": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"threshold": &schema.Schema{
Type: schema.TypeFloat,
Required: true,
},
"actions_enabled": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"alarm_actions": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: func(v interface{}) int {
return hashcode.String(v.(string))
},
},
"alarm_description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"dimensions": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
},
"insufficient_data_actions": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: func(v interface{}) int {
return hashcode.String(v.(string))
},
},
"ok_actions": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: func(v interface{}) int {
return hashcode.String(v.(string))
},
},
"unit": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
}
}
func resourceAwsCloudWatchMetricAlarmCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cloudwatchconn
params := getAwsCloudWatchPutMetricAlarmInput(d)
log.Printf("[DEBUG] Creating CloudWatch Metric Alarm: %#v", params)
_, err := conn.PutMetricAlarm(&params)
if err != nil {
return fmt.Errorf("Creating metric alarm failed: %s", err)
}
d.SetId(d.Get("alarm_name").(string))
log.Println("[INFO] CloudWatch Metric Alarm created")
return resourceAwsCloudWatchMetricAlarmRead(d, meta)
}
func resourceAwsCloudWatchMetricAlarmRead(d *schema.ResourceData, meta interface{}) error {
a, err := getAwsCloudWatchMetricAlarm(d, meta)
if err != nil {
return err
}
if a == nil {
d.SetId("")
return nil
}
log.Printf("[DEBUG] Reading CloudWatch Metric Alarm: %s", d.Get("alarm_name"))
d.Set("actions_enabled", a.ActionsEnabled)
if err := d.Set("alarm_actions", _strArrPtrToList(a.AlarmActions)); err != nil {
log.Printf("[WARN] Error setting Alarm Actions: %s", err)
}
d.Set("alarm_description", a.AlarmDescription)
d.Set("alarm_name", a.AlarmName)
d.Set("comparison_operator", a.ComparisonOperator)
d.Set("dimensions", a.Dimensions)
d.Set("evaluation_periods", a.EvaluationPeriods)
if err := d.Set("insufficient_data_actions", _strArrPtrToList(a.InsufficientDataActions)); err != nil {
log.Printf("[WARN] Error setting Insufficient Data Actions: %s", err)
}
d.Set("metric_name", a.MetricName)
d.Set("namespace", a.Namespace)
if err := d.Set("ok_actions", _strArrPtrToList(a.OKActions)); err != nil {
log.Printf("[WARN] Error setting OK Actions: %s", err)
}
d.Set("period", a.Period)
d.Set("statistic", a.Statistic)
d.Set("threshold", a.Threshold)
d.Set("unit", a.Unit)
return nil
}
func resourceAwsCloudWatchMetricAlarmUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cloudwatchconn
params := getAwsCloudWatchPutMetricAlarmInput(d)
log.Printf("[DEBUG] Updating CloudWatch Metric Alarm: %#v", params)
_, err := conn.PutMetricAlarm(&params)
if err != nil {
return fmt.Errorf("Updating metric alarm failed: %s", err)
}
log.Println("[INFO] CloudWatch Metric Alarm updated")
return resourceAwsCloudWatchMetricAlarmRead(d, meta)
}
func resourceAwsCloudWatchMetricAlarmDelete(d *schema.ResourceData, meta interface{}) error {
p, err := getAwsCloudWatchMetricAlarm(d, meta)
if err != nil {
return err
}
if p == nil {
log.Printf("[DEBUG] CloudWatch Metric Alarm %s is already gone", d.Id())
return nil
}
log.Printf("[INFO] Deleting CloudWatch Metric Alarm: %s", d.Id())
conn := meta.(*AWSClient).cloudwatchconn
params := cloudwatch.DeleteAlarmsInput{
AlarmNames: []*string{aws.String(d.Id())},
}
if _, err := conn.DeleteAlarms(&params); err != nil {
return fmt.Errorf("Error deleting CloudWatch Metric Alarm: %s", err)
}
log.Println("[INFO] CloudWatch Metric Alarm deleted")
d.SetId("")
return nil
}
func getAwsCloudWatchPutMetricAlarmInput(d *schema.ResourceData) cloudwatch.PutMetricAlarmInput {
params := cloudwatch.PutMetricAlarmInput{
AlarmName: aws.String(d.Get("alarm_name").(string)),
ComparisonOperator: aws.String(d.Get("comparison_operator").(string)),
EvaluationPeriods: aws.Long(int64(d.Get("evaluation_periods").(int))),
MetricName: aws.String(d.Get("metric_name").(string)),
Namespace: aws.String(d.Get("namespace").(string)),
Period: aws.Long(int64(d.Get("period").(int))),
Statistic: aws.String(d.Get("statistic").(string)),
Threshold: aws.Double(d.Get("threshold").(float64)),
}
if v := d.Get("actions_enabled"); v != nil {
params.ActionsEnabled = aws.Boolean(v.(bool))
}
if v, ok := d.GetOk("alarm_description"); ok {
params.AlarmDescription = aws.String(v.(string))
}
if v, ok := d.GetOk("unit"); ok {
params.Unit = aws.String(v.(string))
}
var alarmActions []*string
if v := d.Get("alarm_actions"); v != nil {
for _, v := range v.(*schema.Set).List() {
str := v.(string)
alarmActions = append(alarmActions, aws.String(str))
}
params.AlarmActions = alarmActions
}
var insufficientDataActions []*string
if v := d.Get("insufficient_data_actions"); v != nil {
for _, v := range v.(*schema.Set).List() {
str := v.(string)
insufficientDataActions = append(insufficientDataActions, aws.String(str))
}
params.InsufficientDataActions = insufficientDataActions
}
var okActions []*string
if v := d.Get("ok_actions"); v != nil {
for _, v := range v.(*schema.Set).List() {
str := v.(string)
okActions = append(okActions, aws.String(str))
}
params.OKActions = okActions
}
a := d.Get("dimensions").(map[string]interface{})
dimensions := make([]*cloudwatch.Dimension, 0, len(a))
for k, v := range a {
dimensions = append(dimensions, &cloudwatch.Dimension{
Name: aws.String(k),
Value: aws.String(v.(string)),
})
}
params.Dimensions = dimensions
return params
}
func getAwsCloudWatchMetricAlarm(d *schema.ResourceData, meta interface{}) (*cloudwatch.MetricAlarm, error) {
conn := meta.(*AWSClient).cloudwatchconn
params := cloudwatch.DescribeAlarmsInput{
AlarmNames: []*string{aws.String(d.Id())},
}
resp, err := conn.DescribeAlarms(&params)
if err != nil {
return nil, nil
}
// Find it and return it
for idx, ma := range resp.MetricAlarms {
if *ma.AlarmName == d.Id() {
return resp.MetricAlarms[idx], nil
}
}
return nil, nil
}
func _strArrPtrToList(strArrPtr []*string) []string {
var result []string
for _, elem := range strArrPtr {
result = append(result, *elem)
}
return result
}

View File

@ -0,0 +1,95 @@
package aws
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSCloudWatchMetricAlarm_basic(t *testing.T) {
var alarm cloudwatch.MetricAlarm
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSCloudWatchMetricAlarmDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSCloudWatchMetricAlarmConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudWatchMetricAlarmExists("aws_cloudwatch_metric_alarm.foobar", &alarm),
resource.TestCheckResourceAttr("aws_cloudwatch_metric_alarm.foobar", "metric_name", "CPUUtilization"),
resource.TestCheckResourceAttr("aws_cloudwatch_metric_alarm.foobar", "statistic", "Average"),
),
},
},
})
}
func testAccCheckCloudWatchMetricAlarmExists(n string, alarm *cloudwatch.MetricAlarm) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
conn := testAccProvider.Meta().(*AWSClient).cloudwatchconn
params := cloudwatch.DescribeAlarmsInput{
AlarmNames: []*string{aws.String(rs.Primary.ID)},
}
resp, err := conn.DescribeAlarms(&params)
if err != nil {
return err
}
if len(resp.MetricAlarms) == 0 {
return fmt.Errorf("Alarm not found")
}
*alarm = *resp.MetricAlarms[0]
return nil
}
}
func testAccCheckAWSCloudWatchMetricAlarmDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).cloudwatchconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_cloudwatch_metric_alarm" {
continue
}
params := cloudwatch.DescribeAlarmsInput{
AlarmNames: []*string{aws.String(rs.Primary.ID)},
}
resp, err := conn.DescribeAlarms(&params)
if err == nil {
if len(resp.MetricAlarms) != 0 &&
*resp.MetricAlarms[0].AlarmName == rs.Primary.ID {
return fmt.Errorf("Alarm Still Exists: %s", rs.Primary.ID)
}
}
}
return nil
}
var testAccAWSCloudWatchMetricAlarmConfig = fmt.Sprintf(`
resource "aws_cloudwatch_metric_alarm" "foobar" {
alarm_name = "terraform-test-foobar5"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "120"
statistic = "Average"
threshold = "80"
alarm_description = "This metric monitor ec2 cpu utilization"
insufficient_data_actions = []
}
`)

View File

@ -5,8 +5,9 @@ import (
"log" "log"
"time" "time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
@ -82,7 +83,7 @@ func resourceAwsCustomerGatewayCreate(d *schema.ResourceData, meta interface{})
} }
// Create tags. // Create tags.
if err := setTagsSDK(conn, d); err != nil { if err := setTags(conn, d); err != nil {
return err return err
} }
@ -100,7 +101,7 @@ func customerGatewayRefreshFunc(conn *ec2.EC2, gatewayId string) resource.StateR
Filters: []*ec2.Filter{gatewayFilter}, Filters: []*ec2.Filter{gatewayFilter},
}) })
if err != nil { if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidCustomerGatewayID.NotFound" { if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidCustomerGatewayID.NotFound" {
resp = nil resp = nil
} else { } else {
log.Printf("Error on CustomerGatewayRefresh: %s", err) log.Printf("Error on CustomerGatewayRefresh: %s", err)
@ -130,7 +131,7 @@ func resourceAwsCustomerGatewayRead(d *schema.ResourceData, meta interface{}) er
Filters: []*ec2.Filter{gatewayFilter}, Filters: []*ec2.Filter{gatewayFilter},
}) })
if err != nil { if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidCustomerGatewayID.NotFound" { if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidCustomerGatewayID.NotFound" {
d.SetId("") d.SetId("")
return nil return nil
} else { } else {
@ -147,7 +148,7 @@ func resourceAwsCustomerGatewayRead(d *schema.ResourceData, meta interface{}) er
d.Set("bgp_asn", customerGateway.BGPASN) d.Set("bgp_asn", customerGateway.BGPASN)
d.Set("ip_address", customerGateway.IPAddress) d.Set("ip_address", customerGateway.IPAddress)
d.Set("type", customerGateway.Type) d.Set("type", customerGateway.Type)
d.Set("tags", tagsToMapSDK(customerGateway.Tags)) d.Set("tags", tagsToMap(customerGateway.Tags))
return nil return nil
} }
@ -156,7 +157,7 @@ func resourceAwsCustomerGatewayUpdate(d *schema.ResourceData, meta interface{})
conn := meta.(*AWSClient).ec2conn conn := meta.(*AWSClient).ec2conn
// Update tags if required. // Update tags if required.
if err := setTagsSDK(conn, d); err != nil { if err := setTags(conn, d); err != nil {
return err return err
} }
@ -172,7 +173,7 @@ func resourceAwsCustomerGatewayDelete(d *schema.ResourceData, meta interface{})
CustomerGatewayID: aws.String(d.Id()), CustomerGatewayID: aws.String(d.Id()),
}) })
if err != nil { if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidCustomerGatewayID.NotFound" { if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidCustomerGatewayID.NotFound" {
d.SetId("") d.SetId("")
return nil return nil
} else { } else {

View File

@ -4,14 +4,14 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccCustomerGateway(t *testing.T) { func TestAccCustomerGateway_basic(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,

View File

@ -3,12 +3,14 @@ package aws
import ( import (
"fmt" "fmt"
"log" "log"
"regexp"
"strings" "strings"
"time" "time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
@ -25,6 +27,7 @@ func resourceAwsDbInstance() *schema.Resource {
"name": &schema.Schema{ "name": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true,
ForceNew: true, ForceNew: true,
}, },
@ -71,6 +74,26 @@ func resourceAwsDbInstance() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if !regexp.MustCompile(`^[0-9a-z-]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"only lowercase alphanumeric characters and hyphens allowed in %q", k))
}
if !regexp.MustCompile(`^[a-z]`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"first character of %q must be a letter", k))
}
if regexp.MustCompile(`--`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q cannot contain two consecutive hyphens", k))
}
if regexp.MustCompile(`-$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q cannot end with a hyphen", k))
}
return
},
}, },
"instance_class": &schema.Schema{ "instance_class": &schema.Schema{
@ -88,7 +111,7 @@ func resourceAwsDbInstance() *schema.Resource {
"backup_retention_period": &schema.Schema{ "backup_retention_period": &schema.Schema{
Type: schema.TypeInt, Type: schema.TypeInt,
Optional: true, Optional: true,
Default: 1, Computed: true,
}, },
"backup_window": &schema.Schema{ "backup_window": &schema.Schema{
@ -102,6 +125,12 @@ func resourceAwsDbInstance() *schema.Resource {
Optional: true, Optional: true,
}, },
"license_model": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"maintenance_window": &schema.Schema{ "maintenance_window": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
@ -145,6 +174,20 @@ func resourceAwsDbInstance() *schema.Resource {
"final_snapshot_identifier": &schema.Schema{ "final_snapshot_identifier": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
value := v.(string)
if !regexp.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) {
es = append(es, fmt.Errorf(
"only alphanumeric characters and hyphens allowed in %q", k))
}
if regexp.MustCompile(`--`).MatchString(value) {
es = append(es, fmt.Errorf("%q cannot contain two consecutive hyphens", k))
}
if regexp.MustCompile(`-$`).MatchString(value) {
es = append(es, fmt.Errorf("%q cannot end in a hyphen", k))
}
return
},
}, },
"db_subnet_group_name": &schema.Schema{ "db_subnet_group_name": &schema.Schema{
@ -184,6 +227,30 @@ func resourceAwsDbInstance() *schema.Resource {
Computed: true, Computed: true,
}, },
"replicate_source_db": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"replicas": &schema.Schema{
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"snapshot_identifier": &schema.Schema{
Type: schema.TypeString,
Computed: false,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"auto_minor_version_upgrade": &schema.Schema{
Type: schema.TypeBool,
Computed: false,
Optional: true,
},
"tags": tagsSchema(), "tags": tagsSchema(),
}, },
} }
@ -192,82 +259,173 @@ func resourceAwsDbInstance() *schema.Resource {
func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error { func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).rdsconn conn := meta.(*AWSClient).rdsconn
tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{})) tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{}))
opts := rds.CreateDBInstanceInput{
AllocatedStorage: aws.Long(int64(d.Get("allocated_storage").(int))),
DBInstanceClass: aws.String(d.Get("instance_class").(string)),
DBInstanceIdentifier: aws.String(d.Get("identifier").(string)),
DBName: aws.String(d.Get("name").(string)),
MasterUsername: aws.String(d.Get("username").(string)),
MasterUserPassword: aws.String(d.Get("password").(string)),
Engine: aws.String(d.Get("engine").(string)),
EngineVersion: aws.String(d.Get("engine_version").(string)),
StorageEncrypted: aws.Boolean(d.Get("storage_encrypted").(bool)),
Tags: tags,
}
if attr, ok := d.GetOk("storage_type"); ok { if v, ok := d.GetOk("replicate_source_db"); ok {
opts.StorageType = aws.String(attr.(string)) opts := rds.CreateDBInstanceReadReplicaInput{
} SourceDBInstanceIdentifier: aws.String(v.(string)),
DBInstanceClass: aws.String(d.Get("instance_class").(string)),
attr := d.Get("backup_retention_period") DBInstanceIdentifier: aws.String(d.Get("identifier").(string)),
opts.BackupRetentionPeriod = aws.Long(int64(attr.(int))) Tags: tags,
if attr, ok := d.GetOk("iops"); ok {
opts.IOPS = aws.Long(int64(attr.(int)))
}
if attr, ok := d.GetOk("port"); ok {
opts.Port = aws.Long(int64(attr.(int)))
}
if attr, ok := d.GetOk("multi_az"); ok {
opts.MultiAZ = aws.Boolean(attr.(bool))
}
if attr, ok := d.GetOk("availability_zone"); ok {
opts.AvailabilityZone = aws.String(attr.(string))
}
if attr, ok := d.GetOk("maintenance_window"); ok {
opts.PreferredMaintenanceWindow = aws.String(attr.(string))
}
if attr, ok := d.GetOk("backup_window"); ok {
opts.PreferredBackupWindow = aws.String(attr.(string))
}
if attr, ok := d.GetOk("publicly_accessible"); ok {
opts.PubliclyAccessible = aws.Boolean(attr.(bool))
}
if attr, ok := d.GetOk("db_subnet_group_name"); ok {
opts.DBSubnetGroupName = aws.String(attr.(string))
}
if attr, ok := d.GetOk("parameter_group_name"); ok {
opts.DBParameterGroupName = aws.String(attr.(string))
}
if attr := d.Get("vpc_security_group_ids").(*schema.Set); attr.Len() > 0 {
var s []*string
for _, v := range attr.List() {
s = append(s, aws.String(v.(string)))
} }
opts.VPCSecurityGroupIDs = s if attr, ok := d.GetOk("iops"); ok {
} opts.IOPS = aws.Long(int64(attr.(int)))
if attr := d.Get("security_group_names").(*schema.Set); attr.Len() > 0 {
var s []*string
for _, v := range attr.List() {
s = append(s, aws.String(v.(string)))
} }
opts.DBSecurityGroups = s
}
log.Printf("[DEBUG] DB Instance create configuration: %#v", opts) if attr, ok := d.GetOk("port"); ok {
_, err := conn.CreateDBInstance(&opts) opts.Port = aws.Long(int64(attr.(int)))
if err != nil { }
return fmt.Errorf("Error creating DB Instance: %s", err)
if attr, ok := d.GetOk("availability_zone"); ok {
opts.AvailabilityZone = aws.String(attr.(string))
}
if attr, ok := d.GetOk("publicly_accessible"); ok {
opts.PubliclyAccessible = aws.Boolean(attr.(bool))
}
_, err := conn.CreateDBInstanceReadReplica(&opts)
if err != nil {
return fmt.Errorf("Error creating DB Instance: %s", err)
}
} else if _, ok := d.GetOk("snapshot_identifier"); ok {
opts := rds.RestoreDBInstanceFromDBSnapshotInput{
DBInstanceClass: aws.String(d.Get("instance_class").(string)),
DBInstanceIdentifier: aws.String(d.Get("identifier").(string)),
DBSnapshotIdentifier: aws.String(d.Get("snapshot_identifier").(string)),
Tags: tags,
}
if attr, ok := d.GetOk("auto_minor_version_upgrade"); ok {
opts.AutoMinorVersionUpgrade = aws.Boolean(attr.(bool))
}
if attr, ok := d.GetOk("availability_zone"); ok {
opts.AvailabilityZone = aws.String(attr.(string))
}
if attr, ok := d.GetOk("db_subnet_group_name"); ok {
opts.DBSubnetGroupName = aws.String(attr.(string))
}
if attr, ok := d.GetOk("engine"); ok {
opts.Engine = aws.String(attr.(string))
}
if attr, ok := d.GetOk("iops"); ok {
opts.IOPS = aws.Long(int64(attr.(int)))
}
if attr, ok := d.GetOk("license_model"); ok {
opts.LicenseModel = aws.String(attr.(string))
}
if attr, ok := d.GetOk("multi_az"); ok {
opts.MultiAZ = aws.Boolean(attr.(bool))
}
if attr, ok := d.GetOk("option_group_name"); ok {
opts.OptionGroupName = aws.String(attr.(string))
}
if attr, ok := d.GetOk("port"); ok {
opts.Port = aws.Long(int64(attr.(int)))
}
if attr, ok := d.GetOk("publicly_accessible"); ok {
opts.PubliclyAccessible = aws.Boolean(attr.(bool))
}
if attr, ok := d.GetOk("tde_credential_arn"); ok {
opts.TDECredentialARN = aws.String(attr.(string))
}
if attr, ok := d.GetOk("storage_type"); ok {
opts.StorageType = aws.String(attr.(string))
}
_, err := conn.RestoreDBInstanceFromDBSnapshot(&opts)
if err != nil {
return fmt.Errorf("Error creating DB Instance: %s", err)
}
} else {
opts := rds.CreateDBInstanceInput{
AllocatedStorage: aws.Long(int64(d.Get("allocated_storage").(int))),
DBName: aws.String(d.Get("name").(string)),
DBInstanceClass: aws.String(d.Get("instance_class").(string)),
DBInstanceIdentifier: aws.String(d.Get("identifier").(string)),
MasterUsername: aws.String(d.Get("username").(string)),
MasterUserPassword: aws.String(d.Get("password").(string)),
Engine: aws.String(d.Get("engine").(string)),
EngineVersion: aws.String(d.Get("engine_version").(string)),
StorageEncrypted: aws.Boolean(d.Get("storage_encrypted").(bool)),
Tags: tags,
}
attr := d.Get("backup_retention_period")
opts.BackupRetentionPeriod = aws.Long(int64(attr.(int)))
if attr, ok := d.GetOk("multi_az"); ok {
opts.MultiAZ = aws.Boolean(attr.(bool))
}
if attr, ok := d.GetOk("maintenance_window"); ok {
opts.PreferredMaintenanceWindow = aws.String(attr.(string))
}
if attr, ok := d.GetOk("backup_window"); ok {
opts.PreferredBackupWindow = aws.String(attr.(string))
}
if attr, ok := d.GetOk("license_model"); ok {
opts.LicenseModel = aws.String(attr.(string))
}
if attr, ok := d.GetOk("parameter_group_name"); ok {
opts.DBParameterGroupName = aws.String(attr.(string))
}
if attr := d.Get("vpc_security_group_ids").(*schema.Set); attr.Len() > 0 {
var s []*string
for _, v := range attr.List() {
s = append(s, aws.String(v.(string)))
}
opts.VPCSecurityGroupIDs = s
}
if attr := d.Get("security_group_names").(*schema.Set); attr.Len() > 0 {
var s []*string
for _, v := range attr.List() {
s = append(s, aws.String(v.(string)))
}
opts.DBSecurityGroups = s
}
if attr, ok := d.GetOk("storage_type"); ok {
opts.StorageType = aws.String(attr.(string))
}
if attr, ok := d.GetOk("db_subnet_group_name"); ok {
opts.DBSubnetGroupName = aws.String(attr.(string))
}
if attr, ok := d.GetOk("iops"); ok {
opts.IOPS = aws.Long(int64(attr.(int)))
}
if attr, ok := d.GetOk("port"); ok {
opts.Port = aws.Long(int64(attr.(int)))
}
if attr, ok := d.GetOk("availability_zone"); ok {
opts.AvailabilityZone = aws.String(attr.(string))
}
if attr, ok := d.GetOk("publicly_accessible"); ok {
opts.PubliclyAccessible = aws.Boolean(attr.(bool))
}
log.Printf("[DEBUG] DB Instance create configuration: %#v", opts)
var err error
_, err = conn.CreateDBInstance(&opts)
if err != nil {
return fmt.Errorf("Error creating DB Instance: %s", err)
}
} }
d.SetId(d.Get("identifier").(string)) d.SetId(d.Get("identifier").(string))
@ -287,7 +445,7 @@ func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error
} }
// Wait, catching any errors // Wait, catching any errors
_, err = stateConf.WaitForState() _, err := stateConf.WaitForState()
if err != nil { if err != nil {
return err return err
} }
@ -316,6 +474,7 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("availability_zone", v.AvailabilityZone) d.Set("availability_zone", v.AvailabilityZone)
d.Set("backup_retention_period", v.BackupRetentionPeriod) d.Set("backup_retention_period", v.BackupRetentionPeriod)
d.Set("backup_window", v.PreferredBackupWindow) d.Set("backup_window", v.PreferredBackupWindow)
d.Set("license_model", v.LicenseModel)
d.Set("maintenance_window", v.PreferredMaintenanceWindow) d.Set("maintenance_window", v.PreferredMaintenanceWindow)
d.Set("multi_az", v.MultiAZ) d.Set("multi_az", v.MultiAZ)
if v.DBSubnetGroup != nil { if v.DBSubnetGroup != nil {
@ -384,6 +543,18 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error {
} }
d.Set("security_group_names", sgn) d.Set("security_group_names", sgn)
// replica things
var replicas []string
for _, v := range v.ReadReplicaDBInstanceIdentifiers {
replicas = append(replicas, *v)
}
if err := d.Set("replicas", replicas); err != nil {
return fmt.Errorf("[DEBUG] Error setting replicas attribute: %#v, error: %#v", replicas, err)
}
d.Set("replicate_source_db", v.ReadReplicaSourceDBInstanceIdentifier)
return nil return nil
} }
@ -523,6 +694,28 @@ func resourceAwsDbInstanceUpdate(d *schema.ResourceData, meta interface{}) error
} }
} }
// seperate request to promote a database
if d.HasChange("replicate_source_db") {
if d.Get("replicate_source_db").(string) == "" {
// promote
opts := rds.PromoteReadReplicaInput{
DBInstanceIdentifier: aws.String(d.Id()),
}
attr := d.Get("backup_retention_period")
opts.BackupRetentionPeriod = aws.Long(int64(attr.(int)))
if attr, ok := d.GetOk("backup_window"); ok {
opts.PreferredBackupWindow = aws.String(attr.(string))
}
_, err := conn.PromoteReadReplica(&opts)
if err != nil {
return fmt.Errorf("Error promoting database: %#v", err)
}
d.Set("replicate_source_db", "")
} else {
return fmt.Errorf("cannot elect new source database for replication")
}
}
if arn, err := buildRDSARN(d, meta); err == nil { if arn, err := buildRDSARN(d, meta); err == nil {
if err := setTagsRDS(conn, d, arn); err != nil { if err := setTagsRDS(conn, d, arn); err != nil {
return err return err
@ -547,8 +740,8 @@ func resourceAwsDbInstanceRetrieve(
resp, err := conn.DescribeDBInstances(&opts) resp, err := conn.DescribeDBInstances(&opts)
if err != nil { if err != nil {
dbinstanceerr, ok := err.(aws.APIError) dbinstanceerr, ok := err.(awserr.Error)
if ok && dbinstanceerr.Code == "DBInstanceNotFound" { if ok && dbinstanceerr.Code() == "DBInstanceNotFound" {
return nil, nil return nil, nil
} }
return nil, fmt.Errorf("Error retrieving DB Instances: %s", err) return nil, fmt.Errorf("Error retrieving DB Instances: %s", err)
@ -578,6 +771,10 @@ func resourceAwsDbInstanceStateRefreshFunc(
return nil, "", nil return nil, "", nil
} }
if v.DBInstanceStatus != nil {
log.Printf("[DEBUG] DB Instance status for instance %s: %s", d.Id(), *v.DBInstanceStatus)
}
return v, *v.DBInstanceStatus, nil return v, *v.DBInstanceStatus, nil
} }
} }

View File

@ -9,11 +9,12 @@ import (
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/rds"
) )
func TestAccAWSDBInstance(t *testing.T) { func TestAccAWSDBInstance_basic(t *testing.T) {
var v rds.DBInstance var v rds.DBInstance
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
@ -32,6 +33,8 @@ func TestAccAWSDBInstance(t *testing.T) {
"aws_db_instance.bar", "engine", "mysql"), "aws_db_instance.bar", "engine", "mysql"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_db_instance.bar", "engine_version", "5.6.21"), "aws_db_instance.bar", "engine_version", "5.6.21"),
resource.TestCheckResourceAttr(
"aws_db_instance.bar", "license_model", "general-public-license"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_db_instance.bar", "instance_class", "db.t1.micro"), "aws_db_instance.bar", "instance_class", "db.t1.micro"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
@ -46,6 +49,26 @@ func TestAccAWSDBInstance(t *testing.T) {
}) })
} }
func TestAccAWSDBInstanceReplica(t *testing.T) {
var s, r rds.DBInstance
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccReplicaInstanceConfig(rand.New(rand.NewSource(time.Now().UnixNano())).Int()),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.bar", &s),
testAccCheckAWSDBInstanceExists("aws_db_instance.replica", &r),
testAccCheckAWSDBInstanceReplicaAttributes(&s, &r),
),
},
},
})
}
func testAccCheckAWSDBInstanceDestroy(s *terraform.State) error { func testAccCheckAWSDBInstanceDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).rdsconn conn := testAccProvider.Meta().(*AWSClient).rdsconn
@ -55,6 +78,7 @@ func testAccCheckAWSDBInstanceDestroy(s *terraform.State) error {
} }
// Try to find the Group // Try to find the Group
var err error
resp, err := conn.DescribeDBInstances( resp, err := conn.DescribeDBInstances(
&rds.DescribeDBInstancesInput{ &rds.DescribeDBInstancesInput{
DBInstanceIdentifier: aws.String(rs.Primary.ID), DBInstanceIdentifier: aws.String(rs.Primary.ID),
@ -68,11 +92,11 @@ func testAccCheckAWSDBInstanceDestroy(s *terraform.State) error {
} }
// Verify the error // Verify the error
newerr, ok := err.(*aws.APIError) newerr, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if newerr.Code != "InvalidDBInstance.NotFound" { if newerr.Code() != "InvalidDBInstance.NotFound" {
return err return err
} }
} }
@ -99,6 +123,17 @@ func testAccCheckAWSDBInstanceAttributes(v *rds.DBInstance) resource.TestCheckFu
} }
} }
func testAccCheckAWSDBInstanceReplicaAttributes(source, replica *rds.DBInstance) resource.TestCheckFunc {
return func(s *terraform.State) error {
if replica.ReadReplicaSourceDBInstanceIdentifier != nil && *replica.ReadReplicaSourceDBInstanceIdentifier != *source.DBInstanceIdentifier {
return fmt.Errorf("bad source identifier for replica, expected: '%s', got: '%s'", *source.DBInstanceIdentifier, *replica.ReadReplicaSourceDBInstanceIdentifier)
}
return nil
}
}
func testAccCheckAWSDBInstanceExists(n string, v *rds.DBInstance) resource.TestCheckFunc { func testAccCheckAWSDBInstanceExists(n string, v *rds.DBInstance) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n] rs, ok := s.RootModule().Resources[n]
@ -152,3 +187,38 @@ resource "aws_db_instance" "bar" {
parameter_group_name = "default.mysql5.6" parameter_group_name = "default.mysql5.6"
}`, rand.New(rand.NewSource(time.Now().UnixNano())).Int()) }`, rand.New(rand.NewSource(time.Now().UnixNano())).Int())
func testAccReplicaInstanceConfig(val int) string {
return fmt.Sprintf(`
resource "aws_db_instance" "bar" {
identifier = "foobarbaz-test-terraform-%d"
allocated_storage = 5
engine = "mysql"
engine_version = "5.6.21"
instance_class = "db.t1.micro"
name = "baz"
password = "barbarbarbar"
username = "foo"
backup_retention_period = 1
parameter_group_name = "default.mysql5.6"
}
resource "aws_db_instance" "replica" {
identifier = "tf-replica-db-%d"
backup_retention_period = 0
replicate_source_db = "${aws_db_instance.bar.identifier}"
allocated_storage = "${aws_db_instance.bar.allocated_storage}"
engine = "${aws_db_instance.bar.engine}"
engine_version = "${aws_db_instance.bar.engine_version}"
instance_class = "${aws_db_instance.bar.instance_class}"
password = "${aws_db_instance.bar.password}"
username = "${aws_db_instance.bar.username}"
tags {
Name = "tf-replica-db"
}
}
`, val, val)
}

View File

@ -11,8 +11,9 @@ import (
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/rds"
) )
func resourceAwsDbParameterGroup() *schema.Resource { func resourceAwsDbParameterGroup() *schema.Resource {
@ -203,12 +204,12 @@ func resourceAwsDbParameterGroupDeleteRefreshFunc(
} }
if _, err := rdsconn.DeleteDBParameterGroup(&deleteOpts); err != nil { if _, err := rdsconn.DeleteDBParameterGroup(&deleteOpts); err != nil {
rdserr, ok := err.(aws.APIError) rdserr, ok := err.(awserr.Error)
if !ok { if !ok {
return d, "error", err return d, "error", err
} }
if rdserr.Code != "DBParameterGroupNotFoundFault" { if rdserr.Code() != "DBParameterGroupNotFoundFault" {
return d, "error", err return d, "error", err
} }
} }

View File

@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSDBParameterGroup(t *testing.T) { func TestAccAWSDBParameterGroup_basic(t *testing.T) {
var v rds.DBParameterGroup var v rds.DBParameterGroup
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
@ -127,11 +128,11 @@ func testAccCheckAWSDBParameterGroupDestroy(s *terraform.State) error {
} }
// Verify the error // Verify the error
newerr, ok := err.(aws.APIError) newerr, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if newerr.Code != "InvalidDBParameterGroup.NotFound" { if newerr.Code() != "InvalidDBParameterGroup.NotFound" {
return err return err
} }
} }

View File

@ -6,8 +6,9 @@ import (
"log" "log"
"time" "time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/multierror" "github.com/hashicorp/terraform/helper/multierror"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
@ -170,8 +171,8 @@ func resourceAwsDbSecurityGroupDelete(d *schema.ResourceData, meta interface{})
_, err := conn.DeleteDBSecurityGroup(&opts) _, err := conn.DeleteDBSecurityGroup(&opts)
if err != nil { if err != nil {
newerr, ok := err.(aws.APIError) newerr, ok := err.(awserr.Error)
if ok && newerr.Code == "InvalidDBSecurityGroup.NotFound" { if ok && newerr.Code() == "InvalidDBSecurityGroup.NotFound" {
return nil return nil
} }
return err return err

View File

@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSDBSecurityGroup(t *testing.T) { func TestAccAWSDBSecurityGroup_basic(t *testing.T) {
var v rds.DBSecurityGroup var v rds.DBSecurityGroup
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
@ -59,11 +60,11 @@ func testAccCheckAWSDBSecurityGroupDestroy(s *terraform.State) error {
} }
// Verify the error // Verify the error
newerr, ok := err.(aws.APIError) newerr, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if newerr.Code != "InvalidDBSecurityGroup.NotFound" { if newerr.Code() != "InvalidDBSecurityGroup.NotFound" {
return err return err
} }
} }

View File

@ -3,11 +3,13 @@ package aws
import ( import (
"fmt" "fmt"
"log" "log"
"regexp"
"strings" "strings"
"time" "time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -23,6 +25,22 @@ func resourceAwsDbSubnetGroup() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
ForceNew: true, ForceNew: true,
Required: true, Required: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if !regexp.MustCompile(`^[.0-9A-Za-z-_]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"only alphanumeric characters, hyphens, underscores, and periods allowed in %q", k))
}
if len(value) > 255 {
errors = append(errors, fmt.Errorf(
"%q cannot be longer than 255 characters", k))
}
if regexp.MustCompile(`(?i)^default$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q is not allowed as %q", "Default", k))
}
return
},
}, },
"description": &schema.Schema{ "description": &schema.Schema{
@ -77,7 +95,7 @@ func resourceAwsDbSubnetGroupRead(d *schema.ResourceData, meta interface{}) erro
describeResp, err := rdsconn.DescribeDBSubnetGroups(&describeOpts) describeResp, err := rdsconn.DescribeDBSubnetGroups(&describeOpts)
if err != nil { if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "DBSubnetGroupNotFoundFault" { if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "DBSubnetGroupNotFoundFault" {
// Update state to indicate the db subnet no longer exists. // Update state to indicate the db subnet no longer exists.
d.SetId("") d.SetId("")
return nil return nil
@ -139,12 +157,12 @@ func resourceAwsDbSubnetGroupDeleteRefreshFunc(
} }
if _, err := rdsconn.DeleteDBSubnetGroup(&deleteOpts); err != nil { if _, err := rdsconn.DeleteDBSubnetGroup(&deleteOpts); err != nil {
rdserr, ok := err.(aws.APIError) rdserr, ok := err.(awserr.Error)
if !ok { if !ok {
return d, "error", err return d, "error", err
} }
if rdserr.Code != "DBSubnetGroupNotFoundFault" { if rdserr.Code() != "DBSubnetGroupNotFoundFault" {
return d, "error", err return d, "error", err
} }
} }

View File

@ -7,11 +7,12 @@ import (
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/rds"
) )
func TestAccAWSDBSubnetGroup(t *testing.T) { func TestAccAWSDBSubnetGroup_basic(t *testing.T) {
var v rds.DBSubnetGroup var v rds.DBSubnetGroup
testCheck := func(*terraform.State) error { testCheck := func(*terraform.State) error {
@ -35,6 +36,34 @@ func TestAccAWSDBSubnetGroup(t *testing.T) {
}) })
} }
// Regression test for https://github.com/hashicorp/terraform/issues/2603 and
// https://github.com/hashicorp/terraform/issues/2664
func TestAccAWSDBSubnetGroup_withUndocumentedCharacters(t *testing.T) {
var v rds.DBSubnetGroup
testCheck := func(*terraform.State) error {
return nil
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDBSubnetGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDBSubnetGroupConfig_withUnderscoresAndPeriods,
Check: resource.ComposeTestCheckFunc(
testAccCheckDBSubnetGroupExists(
"aws_db_subnet_group.underscores", &v),
testAccCheckDBSubnetGroupExists(
"aws_db_subnet_group.periods", &v),
testCheck,
),
},
},
})
}
func testAccCheckDBSubnetGroupDestroy(s *terraform.State) error { func testAccCheckDBSubnetGroupDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).rdsconn conn := testAccProvider.Meta().(*AWSClient).rdsconn
@ -55,11 +84,11 @@ func testAccCheckDBSubnetGroupDestroy(s *terraform.State) error {
} }
// Verify the error is what we want // Verify the error is what we want
rdserr, ok := err.(aws.APIError) rdserr, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if rdserr.Code != "DBSubnetGroupNotFoundFault" { if rdserr.Code() != "DBSubnetGroupNotFoundFault" {
return err return err
} }
} }
@ -123,3 +152,33 @@ resource "aws_db_subnet_group" "foo" {
subnet_ids = ["${aws_subnet.foo.id}", "${aws_subnet.bar.id}"] subnet_ids = ["${aws_subnet.foo.id}", "${aws_subnet.bar.id}"]
} }
` `
const testAccDBSubnetGroupConfig_withUnderscoresAndPeriods = `
resource "aws_vpc" "main" {
cidr_block = "192.168.0.0/16"
}
resource "aws_subnet" "frontend" {
vpc_id = "${aws_vpc.main.id}"
availability_zone = "us-west-2b"
cidr_block = "192.168.1.0/24"
}
resource "aws_subnet" "backend" {
vpc_id = "${aws_vpc.main.id}"
availability_zone = "us-west-2c"
cidr_block = "192.168.2.0/24"
}
resource "aws_db_subnet_group" "underscores" {
name = "with_underscores"
description = "Our main group of subnets"
subnet_ids = ["${aws_subnet.frontend.id}", "${aws_subnet.backend.id}"]
}
resource "aws_db_subnet_group" "periods" {
name = "with.periods"
description = "Our main group of subnets"
subnet_ids = ["${aws_subnet.frontend.id}", "${aws_subnet.backend.id}"]
}
`

View File

@ -0,0 +1,735 @@
package aws
import (
"bytes"
"fmt"
"log"
"time"
"github.com/hashicorp/terraform/helper/schema"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/hashicorp/terraform/helper/hashcode"
)
// Number of times to retry if a throttling-related exception occurs
const DYNAMODB_MAX_THROTTLE_RETRIES = 5
// How long to sleep when a throttle-event happens
const DYNAMODB_THROTTLE_SLEEP = 5 * time.Second
// How long to sleep if a limit-exceeded event happens
const DYNAMODB_LIMIT_EXCEEDED_SLEEP = 10 * time.Second
// A number of these are marked as computed because if you don't
// provide a value, DynamoDB will provide you with defaults (which are the
// default values specified below)
func resourceAwsDynamoDbTable() *schema.Resource {
return &schema.Resource{
Create: resourceAwsDynamoDbTableCreate,
Read: resourceAwsDynamoDbTableRead,
Update: resourceAwsDynamoDbTableUpdate,
Delete: resourceAwsDynamoDbTableDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"hash_key": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"range_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"write_capacity": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"read_capacity": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"attribute": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"type": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},
},
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
return hashcode.String(buf.String())
},
},
"local_secondary_index": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"range_key": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"projection_type": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"non_key_attributes": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
return hashcode.String(buf.String())
},
},
"global_secondary_index": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"write_capacity": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"read_capacity": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"hash_key": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"range_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"projection_type": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"non_key_attributes": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
// GSI names are the uniqueness constraint
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
buf.WriteString(fmt.Sprintf("%d-", m["write_capacity"].(int)))
buf.WriteString(fmt.Sprintf("%d-", m["read_capacity"].(int)))
return hashcode.String(buf.String())
},
},
},
}
}
func resourceAwsDynamoDbTableCreate(d *schema.ResourceData, meta interface{}) error {
dynamodbconn := meta.(*AWSClient).dynamodbconn
name := d.Get("name").(string)
log.Printf("[DEBUG] DynamoDB table create: %s", name)
throughput := &dynamodb.ProvisionedThroughput{
ReadCapacityUnits: aws.Long(int64(d.Get("read_capacity").(int))),
WriteCapacityUnits: aws.Long(int64(d.Get("write_capacity").(int))),
}
hash_key_name := d.Get("hash_key").(string)
keyschema := []*dynamodb.KeySchemaElement{
&dynamodb.KeySchemaElement{
AttributeName: aws.String(hash_key_name),
KeyType: aws.String("HASH"),
},
}
if range_key, ok := d.GetOk("range_key"); ok {
range_schema_element := &dynamodb.KeySchemaElement{
AttributeName: aws.String(range_key.(string)),
KeyType: aws.String("RANGE"),
}
keyschema = append(keyschema, range_schema_element)
}
req := &dynamodb.CreateTableInput{
TableName: aws.String(name),
ProvisionedThroughput: throughput,
KeySchema: keyschema,
}
if attributedata, ok := d.GetOk("attribute"); ok {
attributes := []*dynamodb.AttributeDefinition{}
attributeSet := attributedata.(*schema.Set)
for _, attribute := range attributeSet.List() {
attr := attribute.(map[string]interface{})
attributes = append(attributes, &dynamodb.AttributeDefinition{
AttributeName: aws.String(attr["name"].(string)),
AttributeType: aws.String(attr["type"].(string)),
})
}
req.AttributeDefinitions = attributes
}
if lsidata, ok := d.GetOk("local_secondary_index"); ok {
fmt.Printf("[DEBUG] Adding LSI data to the table")
lsiSet := lsidata.(*schema.Set)
localSecondaryIndexes := []*dynamodb.LocalSecondaryIndex{}
for _, lsiObject := range lsiSet.List() {
lsi := lsiObject.(map[string]interface{})
projection := &dynamodb.Projection{
ProjectionType: aws.String(lsi["projection_type"].(string)),
}
if lsi["projection_type"] == "INCLUDE" {
non_key_attributes := []*string{}
for _, attr := range lsi["non_key_attributes"].([]interface{}) {
non_key_attributes = append(non_key_attributes, aws.String(attr.(string)))
}
projection.NonKeyAttributes = non_key_attributes
}
localSecondaryIndexes = append(localSecondaryIndexes, &dynamodb.LocalSecondaryIndex{
IndexName: aws.String(lsi["name"].(string)),
KeySchema: []*dynamodb.KeySchemaElement{
&dynamodb.KeySchemaElement{
AttributeName: aws.String(hash_key_name),
KeyType: aws.String("HASH"),
},
&dynamodb.KeySchemaElement{
AttributeName: aws.String(lsi["range_key"].(string)),
KeyType: aws.String("RANGE"),
},
},
Projection: projection,
})
}
req.LocalSecondaryIndexes = localSecondaryIndexes
fmt.Printf("[DEBUG] Added %d LSI definitions", len(localSecondaryIndexes))
}
if gsidata, ok := d.GetOk("global_secondary_index"); ok {
globalSecondaryIndexes := []*dynamodb.GlobalSecondaryIndex{}
gsiSet := gsidata.(*schema.Set)
for _, gsiObject := range gsiSet.List() {
gsi := gsiObject.(map[string]interface{})
gsiObject := createGSIFromData(&gsi)
globalSecondaryIndexes = append(globalSecondaryIndexes, &gsiObject)
}
req.GlobalSecondaryIndexes = globalSecondaryIndexes
}
attemptCount := 1
for attemptCount <= DYNAMODB_MAX_THROTTLE_RETRIES {
output, err := dynamodbconn.CreateTable(req)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "ThrottlingException" {
log.Printf("[DEBUG] Attempt %d/%d: Sleeping for a bit to throttle back create request", attemptCount, DYNAMODB_MAX_THROTTLE_RETRIES)
time.Sleep(DYNAMODB_THROTTLE_SLEEP)
attemptCount += 1
} else if awsErr.Code() == "LimitExceededException" {
log.Printf("[DEBUG] Limit on concurrent table creations hit, sleeping for a bit")
time.Sleep(DYNAMODB_LIMIT_EXCEEDED_SLEEP)
attemptCount += 1
} else {
// Some other non-retryable exception occurred
return fmt.Errorf("AWS Error creating DynamoDB table: %s", err)
}
} else {
// Non-AWS exception occurred, give up
return fmt.Errorf("Error creating DynamoDB table: %s", err)
}
} else {
// No error, set ID and return
d.SetId(*output.TableDescription.TableName)
return nil
}
}
// Too many throttling events occurred, give up
return fmt.Errorf("Unable to create DynamoDB table '%s' after %d attempts", name, attemptCount)
}
func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG] Updating DynamoDB table %s", d.Id())
dynamodbconn := meta.(*AWSClient).dynamodbconn
// Ensure table is active before trying to update
waitForTableToBeActive(d.Id(), meta)
// LSI can only be done at create-time, abort if it's been changed
if d.HasChange("local_secondary_index") {
return fmt.Errorf("Local secondary indexes can only be built at creation, you cannot update them!")
}
if d.HasChange("hash_key") {
return fmt.Errorf("Hash key can only be specified at creation, you cannot modify it.")
}
if d.HasChange("range_key") {
return fmt.Errorf("Range key can only be specified at creation, you cannot modify it.")
}
if d.HasChange("read_capacity") || d.HasChange("write_capacity") {
req := &dynamodb.UpdateTableInput{
TableName: aws.String(d.Id()),
}
throughput := &dynamodb.ProvisionedThroughput{
ReadCapacityUnits: aws.Long(int64(d.Get("read_capacity").(int))),
WriteCapacityUnits: aws.Long(int64(d.Get("write_capacity").(int))),
}
req.ProvisionedThroughput = throughput
_, err := dynamodbconn.UpdateTable(req)
if err != nil {
return err
}
waitForTableToBeActive(d.Id(), meta)
}
if d.HasChange("global_secondary_index") {
log.Printf("[DEBUG] Changed GSI data")
req := &dynamodb.UpdateTableInput{
TableName: aws.String(d.Id()),
}
o, n := d.GetChange("global_secondary_index")
oldSet := o.(*schema.Set)
newSet := n.(*schema.Set)
// Track old names so we can know which ones we need to just update based on
// capacity changes, terraform appears to only diff on the set hash, not the
// contents so we need to make sure we don't delete any indexes that we
// just want to update the capacity for
oldGsiNameSet := make(map[string]bool)
newGsiNameSet := make(map[string]bool)
for _, gsidata := range oldSet.List() {
gsiName := gsidata.(map[string]interface{})["name"].(string)
oldGsiNameSet[gsiName] = true
}
for _, gsidata := range newSet.List() {
gsiName := gsidata.(map[string]interface{})["name"].(string)
newGsiNameSet[gsiName] = true
}
// First determine what's new
for _, newgsidata := range newSet.List() {
updates := []*dynamodb.GlobalSecondaryIndexUpdate{}
newGsiName := newgsidata.(map[string]interface{})["name"].(string)
if _, exists := oldGsiNameSet[newGsiName]; !exists {
attributes := []*dynamodb.AttributeDefinition{}
gsidata := newgsidata.(map[string]interface{})
gsi := createGSIFromData(&gsidata)
log.Printf("[DEBUG] Adding GSI %s", *gsi.IndexName)
update := &dynamodb.GlobalSecondaryIndexUpdate{
Create: &dynamodb.CreateGlobalSecondaryIndexAction{
IndexName: gsi.IndexName,
KeySchema: gsi.KeySchema,
ProvisionedThroughput: gsi.ProvisionedThroughput,
Projection: gsi.Projection,
},
}
updates = append(updates, update)
// Hash key is required, range key isn't
hashkey_type, err := getAttributeType(d, *(gsi.KeySchema[0].AttributeName))
if err != nil {
return err
}
attributes = append(attributes, &dynamodb.AttributeDefinition{
AttributeName: gsi.KeySchema[0].AttributeName,
AttributeType: aws.String(hashkey_type),
})
// If there's a range key, there will be 2 elements in KeySchema
if len(gsi.KeySchema) == 2 {
rangekey_type, err := getAttributeType(d, *(gsi.KeySchema[1].AttributeName))
if err != nil {
return err
}
attributes = append(attributes, &dynamodb.AttributeDefinition{
AttributeName: gsi.KeySchema[1].AttributeName,
AttributeType: aws.String(rangekey_type),
})
}
req.AttributeDefinitions = attributes
req.GlobalSecondaryIndexUpdates = updates
_, err = dynamodbconn.UpdateTable(req)
if err != nil {
return err
}
waitForTableToBeActive(d.Id(), meta)
waitForGSIToBeActive(d.Id(), *gsi.IndexName, meta)
}
}
for _, oldgsidata := range oldSet.List() {
updates := []*dynamodb.GlobalSecondaryIndexUpdate{}
oldGsiName := oldgsidata.(map[string]interface{})["name"].(string)
if _, exists := newGsiNameSet[oldGsiName]; !exists {
gsidata := oldgsidata.(map[string]interface{})
log.Printf("[DEBUG] Deleting GSI %s", gsidata["name"].(string))
update := &dynamodb.GlobalSecondaryIndexUpdate{
Delete: &dynamodb.DeleteGlobalSecondaryIndexAction{
IndexName: aws.String(gsidata["name"].(string)),
},
}
updates = append(updates, update)
req.GlobalSecondaryIndexUpdates = updates
_, err := dynamodbconn.UpdateTable(req)
if err != nil {
return err
}
waitForTableToBeActive(d.Id(), meta)
}
}
}
// Update any out-of-date read / write capacity
if gsiObjects, ok := d.GetOk("global_secondary_index"); ok {
gsiSet := gsiObjects.(*schema.Set)
if len(gsiSet.List()) > 0 {
log.Printf("Updating capacity as needed!")
// We can only change throughput, but we need to make sure it's actually changed
tableDescription, err := dynamodbconn.DescribeTable(&dynamodb.DescribeTableInput{
TableName: aws.String(d.Id()),
})
if err != nil {
return err
}
table := tableDescription.Table
updates := []*dynamodb.GlobalSecondaryIndexUpdate{}
for _, updatedgsidata := range gsiSet.List() {
gsidata := updatedgsidata.(map[string]interface{})
gsiName := gsidata["name"].(string)
gsiWriteCapacity := gsidata["write_capacity"].(int)
gsiReadCapacity := gsidata["read_capacity"].(int)
log.Printf("[DEBUG] Updating GSI %s", gsiName)
gsi, err := getGlobalSecondaryIndex(gsiName, table.GlobalSecondaryIndexes)
if err != nil {
return err
}
capacityUpdated := false
if int64(gsiReadCapacity) != *(gsi.ProvisionedThroughput.ReadCapacityUnits) ||
int64(gsiWriteCapacity) != *(gsi.ProvisionedThroughput.WriteCapacityUnits) {
capacityUpdated = true
}
if capacityUpdated {
update := &dynamodb.GlobalSecondaryIndexUpdate{
Update: &dynamodb.UpdateGlobalSecondaryIndexAction{
IndexName: aws.String(gsidata["name"].(string)),
ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
WriteCapacityUnits: aws.Long(int64(gsiWriteCapacity)),
ReadCapacityUnits: aws.Long(int64(gsiReadCapacity)),
},
},
}
updates = append(updates, update)
}
if len(updates) > 0 {
req := &dynamodb.UpdateTableInput{
TableName: aws.String(d.Id()),
}
req.GlobalSecondaryIndexUpdates = updates
log.Printf("[DEBUG] Updating GSI read / write capacity on %s", d.Id())
_, err := dynamodbconn.UpdateTable(req)
if err != nil {
log.Printf("[DEBUG] Error updating table: %s", err)
return err
}
}
}
}
}
return resourceAwsDynamoDbTableRead(d, meta)
}
func resourceAwsDynamoDbTableRead(d *schema.ResourceData, meta interface{}) error {
dynamodbconn := meta.(*AWSClient).dynamodbconn
log.Printf("[DEBUG] Loading data for DynamoDB table '%s'", d.Id())
req := &dynamodb.DescribeTableInput{
TableName: aws.String(d.Id()),
}
result, err := dynamodbconn.DescribeTable(req)
if err != nil {
return err
}
table := result.Table
d.Set("write_capacity", table.ProvisionedThroughput.WriteCapacityUnits)
d.Set("read_capacity", table.ProvisionedThroughput.ReadCapacityUnits)
attributes := []interface{}{}
for _, attrdef := range table.AttributeDefinitions {
attribute := map[string]string{
"name": *(attrdef.AttributeName),
"type": *(attrdef.AttributeType),
}
attributes = append(attributes, attribute)
log.Printf("[DEBUG] Added Attribute: %s", attribute["name"])
}
d.Set("attribute", attributes)
gsiList := make([]map[string]interface{}, 0, len(table.GlobalSecondaryIndexes))
for _, gsiObject := range table.GlobalSecondaryIndexes {
gsi := map[string]interface{}{
"write_capacity": *(gsiObject.ProvisionedThroughput.WriteCapacityUnits),
"read_capacity": *(gsiObject.ProvisionedThroughput.ReadCapacityUnits),
"name": *(gsiObject.IndexName),
}
for _, attribute := range gsiObject.KeySchema {
if *attribute.KeyType == "HASH" {
gsi["hash_key"] = *attribute.AttributeName
}
if *attribute.KeyType == "RANGE" {
gsi["range_key"] = *attribute.AttributeName
}
}
gsi["projection_type"] = *(gsiObject.Projection.ProjectionType)
gsi["non_key_attributes"] = gsiObject.Projection.NonKeyAttributes
gsiList = append(gsiList, gsi)
log.Printf("[DEBUG] Added GSI: %s - Read: %d / Write: %d", gsi["name"], gsi["read_capacity"], gsi["write_capacity"])
}
d.Set("global_secondary_index", gsiList)
return nil
}
func resourceAwsDynamoDbTableDelete(d *schema.ResourceData, meta interface{}) error {
dynamodbconn := meta.(*AWSClient).dynamodbconn
waitForTableToBeActive(d.Id(), meta)
log.Printf("[DEBUG] DynamoDB delete table: %s", d.Id())
_, err := dynamodbconn.DeleteTable(&dynamodb.DeleteTableInput{
TableName: aws.String(d.Id()),
})
if err != nil {
return err
}
return nil
}
func createGSIFromData(data *map[string]interface{}) dynamodb.GlobalSecondaryIndex {
projection := &dynamodb.Projection{
ProjectionType: aws.String((*data)["projection_type"].(string)),
}
if (*data)["projection_type"] == "INCLUDE" {
non_key_attributes := []*string{}
for _, attr := range (*data)["non_key_attributes"].([]interface{}) {
non_key_attributes = append(non_key_attributes, aws.String(attr.(string)))
}
projection.NonKeyAttributes = non_key_attributes
}
writeCapacity := (*data)["write_capacity"].(int)
readCapacity := (*data)["read_capacity"].(int)
key_schema := []*dynamodb.KeySchemaElement{
&dynamodb.KeySchemaElement{
AttributeName: aws.String((*data)["hash_key"].(string)),
KeyType: aws.String("HASH"),
},
}
range_key_name := (*data)["range_key"]
if range_key_name != "" {
range_key_element := &dynamodb.KeySchemaElement{
AttributeName: aws.String(range_key_name.(string)),
KeyType: aws.String("RANGE"),
}
key_schema = append(key_schema, range_key_element)
}
return dynamodb.GlobalSecondaryIndex{
IndexName: aws.String((*data)["name"].(string)),
KeySchema: key_schema,
Projection: projection,
ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
WriteCapacityUnits: aws.Long(int64(writeCapacity)),
ReadCapacityUnits: aws.Long(int64(readCapacity)),
},
}
}
func getGlobalSecondaryIndex(indexName string, indexList []*dynamodb.GlobalSecondaryIndexDescription) (*dynamodb.GlobalSecondaryIndexDescription, error) {
for _, gsi := range indexList {
if *(gsi.IndexName) == indexName {
return gsi, nil
}
}
return &dynamodb.GlobalSecondaryIndexDescription{}, fmt.Errorf("Can't find a GSI by that name...")
}
func getAttributeType(d *schema.ResourceData, attributeName string) (string, error) {
if attributedata, ok := d.GetOk("attribute"); ok {
attributeSet := attributedata.(*schema.Set)
for _, attribute := range attributeSet.List() {
attr := attribute.(map[string]interface{})
if attr["name"] == attributeName {
return attr["type"].(string), nil
}
}
}
return "", fmt.Errorf("Unable to find an attribute named %s", attributeName)
}
func waitForGSIToBeActive(tableName string, gsiName string, meta interface{}) error {
dynamodbconn := meta.(*AWSClient).dynamodbconn
req := &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
}
activeIndex := false
for activeIndex == false {
result, err := dynamodbconn.DescribeTable(req)
if err != nil {
return err
}
table := result.Table
var targetGSI *dynamodb.GlobalSecondaryIndexDescription = nil
for _, gsi := range table.GlobalSecondaryIndexes {
if *gsi.IndexName == gsiName {
targetGSI = gsi
}
}
if targetGSI != nil {
activeIndex = *targetGSI.IndexStatus == "ACTIVE"
if !activeIndex {
log.Printf("[DEBUG] Sleeping for 5 seconds for %s GSI to become active", gsiName)
time.Sleep(5 * time.Second)
}
} else {
log.Printf("[DEBUG] GSI %s did not exist, giving up", gsiName)
break
}
}
return nil
}
func waitForTableToBeActive(tableName string, meta interface{}) error {
dynamodbconn := meta.(*AWSClient).dynamodbconn
req := &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
}
activeState := false
for activeState == false {
result, err := dynamodbconn.DescribeTable(req)
if err != nil {
return err
}
activeState = *(result.Table.TableStatus) == "ACTIVE"
// Wait for a few seconds
if !activeState {
log.Printf("[DEBUG] Sleeping for 5 seconds for table to become active")
time.Sleep(5 * time.Second)
}
}
return nil
}

View File

@ -0,0 +1,297 @@
package aws
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSDynamoDbTable(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDynamoDbTableDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSDynamoDbConfigInitialState,
Check: resource.ComposeTestCheckFunc(
testAccCheckInitialAWSDynamoDbTableExists("aws_dynamodb_table.basic-dynamodb-table"),
),
},
resource.TestStep{
Config: testAccAWSDynamoDbConfigAddSecondaryGSI,
Check: resource.ComposeTestCheckFunc(
testAccCheckDynamoDbTableWasUpdated("aws_dynamodb_table.basic-dynamodb-table"),
),
},
},
})
}
func testAccCheckAWSDynamoDbTableDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).dynamodbconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_dynamodb_table" {
continue
}
fmt.Printf("[DEBUG] Checking if DynamoDB table %s exists", rs.Primary.ID)
// Check if queue exists by checking for its attributes
params := &dynamodb.DescribeTableInput{
TableName: aws.String(rs.Primary.ID),
}
_, err := conn.DescribeTable(params)
if err == nil {
return fmt.Errorf("DynamoDB table %s still exists. Failing!", rs.Primary.ID)
}
// Verify the error is what we want
_, ok := err.(awserr.Error)
if !ok {
return err
}
}
return nil
}
func testAccCheckInitialAWSDynamoDbTableExists(n string) resource.TestCheckFunc {
return func(s *terraform.State) error {
fmt.Printf("[DEBUG] Trying to create initial table state!")
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No DynamoDB table name specified!")
}
conn := testAccProvider.Meta().(*AWSClient).dynamodbconn
params := &dynamodb.DescribeTableInput{
TableName: aws.String(rs.Primary.ID),
}
resp, err := conn.DescribeTable(params)
if err != nil {
fmt.Printf("[ERROR] Problem describing table '%s': %s", rs.Primary.ID, err)
return err
}
table := resp.Table
fmt.Printf("[DEBUG] Checking on table %s", rs.Primary.ID)
if *table.ProvisionedThroughput.WriteCapacityUnits != 20 {
return fmt.Errorf("Provisioned write capacity was %d, not 20!", table.ProvisionedThroughput.WriteCapacityUnits)
}
if *table.ProvisionedThroughput.ReadCapacityUnits != 10 {
return fmt.Errorf("Provisioned read capacity was %d, not 10!", table.ProvisionedThroughput.ReadCapacityUnits)
}
attrCount := len(table.AttributeDefinitions)
gsiCount := len(table.GlobalSecondaryIndexes)
lsiCount := len(table.LocalSecondaryIndexes)
if attrCount != 4 {
return fmt.Errorf("There were %d attributes, not 4 like there should have been!", attrCount)
}
if gsiCount != 1 {
return fmt.Errorf("There were %d GSIs, not 1 like there should have been!", gsiCount)
}
if lsiCount != 1 {
return fmt.Errorf("There were %d LSIs, not 1 like there should have been!", lsiCount)
}
attrmap := dynamoDbAttributesToMap(&table.AttributeDefinitions)
if attrmap["TestTableHashKey"] != "S" {
return fmt.Errorf("Test table hash key was of type %s instead of S!", attrmap["TestTableHashKey"])
}
if attrmap["TestTableRangeKey"] != "S" {
return fmt.Errorf("Test table range key was of type %s instead of S!", attrmap["TestTableRangeKey"])
}
if attrmap["TestLSIRangeKey"] != "N" {
return fmt.Errorf("Test table LSI range key was of type %s instead of N!", attrmap["TestLSIRangeKey"])
}
if attrmap["TestGSIRangeKey"] != "S" {
return fmt.Errorf("Test table GSI range key was of type %s instead of S!", attrmap["TestGSIRangeKey"])
}
return nil
}
}
func testAccCheckDynamoDbTableWasUpdated(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 DynamoDB table name specified!")
}
conn := testAccProvider.Meta().(*AWSClient).dynamodbconn
params := &dynamodb.DescribeTableInput{
TableName: aws.String(rs.Primary.ID),
}
resp, err := conn.DescribeTable(params)
table := resp.Table
if err != nil {
return err
}
attrCount := len(table.AttributeDefinitions)
gsiCount := len(table.GlobalSecondaryIndexes)
lsiCount := len(table.LocalSecondaryIndexes)
if attrCount != 4 {
return fmt.Errorf("There were %d attributes, not 4 like there should have been!", attrCount)
}
if gsiCount != 1 {
return fmt.Errorf("There were %d GSIs, not 1 like there should have been!", gsiCount)
}
if lsiCount != 1 {
return fmt.Errorf("There were %d LSIs, not 1 like there should have been!", lsiCount)
}
if dynamoDbGetGSIIndex(&table.GlobalSecondaryIndexes, "ReplacementTestTableGSI") == -1 {
return fmt.Errorf("Could not find GSI named 'ReplacementTestTableGSI' in the table!")
}
if dynamoDbGetGSIIndex(&table.GlobalSecondaryIndexes, "InitialTestTableGSI") != -1 {
return fmt.Errorf("Should have removed 'InitialTestTableGSI' but it still exists!")
}
attrmap := dynamoDbAttributesToMap(&table.AttributeDefinitions)
if attrmap["TestTableHashKey"] != "S" {
return fmt.Errorf("Test table hash key was of type %s instead of S!", attrmap["TestTableHashKey"])
}
if attrmap["TestTableRangeKey"] != "S" {
return fmt.Errorf("Test table range key was of type %s instead of S!", attrmap["TestTableRangeKey"])
}
if attrmap["TestLSIRangeKey"] != "N" {
return fmt.Errorf("Test table LSI range key was of type %s instead of N!", attrmap["TestLSIRangeKey"])
}
if attrmap["ReplacementGSIRangeKey"] != "N" {
return fmt.Errorf("Test table replacement GSI range key was of type %s instead of N!", attrmap["ReplacementGSIRangeKey"])
}
return nil
}
}
func dynamoDbGetGSIIndex(gsiList *[]*dynamodb.GlobalSecondaryIndexDescription, target string) int {
for idx, gsiObject := range *gsiList {
if *gsiObject.IndexName == target {
return idx
}
}
return -1
}
func dynamoDbAttributesToMap(attributes *[]*dynamodb.AttributeDefinition) map[string]string {
attrmap := make(map[string]string)
for _, attrdef := range *attributes {
attrmap[*(attrdef.AttributeName)] = *(attrdef.AttributeType)
}
return attrmap
}
const testAccAWSDynamoDbConfigInitialState = `
resource "aws_dynamodb_table" "basic-dynamodb-table" {
name = "TerraformTestTable"
read_capacity = 10
write_capacity = 20
hash_key = "TestTableHashKey"
range_key = "TestTableRangeKey"
attribute {
name = "TestTableHashKey"
type = "S"
}
attribute {
name = "TestTableRangeKey"
type = "S"
}
attribute {
name = "TestLSIRangeKey"
type = "N"
}
attribute {
name = "TestGSIRangeKey"
type = "S"
}
local_secondary_index {
name = "TestTableLSI"
range_key = "TestLSIRangeKey"
projection_type = "ALL"
}
global_secondary_index {
name = "InitialTestTableGSI"
hash_key = "TestTableHashKey"
range_key = "TestGSIRangeKey"
write_capacity = 10
read_capacity = 10
projection_type = "KEYS_ONLY"
}
}
`
const testAccAWSDynamoDbConfigAddSecondaryGSI = `
resource "aws_dynamodb_table" "basic-dynamodb-table" {
name = "TerraformTestTable"
read_capacity = 20
write_capacity = 20
hash_key = "TestTableHashKey"
range_key = "TestTableRangeKey"
attribute {
name = "TestTableHashKey"
type = "S"
}
attribute {
name = "TestTableRangeKey"
type = "S"
}
attribute {
name = "TestLSIRangeKey"
type = "N"
}
attribute {
name = "ReplacementGSIRangeKey"
type = "N"
}
local_secondary_index {
name = "TestTableLSI"
range_key = "TestLSIRangeKey"
projection_type = "ALL"
}
global_secondary_index {
name = "ReplacementTestTableGSI"
hash_key = "TestTableHashKey"
range_key = "ReplacementGSIRangeKey"
write_capacity = 5
read_capacity = 5
projection_type = "INCLUDE"
non_key_attributes = ["TestNonKeyAttribute"]
}
}
`

View File

@ -2,10 +2,14 @@ package aws
import ( import (
"fmt" "fmt"
"log"
"time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -13,6 +17,7 @@ func resourceAwsEbsVolume() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
Create: resourceAwsEbsVolumeCreate, Create: resourceAwsEbsVolumeCreate,
Read: resourceAwsEbsVolumeRead, Read: resourceAwsEbsVolumeRead,
Update: resourceAWSEbsVolumeUpdate,
Delete: resourceAwsEbsVolumeDelete, Delete: resourceAwsEbsVolumeDelete,
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
@ -57,6 +62,7 @@ func resourceAwsEbsVolume() *schema.Resource {
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"tags": tagsSchema(),
}, },
} }
} }
@ -90,9 +96,69 @@ func resourceAwsEbsVolumeCreate(d *schema.ResourceData, meta interface{}) error
if err != nil { if err != nil {
return fmt.Errorf("Error creating EC2 volume: %s", err) return fmt.Errorf("Error creating EC2 volume: %s", err)
} }
log.Printf(
"[DEBUG] Waiting for Volume (%s) to become available",
d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"creating"},
Target: "available",
Refresh: volumeStateRefreshFunc(conn, *result.VolumeID),
Timeout: 5 * 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",
*result.VolumeID, err)
}
d.SetId(*result.VolumeID)
if _, ok := d.GetOk("tags"); ok {
setTags(conn, d)
}
return readVolume(d, result) return readVolume(d, result)
} }
func resourceAWSEbsVolumeUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
if _, ok := d.GetOk("tags"); ok {
setTags(conn, d)
}
return resourceAwsEbsVolumeRead(d, meta)
}
// volumeStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
// a the state of a Volume. Returns successfully when volume is available
func volumeStateRefreshFunc(conn *ec2.EC2, volumeID string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.DescribeVolumes(&ec2.DescribeVolumesInput{
VolumeIDs: []*string{aws.String(volumeID)},
})
if err != nil {
if ec2err, ok := err.(awserr.Error); ok {
// Set this to nil as if we didn't find anything.
log.Printf("Error on Volume State Refresh: message: \"%s\", code:\"%s\"", ec2err.Message(), ec2err.Code())
resp = nil
return nil, "", err
} else {
log.Printf("Error on Volume State Refresh: %s", err)
return nil, "", err
}
}
v := resp.Volumes[0]
return v, *v.State, nil
}
}
func resourceAwsEbsVolumeRead(d *schema.ResourceData, meta interface{}) error { func resourceAwsEbsVolumeRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn conn := meta.(*AWSClient).ec2conn
@ -102,7 +168,7 @@ func resourceAwsEbsVolumeRead(d *schema.ResourceData, meta interface{}) error {
response, err := conn.DescribeVolumes(request) response, err := conn.DescribeVolumes(request)
if err != nil { if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidVolume.NotFound" { if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidVolume.NotFound" {
d.SetId("") d.SetId("")
return nil return nil
} }
@ -148,6 +214,9 @@ func readVolume(d *schema.ResourceData, volume *ec2.Volume) error {
if volume.VolumeType != nil { if volume.VolumeType != nil {
d.Set("type", *volume.VolumeType) d.Set("type", *volume.VolumeType)
} }
if volume.Tags != nil {
d.Set("tags", tagsToMap(volume.Tags))
}
return nil return nil
} }

View File

@ -1,26 +1,88 @@
package aws package aws
import ( import (
"fmt"
"testing" "testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSEBSVolume(t *testing.T) { func TestAccAWSEBSVolume_basic(t *testing.T) {
var v ec2.Volume
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,
Steps: []resource.TestStep{ Steps: []resource.TestStep{
resource.TestStep{ resource.TestStep{
Config: testAccAwsEbsVolumeConfig, Config: testAccAwsEbsVolumeConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckVolumeExists("aws_ebs_volume.test", &v),
),
}, },
}, },
}) })
} }
func TestAccAWSEBSVolume_withTags(t *testing.T) {
var v ec2.Volume
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAwsEbsVolumeConfigWithTags,
Check: resource.ComposeTestCheckFunc(
testAccCheckVolumeExists("aws_ebs_volume.tags_test", &v),
),
},
},
})
}
func testAccCheckVolumeExists(n string, v *ec2.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")
}
conn := testAccProvider.Meta().(*AWSClient).ec2conn
request := &ec2.DescribeVolumesInput{
VolumeIDs: []*string{aws.String(rs.Primary.ID)},
}
response, err := conn.DescribeVolumes(request)
if err == nil {
if response.Volumes != nil && len(response.Volumes) > 0 {
*v = *response.Volumes[0]
return nil
}
}
return fmt.Errorf("Error finding EC2 volume %s", rs.Primary.ID)
}
}
const testAccAwsEbsVolumeConfig = ` const testAccAwsEbsVolumeConfig = `
resource "aws_ebs_volume" "test" { resource "aws_ebs_volume" "test" {
availability_zone = "us-west-2a" availability_zone = "us-west-2a"
size = 1 size = 1
} }
` `
const testAccAwsEbsVolumeConfigWithTags = `
resource "aws_ebs_volume" "tags_test" {
availability_zone = "us-west-2a"
size = 1
tags {
Name = "TerraformTest"
}
}
`

View File

@ -0,0 +1,101 @@
package aws
import (
"log"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsEcsCluster() *schema.Resource {
return &schema.Resource{
Create: resourceAwsEcsClusterCreate,
Read: resourceAwsEcsClusterRead,
Delete: resourceAwsEcsClusterDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}
func resourceAwsEcsClusterCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ecsconn
clusterName := d.Get("name").(string)
log.Printf("[DEBUG] Creating ECS cluster %s", clusterName)
out, err := conn.CreateCluster(&ecs.CreateClusterInput{
ClusterName: aws.String(clusterName),
})
if err != nil {
return err
}
log.Printf("[DEBUG] ECS cluster %s created", *out.Cluster.ClusterARN)
d.SetId(*out.Cluster.ClusterARN)
d.Set("name", *out.Cluster.ClusterName)
return nil
}
func resourceAwsEcsClusterRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ecsconn
clusterName := d.Get("name").(string)
log.Printf("[DEBUG] Reading ECS cluster %s", clusterName)
out, err := conn.DescribeClusters(&ecs.DescribeClustersInput{
Clusters: []*string{aws.String(clusterName)},
})
if err != nil {
return err
}
log.Printf("[DEBUG] Received ECS clusters: %s", awsutil.StringValue(out.Clusters))
d.SetId(*out.Clusters[0].ClusterARN)
d.Set("name", *out.Clusters[0].ClusterName)
return nil
}
func resourceAwsEcsClusterDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ecsconn
log.Printf("[DEBUG] Deleting ECS cluster %s", d.Id())
return resource.Retry(10*time.Minute, func() error {
out, err := conn.DeleteCluster(&ecs.DeleteClusterInput{
Cluster: aws.String(d.Id()),
})
if err == nil {
log.Printf("[DEBUG] ECS cluster %s deleted: %s", d.Id(), awsutil.StringValue(out))
return nil
}
awsErr, ok := err.(awserr.Error)
if !ok {
return resource.RetryError{Err: err}
}
if awsErr.Code() == "ClusterContainsContainerInstancesException" {
log.Printf("[TRACE] Retrying ECS cluster %q deletion after %q", d.Id(), awsErr.Code())
return err
}
if awsErr.Code() == "ClusterContainsServicesException" {
log.Printf("[TRACE] Retrying ECS cluster %q deletion after %q", d.Id(), awsErr.Code())
return err
}
return resource.RetryError{Err: err}
})
}

View File

@ -0,0 +1,68 @@
package aws
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSEcsCluster_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsClusterDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSEcsCluster,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsClusterExists("aws_ecs_cluster.foo"),
),
},
},
})
}
func testAccCheckAWSEcsClusterDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ecsconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_ecs_cluster" {
continue
}
out, err := conn.DescribeClusters(&ecs.DescribeClustersInput{
Clusters: []*string{aws.String(rs.Primary.ID)},
})
if err == nil {
if len(out.Clusters) != 0 {
return fmt.Errorf("ECS cluster still exists:\n%#v", out.Clusters)
}
}
return err
}
return nil
}
func testAccCheckAWSEcsClusterExists(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
_, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}
return nil
}
}
var testAccAWSEcsCluster = `
resource "aws_ecs_cluster" "foo" {
name = "red-grapes"
}
`

View File

@ -0,0 +1,323 @@
package aws
import (
"bytes"
"fmt"
"log"
"regexp"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
var taskDefinitionRE = regexp.MustCompile("^([a-zA-Z0-9_-]+):([0-9]+)$")
func resourceAwsEcsService() *schema.Resource {
return &schema.Resource{
Create: resourceAwsEcsServiceCreate,
Read: resourceAwsEcsServiceRead,
Update: resourceAwsEcsServiceUpdate,
Delete: resourceAwsEcsServiceDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"cluster": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"task_definition": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"desired_count": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"iam_role": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"load_balancer": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"elb_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"container_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"container_port": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
},
},
Set: resourceAwsEcsLoadBalancerHash,
},
},
}
}
func resourceAwsEcsServiceCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ecsconn
input := ecs.CreateServiceInput{
ServiceName: aws.String(d.Get("name").(string)),
TaskDefinition: aws.String(d.Get("task_definition").(string)),
DesiredCount: aws.Long(int64(d.Get("desired_count").(int))),
ClientToken: aws.String(resource.UniqueId()),
}
if v, ok := d.GetOk("cluster"); ok {
input.Cluster = aws.String(v.(string))
}
loadBalancers := expandEcsLoadBalancers(d.Get("load_balancer").(*schema.Set).List())
if len(loadBalancers) > 0 {
log.Printf("[DEBUG] Adding ECS load balancers: %s", awsutil.StringValue(loadBalancers))
input.LoadBalancers = loadBalancers
}
if v, ok := d.GetOk("iam_role"); ok {
input.Role = aws.String(v.(string))
}
log.Printf("[DEBUG] Creating ECS service: %s", awsutil.StringValue(input))
out, err := conn.CreateService(&input)
if err != nil {
return err
}
service := *out.Service
log.Printf("[DEBUG] ECS service created: %s", *service.ServiceARN)
d.SetId(*service.ServiceARN)
d.Set("cluster", *service.ClusterARN)
return resourceAwsEcsServiceUpdate(d, meta)
}
func resourceAwsEcsServiceRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ecsconn
log.Printf("[DEBUG] Reading ECS service %s", d.Id())
input := ecs.DescribeServicesInput{
Services: []*string{aws.String(d.Id())},
Cluster: aws.String(d.Get("cluster").(string)),
}
out, err := conn.DescribeServices(&input)
if err != nil {
return err
}
if len(out.Services) < 1 {
return nil
}
service := out.Services[0]
log.Printf("[DEBUG] Received ECS service %s", awsutil.StringValue(service))
d.SetId(*service.ServiceARN)
d.Set("name", *service.ServiceName)
// Save task definition in the same format
if strings.HasPrefix(d.Get("task_definition").(string), "arn:aws:ecs:") {
d.Set("task_definition", *service.TaskDefinition)
} else {
taskDefinition := buildFamilyAndRevisionFromARN(*service.TaskDefinition)
d.Set("task_definition", taskDefinition)
}
d.Set("desired_count", *service.DesiredCount)
d.Set("cluster", *service.ClusterARN)
if service.RoleARN != nil {
d.Set("iam_role", *service.RoleARN)
}
if service.LoadBalancers != nil {
d.Set("load_balancers", flattenEcsLoadBalancers(service.LoadBalancers))
}
return nil
}
func resourceAwsEcsServiceUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ecsconn
log.Printf("[DEBUG] Updating ECS service %s", d.Id())
input := ecs.UpdateServiceInput{
Service: aws.String(d.Id()),
Cluster: aws.String(d.Get("cluster").(string)),
}
if d.HasChange("desired_count") {
_, n := d.GetChange("desired_count")
input.DesiredCount = aws.Long(int64(n.(int)))
}
if d.HasChange("task_definition") {
_, n := d.GetChange("task_definition")
input.TaskDefinition = aws.String(n.(string))
}
out, err := conn.UpdateService(&input)
if err != nil {
return err
}
service := out.Service
log.Printf("[DEBUG] Updated ECS service %s", awsutil.StringValue(service))
return resourceAwsEcsServiceRead(d, meta)
}
func resourceAwsEcsServiceDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ecsconn
// Check if it's not already gone
resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{
Services: []*string{aws.String(d.Id())},
Cluster: aws.String(d.Get("cluster").(string)),
})
if err != nil {
return err
}
log.Printf("[DEBUG] ECS service %s is currently %s", d.Id(), *resp.Services[0].Status)
if *resp.Services[0].Status == "INACTIVE" {
return nil
}
// Drain the ECS service
if *resp.Services[0].Status != "DRAINING" {
log.Printf("[DEBUG] Draining ECS service %s", d.Id())
_, err = conn.UpdateService(&ecs.UpdateServiceInput{
Service: aws.String(d.Id()),
Cluster: aws.String(d.Get("cluster").(string)),
DesiredCount: aws.Long(int64(0)),
})
if err != nil {
return err
}
}
input := ecs.DeleteServiceInput{
Service: aws.String(d.Id()),
Cluster: aws.String(d.Get("cluster").(string)),
}
log.Printf("[DEBUG] Deleting ECS service %s", awsutil.StringValue(input))
out, err := conn.DeleteService(&input)
if err != nil {
return err
}
// Wait until it's deleted
wait := resource.StateChangeConf{
Pending: []string{"DRAINING"},
Target: "INACTIVE",
Timeout: 5 * time.Minute,
MinTimeout: 1 * time.Second,
Refresh: func() (interface{}, string, error) {
log.Printf("[DEBUG] Checking if ECS service %s is INACTIVE", d.Id())
resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{
Services: []*string{aws.String(d.Id())},
Cluster: aws.String(d.Get("cluster").(string)),
})
if err != nil {
return resp, "FAILED", err
}
return resp, *resp.Services[0].Status, nil
},
}
_, err = wait.WaitForState()
if err != nil {
return err
}
log.Printf("[DEBUG] ECS service %s deleted.", *out.Service.ServiceARN)
return nil
}
func resourceAwsEcsLoadBalancerHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["elb_name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["container_name"].(string)))
buf.WriteString(fmt.Sprintf("%d-", m["container_port"].(int)))
return hashcode.String(buf.String())
}
func buildFamilyAndRevisionFromARN(arn string) string {
return strings.Split(arn, "/")[1]
}
func buildTaskDefinitionARN(taskDefinition string, meta interface{}) (string, error) {
// If it's already an ARN, just return it
if strings.HasPrefix(taskDefinition, "arn:aws:ecs:") {
return taskDefinition, nil
}
// Parse out family & revision
family, revision, err := parseTaskDefinition(taskDefinition)
if err != nil {
return "", err
}
iamconn := meta.(*AWSClient).iamconn
region := meta.(*AWSClient).region
// An zero value GetUserInput{} defers to the currently logged in user
resp, err := iamconn.GetUser(&iam.GetUserInput{})
if err != nil {
return "", fmt.Errorf("GetUser ERROR: %#v", err)
}
// arn:aws:iam::0123456789:user/username
userARN := *resp.User.ARN
accountID := strings.Split(userARN, ":")[4]
// arn:aws:ecs:us-west-2:01234567890:task-definition/mongodb:3
arn := fmt.Sprintf("arn:aws:ecs:%s:%s:task-definition/%s:%s",
region, accountID, family, revision)
log.Printf("[DEBUG] Built task definition ARN: %s", arn)
return arn, nil
}
func parseTaskDefinition(taskDefinition string) (string, string, error) {
matches := taskDefinitionRE.FindAllStringSubmatch(taskDefinition, 2)
if len(matches) == 0 || len(matches[0]) != 3 {
return "", "", fmt.Errorf(
"Invalid task definition format, family:rev or ARN expected (%#v)",
taskDefinition)
}
return matches[0][1], matches[0][2], nil
}

View File

@ -0,0 +1,362 @@
package aws
import (
"fmt"
"regexp"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestParseTaskDefinition(t *testing.T) {
cases := map[string]map[string]interface{}{
"invalid": map[string]interface{}{
"family": "",
"revision": "",
"isValid": false,
},
"invalidWithColon:": map[string]interface{}{
"family": "",
"revision": "",
"isValid": false,
},
"1234": map[string]interface{}{
"family": "",
"revision": "",
"isValid": false,
},
"invalid:aaa": map[string]interface{}{
"family": "",
"revision": "",
"isValid": false,
},
"invalid=family:1": map[string]interface{}{
"family": "",
"revision": "",
"isValid": false,
},
"invalid:name:1": map[string]interface{}{
"family": "",
"revision": "",
"isValid": false,
},
"valid:1": map[string]interface{}{
"family": "valid",
"revision": "1",
"isValid": true,
},
"abc12-def:54": map[string]interface{}{
"family": "abc12-def",
"revision": "54",
"isValid": true,
},
"lorem_ip-sum:123": map[string]interface{}{
"family": "lorem_ip-sum",
"revision": "123",
"isValid": true,
},
"lorem-ipsum:1": map[string]interface{}{
"family": "lorem-ipsum",
"revision": "1",
"isValid": true,
},
}
for input, expectedOutput := range cases {
family, revision, err := parseTaskDefinition(input)
isValid := expectedOutput["isValid"].(bool)
if !isValid && err == nil {
t.Fatalf("Task definition %s should fail", input)
}
expectedFamily := expectedOutput["family"].(string)
if family != expectedFamily {
t.Fatalf("Unexpected family (%#v) for task definition %s\n%#v", family, input, err)
}
expectedRevision := expectedOutput["revision"].(string)
if revision != expectedRevision {
t.Fatalf("Unexpected revision (%#v) for task definition %s\n%#v", revision, input, err)
}
}
}
func TestAccAWSEcsServiceWithARN(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSEcsService,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"),
),
},
resource.TestStep{
Config: testAccAWSEcsServiceModified,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"),
),
},
},
})
}
func TestAccAWSEcsServiceWithFamilyAndRevision(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSEcsServiceWithFamilyAndRevision,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.jenkins"),
),
},
resource.TestStep{
Config: testAccAWSEcsServiceWithFamilyAndRevisionModified,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.jenkins"),
),
},
},
})
}
// Regression for https://github.com/hashicorp/terraform/issues/2427
func TestAccAWSEcsServiceWithRenamedCluster(t *testing.T) {
originalRegexp := regexp.MustCompile(
"^arn:aws:ecs:[^:]+:[0-9]+:cluster/terraformecstest3$")
modifiedRegexp := regexp.MustCompile(
"^arn:aws:ecs:[^:]+:[0-9]+:cluster/terraformecstest3modified$")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSEcsServiceWithRenamedCluster,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.ghost"),
resource.TestMatchResourceAttr(
"aws_ecs_service.ghost", "cluster", originalRegexp),
),
},
resource.TestStep{
Config: testAccAWSEcsServiceWithRenamedClusterModified,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.ghost"),
resource.TestMatchResourceAttr(
"aws_ecs_service.ghost", "cluster", modifiedRegexp),
),
},
},
})
}
func testAccCheckAWSEcsServiceDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ecsconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_ecs_service" {
continue
}
out, err := conn.DescribeServices(&ecs.DescribeServicesInput{
Services: []*string{aws.String(rs.Primary.ID)},
})
if err == nil {
if len(out.Services) > 0 {
return fmt.Errorf("ECS service still exists:\n%#v", out.Services)
}
}
return err
}
return nil
}
func testAccCheckAWSEcsServiceExists(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
_, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}
return nil
}
}
var testAccAWSEcsService = `
resource "aws_ecs_cluster" "default" {
name = "terraformecstest1"
}
resource "aws_ecs_task_definition" "mongo" {
family = "mongodb"
container_definitions = <<DEFINITION
[
{
"cpu": 128,
"essential": true,
"image": "mongo:latest",
"memory": 128,
"name": "mongodb"
}
]
DEFINITION
}
resource "aws_ecs_service" "mongo" {
name = "mongodb"
cluster = "${aws_ecs_cluster.default.id}"
task_definition = "${aws_ecs_task_definition.mongo.arn}"
desired_count = 1
}
`
var testAccAWSEcsServiceModified = `
resource "aws_ecs_cluster" "default" {
name = "terraformecstest1"
}
resource "aws_ecs_task_definition" "mongo" {
family = "mongodb"
container_definitions = <<DEFINITION
[
{
"cpu": 128,
"essential": true,
"image": "mongo:latest",
"memory": 128,
"name": "mongodb"
}
]
DEFINITION
}
resource "aws_ecs_service" "mongo" {
name = "mongodb"
cluster = "${aws_ecs_cluster.default.id}"
task_definition = "${aws_ecs_task_definition.mongo.arn}"
desired_count = 2
}
`
var testAccAWSEcsServiceWithFamilyAndRevision = `
resource "aws_ecs_cluster" "default" {
name = "terraformecstest2"
}
resource "aws_ecs_task_definition" "jenkins" {
family = "jenkins"
container_definitions = <<DEFINITION
[
{
"cpu": 128,
"essential": true,
"image": "jenkins:latest",
"memory": 128,
"name": "jenkins"
}
]
DEFINITION
}
resource "aws_ecs_service" "jenkins" {
name = "jenkins"
cluster = "${aws_ecs_cluster.default.id}"
task_definition = "${aws_ecs_task_definition.jenkins.family}:${aws_ecs_task_definition.jenkins.revision}"
desired_count = 1
}
`
var testAccAWSEcsServiceWithFamilyAndRevisionModified = `
resource "aws_ecs_cluster" "default" {
name = "terraformecstest2"
}
resource "aws_ecs_task_definition" "jenkins" {
family = "jenkins"
container_definitions = <<DEFINITION
[
{
"cpu": 128,
"essential": true,
"image": "jenkins:latest",
"memory": 128,
"name": "jenkins"
}
]
DEFINITION
}
resource "aws_ecs_service" "jenkins" {
name = "jenkins"
cluster = "${aws_ecs_cluster.default.id}"
task_definition = "${aws_ecs_task_definition.jenkins.family}:${aws_ecs_task_definition.jenkins.revision}"
desired_count = 1
}
`
var testAccAWSEcsServiceWithRenamedCluster = `
resource "aws_ecs_cluster" "default" {
name = "terraformecstest3"
}
resource "aws_ecs_task_definition" "ghost" {
family = "ghost"
container_definitions = <<DEFINITION
[
{
"cpu": 128,
"essential": true,
"image": "ghost:latest",
"memory": 128,
"name": "ghost"
}
]
DEFINITION
}
resource "aws_ecs_service" "ghost" {
name = "ghost"
cluster = "${aws_ecs_cluster.default.id}"
task_definition = "${aws_ecs_task_definition.ghost.family}:${aws_ecs_task_definition.ghost.revision}"
desired_count = 1
}
`
var testAccAWSEcsServiceWithRenamedClusterModified = `
resource "aws_ecs_cluster" "default" {
name = "terraformecstest3modified"
}
resource "aws_ecs_task_definition" "ghost" {
family = "ghost"
container_definitions = <<DEFINITION
[
{
"cpu": 128,
"essential": true,
"image": "ghost:latest",
"memory": 128,
"name": "ghost"
}
]
DEFINITION
}
resource "aws_ecs_service" "ghost" {
name = "ghost"
cluster = "${aws_ecs_cluster.default.id}"
task_definition = "${aws_ecs_task_definition.ghost.family}:${aws_ecs_task_definition.ghost.revision}"
desired_count = 1
}
`

View File

@ -0,0 +1,180 @@
package aws
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"fmt"
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsEcsTaskDefinition() *schema.Resource {
return &schema.Resource{
Create: resourceAwsEcsTaskDefinitionCreate,
Read: resourceAwsEcsTaskDefinitionRead,
Update: resourceAwsEcsTaskDefinitionUpdate,
Delete: resourceAwsEcsTaskDefinitionDelete,
Schema: map[string]*schema.Schema{
"arn": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"family": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"revision": &schema.Schema{
Type: schema.TypeInt,
Computed: true,
},
"container_definitions": &schema.Schema{
Type: schema.TypeString,
Required: true,
StateFunc: func(v interface{}) string {
hash := sha1.Sum([]byte(v.(string)))
return hex.EncodeToString(hash[:])
},
},
"volume": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"host_path": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},
},
Set: resourceAwsEcsTaskDefinitionVolumeHash,
},
},
}
}
func resourceAwsEcsTaskDefinitionCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ecsconn
rawDefinitions := d.Get("container_definitions").(string)
definitions, err := expandEcsContainerDefinitions(rawDefinitions)
if err != nil {
return err
}
input := ecs.RegisterTaskDefinitionInput{
ContainerDefinitions: definitions,
Family: aws.String(d.Get("family").(string)),
}
if v, ok := d.GetOk("volume"); ok {
volumes, err := expandEcsVolumes(v.(*schema.Set).List())
if err != nil {
return err
}
input.Volumes = volumes
}
log.Printf("[DEBUG] Registering ECS task definition: %s", awsutil.StringValue(input))
out, err := conn.RegisterTaskDefinition(&input)
if err != nil {
return err
}
taskDefinition := *out.TaskDefinition
log.Printf("[DEBUG] ECS task definition registered: %q (rev. %d)",
*taskDefinition.TaskDefinitionARN, *taskDefinition.Revision)
d.SetId(*taskDefinition.Family)
d.Set("arn", *taskDefinition.TaskDefinitionARN)
return resourceAwsEcsTaskDefinitionRead(d, meta)
}
func resourceAwsEcsTaskDefinitionRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ecsconn
log.Printf("[DEBUG] Reading task definition %s", d.Id())
out, err := conn.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{
TaskDefinition: aws.String(d.Get("arn").(string)),
})
if err != nil {
return err
}
log.Printf("[DEBUG] Received task definition %s", awsutil.StringValue(out))
taskDefinition := out.TaskDefinition
d.SetId(*taskDefinition.Family)
d.Set("arn", *taskDefinition.TaskDefinitionARN)
d.Set("family", *taskDefinition.Family)
d.Set("revision", *taskDefinition.Revision)
d.Set("container_definitions", taskDefinition.ContainerDefinitions)
d.Set("volumes", flattenEcsVolumes(taskDefinition.Volumes))
return nil
}
func resourceAwsEcsTaskDefinitionUpdate(d *schema.ResourceData, meta interface{}) error {
oldArn := d.Get("arn").(string)
log.Printf("[DEBUG] Creating new revision of task definition %q", d.Id())
err := resourceAwsEcsTaskDefinitionCreate(d, meta)
if err != nil {
return err
}
log.Printf("[DEBUG] New revision of %q created: %q", d.Id(), d.Get("arn").(string))
log.Printf("[DEBUG] Deregistering old revision of task definition %q: %q", d.Id(), oldArn)
conn := meta.(*AWSClient).ecsconn
_, err = conn.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{
TaskDefinition: aws.String(oldArn),
})
if err != nil {
return err
}
log.Printf("[DEBUG] Old revision of task definition deregistered: %q", oldArn)
return resourceAwsEcsTaskDefinitionRead(d, meta)
}
func resourceAwsEcsTaskDefinitionDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ecsconn
_, err := conn.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{
TaskDefinition: aws.String(d.Get("arn").(string)),
})
if err != nil {
return err
}
log.Printf("[DEBUG] Task definition %q deregistered.", d.Get("arn").(string))
return nil
}
func resourceAwsEcsTaskDefinitionVolumeHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["host_path"].(string)))
return hashcode.String(buf.String())
}

View File

@ -0,0 +1,166 @@
package aws
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSEcsTaskDefinition_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsTaskDefinitionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSEcsTaskDefinition,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsTaskDefinitionExists("aws_ecs_task_definition.jenkins"),
),
},
resource.TestStep{
Config: testAccAWSEcsTaskDefinitionModifier,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsTaskDefinitionExists("aws_ecs_task_definition.jenkins"),
),
},
},
})
}
func testAccCheckAWSEcsTaskDefinitionDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ecsconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_ecs_task_definition" {
continue
}
out, err := conn.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{
TaskDefinition: aws.String(rs.Primary.ID),
})
if err == nil {
if out.TaskDefinition != nil {
return fmt.Errorf("ECS task definition still exists:\n%#v", *out.TaskDefinition)
}
}
return err
}
return nil
}
func testAccCheckAWSEcsTaskDefinitionExists(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
_, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}
return nil
}
}
var testAccAWSEcsTaskDefinition = `
resource "aws_ecs_task_definition" "jenkins" {
family = "terraform-acc-test"
container_definitions = <<TASK_DEFINITION
[
{
"cpu": 10,
"command": ["sleep", "10"],
"entryPoint": ["/"],
"environment": [
{"name": "VARNAME", "value": "VARVAL"}
],
"essential": true,
"image": "jenkins",
"links": ["mongodb"],
"memory": 128,
"name": "jenkins",
"portMappings": [
{
"containerPort": 80,
"hostPort": 8080
}
]
},
{
"cpu": 10,
"command": ["sleep", "10"],
"entryPoint": ["/"],
"essential": true,
"image": "mongodb",
"memory": 128,
"name": "mongodb",
"portMappings": [
{
"containerPort": 28017,
"hostPort": 28017
}
]
}
]
TASK_DEFINITION
volume {
name = "jenkins-home"
host_path = "/ecs/jenkins-home"
}
}
`
var testAccAWSEcsTaskDefinitionModifier = `
resource "aws_ecs_task_definition" "jenkins" {
family = "terraform-acc-test"
container_definitions = <<TASK_DEFINITION
[
{
"cpu": 10,
"command": ["sleep", "10"],
"entryPoint": ["/"],
"environment": [
{"name": "VARNAME", "value": "VARVAL"}
],
"essential": true,
"image": "jenkins",
"links": ["mongodb"],
"memory": 128,
"name": "jenkins",
"portMappings": [
{
"containerPort": 80,
"hostPort": 8080
}
]
},
{
"cpu": 20,
"command": ["sleep", "10"],
"entryPoint": ["/"],
"essential": true,
"image": "mongodb",
"memory": 128,
"name": "mongodb",
"portMappings": [
{
"containerPort": 28017,
"hostPort": 28017
}
]
}
]
TASK_DEFINITION
volume {
name = "jenkins-home"
host_path = "/ecs/jenkins-home"
}
}
`

View File

@ -6,8 +6,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -123,7 +124,7 @@ func resourceAwsEipRead(d *schema.ResourceData, meta interface{}) error {
describeAddresses, err := ec2conn.DescribeAddresses(req) describeAddresses, err := ec2conn.DescribeAddresses(req)
if err != nil { if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidAllocationID.NotFound" { if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidAllocationID.NotFound" {
d.SetId("") d.SetId("")
return nil return nil
} }
@ -221,6 +222,17 @@ func resourceAwsEipDelete(d *schema.ResourceData, meta interface{}) error {
PublicIP: aws.String(d.Get("public_ip").(string)), PublicIP: aws.String(d.Get("public_ip").(string)),
}) })
} }
if err != nil {
// First check if the association ID is not found. If this
// is the case, then it was already disassociated somehow,
// and that is okay. The most commmon reason for this is that
// the instance or ENI it was attached it was destroyed.
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidAssociationID.NotFound" {
err = nil
}
}
if err != nil { if err != nil {
return err return err
} }
@ -247,7 +259,7 @@ func resourceAwsEipDelete(d *schema.ResourceData, meta interface{}) error {
if err == nil { if err == nil {
return nil return nil
} }
if _, ok := err.(aws.APIError); !ok { if _, ok := err.(awserr.Error); !ok {
return resource.RetryError{Err: err} return resource.RetryError{Err: err}
} }

View File

@ -5,13 +5,14 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSEIP_normal(t *testing.T) { func TestAccAWSEIP_basic(t *testing.T) {
var conf ec2.Address var conf ec2.Address
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
@ -98,12 +99,12 @@ func testAccCheckAWSEIPDestroy(s *terraform.State) error {
} }
// Verify the error // Verify the error
providerErr, ok := err.(aws.APIError) providerErr, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if providerErr.Code != "InvalidAllocationID.NotFound" { if providerErr.Code() != "InvalidAllocationID.NotFound" {
return fmt.Errorf("Unexpected error: %s", err) return fmt.Errorf("Unexpected error: %s", err)
} }
} }

View File

@ -3,10 +3,15 @@ package aws
import ( import (
"fmt" "fmt"
"log" "log"
"sort"
"strings"
"time" "time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/elasticache" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/elasticache"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
@ -16,6 +21,7 @@ func resourceAwsElasticacheCluster() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
Create: resourceAwsElasticacheClusterCreate, Create: resourceAwsElasticacheClusterCreate,
Read: resourceAwsElasticacheClusterRead, Read: resourceAwsElasticacheClusterRead,
Update: resourceAwsElasticacheClusterUpdate,
Delete: resourceAwsElasticacheClusterDelete, Delete: resourceAwsElasticacheClusterDelete,
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
@ -27,7 +33,6 @@ func resourceAwsElasticacheCluster() *schema.Resource {
"engine": &schema.Schema{ "engine": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true,
}, },
"node_type": &schema.Schema{ "node_type": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -37,24 +42,26 @@ func resourceAwsElasticacheCluster() *schema.Resource {
"num_cache_nodes": &schema.Schema{ "num_cache_nodes": &schema.Schema{
Type: schema.TypeInt, Type: schema.TypeInt,
Required: true, Required: true,
ForceNew: true,
}, },
"parameter_group_name": &schema.Schema{ "parameter_group_name": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
ForceNew: true, Computed: true,
}, },
"port": &schema.Schema{ "port": &schema.Schema{
Type: schema.TypeInt, Type: schema.TypeInt,
Default: 11211, Required: true,
Optional: true,
ForceNew: true, ForceNew: true,
}, },
"engine_version": &schema.Schema{ "engine_version": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, },
"maintenance_window": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
}, },
"subnet_group_name": &schema.Schema{ "subnet_group_name": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -76,12 +83,59 @@ func resourceAwsElasticacheCluster() *schema.Resource {
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true, Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: func(v interface{}) int {
return hashcode.String(v.(string))
},
},
// Exported Attributes
"cache_nodes": &schema.Schema{
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"address": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"port": &schema.Schema{
Type: schema.TypeInt,
Computed: true,
},
},
},
},
// A single-element string list containing an Amazon Resource Name (ARN) that
// uniquely identifies a Redis RDB snapshot file stored in Amazon S3. The snapshot
// file will be used to populate the node group.
//
// See also:
// https://github.com/aws/aws-sdk-go/blob/4862a174f7fc92fb523fc39e68f00b87d91d2c3d/service/elasticache/api.go#L2079
"snapshot_arns": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true, ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString}, Elem: &schema.Schema{Type: schema.TypeString},
Set: func(v interface{}) int { Set: func(v interface{}) int {
return hashcode.String(v.(string)) return hashcode.String(v.(string))
}, },
}, },
"tags": tagsSchema(),
// apply_immediately is used to determine when the update modifications
// take place.
// See http://docs.aws.amazon.com/AmazonElastiCache/latest/APIReference/API_ModifyCacheCluster.html
"apply_immediately": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Computed: true,
},
}, },
} }
} }
@ -94,15 +148,15 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{
numNodes := int64(d.Get("num_cache_nodes").(int)) // 2 numNodes := int64(d.Get("num_cache_nodes").(int)) // 2
engine := d.Get("engine").(string) // memcached engine := d.Get("engine").(string) // memcached
engineVersion := d.Get("engine_version").(string) // 1.4.14 engineVersion := d.Get("engine_version").(string) // 1.4.14
port := int64(d.Get("port").(int)) // 11211 port := int64(d.Get("port").(int)) // e.g) 11211
subnetGroupName := d.Get("subnet_group_name").(string) subnetGroupName := d.Get("subnet_group_name").(string)
securityNameSet := d.Get("security_group_names").(*schema.Set) securityNameSet := d.Get("security_group_names").(*schema.Set)
securityIdSet := d.Get("security_group_ids").(*schema.Set) securityIdSet := d.Get("security_group_ids").(*schema.Set)
paramGroupName := d.Get("parameter_group_name").(string) // default.memcached1.4
securityNames := expandStringList(securityNameSet.List()) securityNames := expandStringList(securityNameSet.List())
securityIds := expandStringList(securityIdSet.List()) securityIds := expandStringList(securityIdSet.List())
tags := tagsFromMapEC(d.Get("tags").(map[string]interface{}))
req := &elasticache.CreateCacheClusterInput{ req := &elasticache.CreateCacheClusterInput{
CacheClusterID: aws.String(clusterId), CacheClusterID: aws.String(clusterId),
CacheNodeType: aws.String(nodeType), CacheNodeType: aws.String(nodeType),
@ -113,7 +167,23 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{
CacheSubnetGroupName: aws.String(subnetGroupName), CacheSubnetGroupName: aws.String(subnetGroupName),
CacheSecurityGroupNames: securityNames, CacheSecurityGroupNames: securityNames,
SecurityGroupIDs: securityIds, SecurityGroupIDs: securityIds,
CacheParameterGroupName: aws.String(paramGroupName), Tags: tags,
}
// parameter groups are optional and can be defaulted by AWS
if v, ok := d.GetOk("parameter_group_name"); ok {
req.CacheParameterGroupName = aws.String(v.(string))
}
if v, ok := d.GetOk("maintenance_window"); ok {
req.PreferredMaintenanceWindow = aws.String(v.(string))
}
snaps := d.Get("snapshot_arns").(*schema.Set).List()
if len(snaps) > 0 {
s := expandStringList(snaps)
req.SnapshotARNs = s
log.Printf("[DEBUG] Restoring Redis cluster from S3 snapshot: %#v", s)
} }
_, err := conn.CreateCacheCluster(req) _, err := conn.CreateCacheCluster(req)
@ -125,7 +195,7 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{
stateConf := &resource.StateChangeConf{ stateConf := &resource.StateChangeConf{
Pending: pending, Pending: pending,
Target: "available", Target: "available",
Refresh: CacheClusterStateRefreshFunc(conn, d.Id(), "available", pending), Refresh: cacheClusterStateRefreshFunc(conn, d.Id(), "available", pending),
Timeout: 10 * time.Minute, Timeout: 10 * time.Minute,
Delay: 10 * time.Second, Delay: 10 * time.Second,
MinTimeout: 3 * time.Second, MinTimeout: 3 * time.Second,
@ -139,13 +209,14 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{
d.SetId(clusterId) d.SetId(clusterId)
return nil return resourceAwsElasticacheClusterRead(d, meta)
} }
func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{}) error { func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticacheconn conn := meta.(*AWSClient).elasticacheconn
req := &elasticache.DescribeCacheClustersInput{ req := &elasticache.DescribeCacheClustersInput{
CacheClusterID: aws.String(d.Id()), CacheClusterID: aws.String(d.Id()),
ShowCacheNodeInfo: aws.Boolean(true),
} }
res, err := conn.DescribeCacheClusters(req) res, err := conn.DescribeCacheClusters(req)
@ -167,11 +238,137 @@ func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{})
d.Set("security_group_names", c.CacheSecurityGroups) d.Set("security_group_names", c.CacheSecurityGroups)
d.Set("security_group_ids", c.SecurityGroups) d.Set("security_group_ids", c.SecurityGroups)
d.Set("parameter_group_name", c.CacheParameterGroup) d.Set("parameter_group_name", c.CacheParameterGroup)
d.Set("maintenance_window", c.PreferredMaintenanceWindow)
if err := setCacheNodeData(d, c); err != nil {
return err
}
// list tags for resource
// set tags
arn, err := buildECARN(d, meta)
if err != nil {
log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not setting Tags for cluster %s", *c.CacheClusterID)
} else {
resp, err := conn.ListTagsForResource(&elasticache.ListTagsForResourceInput{
ResourceName: aws.String(arn),
})
if err != nil {
log.Printf("[DEBUG] Error retreiving tags for ARN: %s", arn)
}
var et []*elasticache.Tag
if len(resp.TagList) > 0 {
et = resp.TagList
}
d.Set("tags", tagsToMapEC(et))
}
} }
return nil return nil
} }
func resourceAwsElasticacheClusterUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticacheconn
arn, err := buildECARN(d, meta)
if err != nil {
log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not updating Tags for cluster %s", d.Id())
} else {
if err := setTagsEC(conn, d, arn); err != nil {
return err
}
}
req := &elasticache.ModifyCacheClusterInput{
CacheClusterID: aws.String(d.Id()),
ApplyImmediately: aws.Boolean(d.Get("apply_immediately").(bool)),
}
requestUpdate := false
if d.HasChange("security_group_ids") {
if attr := d.Get("security_group_ids").(*schema.Set); attr.Len() > 0 {
req.SecurityGroupIDs = expandStringList(attr.List())
requestUpdate = true
}
}
if d.HasChange("parameter_group_name") {
req.CacheParameterGroupName = aws.String(d.Get("parameter_group_name").(string))
requestUpdate = true
}
if d.HasChange("maintenance_window") {
req.PreferredMaintenanceWindow = aws.String(d.Get("maintenance_window").(string))
requestUpdate = true
}
if d.HasChange("engine_version") {
req.EngineVersion = aws.String(d.Get("engine_version").(string))
requestUpdate = true
}
if d.HasChange("num_cache_nodes") {
req.NumCacheNodes = aws.Long(int64(d.Get("num_cache_nodes").(int)))
requestUpdate = true
}
if requestUpdate {
log.Printf("[DEBUG] Modifying ElastiCache Cluster (%s), opts:\n%s", d.Id(), awsutil.StringValue(req))
_, err := conn.ModifyCacheCluster(req)
if err != nil {
return fmt.Errorf("[WARN] Error updating ElastiCache cluster (%s), error: %s", d.Id(), err)
}
log.Printf("[DEBUG] Waiting for update: %s", d.Id())
pending := []string{"modifying", "rebooting cache cluster nodes", "snapshotting"}
stateConf := &resource.StateChangeConf{
Pending: pending,
Target: "available",
Refresh: cacheClusterStateRefreshFunc(conn, d.Id(), "available", pending),
Timeout: 5 * time.Minute,
Delay: 5 * time.Second,
MinTimeout: 3 * time.Second,
}
_, sterr := stateConf.WaitForState()
if sterr != nil {
return fmt.Errorf("Error waiting for elasticache (%s) to update: %s", d.Id(), sterr)
}
}
return resourceAwsElasticacheClusterRead(d, meta)
}
func setCacheNodeData(d *schema.ResourceData, c *elasticache.CacheCluster) error {
sortedCacheNodes := make([]*elasticache.CacheNode, len(c.CacheNodes))
copy(sortedCacheNodes, c.CacheNodes)
sort.Sort(byCacheNodeId(sortedCacheNodes))
cacheNodeData := make([]map[string]interface{}, 0, len(sortedCacheNodes))
for _, node := range sortedCacheNodes {
if node.CacheNodeID == nil || node.Endpoint == nil || node.Endpoint.Address == nil || node.Endpoint.Port == nil {
return fmt.Errorf("Unexpected nil pointer in: %s", awsutil.StringValue(node))
}
cacheNodeData = append(cacheNodeData, map[string]interface{}{
"id": *node.CacheNodeID,
"address": *node.Endpoint.Address,
"port": int(*node.Endpoint.Port),
})
}
return d.Set("cache_nodes", cacheNodeData)
}
type byCacheNodeId []*elasticache.CacheNode
func (b byCacheNodeId) Len() int { return len(b) }
func (b byCacheNodeId) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
func (b byCacheNodeId) Less(i, j int) bool {
return b[i].CacheNodeID != nil && b[j].CacheNodeID != nil &&
*b[i].CacheNodeID < *b[j].CacheNodeID
}
func resourceAwsElasticacheClusterDelete(d *schema.ResourceData, meta interface{}) error { func resourceAwsElasticacheClusterDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticacheconn conn := meta.(*AWSClient).elasticacheconn
@ -187,7 +384,7 @@ func resourceAwsElasticacheClusterDelete(d *schema.ResourceData, meta interface{
stateConf := &resource.StateChangeConf{ stateConf := &resource.StateChangeConf{
Pending: []string{"creating", "available", "deleting", "incompatible-parameters", "incompatible-network", "restore-failed"}, Pending: []string{"creating", "available", "deleting", "incompatible-parameters", "incompatible-network", "restore-failed"},
Target: "", Target: "",
Refresh: CacheClusterStateRefreshFunc(conn, d.Id(), "", []string{}), Refresh: cacheClusterStateRefreshFunc(conn, d.Id(), "", []string{}),
Timeout: 10 * time.Minute, Timeout: 10 * time.Minute,
Delay: 10 * time.Second, Delay: 10 * time.Second,
MinTimeout: 3 * time.Second, MinTimeout: 3 * time.Second,
@ -203,15 +400,16 @@ func resourceAwsElasticacheClusterDelete(d *schema.ResourceData, meta interface{
return nil return nil
} }
func CacheClusterStateRefreshFunc(conn *elasticache.ElastiCache, clusterID, givenState string, pending []string) resource.StateRefreshFunc { func cacheClusterStateRefreshFunc(conn *elasticache.ElastiCache, clusterID, givenState string, pending []string) resource.StateRefreshFunc {
return func() (interface{}, string, error) { return func() (interface{}, string, error) {
resp, err := conn.DescribeCacheClusters(&elasticache.DescribeCacheClustersInput{ resp, err := conn.DescribeCacheClusters(&elasticache.DescribeCacheClustersInput{
CacheClusterID: aws.String(clusterID), CacheClusterID: aws.String(clusterID),
ShowCacheNodeInfo: aws.Boolean(true),
}) })
if err != nil { if err != nil {
apierr := err.(aws.APIError) apierr := err.(awserr.Error)
log.Printf("[DEBUG] message: %v, code: %v", apierr.Message, apierr.Code) log.Printf("[DEBUG] message: %v, code: %v", apierr.Message(), apierr.Code())
if apierr.Message == fmt.Sprintf("CacheCluster not found: %v", clusterID) { if apierr.Message() == fmt.Sprintf("CacheCluster not found: %v", clusterID) {
log.Printf("[DEBUG] Detect deletion") log.Printf("[DEBUG] Detect deletion")
return nil, "", nil return nil, "", nil
} }
@ -234,9 +432,35 @@ func CacheClusterStateRefreshFunc(conn *elasticache.ElastiCache, clusterID, give
// return given state if it's not in pending // return given state if it's not in pending
if givenState != "" { if givenState != "" {
// check to make sure we have the node count we're expecting
if int64(len(c.CacheNodes)) != *c.NumCacheNodes {
log.Printf("[DEBUG] Node count is not what is expected: %d found, %d expected", len(c.CacheNodes), *c.NumCacheNodes)
return nil, "creating", nil
}
// loop the nodes and check their status as well
for _, n := range c.CacheNodes {
if n.CacheNodeStatus != nil && *n.CacheNodeStatus != "available" {
log.Printf("[DEBUG] Node (%s) is not yet available, status: %s", *n.CacheNodeID, *n.CacheNodeStatus)
return nil, "creating", nil
}
}
return c, givenState, nil return c, givenState, nil
} }
log.Printf("[DEBUG] current status: %v", *c.CacheClusterStatus) log.Printf("[DEBUG] current status: %v", *c.CacheClusterStatus)
return c, *c.CacheClusterStatus, nil return c, *c.CacheClusterStatus, nil
} }
} }
func buildECARN(d *schema.ResourceData, meta interface{}) (string, error) {
iamconn := meta.(*AWSClient).iamconn
region := meta.(*AWSClient).region
// An zero value GetUserInput{} defers to the currently logged in user
resp, err := iamconn.GetUser(&iam.GetUserInput{})
if err != nil {
return "", err
}
userARN := *resp.User.ARN
accountID := strings.Split(userARN, ":")[4]
arn := fmt.Sprintf("arn:aws:elasticache:%s:%s:cluster:%s", region, accountID, d.Id())
return arn, nil
}

View File

@ -6,13 +6,13 @@ import (
"testing" "testing"
"time" "time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/elasticache" "github.com/aws/aws-sdk-go/service/elasticache"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSElasticacheCluster(t *testing.T) { func TestAccAWSElasticacheCluster_basic(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,
@ -23,6 +23,8 @@ func TestAccAWSElasticacheCluster(t *testing.T) {
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckAWSElasticacheSecurityGroupExists("aws_elasticache_security_group.bar"), testAccCheckAWSElasticacheSecurityGroupExists("aws_elasticache_security_group.bar"),
testAccCheckAWSElasticacheClusterExists("aws_elasticache_cluster.bar"), testAccCheckAWSElasticacheClusterExists("aws_elasticache_cluster.bar"),
resource.TestCheckResourceAttr(
"aws_elasticache_cluster.bar", "cache_nodes.0.id", "0001"),
), ),
}, },
}, },
@ -30,6 +32,7 @@ func TestAccAWSElasticacheCluster(t *testing.T) {
} }
func TestAccAWSElasticacheCluster_vpc(t *testing.T) { func TestAccAWSElasticacheCluster_vpc(t *testing.T) {
var csg elasticache.CacheSubnetGroup
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,
@ -38,7 +41,7 @@ func TestAccAWSElasticacheCluster_vpc(t *testing.T) {
resource.TestStep{ resource.TestStep{
Config: testAccAWSElasticacheClusterInVPCConfig, Config: testAccAWSElasticacheClusterInVPCConfig,
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckAWSElasticacheSubnetGroupExists("aws_elasticache_subnet_group.bar"), testAccCheckAWSElasticacheSubnetGroupExists("aws_elasticache_subnet_group.bar", &csg),
testAccCheckAWSElasticacheClusterExists("aws_elasticache_cluster.bar"), testAccCheckAWSElasticacheClusterExists("aws_elasticache_cluster.bar"),
), ),
}, },
@ -93,6 +96,9 @@ func genRandInt() int {
} }
var testAccAWSElasticacheClusterConfig = fmt.Sprintf(` var testAccAWSElasticacheClusterConfig = fmt.Sprintf(`
provider "aws" {
region = "us-east-1"
}
resource "aws_security_group" "bar" { resource "aws_security_group" "bar" {
name = "tf-test-security-group-%03d" name = "tf-test-security-group-%03d"
description = "tf-test-security-group-descr" description = "tf-test-security-group-descr"
@ -115,6 +121,7 @@ resource "aws_elasticache_cluster" "bar" {
engine = "memcached" engine = "memcached"
node_type = "cache.m1.small" node_type = "cache.m1.small"
num_cache_nodes = 1 num_cache_nodes = 1
port = 11211
parameter_group_name = "default.memcached1.4" parameter_group_name = "default.memcached1.4"
security_group_names = ["${aws_elasticache_security_group.bar.name}"] security_group_names = ["${aws_elasticache_security_group.bar.name}"]
} }

View File

@ -0,0 +1,211 @@
package aws
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/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elasticache"
)
func resourceAwsElasticacheParameterGroup() *schema.Resource {
return &schema.Resource{
Create: resourceAwsElasticacheParameterGroupCreate,
Read: resourceAwsElasticacheParameterGroupRead,
Update: resourceAwsElasticacheParameterGroupUpdate,
Delete: resourceAwsElasticacheParameterGroupDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
ForceNew: true,
Required: true,
},
"family": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"description": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"parameter": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: false,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"value": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},
},
Set: resourceAwsElasticacheParameterHash,
},
},
}
}
func resourceAwsElasticacheParameterGroupCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticacheconn
createOpts := elasticache.CreateCacheParameterGroupInput{
CacheParameterGroupName: aws.String(d.Get("name").(string)),
CacheParameterGroupFamily: aws.String(d.Get("family").(string)),
Description: aws.String(d.Get("description").(string)),
}
log.Printf("[DEBUG] Create Cache Parameter Group: %#v", createOpts)
_, err := conn.CreateCacheParameterGroup(&createOpts)
if err != nil {
return fmt.Errorf("Error creating DB Parameter Group: %s", err)
}
d.Partial(true)
d.SetPartial("name")
d.SetPartial("family")
d.SetPartial("description")
d.Partial(false)
d.SetId(*createOpts.CacheParameterGroupName)
log.Printf("[INFO] Cache Parameter Group ID: %s", d.Id())
return resourceAwsElasticacheParameterGroupUpdate(d, meta)
}
func resourceAwsElasticacheParameterGroupRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticacheconn
describeOpts := elasticache.DescribeCacheParameterGroupsInput{
CacheParameterGroupName: aws.String(d.Id()),
}
describeResp, err := conn.DescribeCacheParameterGroups(&describeOpts)
if err != nil {
return err
}
if len(describeResp.CacheParameterGroups) != 1 ||
*describeResp.CacheParameterGroups[0].CacheParameterGroupName != d.Id() {
return fmt.Errorf("Unable to find Parameter Group: %#v", describeResp.CacheParameterGroups)
}
d.Set("name", describeResp.CacheParameterGroups[0].CacheParameterGroupName)
d.Set("family", describeResp.CacheParameterGroups[0].CacheParameterGroupFamily)
d.Set("description", describeResp.CacheParameterGroups[0].Description)
// Only include user customized parameters as there's hundreds of system/default ones
describeParametersOpts := elasticache.DescribeCacheParametersInput{
CacheParameterGroupName: aws.String(d.Id()),
Source: aws.String("user"),
}
describeParametersResp, err := conn.DescribeCacheParameters(&describeParametersOpts)
if err != nil {
return err
}
d.Set("parameter", flattenElastiCacheParameters(describeParametersResp.Parameters))
return nil
}
func resourceAwsElasticacheParameterGroupUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticacheconn
d.Partial(true)
if d.HasChange("parameter") {
o, n := d.GetChange("parameter")
if o == nil {
o = new(schema.Set)
}
if n == nil {
n = new(schema.Set)
}
os := o.(*schema.Set)
ns := n.(*schema.Set)
// Expand the "parameter" set to aws-sdk-go compat []elasticacheconn.Parameter
parameters, err := expandElastiCacheParameters(ns.Difference(os).List())
if err != nil {
return err
}
if len(parameters) > 0 {
modifyOpts := elasticache.ModifyCacheParameterGroupInput{
CacheParameterGroupName: aws.String(d.Get("name").(string)),
ParameterNameValues: parameters,
}
log.Printf("[DEBUG] Modify Cache Parameter Group: %#v", modifyOpts)
_, err = conn.ModifyCacheParameterGroup(&modifyOpts)
if err != nil {
return fmt.Errorf("Error modifying Cache Parameter Group: %s", err)
}
}
d.SetPartial("parameter")
}
d.Partial(false)
return resourceAwsElasticacheParameterGroupRead(d, meta)
}
func resourceAwsElasticacheParameterGroupDelete(d *schema.ResourceData, meta interface{}) error {
stateConf := &resource.StateChangeConf{
Pending: []string{"pending"},
Target: "destroyed",
Refresh: resourceAwsElasticacheParameterGroupDeleteRefreshFunc(d, meta),
Timeout: 3 * time.Minute,
MinTimeout: 1 * time.Second,
}
_, err := stateConf.WaitForState()
return err
}
func resourceAwsElasticacheParameterGroupDeleteRefreshFunc(
d *schema.ResourceData,
meta interface{}) resource.StateRefreshFunc {
conn := meta.(*AWSClient).elasticacheconn
return func() (interface{}, string, error) {
deleteOpts := elasticache.DeleteCacheParameterGroupInput{
CacheParameterGroupName: aws.String(d.Id()),
}
if _, err := conn.DeleteCacheParameterGroup(&deleteOpts); err != nil {
elasticahceerr, ok := err.(awserr.Error)
if ok && elasticahceerr.Code() == "CacheParameterGroupNotFoundFault" {
d.SetId("")
return d, "error", err
}
return d, "error", err
}
return d, "destroyed", nil
}
}
func resourceAwsElasticacheParameterHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["value"].(string)))
return hashcode.String(buf.String())
}

View File

@ -0,0 +1,210 @@
package aws
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elasticache"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSElasticacheParameterGroup_basic(t *testing.T) {
var v elasticache.CacheParameterGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSElasticacheParameterGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSElasticacheParameterGroupConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSElasticacheParameterGroupExists("aws_elasticache_parameter_group.bar", &v),
testAccCheckAWSElasticacheParameterGroupAttributes(&v),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "name", "parameter-group-test-terraform"),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "family", "redis2.8"),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "description", "Test parameter group for terraform"),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "parameter.283487565.name", "appendonly"),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "parameter.283487565.value", "yes"),
),
},
resource.TestStep{
Config: testAccAWSElasticacheParameterGroupAddParametersConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSElasticacheParameterGroupExists("aws_elasticache_parameter_group.bar", &v),
testAccCheckAWSElasticacheParameterGroupAttributes(&v),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "name", "parameter-group-test-terraform"),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "family", "redis2.8"),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "description", "Test parameter group for terraform"),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "parameter.283487565.name", "appendonly"),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "parameter.283487565.value", "yes"),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "parameter.2196914567.name", "appendfsync"),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "parameter.2196914567.value", "always"),
),
},
},
})
}
func TestAccAWSElasticacheParameterGroupOnly(t *testing.T) {
var v elasticache.CacheParameterGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSElasticacheParameterGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSElasticacheParameterGroupOnlyConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSElasticacheParameterGroupExists("aws_elasticache_parameter_group.bar", &v),
testAccCheckAWSElasticacheParameterGroupAttributes(&v),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "name", "parameter-group-test-terraform"),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "family", "redis2.8"),
resource.TestCheckResourceAttr(
"aws_elasticache_parameter_group.bar", "description", "Test parameter group for terraform"),
),
},
},
})
}
func testAccCheckAWSElasticacheParameterGroupDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).elasticacheconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_elasticache_parameter_group" {
continue
}
// Try to find the Group
resp, err := conn.DescribeCacheParameterGroups(
&elasticache.DescribeCacheParameterGroupsInput{
CacheParameterGroupName: aws.String(rs.Primary.ID),
})
if err == nil {
if len(resp.CacheParameterGroups) != 0 &&
*resp.CacheParameterGroups[0].CacheParameterGroupName == rs.Primary.ID {
return fmt.Errorf("Cache Parameter Group still exists")
}
}
// Verify the error
newerr, ok := err.(awserr.Error)
if !ok {
return err
}
if newerr.Code() != "InvalidCacheParameterGroup.NotFound" {
return err
}
}
return nil
}
func testAccCheckAWSElasticacheParameterGroupAttributes(v *elasticache.CacheParameterGroup) resource.TestCheckFunc {
return func(s *terraform.State) error {
if *v.CacheParameterGroupName != "parameter-group-test-terraform" {
return fmt.Errorf("bad name: %#v", v.CacheParameterGroupName)
}
if *v.CacheParameterGroupFamily != "redis2.8" {
return fmt.Errorf("bad family: %#v", v.CacheParameterGroupFamily)
}
if *v.Description != "Test parameter group for terraform" {
return fmt.Errorf("bad description: %#v", v.Description)
}
return nil
}
}
func testAccCheckAWSElasticacheParameterGroupExists(n string, v *elasticache.CacheParameterGroup) 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 Cache Parameter Group ID is set")
}
conn := testAccProvider.Meta().(*AWSClient).elasticacheconn
opts := elasticache.DescribeCacheParameterGroupsInput{
CacheParameterGroupName: aws.String(rs.Primary.ID),
}
resp, err := conn.DescribeCacheParameterGroups(&opts)
if err != nil {
return err
}
if len(resp.CacheParameterGroups) != 1 ||
*resp.CacheParameterGroups[0].CacheParameterGroupName != rs.Primary.ID {
return fmt.Errorf("Cache Parameter Group not found")
}
*v = *resp.CacheParameterGroups[0]
return nil
}
}
const testAccAWSElasticacheParameterGroupConfig = `
resource "aws_elasticache_parameter_group" "bar" {
name = "parameter-group-test-terraform"
family = "redis2.8"
description = "Test parameter group for terraform"
parameter {
name = "appendonly"
value = "yes"
}
}
`
const testAccAWSElasticacheParameterGroupAddParametersConfig = `
resource "aws_elasticache_parameter_group" "bar" {
name = "parameter-group-test-terraform"
family = "redis2.8"
description = "Test parameter group for terraform"
parameter {
name = "appendonly"
value = "yes"
}
parameter {
name = "appendfsync"
value = "always"
}
}
`
const testAccAWSElasticacheParameterGroupOnlyConfig = `
resource "aws_elasticache_parameter_group" "bar" {
name = "parameter-group-test-terraform"
family = "redis2.8"
description = "Test parameter group for terraform"
}
`

View File

@ -5,8 +5,9 @@ import (
"log" "log"
"time" "time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/elasticache" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elasticache"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
@ -125,12 +126,12 @@ func resourceAwsElasticacheSecurityGroupDelete(d *schema.ResourceData, meta inte
CacheSecurityGroupName: aws.String(d.Id()), CacheSecurityGroupName: aws.String(d.Id()),
}) })
if err != nil { if err != nil {
apierr, ok := err.(aws.APIError) apierr, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
log.Printf("[DEBUG] APIError.Code: %v", apierr.Code) log.Printf("[DEBUG] APIError.Code: %v", apierr.Code)
switch apierr.Code { switch apierr.Code() {
case "InvalidCacheSecurityGroupState": case "InvalidCacheSecurityGroupState":
return err return err
case "DependencyViolation": case "DependencyViolation":

View File

@ -4,13 +4,13 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/elasticache" "github.com/aws/aws-sdk-go/service/elasticache"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSElasticacheSecurityGroup(t *testing.T) { func TestAccAWSElasticacheSecurityGroup_basic(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,

View File

@ -5,8 +5,9 @@ import (
"log" "log"
"time" "time"
"github.com/awslabs/aws-sdk-go/service/elasticache" "github.com/aws/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elasticache"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
@ -16,13 +17,13 @@ func resourceAwsElasticacheSubnetGroup() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
Create: resourceAwsElasticacheSubnetGroupCreate, Create: resourceAwsElasticacheSubnetGroupCreate,
Read: resourceAwsElasticacheSubnetGroupRead, Read: resourceAwsElasticacheSubnetGroupRead,
Update: resourceAwsElasticacheSubnetGroupUpdate,
Delete: resourceAwsElasticacheSubnetGroupDelete, Delete: resourceAwsElasticacheSubnetGroupDelete,
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"description": &schema.Schema{ "description": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true,
}, },
"name": &schema.Schema{ "name": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -31,9 +32,7 @@ func resourceAwsElasticacheSubnetGroup() *schema.Resource {
}, },
"subnet_ids": &schema.Schema{ "subnet_ids": &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Required: true,
Computed: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString}, Elem: &schema.Schema{Type: schema.TypeString},
Set: func(v interface{}) int { Set: func(v interface{}) int {
return hashcode.String(v.(string)) return hashcode.String(v.(string))
@ -109,6 +108,29 @@ func resourceAwsElasticacheSubnetGroupRead(d *schema.ResourceData, meta interfac
return nil return nil
} }
func resourceAwsElasticacheSubnetGroupUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticacheconn
if d.HasChange("subnet_ids") || d.HasChange("description") {
var subnets []*string
if v := d.Get("subnet_ids"); v != nil {
for _, v := range v.(*schema.Set).List() {
subnets = append(subnets, aws.String(v.(string)))
}
}
log.Printf("[DEBUG] Updating ElastiCache Subnet Group")
_, err := conn.ModifyCacheSubnetGroup(&elasticache.ModifyCacheSubnetGroupInput{
CacheSubnetGroupName: aws.String(d.Get("name").(string)),
CacheSubnetGroupDescription: aws.String(d.Get("description").(string)),
SubnetIDs: subnets,
})
if err != nil {
return err
}
}
return resourceAwsElasticacheSubnetGroupRead(d, meta)
}
func resourceAwsElasticacheSubnetGroupDelete(d *schema.ResourceData, meta interface{}) error { func resourceAwsElasticacheSubnetGroupDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticacheconn conn := meta.(*AWSClient).elasticacheconn
@ -119,12 +141,12 @@ func resourceAwsElasticacheSubnetGroupDelete(d *schema.ResourceData, meta interf
CacheSubnetGroupName: aws.String(d.Id()), CacheSubnetGroupName: aws.String(d.Id()),
}) })
if err != nil { if err != nil {
apierr, ok := err.(aws.APIError) apierr, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
log.Printf("[DEBUG] APIError.Code: %v", apierr.Code) log.Printf("[DEBUG] APIError.Code: %v", apierr.Code)
switch apierr.Code { switch apierr.Code() {
case "DependencyViolation": case "DependencyViolation":
// If it is a dependency violation, we want to retry // If it is a dependency violation, we want to retry
return err return err

View File

@ -4,22 +4,56 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/elasticache" "github.com/aws/aws-sdk-go/service/elasticache"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSElasticacheSubnetGroup(t *testing.T) { func TestAccAWSElasticacheSubnetGroup_basic(t *testing.T) {
var csg elasticache.CacheSubnetGroup
config := fmt.Sprintf(testAccAWSElasticacheSubnetGroupConfig, genRandInt())
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,
CheckDestroy: testAccCheckAWSElasticacheSubnetGroupDestroy, CheckDestroy: testAccCheckAWSElasticacheSubnetGroupDestroy,
Steps: []resource.TestStep{ Steps: []resource.TestStep{
resource.TestStep{ resource.TestStep{
Config: testAccAWSElasticacheSubnetGroupConfig, Config: config,
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckAWSElasticacheSubnetGroupExists("aws_elasticache_subnet_group.bar"), testAccCheckAWSElasticacheSubnetGroupExists("aws_elasticache_subnet_group.bar", &csg),
),
},
},
})
}
func TestAccAWSElasticacheSubnetGroup_update(t *testing.T) {
var csg elasticache.CacheSubnetGroup
rn := "aws_elasticache_subnet_group.bar"
ri := genRandInt()
preConfig := fmt.Sprintf(testAccAWSElasticacheSubnetGroupUpdateConfigPre, ri)
postConfig := fmt.Sprintf(testAccAWSElasticacheSubnetGroupUpdateConfigPost, ri)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSElasticacheSubnetGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: preConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSElasticacheSubnetGroupExists(rn, &csg),
testAccCheckAWSElastiCacheSubnetGroupAttrs(&csg, rn, 1),
),
},
resource.TestStep{
Config: postConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSElasticacheSubnetGroupExists(rn, &csg),
testAccCheckAWSElastiCacheSubnetGroupAttrs(&csg, rn, 2),
), ),
}, },
}, },
@ -46,7 +80,7 @@ func testAccCheckAWSElasticacheSubnetGroupDestroy(s *terraform.State) error {
return nil return nil
} }
func testAccCheckAWSElasticacheSubnetGroupExists(n string) resource.TestCheckFunc { func testAccCheckAWSElasticacheSubnetGroupExists(n string, csg *elasticache.CacheSubnetGroup) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n] rs, ok := s.RootModule().Resources[n]
if !ok { if !ok {
@ -58,17 +92,47 @@ func testAccCheckAWSElasticacheSubnetGroupExists(n string) resource.TestCheckFun
} }
conn := testAccProvider.Meta().(*AWSClient).elasticacheconn conn := testAccProvider.Meta().(*AWSClient).elasticacheconn
_, err := conn.DescribeCacheSubnetGroups(&elasticache.DescribeCacheSubnetGroupsInput{ resp, err := conn.DescribeCacheSubnetGroups(&elasticache.DescribeCacheSubnetGroupsInput{
CacheSubnetGroupName: aws.String(rs.Primary.ID), CacheSubnetGroupName: aws.String(rs.Primary.ID),
}) })
if err != nil { if err != nil {
return fmt.Errorf("CacheSubnetGroup error: %v", err) return fmt.Errorf("CacheSubnetGroup error: %v", err)
} }
for _, c := range resp.CacheSubnetGroups {
if rs.Primary.ID == *c.CacheSubnetGroupName {
*csg = *c
}
}
if csg == nil {
return fmt.Errorf("cache subnet group not found")
}
return nil return nil
} }
} }
var testAccAWSElasticacheSubnetGroupConfig = fmt.Sprintf(` func testAccCheckAWSElastiCacheSubnetGroupAttrs(csg *elasticache.CacheSubnetGroup, n string, count 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 len(csg.Subnets) != count {
return fmt.Errorf("Bad cache subnet count, expected: %d, got: %d", count, len(csg.Subnets))
}
if rs.Primary.Attributes["description"] != *csg.CacheSubnetGroupDescription {
return fmt.Errorf("Bad cache subnet description, expected: %s, got: %s", rs.Primary.Attributes["description"], *csg.CacheSubnetGroupDescription)
}
return nil
}
}
var testAccAWSElasticacheSubnetGroupConfig = `
resource "aws_vpc" "foo" { resource "aws_vpc" "foo" {
cidr_block = "192.168.0.0/16" cidr_block = "192.168.0.0/16"
tags { tags {
@ -90,4 +154,63 @@ resource "aws_elasticache_subnet_group" "bar" {
description = "tf-test-cache-subnet-group-descr" description = "tf-test-cache-subnet-group-descr"
subnet_ids = ["${aws_subnet.foo.id}"] subnet_ids = ["${aws_subnet.foo.id}"]
} }
`, genRandInt()) `
var testAccAWSElasticacheSubnetGroupUpdateConfigPre = `
resource "aws_vpc" "foo" {
cidr_block = "10.0.0.0/16"
tags {
Name = "tf-elc-sub-test"
}
}
resource "aws_subnet" "foo" {
vpc_id = "${aws_vpc.foo.id}"
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-2a"
tags {
Name = "tf-test"
}
}
resource "aws_elasticache_subnet_group" "bar" {
name = "tf-test-cache-subnet-%03d"
description = "tf-test-cache-subnet-group-descr"
subnet_ids = ["${aws_subnet.foo.id}"]
}
`
var testAccAWSElasticacheSubnetGroupUpdateConfigPost = `
resource "aws_vpc" "foo" {
cidr_block = "10.0.0.0/16"
tags {
Name = "tf-elc-sub-test"
}
}
resource "aws_subnet" "foo" {
vpc_id = "${aws_vpc.foo.id}"
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-2a"
tags {
Name = "tf-test"
}
}
resource "aws_subnet" "bar" {
vpc_id = "${aws_vpc.foo.id}"
cidr_block = "10.0.2.0/24"
availability_zone = "us-west-2a"
tags {
Name = "tf-test-foo-update"
}
}
resource "aws_elasticache_subnet_group" "bar" {
name = "tf-test-cache-subnet-%03d"
description = "tf-test-cache-subnet-group-descr-edited"
subnet_ids = [
"${aws_subnet.foo.id}",
"${aws_subnet.bar.id}",
]
}
`

View File

@ -4,9 +4,12 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"log" "log"
"regexp"
"strings"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/elb" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elb"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -23,6 +26,26 @@ func resourceAwsElb() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if !regexp.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"only alphanumeric characters and hyphens allowed in %q", k))
}
if len(value) > 32 {
errors = append(errors, fmt.Errorf(
"%q cannot be longer than 32 characters", k))
}
if regexp.MustCompile(`^-`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q cannot begin with a hyphen", k))
}
if regexp.MustCompile(`-$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q cannot end with a hyphen", k))
}
return
},
}, },
"internal": &schema.Schema{ "internal": &schema.Schema{
@ -336,7 +359,7 @@ func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
_, err := elbconn.DeleteLoadBalancerListeners(deleteListenersOpts) _, err := elbconn.DeleteLoadBalancerListeners(deleteListenersOpts)
if err != nil { if err != nil {
return fmt.Errorf("Failure removing outdated listeners: %s", err) return fmt.Errorf("Failure removing outdated ELB listeners: %s", err)
} }
} }
@ -348,7 +371,7 @@ func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
_, err := elbconn.CreateLoadBalancerListeners(createListenersOpts) _, err := elbconn.CreateLoadBalancerListeners(createListenersOpts)
if err != nil { if err != nil {
return fmt.Errorf("Failure adding new or updated listeners: %s", err) return fmt.Errorf("Failure adding new or updated ELB listeners: %s", err)
} }
} }
@ -373,7 +396,7 @@ func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
_, err := elbconn.RegisterInstancesWithLoadBalancer(&registerInstancesOpts) _, err := elbconn.RegisterInstancesWithLoadBalancer(&registerInstancesOpts)
if err != nil { if err != nil {
return fmt.Errorf("Failure registering instances: %s", err) return fmt.Errorf("Failure registering instances with ELB: %s", err)
} }
} }
if len(remove) > 0 { if len(remove) > 0 {
@ -384,7 +407,7 @@ func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
_, err := elbconn.DeregisterInstancesFromLoadBalancer(&deRegisterInstancesOpts) _, err := elbconn.DeregisterInstancesFromLoadBalancer(&deRegisterInstancesOpts)
if err != nil { if err != nil {
return fmt.Errorf("Failure deregistering instances: %s", err) return fmt.Errorf("Failure deregistering instances from ELB: %s", err)
} }
} }
@ -406,7 +429,7 @@ func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
_, err := elbconn.ModifyLoadBalancerAttributes(&attrs) _, err := elbconn.ModifyLoadBalancerAttributes(&attrs)
if err != nil { if err != nil {
return fmt.Errorf("Failure configuring elb attributes: %s", err) return fmt.Errorf("Failure configuring ELB attributes: %s", err)
} }
d.SetPartial("cross_zone_load_balancing") d.SetPartial("cross_zone_load_balancing")
@ -434,7 +457,7 @@ func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
_, err := elbconn.ModifyLoadBalancerAttributes(&attrs) _, err := elbconn.ModifyLoadBalancerAttributes(&attrs)
if err != nil { if err != nil {
return fmt.Errorf("Failure configuring elb attributes: %s", err) return fmt.Errorf("Failure configuring ELB attributes: %s", err)
} }
d.SetPartial("connection_draining_timeout") d.SetPartial("connection_draining_timeout")
@ -454,7 +477,7 @@ func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
_, err := elbconn.ModifyLoadBalancerAttributes(&attrs) _, err := elbconn.ModifyLoadBalancerAttributes(&attrs)
if err != nil { if err != nil {
return fmt.Errorf("Failure configuring elb attributes: %s", err) return fmt.Errorf("Failure configuring ELB attributes: %s", err)
} }
d.SetPartial("connection_draining") d.SetPartial("connection_draining")
@ -476,7 +499,7 @@ func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
} }
_, err := elbconn.ConfigureHealthCheck(&configureHealthCheckOpts) _, err := elbconn.ConfigureHealthCheck(&configureHealthCheckOpts)
if err != nil { if err != nil {
return fmt.Errorf("Failure configuring health check: %s", err) return fmt.Errorf("Failure configuring health check for ELB: %s", err)
} }
d.SetPartial("health_check") d.SetPartial("health_check")
} }
@ -492,7 +515,7 @@ func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
_, err := elbconn.ApplySecurityGroupsToLoadBalancer(&applySecurityGroupsOpts) _, err := elbconn.ApplySecurityGroupsToLoadBalancer(&applySecurityGroupsOpts)
if err != nil { if err != nil {
return fmt.Errorf("Failure applying security groups: %s", err) return fmt.Errorf("Failure applying security groups to ELB: %s", err)
} }
d.SetPartial("security_groups") d.SetPartial("security_groups")
@ -540,9 +563,11 @@ func resourceAwsElbListenerHash(v interface{}) int {
var buf bytes.Buffer var buf bytes.Buffer
m := v.(map[string]interface{}) m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%d-", m["instance_port"].(int))) buf.WriteString(fmt.Sprintf("%d-", m["instance_port"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["instance_protocol"].(string))) buf.WriteString(fmt.Sprintf("%s-",
strings.ToLower(m["instance_protocol"].(string))))
buf.WriteString(fmt.Sprintf("%d-", m["lb_port"].(int))) buf.WriteString(fmt.Sprintf("%d-", m["lb_port"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["lb_protocol"].(string))) buf.WriteString(fmt.Sprintf("%s-",
strings.ToLower(m["lb_protocol"].(string))))
if v, ok := m["ssl_certificate_id"]; ok { if v, ok := m["ssl_certificate_id"]; ok {
buf.WriteString(fmt.Sprintf("%s-", v.(string))) buf.WriteString(fmt.Sprintf("%s-", v.(string)))
@ -552,6 +577,6 @@ func resourceAwsElbListenerHash(v interface{}) int {
} }
func isLoadBalancerNotFound(err error) bool { func isLoadBalancerNotFound(err error) bool {
elberr, ok := err.(aws.APIError) elberr, ok := err.(awserr.Error)
return ok && elberr.Code == "LoadBalancerNotFound" return ok && elberr.Code() == "LoadBalancerNotFound"
} }

View File

@ -7,8 +7,9 @@ import (
"sort" "sort"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/elb" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elb"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -53,6 +54,26 @@ func TestAccAWSELB_basic(t *testing.T) {
}) })
} }
func TestAccAWSELB_fullCharacterRange(t *testing.T) {
var conf elb.LoadBalancerDescription
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSELBDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSELBFullRangeOfCharacters,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSELBExists("aws_elb.foo", &conf),
resource.TestCheckResourceAttr(
"aws_elb.foo", "name", "FoobarTerraform-test123"),
),
},
},
})
}
func TestAccAWSELB_tags(t *testing.T) { func TestAccAWSELB_tags(t *testing.T) {
var conf elb.LoadBalancerDescription var conf elb.LoadBalancerDescription
var td elb.TagDescription var td elb.TagDescription
@ -361,6 +382,39 @@ func TestAccAWSELB_SecurityGroups(t *testing.T) {
}) })
} }
// Unit test for listeners hash
func TestResourceAwsElbListenerHash(t *testing.T) {
cases := map[string]struct {
Left map[string]interface{}
Right map[string]interface{}
Match bool
}{
"protocols are case insensitive": {
map[string]interface{}{
"instance_port": 80,
"instance_protocol": "TCP",
"lb_port": 80,
"lb_protocol": "TCP",
},
map[string]interface{}{
"instance_port": 80,
"instance_protocol": "Tcp",
"lb_port": 80,
"lb_protocol": "tcP",
},
true,
},
}
for tn, tc := range cases {
leftHash := resourceAwsElbListenerHash(tc.Left)
rightHash := resourceAwsElbListenerHash(tc.Right)
if (leftHash == rightHash) != tc.Match {
t.Fatalf("%s: expected match: %t, but did not get it", tn, tc.Match)
}
}
}
func testAccCheckAWSELBDestroy(s *terraform.State) error { func testAccCheckAWSELBDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).elbconn conn := testAccProvider.Meta().(*AWSClient).elbconn
@ -381,12 +435,12 @@ func testAccCheckAWSELBDestroy(s *terraform.State) error {
} }
// Verify the error // Verify the error
providerErr, ok := err.(aws.APIError) providerErr, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if providerErr.Code != "InvalidLoadBalancerName.NotFound" { if providerErr.Code() != "InvalidLoadBalancerName.NotFound" {
return fmt.Errorf("Unexpected error: %s", err) return fmt.Errorf("Unexpected error: %s", err)
} }
} }
@ -512,7 +566,8 @@ resource "aws_elb" "bar" {
instance_port = 8000 instance_port = 8000
instance_protocol = "http" instance_protocol = "http"
lb_port = 80 lb_port = 80
lb_protocol = "http" // Protocol should be case insensitive
lb_protocol = "HttP"
} }
tags { tags {
@ -523,6 +578,20 @@ resource "aws_elb" "bar" {
} }
` `
const testAccAWSELBFullRangeOfCharacters = `
resource "aws_elb" "foo" {
name = "FoobarTerraform-test123"
availability_zones = ["us-west-2a", "us-west-2b", "us-west-2c"]
listener {
instance_port = 8000
instance_protocol = "http"
lb_port = 80
lb_protocol = "http"
}
}
`
const testAccAWSELBConfig_TagUpdate = ` const testAccAWSELBConfig_TagUpdate = `
resource "aws_elb" "bar" { resource "aws_elb" "bar" {
name = "foobar-terraform-test" name = "foobar-terraform-test"

View File

@ -0,0 +1,155 @@
package aws
import (
"fmt"
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsFlowLog() *schema.Resource {
return &schema.Resource{
Create: resourceAwsLogFlowCreate,
Read: resourceAwsLogFlowRead,
Delete: resourceAwsLogFlowDelete,
Schema: map[string]*schema.Schema{
"iam_role_arn": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"log_group_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"vpc_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"subnet_id", "eni_id"},
},
"subnet_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"eni_id", "vpc_id"},
},
"eni_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"subnet_id", "vpc_id"},
},
"traffic_type": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}
func resourceAwsLogFlowCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
types := []struct {
ID string
Type string
}{
{ID: d.Get("vpc_id").(string), Type: "VPC"},
{ID: d.Get("subnet_id").(string), Type: "Subnet"},
{ID: d.Get("eni_id").(string), Type: "NetworkInterface"},
}
var resourceId string
var resourceType string
for _, t := range types {
if t.ID != "" {
resourceId = t.ID
resourceType = t.Type
break
}
}
if resourceId == "" || resourceType == "" {
return fmt.Errorf("Error: Flow Logs require either a VPC, Subnet, or ENI ID")
}
opts := &ec2.CreateFlowLogsInput{
DeliverLogsPermissionARN: aws.String(d.Get("iam_role_arn").(string)),
LogGroupName: aws.String(d.Get("log_group_name").(string)),
ResourceIDs: []*string{aws.String(resourceId)},
ResourceType: aws.String(resourceType),
TrafficType: aws.String(d.Get("traffic_type").(string)),
}
log.Printf(
"[DEBUG] Flow Log Create configuration: %s", awsutil.StringValue(opts))
resp, err := conn.CreateFlowLogs(opts)
if err != nil {
return fmt.Errorf("Error creating Flow Log for (%s), error: %s", resourceId, err)
}
if len(resp.FlowLogIDs) > 1 {
return fmt.Errorf("Error: multiple Flow Logs created for (%s)", resourceId)
}
d.SetId(*resp.FlowLogIDs[0])
return resourceAwsLogFlowRead(d, meta)
}
func resourceAwsLogFlowRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
opts := &ec2.DescribeFlowLogsInput{
FlowLogIDs: []*string{aws.String(d.Id())},
}
resp, err := conn.DescribeFlowLogs(opts)
if err != nil {
log.Printf("[WARN] Error describing Flow Logs for id (%s)", d.Id())
d.SetId("")
return nil
}
if len(resp.FlowLogs) == 0 {
log.Printf("[WARN] No Flow Logs found for id (%s)", d.Id())
d.SetId("")
return nil
}
fl := resp.FlowLogs[0]
d.Set("traffic_type", fl.TrafficType)
d.Set("log_group_name", fl.LogGroupName)
d.Set("iam_role_arn", fl.DeliverLogsPermissionARN)
return nil
}
func resourceAwsLogFlowDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
log.Printf(
"[DEBUG] Flow Log Destroy: %s", d.Id())
_, err := conn.DeleteFlowLogs(&ec2.DeleteFlowLogsInput{
FlowLogIDs: []*string{aws.String(d.Id())},
})
if err != nil {
return fmt.Errorf("[WARN] Error deleting Flow Log with ID (%s), error: %s", d.Id(), err)
}
return nil
}

View File

@ -0,0 +1,212 @@
package aws
import (
"fmt"
"os"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccFlowLog_basic(t *testing.T) {
var flowLog ec2.FlowLog
lgn := os.Getenv("LOG_GROUP_NAME")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckFlowLogDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: fmt.Sprintf(testAccFlowLogConfig_basic, lgn),
Check: resource.ComposeTestCheckFunc(
testAccCheckFlowLogExists("aws_flow_log.test_flow_log", &flowLog),
testAccCheckAWSFlowLogAttributes(&flowLog),
),
},
},
})
}
func TestAccFlowLog_subnet(t *testing.T) {
var flowLog ec2.FlowLog
lgn := os.Getenv("LOG_GROUP_NAME")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckFlowLogDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: fmt.Sprintf(testAccFlowLogConfig_subnet, lgn),
Check: resource.ComposeTestCheckFunc(
testAccCheckFlowLogExists("aws_flow_log.test_flow_log_subnet", &flowLog),
testAccCheckAWSFlowLogAttributes(&flowLog),
),
},
},
})
}
func testAccCheckFlowLogExists(n string, flowLog *ec2.FlowLog) 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 Flow Log ID is set")
}
conn := testAccProvider.Meta().(*AWSClient).ec2conn
describeOpts := &ec2.DescribeFlowLogsInput{
FlowLogIDs: []*string{aws.String(rs.Primary.ID)},
}
resp, err := conn.DescribeFlowLogs(describeOpts)
if err != nil {
return err
}
if len(resp.FlowLogs) > 0 {
*flowLog = *resp.FlowLogs[0]
return nil
}
return fmt.Errorf("No Flow Logs found for id (%s)", rs.Primary.ID)
}
}
func testAccCheckAWSFlowLogAttributes(flowLog *ec2.FlowLog) resource.TestCheckFunc {
return func(s *terraform.State) error {
if flowLog.FlowLogStatus != nil && *flowLog.FlowLogStatus == "ACTIVE" {
return nil
}
if flowLog.FlowLogStatus == nil {
return fmt.Errorf("Flow Log status is not ACTIVE, is nil")
} else {
return fmt.Errorf("Flow Log status is not ACTIVE, got: %s", *flowLog.FlowLogStatus)
}
}
}
func testAccCheckFlowLogDestroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_flow_log" {
continue
}
return nil
}
return nil
}
var testAccFlowLogConfig_basic = `
resource "aws_vpc" "default" {
cidr_block = "10.0.0.0/16"
tags {
Name = "tf-flow-log-test"
}
}
resource "aws_subnet" "test_subnet" {
vpc_id = "${aws_vpc.default.id}"
cidr_block = "10.0.1.0/24"
tags {
Name = "tf-flow-test"
}
}
resource "aws_iam_role" "test_role" {
name = "test_role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"ec2.amazonaws.com"
]
},
"Action": [
"sts:AssumeRole"
]
}
]
}
EOF
}
resource "aws_flow_log" "test_flow_log" {
# log_group_name needs to exist before hand
# until we have a CloudWatch Log Group Resource
log_group_name = "tf-test-log-group"
iam_role_arn = "${aws_iam_role.test_role.arn}"
vpc_id = "${aws_vpc.default.id}"
traffic_type = "ALL"
}
resource "aws_flow_log" "test_flow_log_subnet" {
# log_group_name needs to exist before hand
# until we have a CloudWatch Log Group Resource
log_group_name = "%s"
iam_role_arn = "${aws_iam_role.test_role.arn}"
subnet_id = "${aws_subnet.test_subnet.id}"
traffic_type = "ALL"
}
`
var testAccFlowLogConfig_subnet = `
resource "aws_vpc" "default" {
cidr_block = "10.0.0.0/16"
tags {
Name = "tf-flow-log-test"
}
}
resource "aws_subnet" "test_subnet" {
vpc_id = "${aws_vpc.default.id}"
cidr_block = "10.0.1.0/24"
tags {
Name = "tf-flow-test"
}
}
resource "aws_iam_role" "test_role" {
name = "test_role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"ec2.amazonaws.com"
]
},
"Action": [
"sts:AssumeRole"
]
}
]
}
EOF
}
resource "aws_flow_log" "test_flow_log_subnet" {
# log_group_name needs to exist before hand
# until we have a CloudWatch Log Group Resource
log_group_name = "%s"
iam_role_arn = "${aws_iam_role.test_role.arn}"
subnet_id = "${aws_subnet.test_subnet.id}"
traffic_type = "ALL"
}
`

View File

@ -3,8 +3,9 @@ package aws
import ( import (
"fmt" "fmt"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -71,7 +72,7 @@ func resourceAwsIamAccessKeyRead(d *schema.ResourceData, meta interface{}) error
getResp, err := iamconn.ListAccessKeys(request) getResp, err := iamconn.ListAccessKeys(request)
if err != nil { if err != nil {
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX TEST ME if iamerr, ok := err.(awserr.Error); ok && iamerr.Code() == "NoSuchEntity" { // XXX TEST ME
// the user does not exist, so the key can't exist. // the user does not exist, so the key can't exist.
d.SetId("") d.SetId("")
return nil return nil

View File

@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSAccessKey_normal(t *testing.T) { func TestAccAWSAccessKey_basic(t *testing.T) {
var conf iam.AccessKeyMetadata var conf iam.AccessKeyMetadata
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
@ -49,11 +50,11 @@ func testAccCheckAWSAccessKeyDestroy(s *terraform.State) error {
} }
// Verify the error is what we want // Verify the error is what we want
ec2err, ok := err.(aws.APIError) ec2err, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if ec2err.Code != "NoSuchEntity" { if ec2err.Code() != "NoSuchEntity" {
return err return err
} }
} }

View File

@ -3,8 +3,9 @@ package aws
import ( import (
"fmt" "fmt"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -66,7 +67,7 @@ func resourceAwsIamGroupRead(d *schema.ResourceData, meta interface{}) error {
getResp, err := iamconn.GetGroup(request) getResp, err := iamconn.GetGroup(request)
if err != nil { if err != nil {
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { if iamerr, ok := err.(awserr.Error); ok && iamerr.Code() == "NoSuchEntity" {
d.SetId("") d.SetId("")
return nil return nil
} }

View File

@ -0,0 +1,156 @@
package aws
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsIamGroupMembership() *schema.Resource {
return &schema.Resource{
Create: resourceAwsIamGroupMembershipCreate,
Read: resourceAwsIamGroupMembershipRead,
Update: resourceAwsIamGroupMembershipUpdate,
Delete: resourceAwsIamGroupMembershipDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"users": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"group": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}
func resourceAwsIamGroupMembershipCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).iamconn
group := d.Get("group").(string)
userList := expandStringList(d.Get("users").(*schema.Set).List())
if err := addUsersToGroup(conn, userList, group); err != nil {
return err
}
d.SetId(d.Get("name").(string))
return resourceAwsIamGroupMembershipRead(d, meta)
}
func resourceAwsIamGroupMembershipRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).iamconn
group := d.Get("group").(string)
resp, err := conn.GetGroup(&iam.GetGroupInput{
GroupName: aws.String(group),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
// aws specific error
if awsErr.Code() == "NoSuchEntity" {
// group not found
d.SetId("")
return nil
}
}
return err
}
ul := make([]string, 0, len(resp.Users))
for _, u := range resp.Users {
ul = append(ul, *u.UserName)
}
if err := d.Set("users", ul); err != nil {
return fmt.Errorf("[WARN] Error setting user list from IAM Group Membership (%s), error: %s", group, err)
}
return nil
}
func resourceAwsIamGroupMembershipUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).iamconn
if d.HasChange("users") {
group := d.Get("group").(string)
o, n := d.GetChange("users")
if o == nil {
o = new(schema.Set)
}
if n == nil {
n = new(schema.Set)
}
os := o.(*schema.Set)
ns := n.(*schema.Set)
remove := expandStringList(os.Difference(ns).List())
add := expandStringList(ns.Difference(os).List())
if err := removeUsersFromGroup(conn, remove, group); err != nil {
return err
}
if err := addUsersToGroup(conn, add, group); err != nil {
return err
}
}
return resourceAwsIamGroupMembershipRead(d, meta)
}
func resourceAwsIamGroupMembershipDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).iamconn
userList := expandStringList(d.Get("users").(*schema.Set).List())
group := d.Get("group").(string)
if err := removeUsersFromGroup(conn, userList, group); err != nil {
return err
}
return nil
}
func removeUsersFromGroup(conn *iam.IAM, users []*string, group string) error {
for _, u := range users {
_, err := conn.RemoveUserFromGroup(&iam.RemoveUserFromGroupInput{
UserName: u,
GroupName: aws.String(group),
})
if err != nil {
return err
}
}
return nil
}
func addUsersToGroup(conn *iam.IAM, users []*string, group string) error {
for _, u := range users {
_, err := conn.AddUserToGroup(&iam.AddUserToGroupInput{
UserName: u,
GroupName: aws.String(group),
})
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,169 @@
package aws
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSGroupMembership_basic(t *testing.T) {
var group iam.GetGroupOutput
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSGroupMembershipDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSGroupMemberConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSGroupMembershipExists("aws_iam_group_membership.team", &group),
testAccCheckAWSGroupMembershipAttributes(&group, []string{"test-user"}),
),
},
resource.TestStep{
Config: testAccAWSGroupMemberConfigUpdate,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSGroupMembershipExists("aws_iam_group_membership.team", &group),
testAccCheckAWSGroupMembershipAttributes(&group, []string{"test-user-two", "test-user-three"}),
),
},
},
})
}
func testAccCheckAWSGroupMembershipDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).iamconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_iam_group_membership" {
continue
}
group := rs.Primary.Attributes["group"]
resp, err := conn.GetGroup(&iam.GetGroupInput{
GroupName: aws.String(group),
})
if err != nil {
// might error here
return err
}
users := []string{"test-user", "test-user-two", "test-user-three"}
for _, u := range resp.Users {
for _, i := range users {
if i == *u.UserName {
return fmt.Errorf("Error: User (%s) still a member of Group (%s)", i, *resp.Group.GroupName)
}
}
}
}
return nil
}
func testAccCheckAWSGroupMembershipExists(n string, g *iam.GetGroupOutput) 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 User name is set")
}
conn := testAccProvider.Meta().(*AWSClient).iamconn
gn := rs.Primary.Attributes["group"]
resp, err := conn.GetGroup(&iam.GetGroupInput{
GroupName: aws.String(gn),
})
if err != nil {
return fmt.Errorf("Error: Group (%s) not found", gn)
}
*g = *resp
return nil
}
}
func testAccCheckAWSGroupMembershipAttributes(group *iam.GetGroupOutput, users []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if *group.Group.GroupName != "test-group" {
return fmt.Errorf("Bad group membership: expected %s, got %s", "test-group", *group.Group.GroupName)
}
uc := len(users)
for _, u := range users {
for _, gu := range group.Users {
if u == *gu.UserName {
uc--
}
}
}
if uc > 0 {
return fmt.Errorf("Bad group membership count, expected (%d), but only (%d) found", len(users), uc)
}
return nil
}
}
const testAccAWSGroupMemberConfig = `
resource "aws_iam_group" "group" {
name = "test-group"
path = "/"
}
resource "aws_iam_user" "user" {
name = "test-user"
path = "/"
}
resource "aws_iam_group_membership" "team" {
name = "tf-testing-group-membership"
users = ["${aws_iam_user.user.name}"]
group = "${aws_iam_group.group.name}"
}
`
const testAccAWSGroupMemberConfigUpdate = `
resource "aws_iam_group" "group" {
name = "test-group"
path = "/"
}
resource "aws_iam_user" "user" {
name = "test-user"
path = "/"
}
resource "aws_iam_user" "user_two" {
name = "test-user-two"
path = "/"
}
resource "aws_iam_user" "user_three" {
name = "test-user-three"
path = "/"
}
resource "aws_iam_group_membership" "team" {
name = "tf-testing-group-membership"
users = [
"${aws_iam_user.user_two.name}",
"${aws_iam_user.user_three.name}",
]
group = "${aws_iam_group.group.name}"
}
`

View File

@ -5,8 +5,9 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -66,9 +67,10 @@ func resourceAwsIamGroupPolicyRead(d *schema.ResourceData, meta interface{}) err
GroupName: aws.String(group), GroupName: aws.String(group),
} }
var err error
getResp, err := iamconn.GetGroupPolicy(request) getResp, err := iamconn.GetGroupPolicy(request)
if err != nil { if err != nil {
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX test me if iamerr, ok := err.(awserr.Error); ok && iamerr.Code() == "NoSuchEntity" { // XXX test me
d.SetId("") d.SetId("")
return nil return nil
} }

View File

@ -4,13 +4,13 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSIAMGroupPolicy(t *testing.T) { func TestAccAWSIAMGroupPolicy_basic(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,

View File

@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSIAMGroup_normal(t *testing.T) { func TestAccAWSIAMGroup_basic(t *testing.T) {
var conf iam.GetGroupOutput var conf iam.GetGroupOutput
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
@ -46,11 +47,11 @@ func testAccCheckAWSGroupDestroy(s *terraform.State) error {
} }
// Verify the error is what we want // Verify the error is what we want
ec2err, ok := err.(aws.APIError) ec2err, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if ec2err.Code != "NoSuchEntity" { if ec2err.Code() != "NoSuchEntity" {
return err return err
} }
} }

View File

@ -2,9 +2,11 @@ package aws
import ( import (
"fmt" "fmt"
"regexp"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -33,6 +35,19 @@ func resourceAwsIamInstanceProfile() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
// https://github.com/boto/botocore/blob/2485f5c/botocore/data/iam/2010-05-08/service-2.json#L8196-L8201
value := v.(string)
if len(value) > 128 {
errors = append(errors, fmt.Errorf(
"%q cannot be longer than 128 characters", k))
}
if !regexp.MustCompile("^[\\w+=,.@-]+$").MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q must match [\\w+=,.@-]", k))
}
return
},
}, },
"path": &schema.Schema{ "path": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -59,6 +74,7 @@ func resourceAwsIamInstanceProfileCreate(d *schema.ResourceData, meta interface{
Path: aws.String(d.Get("path").(string)), Path: aws.String(d.Get("path").(string)),
} }
var err error
response, err := iamconn.CreateInstanceProfile(request) response, err := iamconn.CreateInstanceProfile(request)
if err == nil { if err == nil {
err = instanceProfileReadResult(d, response.InstanceProfile) err = instanceProfileReadResult(d, response.InstanceProfile)
@ -87,7 +103,7 @@ func instanceProfileRemoveRole(iamconn *iam.IAM, profileName, roleName string) e
} }
_, err := iamconn.RemoveRoleFromInstanceProfile(request) _, err := iamconn.RemoveRoleFromInstanceProfile(request)
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { if iamerr, ok := err.(awserr.Error); ok && iamerr.Code() == "NoSuchEntity" {
return nil return nil
} }
return err return err
@ -156,7 +172,7 @@ func resourceAwsIamInstanceProfileRead(d *schema.ResourceData, meta interface{})
result, err := iamconn.GetInstanceProfile(request) result, err := iamconn.GetInstanceProfile(request)
if err != nil { if err != nil {
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { if iamerr, ok := err.(awserr.Error); ok && iamerr.Code() == "NoSuchEntity" {
d.SetId("") d.SetId("")
return nil return nil
} }

View File

@ -6,7 +6,7 @@ import (
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
) )
func TestAccAWSIAMInstanceProfile(t *testing.T) { func TestAccAWSIAMInstanceProfile_basic(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,

View File

@ -3,8 +3,9 @@ package aws
import ( import (
"fmt" "fmt"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -58,7 +59,7 @@ func resourceAwsIamPolicyCreate(d *schema.ResourceData, meta interface{}) error
response, err := iamconn.CreatePolicy(request) response, err := iamconn.CreatePolicy(request)
if err != nil { if err != nil {
return fmt.Errorf("Error creating IAM policy %s: %#v", name, err) return fmt.Errorf("Error creating IAM policy %s: %s", name, err)
} }
return readIamPolicy(d, response.Policy) return readIamPolicy(d, response.Policy)
@ -73,11 +74,11 @@ func resourceAwsIamPolicyRead(d *schema.ResourceData, meta interface{}) error {
response, err := iamconn.GetPolicy(request) response, err := iamconn.GetPolicy(request)
if err != nil { if err != nil {
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { if iamerr, ok := err.(awserr.Error); ok && iamerr.Code() == "NoSuchEntity" {
d.SetId("") d.SetId("")
return nil return nil
} }
return fmt.Errorf("Error reading IAM policy %s: %#v", d.Id(), err) return fmt.Errorf("Error reading IAM policy %s: %s", d.Id(), err)
} }
return readIamPolicy(d, response.Policy) return readIamPolicy(d, response.Policy)
@ -100,7 +101,7 @@ func resourceAwsIamPolicyUpdate(d *schema.ResourceData, meta interface{}) error
} }
if _, err := iamconn.CreatePolicyVersion(request); err != nil { if _, err := iamconn.CreatePolicyVersion(request); err != nil {
return fmt.Errorf("Error updating IAM policy %s: %#v", d.Id(), err) return fmt.Errorf("Error updating IAM policy %s: %s", d.Id(), err)
} }
return nil return nil
} }
@ -118,7 +119,7 @@ func resourceAwsIamPolicyDelete(d *schema.ResourceData, meta interface{}) error
_, err := iamconn.DeletePolicy(request) _, err := iamconn.DeletePolicy(request)
if err != nil { if err != nil {
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { if iamerr, ok := err.(awserr.Error); ok && iamerr.Code() == "NoSuchEntity" {
return nil return nil
} }
return fmt.Errorf("Error reading IAM policy %s: %#v", d.Id(), err) return fmt.Errorf("Error reading IAM policy %s: %#v", d.Id(), err)
@ -186,7 +187,7 @@ func iamPolicyDeleteVersion(arn, versionID string, iamconn *iam.IAM) error {
_, err := iamconn.DeletePolicyVersion(request) _, err := iamconn.DeletePolicyVersion(request)
if err != nil { if err != nil {
return fmt.Errorf("Error deleting version %s from IAM policy %s: %#v", versionID, arn, err) return fmt.Errorf("Error deleting version %s from IAM policy %s: %s", versionID, arn, err)
} }
return nil return nil
} }
@ -198,7 +199,7 @@ func iamPolicyListVersions(arn string, iamconn *iam.IAM) ([]*iam.PolicyVersion,
response, err := iamconn.ListPolicyVersions(request) response, err := iamconn.ListPolicyVersions(request)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error listing versions for IAM policy %s: %#v", arn, err) return nil, fmt.Errorf("Error listing versions for IAM policy %s: %s", arn, err)
} }
return response.Versions, nil return response.Versions, nil
} }

View File

@ -0,0 +1,328 @@
package aws
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsIamPolicyAttachment() *schema.Resource {
return &schema.Resource{
Create: resourceAwsIamPolicyAttachmentCreate,
Read: resourceAwsIamPolicyAttachmentRead,
Update: resourceAwsIamPolicyAttachmentUpdate,
Delete: resourceAwsIamPolicyAttachmentDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"users": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"roles": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"groups": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"policy_arn": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}
func resourceAwsIamPolicyAttachmentCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).iamconn
name := d.Get("name").(string)
arn := d.Get("policy_arn").(string)
users := expandStringList(d.Get("users").(*schema.Set).List())
roles := expandStringList(d.Get("roles").(*schema.Set).List())
groups := expandStringList(d.Get("groups").(*schema.Set).List())
if len(users) > 0 && len(roles) > 0 && len(groups) > 0 {
return fmt.Errorf("[WARN] No Users, Roles, or Groups specified for IAM Policy Attachment %s", name)
} else {
var userErr, roleErr, groupErr error
if users != nil {
userErr = attachPolicyToUsers(conn, users, arn)
}
if roles != nil {
roleErr = attachPolicyToRoles(conn, roles, arn)
}
if groups != nil {
groupErr = attachPolicyToGroups(conn, groups, arn)
}
if userErr != nil || roleErr != nil || groupErr != nil {
return composeErrors(fmt.Sprint("[WARN] Error attaching policy with IAM Policy Attachment ", name, ":"), userErr, roleErr, groupErr)
}
}
d.SetId(d.Get("name").(string))
return resourceAwsIamPolicyAttachmentRead(d, meta)
}
func resourceAwsIamPolicyAttachmentRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).iamconn
arn := d.Get("policy_arn").(string)
name := d.Get("name").(string)
_, err := conn.GetPolicy(&iam.GetPolicyInput{
PolicyARN: aws.String(arn),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "NoSuchIdentity" {
d.SetId("")
return nil
}
}
return err
}
policyEntities, err := conn.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{
PolicyARN: aws.String(arn),
})
if err != nil {
return err
}
ul := make([]string, 0, len(policyEntities.PolicyUsers))
rl := make([]string, 0, len(policyEntities.PolicyRoles))
gl := make([]string, 0, len(policyEntities.PolicyGroups))
for _, u := range policyEntities.PolicyUsers {
ul = append(ul, *u.UserName)
}
for _, r := range policyEntities.PolicyRoles {
rl = append(rl, *r.RoleName)
}
for _, g := range policyEntities.PolicyGroups {
gl = append(gl, *g.GroupName)
}
userErr := d.Set("users", ul)
roleErr := d.Set("roles", rl)
groupErr := d.Set("groups", gl)
if userErr != nil || roleErr != nil || groupErr != nil {
return composeErrors(fmt.Sprint("[WARN} Error setting user, role, or group list from IAM Policy Attachment ", name, ":"), userErr, roleErr, groupErr)
}
return nil
}
func resourceAwsIamPolicyAttachmentUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).iamconn
name := d.Get("name").(string)
var userErr, roleErr, groupErr error
if d.HasChange("users") {
userErr = updateUsers(conn, d, meta)
}
if d.HasChange("roles") {
roleErr = updateRoles(conn, d, meta)
}
if d.HasChange("groups") {
groupErr = updateGroups(conn, d, meta)
}
if userErr != nil || roleErr != nil || groupErr != nil {
return composeErrors(fmt.Sprint("[WARN] Error updating user, role, or group list from IAM Policy Attachment ", name, ":"), userErr, roleErr, groupErr)
}
return resourceAwsIamPolicyAttachmentRead(d, meta)
}
func resourceAwsIamPolicyAttachmentDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).iamconn
name := d.Get("name").(string)
arn := d.Get("policy_arn").(string)
users := expandStringList(d.Get("users").(*schema.Set).List())
roles := expandStringList(d.Get("roles").(*schema.Set).List())
groups := expandStringList(d.Get("groups").(*schema.Set).List())
var userErr, roleErr, groupErr error
if len(users) != 0 {
userErr = detachPolicyFromUsers(conn, users, arn)
}
if len(roles) != 0 {
roleErr = detachPolicyFromRoles(conn, roles, arn)
}
if len(groups) != 0 {
groupErr = detachPolicyFromGroups(conn, groups, arn)
}
if userErr != nil || roleErr != nil || groupErr != nil {
return composeErrors(fmt.Sprint("[WARN] Error removing user, role, or group list from IAM Policy Detach ", name, ":"), userErr, roleErr, groupErr)
}
return nil
}
func composeErrors(desc string, uErr error, rErr error, gErr error) error {
errMsg := fmt.Sprintf(desc)
errs := []error{uErr, rErr, gErr}
for _, e := range errs {
if e != nil {
errMsg = errMsg + "\n " + e.Error()
}
}
return fmt.Errorf(errMsg)
}
func attachPolicyToUsers(conn *iam.IAM, users []*string, arn string) error {
for _, u := range users {
_, err := conn.AttachUserPolicy(&iam.AttachUserPolicyInput{
UserName: u,
PolicyARN: aws.String(arn),
})
if err != nil {
return err
}
}
return nil
}
func attachPolicyToRoles(conn *iam.IAM, roles []*string, arn string) error {
for _, r := range roles {
_, err := conn.AttachRolePolicy(&iam.AttachRolePolicyInput{
RoleName: r,
PolicyARN: aws.String(arn),
})
if err != nil {
return err
}
}
return nil
}
func attachPolicyToGroups(conn *iam.IAM, groups []*string, arn string) error {
for _, g := range groups {
_, err := conn.AttachGroupPolicy(&iam.AttachGroupPolicyInput{
GroupName: g,
PolicyARN: aws.String(arn),
})
if err != nil {
return err
}
}
return nil
}
func updateUsers(conn *iam.IAM, d *schema.ResourceData, meta interface{}) error {
arn := d.Get("policy_arn").(string)
o, n := d.GetChange("users")
if o == nil {
o = new(schema.Set)
}
if n == nil {
n = new(schema.Set)
}
os := o.(*schema.Set)
ns := n.(*schema.Set)
remove := expandStringList(os.Difference(ns).List())
add := expandStringList(ns.Difference(os).List())
if rErr := detachPolicyFromUsers(conn, remove, arn); rErr != nil {
return rErr
}
if aErr := attachPolicyToUsers(conn, add, arn); aErr != nil {
return aErr
}
return nil
}
func updateRoles(conn *iam.IAM, d *schema.ResourceData, meta interface{}) error {
arn := d.Get("policy_arn").(string)
o, n := d.GetChange("roles")
if o == nil {
o = new(schema.Set)
}
if n == nil {
n = new(schema.Set)
}
os := o.(*schema.Set)
ns := n.(*schema.Set)
remove := expandStringList(os.Difference(ns).List())
add := expandStringList(ns.Difference(os).List())
if rErr := detachPolicyFromRoles(conn, remove, arn); rErr != nil {
return rErr
}
if aErr := attachPolicyToRoles(conn, add, arn); aErr != nil {
return aErr
}
return nil
}
func updateGroups(conn *iam.IAM, d *schema.ResourceData, meta interface{}) error {
arn := d.Get("policy_arn").(string)
o, n := d.GetChange("groups")
if o == nil {
o = new(schema.Set)
}
if n == nil {
n = new(schema.Set)
}
os := o.(*schema.Set)
ns := n.(*schema.Set)
remove := expandStringList(os.Difference(ns).List())
add := expandStringList(ns.Difference(os).List())
if rErr := detachPolicyFromGroups(conn, remove, arn); rErr != nil {
return rErr
}
if aErr := attachPolicyToGroups(conn, add, arn); aErr != nil {
return aErr
}
return nil
}
func detachPolicyFromUsers(conn *iam.IAM, users []*string, arn string) error {
for _, u := range users {
_, err := conn.DetachUserPolicy(&iam.DetachUserPolicyInput{
UserName: u,
PolicyARN: aws.String(arn),
})
if err != nil {
return err
}
}
return nil
}
func detachPolicyFromRoles(conn *iam.IAM, roles []*string, arn string) error {
for _, r := range roles {
_, err := conn.DetachRolePolicy(&iam.DetachRolePolicyInput{
RoleName: r,
PolicyARN: aws.String(arn),
})
if err != nil {
return err
}
}
return nil
}
func detachPolicyFromGroups(conn *iam.IAM, groups []*string, arn string) error {
for _, g := range groups {
_, err := conn.DetachGroupPolicy(&iam.DetachGroupPolicyInput{
GroupName: g,
PolicyARN: aws.String(arn),
})
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,213 @@
package aws
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"testing"
)
func TestAccAWSPolicyAttachment_basic(t *testing.T) {
var out iam.ListEntitiesForPolicyOutput
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSPolicyAttachmentDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSPolicyAttachConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSPolicyAttachmentExists("aws_iam_policy_attachment.test-attachment", 3, &out),
testAccCheckAWSPolicyAttachmentAttributes([]string{"test-user"}, []string{"test-role"}, []string{"test-group"}, &out),
),
},
resource.TestStep{
Config: testAccAWSPolicyAttachConfigUpdate,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSPolicyAttachmentExists("aws_iam_policy_attachment.test-attachment", 6, &out),
testAccCheckAWSPolicyAttachmentAttributes([]string{"test-user3", "test-user3"}, []string{"test-role2", "test-role3"}, []string{"test-group2", "test-group3"}, &out),
),
},
},
})
}
func testAccCheckAWSPolicyAttachmentDestroy(s *terraform.State) error {
return nil
}
func testAccCheckAWSPolicyAttachmentExists(n string, c int64, out *iam.ListEntitiesForPolicyOutput) 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 policy name is set")
}
conn := testAccProvider.Meta().(*AWSClient).iamconn
arn := rs.Primary.Attributes["policy_arn"]
resp, err := conn.GetPolicy(&iam.GetPolicyInput{
PolicyARN: aws.String(arn),
})
if err != nil {
return fmt.Errorf("Error: Policy (%s) not found", n)
}
if c != *resp.Policy.AttachmentCount {
return fmt.Errorf("Error: Policy (%s) has wrong number of entities attached on initial creation", n)
}
resp2, err := conn.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{
PolicyARN: aws.String(arn),
})
if err != nil {
return fmt.Errorf("Error: Failed to get entities for Policy (%s)", arn)
}
*out = *resp2
return nil
}
}
func testAccCheckAWSPolicyAttachmentAttributes(users []string, roles []string, groups []string, out *iam.ListEntitiesForPolicyOutput) resource.TestCheckFunc {
return func(s *terraform.State) error {
uc := len(users)
rc := len(roles)
gc := len(groups)
for _, u := range users {
for _, pu := range out.PolicyUsers {
if u == *pu.UserName {
uc--
}
}
}
for _, r := range roles {
for _, pr := range out.PolicyRoles {
if r == *pr.RoleName {
rc--
}
}
}
for _, g := range users {
for _, pg := range out.PolicyGroups {
if g == *pg.GroupName {
gc--
}
}
}
if uc != 0 || rc != 0 || gc != 0 {
return fmt.Errorf("Error: Number of attached users, roles, or groups was incorrect:\n expected %d users and found %d\nexpected %d roles and found %d\nexpected %d groups and found %d", len(users), (len(users) - uc), len(roles), (len(roles) - rc), len(groups), (len(groups) - gc))
}
return nil
}
}
const testAccAWSPolicyAttachConfig = `
resource "aws_iam_user" "user" {
name = "test-user"
}
resource "aws_iam_role" "role" {
name = "test-role"
}
resource "aws_iam_group" "group" {
name = "test-group"
}
resource "aws_iam_policy" "policy" {
name = "test-policy"
description = "A test policy"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"iam:ChangePassword"
],
"Resource": "*",
"Effect": "Allow"
}
]
}
EOF
}
resource "aws_iam_policy_attachment" "test-attach" {
name = "test-attachment"
users = ["${aws_iam_user.user.name}"]
roles = ["${aws_iam_role.role.name}"]
groups = ["${aws_iam_group.group.name}"]
policy_arn = "${aws_iam_policy.policy.arn}"
}
`
const testAccAWSPolicyAttachConfigUpdate = `
resource "aws_iam_user" "user" {
name = "test-user"
}
resource "aws_iam_user" "user2" {
name = "test-user2"
}
resource "aws_iam_user" "user3" {
name = "test-user3"
}
resource "aws_iam_role" "role" {
name = "test-role"
}
resource "aws_iam_role" "role2" {
name = "test-role2"
}
resource "aws_iam_role" "role3" {
name = "test-role3"
}
resource "aws_iam_group" "group" {
name = "test-group"
}
resource "aws_iam_group" "group2" {
name = "test-group2"
}
resource "aws_iam_group" "group3" {
name = "test-group3"
}
resource "aws_iam_policy" "policy" {
name = "test-policy"
description = "A test policy"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"iam:ChangePassword"
],
"Resource": "*",
"Effect": "Allow"
}
]
}
EOF
}
resource "aws_iam_policy_attachment" "test-attach" {
name = "test-attachment"
users = [
"${aws_iam_user.user2.name}",
"${aws_iam_user.user3.name}"
]
roles = [
"${aws_iam_role.role2.name}",
"${aws_iam_role.role3.name}"
]
groups = [
"${aws_iam_group.group2.name}",
"${aws_iam_group.group3.name}"
]
policy_arn = "${aws_iam_policy.policy.arn}"
}
`

View File

@ -2,9 +2,11 @@ package aws
import ( import (
"fmt" "fmt"
"regexp"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -30,6 +32,19 @@ func resourceAwsIamRole() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
// https://github.com/boto/botocore/blob/2485f5c/botocore/data/iam/2010-05-08/service-2.json#L8329-L8334
value := v.(string)
if len(value) > 64 {
errors = append(errors, fmt.Errorf(
"%q cannot be longer than 64 characters", k))
}
if !regexp.MustCompile("^[\\w+=,.@-]*$").MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q must match [\\w+=,.@-]", k))
}
return
},
}, },
"path": &schema.Schema{ "path": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -72,7 +87,7 @@ func resourceAwsIamRoleRead(d *schema.ResourceData, meta interface{}) error {
getResp, err := iamconn.GetRole(request) getResp, err := iamconn.GetRole(request)
if err != nil { if err != nil {
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX test me if iamerr, ok := err.(awserr.Error); ok && iamerr.Code() == "NoSuchEntity" { // XXX test me
d.SetId("") d.SetId("")
return nil return nil
} }
@ -101,6 +116,27 @@ func resourceAwsIamRoleReadResult(d *schema.ResourceData, role *iam.Role) error
func resourceAwsIamRoleDelete(d *schema.ResourceData, meta interface{}) error { func resourceAwsIamRoleDelete(d *schema.ResourceData, meta interface{}) error {
iamconn := meta.(*AWSClient).iamconn iamconn := meta.(*AWSClient).iamconn
// Roles cannot be destroyed when attached to an existing Instance Profile
resp, err := iamconn.ListInstanceProfilesForRole(&iam.ListInstanceProfilesForRoleInput{
RoleName: aws.String(d.Id()),
})
if err != nil {
return fmt.Errorf("Error listing Profiles for IAM Role (%s) when trying to delete: %s", d.Id(), err)
}
// Loop and remove this Role from any Profiles
if len(resp.InstanceProfiles) > 0 {
for _, i := range resp.InstanceProfiles {
_, err := iamconn.RemoveRoleFromInstanceProfile(&iam.RemoveRoleFromInstanceProfileInput{
InstanceProfileName: i.InstanceProfileName,
RoleName: aws.String(d.Id()),
})
if err != nil {
return fmt.Errorf("Error deleting IAM Role %s: %s", d.Id(), err)
}
}
}
request := &iam.DeleteRoleInput{ request := &iam.DeleteRoleInput{
RoleName: aws.String(d.Id()), RoleName: aws.String(d.Id()),
} }

View File

@ -3,10 +3,12 @@ package aws
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"regexp"
"strings" "strings"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -29,6 +31,19 @@ func resourceAwsIamRolePolicy() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
// https://github.com/boto/botocore/blob/2485f5c/botocore/data/iam/2010-05-08/service-2.json#L8291-L8296
value := v.(string)
if len(value) > 128 {
errors = append(errors, fmt.Errorf(
"%q cannot be longer than 128 characters", k))
}
if !regexp.MustCompile("^[\\w+=,.@-]+$").MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q must match [\\w+=,.@-]", k))
}
return
},
}, },
"role": &schema.Schema{ "role": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -66,9 +81,10 @@ func resourceAwsIamRolePolicyRead(d *schema.ResourceData, meta interface{}) erro
RoleName: aws.String(role), RoleName: aws.String(role),
} }
var err error
getResp, err := iamconn.GetRolePolicy(request) getResp, err := iamconn.GetRolePolicy(request)
if err != nil { if err != nil {
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX test me if iamerr, ok := err.(awserr.Error); ok && iamerr.Code() == "NoSuchEntity" { // XXX test me
d.SetId("") d.SetId("")
return nil return nil
} }

View File

@ -4,13 +4,13 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSIAMRolePolicy(t *testing.T) { func TestAccAWSIAMRolePolicy_basic(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,

View File

@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSRole_normal(t *testing.T) { func TestAccAWSRole_basic(t *testing.T) {
var conf iam.GetRoleOutput var conf iam.GetRoleOutput
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
@ -29,6 +30,31 @@ func TestAccAWSRole_normal(t *testing.T) {
}) })
} }
func TestAccAWSRole_testNameChange(t *testing.T) {
var conf iam.GetRoleOutput
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSRoleDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSRolePre,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSRoleExists("aws_iam_role.role_update_test", &conf),
),
},
resource.TestStep{
Config: testAccAWSRolePost,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSRoleExists("aws_iam_role.role_update_test", &conf),
),
},
},
})
}
func testAccCheckAWSRoleDestroy(s *terraform.State) error { func testAccCheckAWSRoleDestroy(s *terraform.State) error {
iamconn := testAccProvider.Meta().(*AWSClient).iamconn iamconn := testAccProvider.Meta().(*AWSClient).iamconn
@ -46,11 +72,11 @@ func testAccCheckAWSRoleDestroy(s *terraform.State) error {
} }
// Verify the error is what we want // Verify the error is what we want
ec2err, ok := err.(aws.APIError) ec2err, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if ec2err.Code != "NoSuchEntity" { if ec2err.Code() != "NoSuchEntity" {
return err return err
} }
} }
@ -104,3 +130,101 @@ resource "aws_iam_role" "role" {
assume_role_policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ec2.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]}" assume_role_policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ec2.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]}"
} }
` `
const testAccAWSRolePre = `
resource "aws_iam_role" "role_update_test" {
name = "tf_old_name"
path = "/test/"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
resource "aws_iam_role_policy" "role_update_test" {
name = "role_update_test"
role = "${aws_iam_role.role_update_test.id}"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:ListAllMyBuckets"
],
"Resource": "arn:aws:s3:::*"
}
]
}
EOF
}
resource "aws_iam_instance_profile" "role_update_test" {
name = "role_update_test"
path = "/test/"
roles = ["${aws_iam_role.role_update_test.name}"]
}
`
const testAccAWSRolePost = `
resource "aws_iam_role" "role_update_test" {
name = "tf_new_name"
path = "/test/"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
resource "aws_iam_role_policy" "role_update_test" {
name = "role_update_test"
role = "${aws_iam_role.role_update_test.id}"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:ListAllMyBuckets"
],
"Resource": "arn:aws:s3:::*"
}
]
}
EOF
}
resource "aws_iam_instance_profile" "role_update_test" {
name = "role_update_test"
path = "/test/"
roles = ["${aws_iam_role.role_update_test.name}"]
}
`

View File

@ -0,0 +1,148 @@
package aws
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsIAMServerCertificate() *schema.Resource {
return &schema.Resource{
Create: resourceAwsIAMServerCertificateCreate,
Read: resourceAwsIAMServerCertificateRead,
Delete: resourceAwsIAMServerCertificateDelete,
Schema: map[string]*schema.Schema{
"certificate_body": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
StateFunc: normalizeCert,
},
"certificate_chain": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
StateFunc: normalizeCert,
},
"path": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"private_key": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
StateFunc: normalizeCert,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"arn": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
},
}
}
func resourceAwsIAMServerCertificateCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).iamconn
createOpts := &iam.UploadServerCertificateInput{
CertificateBody: aws.String(d.Get("certificate_body").(string)),
PrivateKey: aws.String(d.Get("private_key").(string)),
ServerCertificateName: aws.String(d.Get("name").(string)),
}
if v, ok := d.GetOk("certificate_chain"); ok {
createOpts.CertificateChain = aws.String(v.(string))
}
if v, ok := d.GetOk("Path"); ok {
createOpts.Path = aws.String(v.(string))
}
resp, err := conn.UploadServerCertificate(createOpts)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
return fmt.Errorf("[WARN] Error uploading server certificate, error: %s: %s", awsErr.Code(), awsErr.Message())
}
return fmt.Errorf("[WARN] Error uploading server certificate, error: %s", err)
}
d.SetId(*resp.ServerCertificateMetadata.ServerCertificateID)
return resourceAwsIAMServerCertificateRead(d, meta)
}
func resourceAwsIAMServerCertificateRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).iamconn
resp, err := conn.GetServerCertificate(&iam.GetServerCertificateInput{
ServerCertificateName: aws.String(d.Get("name").(string)),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
return fmt.Errorf("[WARN] Error reading IAM Server Certificate: %s: %s", awsErr.Code(), awsErr.Message())
}
return fmt.Errorf("[WARN] Error reading IAM Server Certificate: %s", err)
}
// these values should always be present, and have a default if not set in
// configuration, and so safe to reference with nil checks
d.Set("certificate_body", normalizeCert(resp.ServerCertificate.CertificateBody))
d.Set("certificate_chain", normalizeCert(resp.ServerCertificate.CertificateChain))
d.Set("path", resp.ServerCertificate.ServerCertificateMetadata.Path)
d.Set("arn", resp.ServerCertificate.ServerCertificateMetadata.ARN)
return nil
}
func resourceAwsIAMServerCertificateDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).iamconn
_, err := conn.DeleteServerCertificate(&iam.DeleteServerCertificateInput{
ServerCertificateName: aws.String(d.Get("name").(string)),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
return fmt.Errorf("[WARN] Error deleting server certificate: %s: %s", awsErr.Code(), awsErr.Message())
}
return err
}
d.SetId("")
return nil
}
func normalizeCert(cert interface{}) string {
if cert == nil {
return ""
}
switch cert.(type) {
case string:
hash := sha1.Sum([]byte(strings.TrimSpace(cert.(string))))
return hex.EncodeToString(hash[:])
case *string:
hash := sha1.Sum([]byte(strings.TrimSpace(*cert.(*string))))
return hex.EncodeToString(hash[:])
default:
return ""
}
}

View File

@ -0,0 +1,191 @@
package aws
import (
"fmt"
"math/rand"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccIAMServerCertificate_basic(t *testing.T) {
var cert iam.ServerCertificate
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckIAMServerCertificateDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccIAMServerCertConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckCertExists("aws_iam_server_certificate.test_cert", &cert),
testAccCheckAWSServerCertAttributes(&cert),
),
},
},
})
}
func testAccCheckCertExists(n string, cert *iam.ServerCertificate) 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 Server Cert ID is set")
}
conn := testAccProvider.Meta().(*AWSClient).iamconn
describeOpts := &iam.GetServerCertificateInput{
ServerCertificateName: aws.String(rs.Primary.Attributes["name"]),
}
resp, err := conn.GetServerCertificate(describeOpts)
if err != nil {
return err
}
*cert = *resp.ServerCertificate
return nil
}
}
func testAccCheckAWSServerCertAttributes(cert *iam.ServerCertificate) resource.TestCheckFunc {
return func(s *terraform.State) error {
if !strings.HasPrefix(*cert.ServerCertificateMetadata.ServerCertificateName, "terraform-test-cert") {
return fmt.Errorf("Bad Server Cert Name: %s", *cert.ServerCertificateMetadata.ServerCertificateName)
}
if *cert.CertificateBody != strings.TrimSpace(certBody) {
return fmt.Errorf("Bad Server Cert body\n\t expected: %s\n\tgot: %s\n", certBody, *cert.CertificateBody)
}
return nil
}
}
func testAccCheckIAMServerCertificateDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).iamconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_iam_server_certificate" {
continue
}
// Try to find the Cert
opts := &iam.GetServerCertificateInput{
ServerCertificateName: aws.String(rs.Primary.Attributes["name"]),
}
resp, err := conn.GetServerCertificate(opts)
if err == nil {
if resp.ServerCertificate != nil {
return fmt.Errorf("Error: Server Cert still exists")
}
return nil
}
}
return nil
}
var certBody = fmt.Sprintf(`
-----BEGIN CERTIFICATE-----
MIIDCDCCAfACAQEwDQYJKoZIhvcNAQELBQAwgY4xCzAJBgNVBAYTAlVTMREwDwYD
VQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3IFlvcmsxFjAUBgNVBAoMDUJhcmVm
b290IExhYnMxGDAWBgNVBAMMD0phc29uIEJlcmxpbnNreTEnMCUGCSqGSIb3DQEJ
ARYYamFzb25AYmFyZWZvb3Rjb2RlcnMuY29tMB4XDTE1MDYyMTA1MzcwNVoXDTE2
MDYyMDA1MzcwNVowgYgxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazEL
MAkGA1UEBwwCTlkxFjAUBgNVBAoMDUJhcmVmb290IExhYnMxGDAWBgNVBAMMD0ph
c29uIEJlcmxpbnNreTEnMCUGCSqGSIb3DQEJARYYamFzb25AYmFyZWZvb3Rjb2Rl
cnMuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD2AVGKRIx+EFM0kkg7
6GoJv9uy0biEDHB4phQBqnDIf8J8/gq9eVvQrR5jJC9Uz4zp5wG/oLZlGuF92/jD
bI/yS+DOAjrh30vN79Au74jGN2Cw8fIak40iDUwjZaczK2Gkna54XIO9pqMcbQ6Q
mLUkQXsqlJ7Q4X2kL3b9iMsXcQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCDGNvU
eioQMVPNlmmxW3+Rwo0Kl+/HtUOmqUDKUDvJnelxulBr7O8w75N/Z7h7+aBJCUkt
tz+DwATZswXtsal6TuzHHpAhpFql82jQZVE8OYkrX84XKRQpm8ZnbyZObMdXTJWk
ArC/rGVIWsvhlbgGM8zu7a3zbeuAESZ8Bn4ZbJxnoaRK8p36/alvzAwkgzSf3oUX
HtU4LrdunevBs6/CbKCWrxYcvNCy8EcmHitqCfQL5nxCCXpgf/Mw1vmIPTwbPSJq
oUkh5yjGRKzhh7QbG1TlFX6zUp4vb+UJn5+g4edHrqivRSjIqYrC45ygVMOABn21
hpMXOlZL+YXfR4Kp
-----END CERTIFICATE-----`)
var testAccIAMServerCertConfig = fmt.Sprintf(`
resource "aws_iam_server_certificate" "test_cert" {
name = "terraform-test-cert-%d"
certificate_body = <<EOF
-----BEGIN CERTIFICATE-----
MIIDCDCCAfACAQEwDQYJKoZIhvcNAQELBQAwgY4xCzAJBgNVBAYTAlVTMREwDwYD
VQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3IFlvcmsxFjAUBgNVBAoMDUJhcmVm
b290IExhYnMxGDAWBgNVBAMMD0phc29uIEJlcmxpbnNreTEnMCUGCSqGSIb3DQEJ
ARYYamFzb25AYmFyZWZvb3Rjb2RlcnMuY29tMB4XDTE1MDYyMTA1MzcwNVoXDTE2
MDYyMDA1MzcwNVowgYgxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazEL
MAkGA1UEBwwCTlkxFjAUBgNVBAoMDUJhcmVmb290IExhYnMxGDAWBgNVBAMMD0ph
c29uIEJlcmxpbnNreTEnMCUGCSqGSIb3DQEJARYYamFzb25AYmFyZWZvb3Rjb2Rl
cnMuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD2AVGKRIx+EFM0kkg7
6GoJv9uy0biEDHB4phQBqnDIf8J8/gq9eVvQrR5jJC9Uz4zp5wG/oLZlGuF92/jD
bI/yS+DOAjrh30vN79Au74jGN2Cw8fIak40iDUwjZaczK2Gkna54XIO9pqMcbQ6Q
mLUkQXsqlJ7Q4X2kL3b9iMsXcQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCDGNvU
eioQMVPNlmmxW3+Rwo0Kl+/HtUOmqUDKUDvJnelxulBr7O8w75N/Z7h7+aBJCUkt
tz+DwATZswXtsal6TuzHHpAhpFql82jQZVE8OYkrX84XKRQpm8ZnbyZObMdXTJWk
ArC/rGVIWsvhlbgGM8zu7a3zbeuAESZ8Bn4ZbJxnoaRK8p36/alvzAwkgzSf3oUX
HtU4LrdunevBs6/CbKCWrxYcvNCy8EcmHitqCfQL5nxCCXpgf/Mw1vmIPTwbPSJq
oUkh5yjGRKzhh7QbG1TlFX6zUp4vb+UJn5+g4edHrqivRSjIqYrC45ygVMOABn21
hpMXOlZL+YXfR4Kp
-----END CERTIFICATE-----
EOF
certificate_chain = <<EOF
-----BEGIN CERTIFICATE-----
MIID8TCCAtmgAwIBAgIJAKX2xeCkfFcbMA0GCSqGSIb3DQEBCwUAMIGOMQswCQYD
VQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZb3JrMRYw
FAYDVQQKDA1CYXJlZm9vdCBMYWJzMRgwFgYDVQQDDA9KYXNvbiBCZXJsaW5za3kx
JzAlBgkqhkiG9w0BCQEWGGphc29uQGJhcmVmb290Y29kZXJzLmNvbTAeFw0xNTA2
MjEwNTM2MDZaFw0yNTA2MTgwNTM2MDZaMIGOMQswCQYDVQQGEwJVUzERMA8GA1UE
CAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZb3JrMRYwFAYDVQQKDA1CYXJlZm9v
dCBMYWJzMRgwFgYDVQQDDA9KYXNvbiBCZXJsaW5za3kxJzAlBgkqhkiG9w0BCQEW
GGphc29uQGJhcmVmb290Y29kZXJzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAMteFbwfLz7NyQn3eDxxw22l1ZPBrzfPON0HOAq8nHat4kT4A2cI
45kCtxKMzCVoG84tXoX/rbjGkez7lz9lEfvEuSh+I+UqinFA/sefhcE63foVMZu1
2t6O3+utdxBvOYJwAQaiGW44x0h6fTyqDv6Gc5Ml0uoIVeMWPhT1MREoOcPDz1gb
Ep3VT2aqFULLJedP37qbzS4D04rn1tS7pcm3wYivRyjVNEvs91NsWEvvE1WtS2Cl
2RBt+ihXwq4UNB9UPYG75+FuRcQQvfqameyweyKT9qBmJLELMtYa/KTCYvSch4JY
YVPAPOlhFlO4BcTto/gpBes2WEAWZtE/jnECAwEAAaNQME4wHQYDVR0OBBYEFOna
aiYnm5583EY7FT/mXwTBuLZgMB8GA1UdIwQYMBaAFOnaaiYnm5583EY7FT/mXwTB
uLZgMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABp/dKQ489CCzzB1
IX78p6RFAdda4e3lL6uVjeS3itzFIIiKvdf1/txhmsEeCEYz0El6aMnXLkpk7jAr
kCwlAOOz2R2hlA8k8opKTYX4IQQau8DATslUFAFOvRGOim/TD/Yuch+a/VF2VQKz
L2lUVi5Hjp9KvWe2HQYPjnJaZs/OKAmZQ4uP547dqFrTz6sWfisF1rJ60JH70cyM
qjZQp/xYHTZIB8TCPvLgtVIGFmd/VAHVBFW2p9IBwtSxBIsEPwYQOV3XbwhhmGIv
DWx5TpnEzH7ZM33RNbAKcdwOBxdRY+SI/ua5hYCm4QngAqY69lEuk4zXZpdDLPq1
qxxQx0E=
-----END CERTIFICATE-----
EOF
private_key = <<EOF
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQD2AVGKRIx+EFM0kkg76GoJv9uy0biEDHB4phQBqnDIf8J8/gq9
eVvQrR5jJC9Uz4zp5wG/oLZlGuF92/jDbI/yS+DOAjrh30vN79Au74jGN2Cw8fIa
k40iDUwjZaczK2Gkna54XIO9pqMcbQ6QmLUkQXsqlJ7Q4X2kL3b9iMsXcQIDAQAB
AoGALmVBQ5p6BKx/hMKx7NqAZSZSAP+clQrji12HGGlUq/usanZfAC0LK+f6eygv
5QbfxJ1UrxdYTukq7dm2qOSooOMUuukWInqC6ztjdLwH70CKnl0bkNB3/NkW2VNc
32YiUuZCM9zaeBuEUclKNs+dhD2EeGdJF8KGntWGOTU/M4ECQQD9gdYb38PvaMdu
opM3sKJF5n9pMoLDleBpCGqq3nD3DFn0V6PHQAwn30EhRN+7BbUEpde5PmfoIdAR
uDlj/XPlAkEA+GyY1e4uU9rz+1K4ubxmtXTp9ZIR2LsqFy5L/MS5hqX2zq5GGq8g
jZYDxnxPEUrxaWQH4nh0qdu3skUBi4a0nQJBAKJaqLkpUd7eB/t++zHLWeHSgP7q
bny8XABod4f+9fICYwntpuJQzngqrxeTeIXaXdggLkxg/0LXhN4UUg0LoVECQQDE
Pi1h2dyY+37/CzLH7q+IKopjJneYqQmv9C+sxs70MgjM7liM3ckub9IdqrdfJr+c
DJw56APo5puvZNm6mbf1AkBVMDyfdOOyoHpJjrhmZWo6QqynujfwErrBYQ0sZQ3l
O57Z0RUNQ8DRyymhLd2t5nAHTfpcFA1sBeKE6CziLbZB
-----END RSA PRIVATE KEY-----
EOF
}
`, rand.New(rand.NewSource(time.Now().UnixNano())).Int())

View File

@ -3,8 +3,9 @@ package aws
import ( import (
"fmt" "fmt"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -75,7 +76,7 @@ func resourceAwsIamUserRead(d *schema.ResourceData, meta interface{}) error {
getResp, err := iamconn.GetUser(request) getResp, err := iamconn.GetUser(request)
if err != nil { if err != nil {
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX test me if iamerr, ok := err.(awserr.Error); ok && iamerr.Code() == "NoSuchEntity" { // XXX test me
d.SetId("") d.SetId("")
return nil return nil
} }

View File

@ -5,8 +5,9 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -66,9 +67,10 @@ func resourceAwsIamUserPolicyRead(d *schema.ResourceData, meta interface{}) erro
UserName: aws.String(user), UserName: aws.String(user),
} }
var err error
getResp, err := iamconn.GetUserPolicy(request) getResp, err := iamconn.GetUserPolicy(request)
if err != nil { if err != nil {
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX test me if iamerr, ok := err.(awserr.Error); ok && iamerr.Code() == "NoSuchEntity" { // XXX test me
d.SetId("") d.SetId("")
return nil return nil
} }

View File

@ -4,13 +4,13 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSIAMUserPolicy(t *testing.T) { func TestAccAWSIAMUserPolicy_basic(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,

View File

@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSUser_normal(t *testing.T) { func TestAccAWSUser_basic(t *testing.T) {
var conf iam.GetUserOutput var conf iam.GetUserOutput
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
@ -46,11 +47,11 @@ func testAccCheckAWSUserDestroy(s *terraform.State) error {
} }
// Verify the error is what we want // Verify the error is what we want
ec2err, ok := err.(aws.APIError) ec2err, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if ec2err.Code != "NoSuchEntity" { if ec2err.Code() != "NoSuchEntity" {
return err return err
} }
} }

View File

@ -10,8 +10,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
@ -36,8 +38,8 @@ func resourceAwsInstance() *schema.Resource {
"associate_public_ip_address": &schema.Schema{ "associate_public_ip_address": &schema.Schema{
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true,
ForceNew: true, ForceNew: true,
Optional: true,
}, },
"availability_zone": &schema.Schema{ "availability_zone": &schema.Schema{
@ -84,6 +86,7 @@ func resourceAwsInstance() *schema.Resource {
"source_dest_check": &schema.Schema{ "source_dest_check": &schema.Schema{
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Default: true,
}, },
"user_data": &schema.Schema{ "user_data": &schema.Schema{
@ -140,6 +143,16 @@ func resourceAwsInstance() *schema.Resource {
Optional: true, Optional: true,
}, },
"disable_api_termination": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"monitoring": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"iam_instance_profile": &schema.Schema{ "iam_instance_profile": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
ForceNew: true, ForceNew: true,
@ -306,204 +319,49 @@ func resourceAwsInstance() *schema.Resource {
func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn conn := meta.(*AWSClient).ec2conn
// Figure out user data instanceOpts, err := buildAwsInstanceOpts(d, meta)
userData := "" if err != nil {
if v := d.Get("user_data"); v != nil { return err
userData = base64.StdEncoding.EncodeToString([]byte(v.(string)))
}
// check for non-default Subnet, and cast it to a String
var hasSubnet bool
subnet, hasSubnet := d.GetOk("subnet_id")
subnetID := subnet.(string)
placement := &ec2.Placement{
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
GroupName: aws.String(d.Get("placement_group").(string)),
}
if hasSubnet {
// Tenancy is only valid inside a VPC
// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Placement.html
if v := d.Get("tenancy").(string); v != "" {
placement.Tenancy = aws.String(v)
}
}
iam := &ec2.IAMInstanceProfileSpecification{
Name: aws.String(d.Get("iam_instance_profile").(string)),
} }
// Build the creation struct // Build the creation struct
runOpts := &ec2.RunInstancesInput{ runOpts := &ec2.RunInstancesInput{
ImageID: aws.String(d.Get("ami").(string)), BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
Placement: placement, DisableAPITermination: instanceOpts.DisableAPITermination,
InstanceType: aws.String(d.Get("instance_type").(string)), EBSOptimized: instanceOpts.EBSOptimized,
MaxCount: aws.Long(int64(1)), Monitoring: instanceOpts.Monitoring,
MinCount: aws.Long(int64(1)), IAMInstanceProfile: instanceOpts.IAMInstanceProfile,
UserData: aws.String(userData), ImageID: instanceOpts.ImageID,
EBSOptimized: aws.Boolean(d.Get("ebs_optimized").(bool)), InstanceType: instanceOpts.InstanceType,
IAMInstanceProfile: iam, KeyName: instanceOpts.KeyName,
} MaxCount: aws.Long(int64(1)),
MinCount: aws.Long(int64(1)),
associatePublicIPAddress := false NetworkInterfaces: instanceOpts.NetworkInterfaces,
if v := d.Get("associate_public_ip_address"); v != nil { Placement: instanceOpts.Placement,
associatePublicIPAddress = v.(bool) PrivateIPAddress: instanceOpts.PrivateIPAddress,
} SecurityGroupIDs: instanceOpts.SecurityGroupIDs,
SecurityGroups: instanceOpts.SecurityGroups,
var groups []*string SubnetID: instanceOpts.SubnetID,
if v := d.Get("security_groups"); v != nil { UserData: instanceOpts.UserData64,
// Security group names.
// For a nondefault VPC, you must use security group IDs instead.
// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RunInstances.html
sgs := v.(*schema.Set).List()
if len(sgs) > 0 && hasSubnet {
log.Printf("[WARN] Deprecated. Attempting to use 'security_groups' within a VPC instance. Use 'vpc_security_group_ids' instead.")
}
for _, v := range sgs {
str := v.(string)
groups = append(groups, aws.String(str))
}
}
if hasSubnet && associatePublicIPAddress {
// If we have a non-default VPC / Subnet specified, we can flag
// AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided.
// You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise
// you get: Network interfaces and an instance-level subnet ID may not be specified on the same request
// You also need to attach Security Groups to the NetworkInterface instead of the instance,
// to avoid: Network interfaces and an instance-level security groups may not be specified on
// the same request
ni := &ec2.InstanceNetworkInterfaceSpecification{
AssociatePublicIPAddress: aws.Boolean(associatePublicIPAddress),
DeviceIndex: aws.Long(int64(0)),
SubnetID: aws.String(subnetID),
Groups: groups,
}
if v, ok := d.GetOk("private_ip"); ok {
ni.PrivateIPAddress = aws.String(v.(string))
}
if v := d.Get("vpc_security_group_ids"); v != nil {
for _, v := range v.(*schema.Set).List() {
ni.Groups = append(ni.Groups, aws.String(v.(string)))
}
}
runOpts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni}
} else {
if subnetID != "" {
runOpts.SubnetID = aws.String(subnetID)
}
if v, ok := d.GetOk("private_ip"); ok {
runOpts.PrivateIPAddress = aws.String(v.(string))
}
if runOpts.SubnetID != nil &&
*runOpts.SubnetID != "" {
runOpts.SecurityGroupIDs = groups
} else {
runOpts.SecurityGroups = groups
}
if v := d.Get("vpc_security_group_ids"); v != nil {
for _, v := range v.(*schema.Set).List() {
runOpts.SecurityGroupIDs = append(runOpts.SecurityGroupIDs, aws.String(v.(string)))
}
}
}
if v, ok := d.GetOk("key_name"); ok {
runOpts.KeyName = aws.String(v.(string))
}
blockDevices := make([]*ec2.BlockDeviceMapping, 0)
if v, ok := d.GetOk("ebs_block_device"); ok {
vL := v.(*schema.Set).List()
for _, v := range vL {
bd := v.(map[string]interface{})
ebs := &ec2.EBSBlockDevice{
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
Encrypted: aws.Boolean(bd["encrypted"].(bool)),
}
if v, ok := bd["snapshot_id"].(string); ok && v != "" {
ebs.SnapshotID = aws.String(v)
}
if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Long(int64(v))
}
if v, ok := bd["volume_type"].(string); ok && v != "" {
ebs.VolumeType = aws.String(v)
}
if v, ok := bd["iops"].(int); ok && v > 0 {
ebs.IOPS = aws.Long(int64(v))
}
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
EBS: ebs,
})
}
}
if v, ok := d.GetOk("ephemeral_block_device"); ok {
vL := v.(*schema.Set).List()
for _, v := range vL {
bd := v.(map[string]interface{})
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
VirtualName: aws.String(bd["virtual_name"].(string)),
})
}
}
if v, ok := d.GetOk("root_block_device"); ok {
vL := v.(*schema.Set).List()
if len(vL) > 1 {
return fmt.Errorf("Cannot specify more than one root_block_device.")
}
for _, v := range vL {
bd := v.(map[string]interface{})
ebs := &ec2.EBSBlockDevice{
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
}
if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Long(int64(v))
}
if v, ok := bd["volume_type"].(string); ok && v != "" {
ebs.VolumeType = aws.String(v)
}
if v, ok := bd["iops"].(int); ok && v > 0 {
ebs.IOPS = aws.Long(int64(v))
}
if dn, err := fetchRootDeviceName(d.Get("ami").(string), conn); err == nil {
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
DeviceName: dn,
EBS: ebs,
})
} else {
return err
}
}
}
if len(blockDevices) > 0 {
runOpts.BlockDeviceMappings = blockDevices
} }
// Create the instance // Create the instance
log.Printf("[DEBUG] Run configuration: %#v", runOpts) log.Printf("[DEBUG] Run configuration: %s", awsutil.StringValue(runOpts))
runResp, err := conn.RunInstances(runOpts)
var runResp *ec2.Reservation
for i := 0; i < 5; i++ {
runResp, err = conn.RunInstances(runOpts)
if awsErr, ok := err.(awserr.Error); ok {
// IAM profiles can take ~10 seconds to propagate in AWS:
// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console
if awsErr.Code() == "InvalidParameterValue" && strings.Contains(awsErr.Message(), "Invalid IAM Instance Profile") {
log.Printf("[DEBUG] Invalid IAM Instance Profile referenced, retrying...")
time.Sleep(2 * time.Second)
continue
}
}
break
}
if err != nil { if err != nil {
return fmt.Errorf("Error launching source instance: %s", err) return fmt.Errorf("Error launching source instance: %s", err)
} }
@ -569,7 +427,7 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
if err != nil { if err != nil {
// If the instance was not found, return nil so that we can show // If the instance was not found, return nil so that we can show
// that the instance is gone. // that the instance is gone.
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidInstanceID.NotFound" { if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
d.SetId("") d.SetId("")
return nil return nil
} }
@ -599,6 +457,8 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("tenancy", instance.Placement.Tenancy) d.Set("tenancy", instance.Placement.Tenancy)
} }
d.Set("ami", instance.ImageID)
d.Set("instance_type", instance.InstanceType)
d.Set("key_name", instance.KeyName) d.Set("key_name", instance.KeyName)
d.Set("public_dns", instance.PublicDNSName) d.Set("public_dns", instance.PublicDNSName)
d.Set("public_ip", instance.PublicIPAddress) d.Set("public_ip", instance.PublicIPAddress)
@ -610,7 +470,13 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("subnet_id", instance.SubnetID) d.Set("subnet_id", instance.SubnetID)
} }
d.Set("ebs_optimized", instance.EBSOptimized) d.Set("ebs_optimized", instance.EBSOptimized)
d.Set("tags", tagsToMapSDK(instance.Tags))
if instance.Monitoring != nil && instance.Monitoring.State != nil {
monitoringState := *instance.Monitoring.State
d.Set("monitoring", monitoringState == "enabled" || monitoringState == "pending")
}
d.Set("tags", tagsToMap(instance.Tags))
// Determine whether we're referring to security groups with // Determine whether we're referring to security groups with
// IDs or names. We use a heuristic to figure this out. By default, // IDs or names. We use a heuristic to figure this out. By default,
@ -665,6 +531,11 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn conn := meta.(*AWSClient).ec2conn
d.Partial(true) d.Partial(true)
if err := setTags(conn, d); err != nil {
return err
} else {
d.SetPartial("tags")
}
// SourceDestCheck can only be set on VPC instances // SourceDestCheck can only be set on VPC instances
if d.Get("subnet_id").(string) != "" { if d.Get("subnet_id").(string) != "" {
@ -682,8 +553,8 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
if d.HasChange("vpc_security_group_ids") { if d.HasChange("vpc_security_group_ids") {
var groups []*string var groups []*string
if v := d.Get("vpc_security_group_ids"); v != nil { if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 {
for _, v := range v.(*schema.Set).List() { for _, v := range v.List() {
groups = append(groups, aws.String(v.(string))) groups = append(groups, aws.String(v.(string)))
} }
} }
@ -694,17 +565,23 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
if err != nil { if err != nil {
return err return err
} }
}
if d.HasChange("disable_api_termination") {
_, err := conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{
InstanceID: aws.String(d.Id()),
DisableAPITermination: &ec2.AttributeBooleanValue{
Value: aws.Boolean(d.Get("disable_api_termination").(bool)),
},
})
if err != nil {
return err
}
} }
// TODO(mitchellh): wait for the attributes we modified to // TODO(mitchellh): wait for the attributes we modified to
// persist the change... // persist the change...
if err := setTagsSDK(conn, d); err != nil {
return err
} else {
d.SetPartial("tags")
}
d.Partial(false) d.Partial(false)
return resourceAwsInstanceRead(d, meta) return resourceAwsInstanceRead(d, meta)
@ -713,32 +590,8 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error { func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn conn := meta.(*AWSClient).ec2conn
log.Printf("[INFO] Terminating instance: %s", d.Id()) if err := awsTerminateInstance(conn, d.Id()); err != nil {
req := &ec2.TerminateInstancesInput{ return err
InstanceIDs: []*string{aws.String(d.Id())},
}
if _, err := conn.TerminateInstances(req); err != nil {
return fmt.Errorf("Error terminating instance: %s", err)
}
log.Printf(
"[DEBUG] Waiting for instance (%s) to become terminated",
d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Target: "terminated",
Refresh: InstanceStateRefreshFunc(conn, 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 instance (%s) to terminate: %s",
d.Id(), err)
} }
d.SetId("") d.SetId("")
@ -753,7 +606,7 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRe
InstanceIDs: []*string{aws.String(instanceID)}, InstanceIDs: []*string{aws.String(instanceID)},
}) })
if err != nil { if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidInstanceID.NotFound" { if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
// Set this to nil as if we didn't find anything. // Set this to nil as if we didn't find anything.
resp = nil resp = nil
} else { } else {
@ -870,14 +723,305 @@ func fetchRootDeviceName(ami string, conn *ec2.EC2) (*string, error) {
} }
log.Printf("[DEBUG] Describing AMI %q to get root block device name", ami) log.Printf("[DEBUG] Describing AMI %q to get root block device name", ami)
req := &ec2.DescribeImagesInput{ImageIDs: []*string{aws.String(ami)}} res, err := conn.DescribeImages(&ec2.DescribeImagesInput{
if res, err := conn.DescribeImages(req); err == nil { ImageIDs: []*string{aws.String(ami)},
if len(res.Images) == 1 { })
return res.Images[0].RootDeviceName, nil if err != nil {
} else {
return nil, fmt.Errorf("Expected 1 AMI for ID: %s, got: %#v", ami, res.Images)
}
} else {
return nil, err return nil, err
} }
// For a bad image, we just return nil so we don't block a refresh
if len(res.Images) == 0 {
return nil, nil
}
image := res.Images[0]
rootDeviceName := image.RootDeviceName
// Some AMIs have a RootDeviceName like "/dev/sda1" that does not appear as a
// DeviceName in the BlockDeviceMapping list (which will instead have
// something like "/dev/sda")
//
// While this seems like it breaks an invariant of AMIs, it ends up working
// on the AWS side, and AMIs like this are common enough that we need to
// special case it so Terraform does the right thing.
//
// Our heuristic is: if the RootDeviceName does not appear in the
// BlockDeviceMapping, assume that the DeviceName of the first
// BlockDeviceMapping entry serves as the root device.
rootDeviceNameInMapping := false
for _, bdm := range image.BlockDeviceMappings {
if bdm.DeviceName == image.RootDeviceName {
rootDeviceNameInMapping = true
}
}
if !rootDeviceNameInMapping && len(image.BlockDeviceMappings) > 0 {
rootDeviceName = image.BlockDeviceMappings[0].DeviceName
}
return rootDeviceName, nil
}
func readBlockDeviceMappingsFromConfig(
d *schema.ResourceData, conn *ec2.EC2) ([]*ec2.BlockDeviceMapping, error) {
blockDevices := make([]*ec2.BlockDeviceMapping, 0)
if v, ok := d.GetOk("ebs_block_device"); ok {
vL := v.(*schema.Set).List()
for _, v := range vL {
bd := v.(map[string]interface{})
ebs := &ec2.EBSBlockDevice{
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
}
if v, ok := bd["snapshot_id"].(string); ok && v != "" {
ebs.SnapshotID = aws.String(v)
}
if v, ok := bd["encrypted"].(bool); ok && v {
ebs.Encrypted = aws.Boolean(v)
}
if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Long(int64(v))
}
if v, ok := bd["volume_type"].(string); ok && v != "" {
ebs.VolumeType = aws.String(v)
}
if v, ok := bd["iops"].(int); ok && v > 0 {
ebs.IOPS = aws.Long(int64(v))
}
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
EBS: ebs,
})
}
}
if v, ok := d.GetOk("ephemeral_block_device"); ok {
vL := v.(*schema.Set).List()
for _, v := range vL {
bd := v.(map[string]interface{})
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
VirtualName: aws.String(bd["virtual_name"].(string)),
})
}
}
if v, ok := d.GetOk("root_block_device"); ok {
vL := v.(*schema.Set).List()
if len(vL) > 1 {
return nil, fmt.Errorf("Cannot specify more than one root_block_device.")
}
for _, v := range vL {
bd := v.(map[string]interface{})
ebs := &ec2.EBSBlockDevice{
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
}
if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Long(int64(v))
}
if v, ok := bd["volume_type"].(string); ok && v != "" {
ebs.VolumeType = aws.String(v)
}
if v, ok := bd["iops"].(int); ok && v > 0 {
ebs.IOPS = aws.Long(int64(v))
}
if dn, err := fetchRootDeviceName(d.Get("ami").(string), conn); err == nil {
if dn == nil {
return nil, fmt.Errorf(
"Expected 1 AMI for ID: %s, got none",
d.Get("ami").(string))
}
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
DeviceName: dn,
EBS: ebs,
})
} else {
return nil, err
}
}
}
return blockDevices, nil
}
type awsInstanceOpts struct {
BlockDeviceMappings []*ec2.BlockDeviceMapping
DisableAPITermination *bool
EBSOptimized *bool
Monitoring *ec2.RunInstancesMonitoringEnabled
IAMInstanceProfile *ec2.IAMInstanceProfileSpecification
ImageID *string
InstanceType *string
KeyName *string
NetworkInterfaces []*ec2.InstanceNetworkInterfaceSpecification
Placement *ec2.Placement
PrivateIPAddress *string
SecurityGroupIDs []*string
SecurityGroups []*string
SpotPlacement *ec2.SpotPlacement
SubnetID *string
UserData64 *string
}
func buildAwsInstanceOpts(
d *schema.ResourceData, meta interface{}) (*awsInstanceOpts, error) {
conn := meta.(*AWSClient).ec2conn
opts := &awsInstanceOpts{
DisableAPITermination: aws.Boolean(d.Get("disable_api_termination").(bool)),
EBSOptimized: aws.Boolean(d.Get("ebs_optimized").(bool)),
ImageID: aws.String(d.Get("ami").(string)),
InstanceType: aws.String(d.Get("instance_type").(string)),
}
opts.Monitoring = &ec2.RunInstancesMonitoringEnabled{
Enabled: aws.Boolean(d.Get("monitoring").(bool)),
}
opts.IAMInstanceProfile = &ec2.IAMInstanceProfileSpecification{
Name: aws.String(d.Get("iam_instance_profile").(string)),
}
opts.UserData64 = aws.String(
base64.StdEncoding.EncodeToString([]byte(d.Get("user_data").(string))))
// check for non-default Subnet, and cast it to a String
subnet, hasSubnet := d.GetOk("subnet_id")
subnetID := subnet.(string)
// Placement is used for aws_instance; SpotPlacement is used for
// aws_spot_instance_request. They represent the same data. :-|
opts.Placement = &ec2.Placement{
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
GroupName: aws.String(d.Get("placement_group").(string)),
}
opts.SpotPlacement = &ec2.SpotPlacement{
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
GroupName: aws.String(d.Get("placement_group").(string)),
}
if v := d.Get("tenancy").(string); v != "" {
opts.Placement.Tenancy = aws.String(v)
}
associatePublicIPAddress := d.Get("associate_public_ip_address").(bool)
var groups []*string
if v := d.Get("security_groups"); v != nil {
// Security group names.
// For a nondefault VPC, you must use security group IDs instead.
// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RunInstances.html
sgs := v.(*schema.Set).List()
if len(sgs) > 0 && hasSubnet {
log.Printf("[WARN] Deprecated. Attempting to use 'security_groups' within a VPC instance. Use 'vpc_security_group_ids' instead.")
}
for _, v := range sgs {
str := v.(string)
groups = append(groups, aws.String(str))
}
}
if hasSubnet && associatePublicIPAddress {
// If we have a non-default VPC / Subnet specified, we can flag
// AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided.
// You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise
// you get: Network interfaces and an instance-level subnet ID may not be specified on the same request
// You also need to attach Security Groups to the NetworkInterface instead of the instance,
// to avoid: Network interfaces and an instance-level security groups may not be specified on
// the same request
ni := &ec2.InstanceNetworkInterfaceSpecification{
AssociatePublicIPAddress: aws.Boolean(associatePublicIPAddress),
DeviceIndex: aws.Long(int64(0)),
SubnetID: aws.String(subnetID),
Groups: groups,
}
if v, ok := d.GetOk("private_ip"); ok {
ni.PrivateIPAddress = aws.String(v.(string))
}
if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 {
for _, v := range v.List() {
ni.Groups = append(ni.Groups, aws.String(v.(string)))
}
}
opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni}
} else {
if subnetID != "" {
opts.SubnetID = aws.String(subnetID)
}
if v, ok := d.GetOk("private_ip"); ok {
opts.PrivateIPAddress = aws.String(v.(string))
}
if opts.SubnetID != nil &&
*opts.SubnetID != "" {
opts.SecurityGroupIDs = groups
} else {
opts.SecurityGroups = groups
}
if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 {
for _, v := range v.List() {
opts.SecurityGroupIDs = append(opts.SecurityGroupIDs, aws.String(v.(string)))
}
}
}
if v, ok := d.GetOk("key_name"); ok {
opts.KeyName = aws.String(v.(string))
}
blockDevices, err := readBlockDeviceMappingsFromConfig(d, conn)
if err != nil {
return nil, err
}
if len(blockDevices) > 0 {
opts.BlockDeviceMappings = blockDevices
}
return opts, nil
}
func awsTerminateInstance(conn *ec2.EC2, id string) error {
log.Printf("[INFO] Terminating instance: %s", id)
req := &ec2.TerminateInstancesInput{
InstanceIDs: []*string{aws.String(id)},
}
if _, err := conn.TerminateInstances(req); err != nil {
return fmt.Errorf("Error terminating instance: %s", err)
}
log.Printf("[DEBUG] Waiting for instance (%s) to become terminated", id)
stateConf := &resource.StateChangeConf{
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Target: "terminated",
Refresh: InstanceStateRefreshFunc(conn, 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 instance (%s) to terminate: %s", id, err)
}
return nil
} }

View File

@ -5,14 +5,16 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSInstance_normal(t *testing.T) { func TestAccAWSInstance_basic(t *testing.T) {
var v ec2.Instance var v ec2.Instance
var vol *ec2.Volume var vol *ec2.Volume
@ -229,6 +231,51 @@ func TestAccAWSInstance_sourceDestCheck(t *testing.T) {
}) })
} }
func TestAccAWSInstance_disableApiTermination(t *testing.T) {
var v ec2.Instance
checkDisableApiTermination := func(expected bool) resource.TestCheckFunc {
return func(*terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
r, err := conn.DescribeInstanceAttribute(&ec2.DescribeInstanceAttributeInput{
InstanceID: v.InstanceID,
Attribute: aws.String("disableApiTermination"),
})
if err != nil {
return err
}
got := *r.DisableAPITermination.Value
if got != expected {
return fmt.Errorf("expected: %t, got: %t", expected, got)
}
return nil
}
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckInstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccInstanceConfigDisableAPITermination(true),
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists("aws_instance.foo", &v),
checkDisableApiTermination(true),
),
},
resource.TestStep{
Config: testAccInstanceConfigDisableAPITermination(false),
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists("aws_instance.foo", &v),
checkDisableApiTermination(false),
),
},
},
})
}
func TestAccAWSInstance_vpc(t *testing.T) { func TestAccAWSInstance_vpc(t *testing.T) {
var v ec2.Instance var v ec2.Instance
@ -334,9 +381,9 @@ func TestAccAWSInstance_tags(t *testing.T) {
Config: testAccCheckInstanceConfigTags, Config: testAccCheckInstanceConfigTags,
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists("aws_instance.foo", &v), testAccCheckInstanceExists("aws_instance.foo", &v),
testAccCheckTagsSDK(&v.Tags, "foo", "bar"), testAccCheckTags(&v.Tags, "foo", "bar"),
// Guard against regression of https://github.com/hashicorp/terraform/issues/914 // Guard against regression of https://github.com/hashicorp/terraform/issues/914
testAccCheckTagsSDK(&v.Tags, "#", ""), testAccCheckTags(&v.Tags, "#", ""),
), ),
}, },
@ -344,8 +391,8 @@ func TestAccAWSInstance_tags(t *testing.T) {
Config: testAccCheckInstanceConfigTagsUpdate, Config: testAccCheckInstanceConfigTagsUpdate,
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists("aws_instance.foo", &v), testAccCheckInstanceExists("aws_instance.foo", &v),
testAccCheckTagsSDK(&v.Tags, "foo", ""), testAccCheckTags(&v.Tags, "foo", ""),
testAccCheckTagsSDK(&v.Tags, "bar", "baz"), testAccCheckTags(&v.Tags, "bar", "baz"),
), ),
}, },
}, },
@ -410,6 +457,60 @@ func TestAccAWSInstance_associatePublicIPAndPrivateIP(t *testing.T) {
}) })
} }
// Guard against regression with KeyPairs
// https://github.com/hashicorp/terraform/issues/2302
func TestAccAWSInstance_keyPairCheck(t *testing.T) {
var v ec2.Instance
testCheckKeyPair := func(keyName string) resource.TestCheckFunc {
return func(*terraform.State) error {
if v.KeyName == nil {
return fmt.Errorf("No Key Pair found, expected(%s)", keyName)
}
if *v.KeyName != keyName {
return fmt.Errorf("Bad key name, expected (%s), got (%s)", keyName, awsutil.StringValue(v.KeyName))
}
return nil
}
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckInstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccInstanceConfigKeyPair,
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists("aws_instance.foo", &v),
testCheckKeyPair("tmp-key"),
),
},
},
})
}
func TestAccAWSInstance_rootBlockDeviceMismatch(t *testing.T) {
var v ec2.Instance
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckInstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccInstanceConfigRootBlockDeviceMismatch,
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists("aws_instance.foo", &v),
resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.0.volume_size", "13"),
),
},
},
})
}
func testAccCheckInstanceDestroy(s *terraform.State) error { func testAccCheckInstanceDestroy(s *terraform.State) error {
return testAccCheckInstanceDestroyWithProvider(s, testAccProvider) return testAccCheckInstanceDestroyWithProvider(s, testAccProvider)
} }
@ -437,6 +538,7 @@ func testAccCheckInstanceDestroyWithProvider(s *terraform.State, provider *schem
} }
// Try to find the resource // Try to find the resource
var err error
resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{ resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIDs: []*string{aws.String(rs.Primary.ID)}, InstanceIDs: []*string{aws.String(rs.Primary.ID)},
}) })
@ -449,11 +551,11 @@ func testAccCheckInstanceDestroyWithProvider(s *terraform.State, provider *schem
} }
// Verify the error is what we want // Verify the error is what we want
ec2err, ok := err.(aws.APIError) ec2err, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if ec2err.Code != "InvalidInstanceID.NotFound" { if ec2err.Code() != "InvalidInstanceID.NotFound" {
return err return err
} }
} }
@ -477,11 +579,16 @@ func testAccCheckInstanceExistsWithProviders(n string, i *ec2.Instance, provider
return fmt.Errorf("No ID is set") return fmt.Errorf("No ID is set")
} }
for _, provider := range *providers { for _, provider := range *providers {
// Ignore if Meta is empty, this can happen for validation providers
if provider.Meta() == nil {
continue
}
conn := provider.Meta().(*AWSClient).ec2conn conn := provider.Meta().(*AWSClient).ec2conn
resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{ resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIDs: []*string{aws.String(rs.Primary.ID)}, InstanceIDs: []*string{aws.String(rs.Primary.ID)},
}) })
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidInstanceID.NotFound" { if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
continue continue
} }
if err != nil { if err != nil {
@ -606,7 +713,6 @@ resource "aws_instance" "foo" {
ami = "ami-4fccb37f" ami = "ami-4fccb37f"
instance_type = "m1.small" instance_type = "m1.small"
subnet_id = "${aws_subnet.foo.id}" subnet_id = "${aws_subnet.foo.id}"
source_dest_check = true
} }
` `
@ -629,6 +735,27 @@ resource "aws_instance" "foo" {
} }
` `
func testAccInstanceConfigDisableAPITermination(val bool) string {
return fmt.Sprintf(`
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}
resource "aws_subnet" "foo" {
cidr_block = "10.1.1.0/24"
vpc_id = "${aws_vpc.foo.id}"
}
resource "aws_instance" "foo" {
# us-west-2
ami = "ami-4fccb37f"
instance_type = "m1.small"
subnet_id = "${aws_subnet.foo.id}"
disable_api_termination = %t
}
`, val)
}
const testAccInstanceConfigVPC = ` const testAccInstanceConfigVPC = `
resource "aws_vpc" "foo" { resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16" cidr_block = "10.1.0.0/16"
@ -822,3 +949,41 @@ resource "aws_eip" "foo_eip" {
depends_on = ["aws_internet_gateway.gw"] depends_on = ["aws_internet_gateway.gw"]
} }
` `
const testAccInstanceConfigKeyPair = `
provider "aws" {
region = "us-east-1"
}
resource "aws_key_pair" "debugging" {
key_name = "tmp-key"
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 phodgson@thoughtworks.com"
}
resource "aws_instance" "foo" {
ami = "ami-408c7f28"
instance_type = "t1.micro"
key_name = "${aws_key_pair.debugging.key_name}"
}
`
const testAccInstanceConfigRootBlockDeviceMismatch = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}
resource "aws_subnet" "foo" {
cidr_block = "10.1.1.0/24"
vpc_id = "${aws_vpc.foo.id}"
}
resource "aws_instance" "foo" {
// This is an AMI with RootDeviceName: "/dev/sda1"; actual root: "/dev/sda"
ami = "ami-ef5b69df"
instance_type = "t1.micro"
subnet_id = "${aws_subnet.foo.id}"
root_block_device {
volume_size = 13
}
}
`

View File

@ -5,8 +5,9 @@ import (
"log" "log"
"time" "time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -33,6 +34,7 @@ func resourceAwsInternetGatewayCreate(d *schema.ResourceData, meta interface{})
// Create the gateway // Create the gateway
log.Printf("[DEBUG] Creating internet gateway") log.Printf("[DEBUG] Creating internet gateway")
var err error
resp, err := conn.CreateInternetGateway(nil) resp, err := conn.CreateInternetGateway(nil)
if err != nil { if err != nil {
return fmt.Errorf("Error creating internet gateway: %s", err) return fmt.Errorf("Error creating internet gateway: %s", err)
@ -43,7 +45,7 @@ func resourceAwsInternetGatewayCreate(d *schema.ResourceData, meta interface{})
d.SetId(*ig.InternetGatewayID) d.SetId(*ig.InternetGatewayID)
log.Printf("[INFO] InternetGateway ID: %s", d.Id()) log.Printf("[INFO] InternetGateway ID: %s", d.Id())
err = setTagsSDK(conn, d) err = setTags(conn, d)
if err != nil { if err != nil {
return err return err
} }
@ -73,7 +75,7 @@ func resourceAwsInternetGatewayRead(d *schema.ResourceData, meta interface{}) er
d.Set("vpc_id", ig.Attachments[0].VPCID) d.Set("vpc_id", ig.Attachments[0].VPCID)
} }
d.Set("tags", tagsToMapSDK(ig.Tags)) d.Set("tags", tagsToMap(ig.Tags))
return nil return nil
} }
@ -93,7 +95,7 @@ func resourceAwsInternetGatewayUpdate(d *schema.ResourceData, meta interface{})
conn := meta.(*AWSClient).ec2conn conn := meta.(*AWSClient).ec2conn
if err := setTagsSDK(conn, d); err != nil { if err := setTags(conn, d); err != nil {
return err return err
} }
@ -120,12 +122,12 @@ func resourceAwsInternetGatewayDelete(d *schema.ResourceData, meta interface{})
return nil return nil
} }
ec2err, ok := err.(aws.APIError) ec2err, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
switch ec2err.Code { switch ec2err.Code() {
case "InvalidInternetGatewayID.NotFound": case "InvalidInternetGatewayID.NotFound":
return nil return nil
case "DependencyViolation": case "DependencyViolation":
@ -205,7 +207,7 @@ func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{})
Pending: []string{"detaching"}, Pending: []string{"detaching"},
Target: "detached", Target: "detached",
Refresh: detachIGStateRefreshFunc(conn, d.Id(), vpcID.(string)), Refresh: detachIGStateRefreshFunc(conn, d.Id(), vpcID.(string)),
Timeout: 2 * time.Minute, Timeout: 5 * time.Minute,
Delay: 10 * time.Second, Delay: 10 * time.Second,
} }
if _, err := stateConf.WaitForState(); err != nil { if _, err := stateConf.WaitForState(); err != nil {
@ -226,13 +228,13 @@ func detachIGStateRefreshFunc(conn *ec2.EC2, instanceID, vpcID string) resource.
VPCID: aws.String(vpcID), VPCID: aws.String(vpcID),
}) })
if err != nil { if err != nil {
ec2err, ok := err.(aws.APIError) ec2err, ok := err.(awserr.Error)
if ok { if ok {
if ec2err.Code == "InvalidInternetGatewayID.NotFound" { if ec2err.Code() == "InvalidInternetGatewayID.NotFound" {
return nil, "Not Found", err return nil, "Not Found", err
} else if ec2err.Code == "Gateway.NotAttached" { } else if ec2err.Code() == "Gateway.NotAttached" {
return "detached", "detached", nil return "detached", "detached", nil
} else if ec2err.Code == "DependencyViolation" { } else if ec2err.Code() == "DependencyViolation" {
return nil, "detaching", nil return nil, "detaching", nil
} }
} }
@ -251,8 +253,8 @@ func IGStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc {
InternetGatewayIDs: []*string{aws.String(id)}, InternetGatewayIDs: []*string{aws.String(id)},
}) })
if err != nil { if err != nil {
ec2err, ok := err.(aws.APIError) ec2err, ok := err.(awserr.Error)
if ok && ec2err.Code == "InvalidInternetGatewayID.NotFound" { if ok && ec2err.Code() == "InvalidInternetGatewayID.NotFound" {
resp = nil resp = nil
} else { } else {
log.Printf("[ERROR] Error on IGStateRefresh: %s", err) log.Printf("[ERROR] Error on IGStateRefresh: %s", err)
@ -284,8 +286,8 @@ func IGAttachStateRefreshFunc(conn *ec2.EC2, id string, expected string) resourc
InternetGatewayIDs: []*string{aws.String(id)}, InternetGatewayIDs: []*string{aws.String(id)},
}) })
if err != nil { if err != nil {
ec2err, ok := err.(aws.APIError) ec2err, ok := err.(awserr.Error)
if ok && ec2err.Code == "InvalidInternetGatewayID.NotFound" { if ok && ec2err.Code() == "InvalidInternetGatewayID.NotFound" {
resp = nil resp = nil
} else { } else {
log.Printf("[ERROR] Error on IGStateRefresh: %s", err) log.Printf("[ERROR] Error on IGStateRefresh: %s", err)

View File

@ -4,8 +4,9 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -98,7 +99,7 @@ func TestAccAWSInternetGateway_tags(t *testing.T) {
Config: testAccCheckInternetGatewayConfigTags, Config: testAccCheckInternetGatewayConfigTags,
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckInternetGatewayExists("aws_internet_gateway.foo", &v), testAccCheckInternetGatewayExists("aws_internet_gateway.foo", &v),
testAccCheckTagsSDK(&v.Tags, "foo", "bar"), testAccCheckTags(&v.Tags, "foo", "bar"),
), ),
}, },
@ -106,8 +107,8 @@ func TestAccAWSInternetGateway_tags(t *testing.T) {
Config: testAccCheckInternetGatewayConfigTagsUpdate, Config: testAccCheckInternetGatewayConfigTagsUpdate,
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckInternetGatewayExists("aws_internet_gateway.foo", &v), testAccCheckInternetGatewayExists("aws_internet_gateway.foo", &v),
testAccCheckTagsSDK(&v.Tags, "foo", ""), testAccCheckTags(&v.Tags, "foo", ""),
testAccCheckTagsSDK(&v.Tags, "bar", "baz"), testAccCheckTags(&v.Tags, "bar", "baz"),
), ),
}, },
}, },
@ -135,11 +136,11 @@ func testAccCheckInternetGatewayDestroy(s *terraform.State) error {
} }
// Verify the error is what we want // Verify the error is what we want
ec2err, ok := err.(aws.APIError) ec2err, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if ec2err.Code != "InvalidInternetGatewayID.NotFound" { if ec2err.Code() != "InvalidInternetGatewayID.NotFound" {
return err return err
} }
} }

View File

@ -6,8 +6,9 @@ import (
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
) )
func resourceAwsKeyPair() *schema.Resource { func resourceAwsKeyPair() *schema.Resource {
@ -65,6 +66,11 @@ func resourceAwsKeyPairRead(d *schema.ResourceData, meta interface{}) error {
} }
resp, err := conn.DescribeKeyPairs(req) resp, err := conn.DescribeKeyPairs(req)
if err != nil { if err != nil {
awsErr, ok := err.(awserr.Error)
if ok && awsErr.Code() == "InvalidKeyPair.NotFound" {
d.SetId("")
return nil
}
return fmt.Errorf("Error retrieving KeyPair: %s", err) return fmt.Errorf("Error retrieving KeyPair: %s", err)
} }

View File

@ -5,13 +5,14 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSKeyPair_normal(t *testing.T) { func TestAccAWSKeyPair_basic(t *testing.T) {
var conf ec2.KeyPairInfo var conf ec2.KeyPairInfo
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
@ -78,11 +79,11 @@ func testAccCheckAWSKeyPairDestroy(s *terraform.State) error {
} }
// Verify the error is what we want // Verify the error is what we want
ec2err, ok := err.(aws.APIError) ec2err, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if ec2err.Code != "InvalidKeyPair.NotFound" { if ec2err.Code() != "InvalidKeyPair.NotFound" {
return err return err
} }
} }

View File

@ -0,0 +1,156 @@
package aws
import (
"fmt"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/kinesis"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsKinesisStream() *schema.Resource {
return &schema.Resource{
Create: resourceAwsKinesisStreamCreate,
Read: resourceAwsKinesisStreamRead,
Delete: resourceAwsKinesisStreamDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"shard_count": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: true,
},
"arn": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
},
}
}
func resourceAwsKinesisStreamCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).kinesisconn
sn := d.Get("name").(string)
createOpts := &kinesis.CreateStreamInput{
ShardCount: aws.Long(int64(d.Get("shard_count").(int))),
StreamName: aws.String(sn),
}
_, err := conn.CreateStream(createOpts)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
return fmt.Errorf("[WARN] Error creating Kinesis Stream: \"%s\", code: \"%s\"", awsErr.Message(), awsErr.Code())
}
return err
}
stateConf := &resource.StateChangeConf{
Pending: []string{"CREATING"},
Target: "ACTIVE",
Refresh: streamStateRefreshFunc(conn, sn),
Timeout: 5 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}
streamRaw, err := stateConf.WaitForState()
if err != nil {
return fmt.Errorf(
"Error waiting for Kinesis Stream (%s) to become active: %s",
sn, err)
}
s := streamRaw.(*kinesis.StreamDescription)
d.SetId(*s.StreamARN)
d.Set("arn", s.StreamARN)
return nil
}
func resourceAwsKinesisStreamRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).kinesisconn
describeOpts := &kinesis.DescribeStreamInput{
StreamName: aws.String(d.Get("name").(string)),
Limit: aws.Long(1),
}
resp, err := conn.DescribeStream(describeOpts)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "ResourceNotFoundException" {
d.SetId("")
return nil
}
return fmt.Errorf("[WARN] Error reading Kinesis Stream: \"%s\", code: \"%s\"", awsErr.Message(), awsErr.Code())
}
return err
}
s := resp.StreamDescription
d.Set("arn", *s.StreamARN)
d.Set("shard_count", len(s.Shards))
return nil
}
func resourceAwsKinesisStreamDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).kinesisconn
sn := d.Get("name").(string)
_, err := conn.DeleteStream(&kinesis.DeleteStreamInput{
StreamName: aws.String(sn),
})
if err != nil {
return err
}
stateConf := &resource.StateChangeConf{
Pending: []string{"DELETING"},
Target: "DESTROYED",
Refresh: streamStateRefreshFunc(conn, sn),
Timeout: 5 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return fmt.Errorf(
"Error waiting for Stream (%s) to be destroyed: %s",
sn, err)
}
d.SetId("")
return nil
}
func streamStateRefreshFunc(conn *kinesis.Kinesis, sn string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
describeOpts := &kinesis.DescribeStreamInput{
StreamName: aws.String(sn),
Limit: aws.Long(1),
}
resp, err := conn.DescribeStream(describeOpts)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "ResourceNotFoundException" {
return 42, "DESTROYED", nil
}
return nil, awsErr.Code(), err
}
return nil, "failed", err
}
return resp.StreamDescription, *resp.StreamDescription.StreamStatus, nil
}
}

View File

@ -0,0 +1,108 @@
package aws
import (
"fmt"
"math/rand"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/kinesis"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccKinesisStream_basic(t *testing.T) {
var stream kinesis.StreamDescription
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckKinesisStreamDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccKinesisStreamConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckKinesisStreamExists("aws_kinesis_stream.test_stream", &stream),
testAccCheckAWSKinesisStreamAttributes(&stream),
),
},
},
})
}
func testAccCheckKinesisStreamExists(n string, stream *kinesis.StreamDescription) 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 Kinesis ID is set")
}
conn := testAccProvider.Meta().(*AWSClient).kinesisconn
describeOpts := &kinesis.DescribeStreamInput{
StreamName: aws.String(rs.Primary.Attributes["name"]),
Limit: aws.Long(1),
}
resp, err := conn.DescribeStream(describeOpts)
if err != nil {
return err
}
*stream = *resp.StreamDescription
return nil
}
}
func testAccCheckAWSKinesisStreamAttributes(stream *kinesis.StreamDescription) resource.TestCheckFunc {
return func(s *terraform.State) error {
if !strings.HasPrefix(*stream.StreamName, "terraform-kinesis-test") {
return fmt.Errorf("Bad Stream name: %s", *stream.StreamName)
}
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_kinesis_stream" {
continue
}
if *stream.StreamARN != rs.Primary.Attributes["arn"] {
return fmt.Errorf("Bad Stream ARN\n\t expected: %s\n\tgot: %s\n", rs.Primary.Attributes["arn"], *stream.StreamARN)
}
}
return nil
}
}
func testAccCheckKinesisStreamDestroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_kinesis_stream" {
continue
}
conn := testAccProvider.Meta().(*AWSClient).kinesisconn
describeOpts := &kinesis.DescribeStreamInput{
StreamName: aws.String(rs.Primary.Attributes["name"]),
Limit: aws.Long(1),
}
resp, err := conn.DescribeStream(describeOpts)
if err == nil {
if resp.StreamDescription != nil && *resp.StreamDescription.StreamStatus != "DELETING" {
return fmt.Errorf("Error: Stream still exists")
}
}
return nil
}
return nil
}
var testAccKinesisStreamConfig = fmt.Sprintf(`
resource "aws_kinesis_stream" "test_stream" {
name = "terraform-kinesis-test-%d"
shard_count = 1
}
`, rand.New(rand.NewSource(time.Now().UnixNano())).Int())

View File

@ -0,0 +1,207 @@
package aws
import (
"crypto/sha256"
"fmt"
"io/ioutil"
"log"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/lambda"
"github.com/mitchellh/go-homedir"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsLambdaFunction() *schema.Resource {
return &schema.Resource{
Create: resourceAwsLambdaFunctionCreate,
Read: resourceAwsLambdaFunctionRead,
Update: resourceAwsLambdaFunctionUpdate,
Delete: resourceAwsLambdaFunctionDelete,
Schema: map[string]*schema.Schema{
"filename": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true, // TODO make this editable
},
"function_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"handler": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true, // TODO make this editable
},
"memory_size": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 128,
ForceNew: true, // TODO make this editable
},
"role": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true, // TODO make this editable
},
"runtime": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "nodejs",
},
"timeout": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 3,
ForceNew: true, // TODO make this editable
},
"arn": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"last_modified": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"source_code_hash": &schema.Schema{
Type: schema.TypeString,
Computed: true,
ForceNew: true,
},
},
}
}
// resourceAwsLambdaFunction maps to:
// CreateFunction in the API / SDK
func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).lambdaconn
functionName := d.Get("function_name").(string)
iamRole := d.Get("role").(string)
log.Printf("[DEBUG] Creating Lambda Function %s with role %s", functionName, iamRole)
filename, err := homedir.Expand(d.Get("filename").(string))
if err != nil {
return err
}
zipfile, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
d.Set("source_code_hash", sha256.Sum256(zipfile))
log.Printf("[DEBUG] ")
params := &lambda.CreateFunctionInput{
Code: &lambda.FunctionCode{
ZipFile: zipfile,
},
Description: aws.String(d.Get("description").(string)),
FunctionName: aws.String(functionName),
Handler: aws.String(d.Get("handler").(string)),
MemorySize: aws.Long(int64(d.Get("memory_size").(int))),
Role: aws.String(iamRole),
Runtime: aws.String(d.Get("runtime").(string)),
Timeout: aws.Long(int64(d.Get("timeout").(int))),
}
for i := 0; i < 5; i++ {
_, err = conn.CreateFunction(params)
if awsErr, ok := err.(awserr.Error); ok {
// IAM profiles can take ~10 seconds to propagate in AWS:
// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console
// Error creating Lambda function: InvalidParameterValueException: The role defined for the task cannot be assumed by Lambda.
if awsErr.Code() == "InvalidParameterValueException" && strings.Contains(awsErr.Message(), "The role defined for the task cannot be assumed by Lambda.") {
log.Printf("[DEBUG] Invalid IAM Instance Profile referenced, retrying...")
time.Sleep(2 * time.Second)
continue
}
}
break
}
if err != nil {
return fmt.Errorf("Error creating Lambda function: %s", err)
}
d.SetId(d.Get("function_name").(string))
return resourceAwsLambdaFunctionRead(d, meta)
}
// resourceAwsLambdaFunctionRead maps to:
// GetFunction in the API / SDK
func resourceAwsLambdaFunctionRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).lambdaconn
log.Printf("[DEBUG] Fetching Lambda Function: %s", d.Id())
params := &lambda.GetFunctionInput{
FunctionName: aws.String(d.Get("function_name").(string)),
}
getFunctionOutput, err := conn.GetFunction(params)
if err != nil {
return err
}
// getFunctionOutput.Code.Location is a pre-signed URL pointing at the zip
// file that we uploaded when we created the resource. You can use it to
// download the code from AWS. The other part is
// getFunctionOutput.Configuration which holds metadata.
function := getFunctionOutput.Configuration
// TODO error checking / handling on the Set() calls.
d.Set("arn", function.FunctionARN)
d.Set("description", function.Description)
d.Set("handler", function.Handler)
d.Set("memory_size", function.MemorySize)
d.Set("last_modified", function.LastModified)
d.Set("role", function.Role)
d.Set("runtime", function.Runtime)
d.Set("timeout", function.Timeout)
return nil
}
// resourceAwsLambdaFunction maps to:
// DeleteFunction in the API / SDK
func resourceAwsLambdaFunctionDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).lambdaconn
log.Printf("[INFO] Deleting Lambda Function: %s", d.Id())
params := &lambda.DeleteFunctionInput{
FunctionName: aws.String(d.Get("function_name").(string)),
}
_, err := conn.DeleteFunction(params)
if err != nil {
return fmt.Errorf("Error deleting Lambda Function: %s", err)
}
d.SetId("")
return nil
}
// resourceAwsLambdaFunctionUpdate maps to:
// UpdateFunctionCode in the API / SDK
func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) error {
// conn := meta.(*AWSClient).lambdaconn
return nil
}

View File

@ -0,0 +1,125 @@
package aws
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/lambda"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSLambdaFunction_normal(t *testing.T) {
var conf lambda.GetFunctionOutput
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckLambdaFunctionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSLambdaConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsLambdaFunctionExists("aws_lambda_function.lambda_function_test", &conf),
testAccCheckAWSLambdaAttributes(&conf),
),
},
},
})
}
func testAccCheckLambdaFunctionDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).lambdaconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_lambda_function" {
continue
}
_, err := conn.GetFunction(&lambda.GetFunctionInput{
FunctionName: aws.String(rs.Primary.ID),
})
if err == nil {
return fmt.Errorf("Lambda Function still exists")
}
}
return nil
}
func testAccCheckAwsLambdaFunctionExists(n string, function *lambda.GetFunctionOutput) resource.TestCheckFunc {
// Wait for IAM role
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Lambda function not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("Lambda function ID not set")
}
conn := testAccProvider.Meta().(*AWSClient).lambdaconn
params := &lambda.GetFunctionInput{
FunctionName: aws.String("example_lambda_name"),
}
getFunction, err := conn.GetFunction(params)
if err != nil {
return err
}
*function = *getFunction
return nil
}
}
func testAccCheckAWSLambdaAttributes(function *lambda.GetFunctionOutput) resource.TestCheckFunc {
return func(s *terraform.State) error {
c := function.Configuration
const expectedName = "example_lambda_name"
if *c.FunctionName != expectedName {
return fmt.Errorf("Expected function name %s, got %s", expectedName, *c.FunctionName)
}
if *c.FunctionARN == "" {
return fmt.Errorf("Could not read Lambda Function's ARN")
}
return nil
}
}
const testAccAWSLambdaConfig = `
resource "aws_iam_role" "iam_for_lambda" {
name = "iam_for_lambda"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
resource "aws_lambda_function" "lambda_function_test" {
filename = "test-fixtures/lambdatest.zip"
function_name = "example_lambda_name"
role = "${aws_iam_role.iam_for_lambda.arn}"
handler = "exports.example"
}
`

View File

@ -9,9 +9,10 @@ import (
"log" "log"
"time" "time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
@ -29,6 +30,15 @@ func resourceAwsLaunchConfiguration() *schema.Resource {
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
// https://github.com/boto/botocore/blob/9f322b1/botocore/data/autoscaling/2011-01-01/service-2.json#L1932-L1939
value := v.(string)
if len(value) > 255 {
errors = append(errors, fmt.Errorf(
"%q cannot be longer than 255 characters", k))
}
return
},
}, },
"image_id": &schema.Schema{ "image_id": &schema.Schema{
@ -105,6 +115,13 @@ func resourceAwsLaunchConfiguration() *schema.Resource {
ForceNew: true, ForceNew: true,
}, },
"enable_monitoring": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
Default: true,
},
"ebs_block_device": &schema.Schema{ "ebs_block_device": &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
@ -255,6 +272,10 @@ func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface
createLaunchConfigurationOpts.UserData = aws.String(userData) createLaunchConfigurationOpts.UserData = aws.String(userData)
} }
createLaunchConfigurationOpts.InstanceMonitoring = &autoscaling.InstanceMonitoring{
Enabled: aws.Boolean(d.Get("enable_monitoring").(bool)),
}
if v, ok := d.GetOk("iam_instance_profile"); ok { if v, ok := d.GetOk("iam_instance_profile"); ok {
createLaunchConfigurationOpts.IAMInstanceProfile = aws.String(v.(string)) createLaunchConfigurationOpts.IAMInstanceProfile = aws.String(v.(string))
} }
@ -371,8 +392,25 @@ func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface
createLaunchConfigurationOpts.LaunchConfigurationName = aws.String(lcName) createLaunchConfigurationOpts.LaunchConfigurationName = aws.String(lcName)
log.Printf( log.Printf(
"[DEBUG] autoscaling create launch configuration: %#v", createLaunchConfigurationOpts) "[DEBUG] autoscaling create launch configuration: %s", createLaunchConfigurationOpts)
_, err := autoscalingconn.CreateLaunchConfiguration(&createLaunchConfigurationOpts)
// IAM profiles can take ~10 seconds to propagate in AWS:
// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console
err := resource.Retry(30*time.Second, func() error {
_, err := autoscalingconn.CreateLaunchConfiguration(&createLaunchConfigurationOpts)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Message() == "Invalid IamInstanceProfile" {
return err
}
}
return &resource.RetryError{
Err: err,
}
}
return nil
})
if err != nil { if err != nil {
return fmt.Errorf("Error creating launch configuration: %s", err) return fmt.Errorf("Error creating launch configuration: %s", err)
} }
@ -395,7 +433,7 @@ func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{}
LaunchConfigurationNames: []*string{aws.String(d.Id())}, LaunchConfigurationNames: []*string{aws.String(d.Id())},
} }
log.Printf("[DEBUG] launch configuration describe configuration: %#v", describeOpts) log.Printf("[DEBUG] launch configuration describe configuration: %s", describeOpts)
describConfs, err := autoscalingconn.DescribeLaunchConfigurations(&describeOpts) describConfs, err := autoscalingconn.DescribeLaunchConfigurations(&describeOpts)
if err != nil { if err != nil {
return fmt.Errorf("Error retrieving launch configuration: %s", err) return fmt.Errorf("Error retrieving launch configuration: %s", err)
@ -422,6 +460,7 @@ func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{}
d.Set("iam_instance_profile", lc.IAMInstanceProfile) d.Set("iam_instance_profile", lc.IAMInstanceProfile)
d.Set("ebs_optimized", lc.EBSOptimized) d.Set("ebs_optimized", lc.EBSOptimized)
d.Set("spot_price", lc.SpotPrice) d.Set("spot_price", lc.SpotPrice)
d.Set("enable_monitoring", lc.InstanceMonitoring.Enabled)
d.Set("security_groups", lc.SecurityGroups) d.Set("security_groups", lc.SecurityGroups)
if err := readLCBlockDevices(d, lc, ec2conn); err != nil { if err := readLCBlockDevices(d, lc, ec2conn); err != nil {
@ -440,8 +479,8 @@ func resourceAwsLaunchConfigurationDelete(d *schema.ResourceData, meta interface
LaunchConfigurationName: aws.String(d.Id()), LaunchConfigurationName: aws.String(d.Id()),
}) })
if err != nil { if err != nil {
autoscalingerr, ok := err.(aws.APIError) autoscalingerr, ok := err.(awserr.Error)
if ok && autoscalingerr.Code == "InvalidConfiguration.NotFound" { if ok && autoscalingerr.Code() == "InvalidConfiguration.NotFound" {
return nil return nil
} }
@ -487,6 +526,11 @@ func readBlockDevicesFromLaunchConfiguration(d *schema.ResourceData, lc *autosca
if err != nil { if err != nil {
return nil, err return nil, err
} }
if rootDeviceName == nil {
// We do this so the value is empty so we don't have to do nil checks later
var blank string
rootDeviceName = &blank
}
for _, bdm := range lc.BlockDeviceMappings { for _, bdm := range lc.BlockDeviceMappings {
bd := make(map[string]interface{}) bd := make(map[string]interface{})
if bdm.EBS != nil && bdm.EBS.DeleteOnTermination != nil { if bdm.EBS != nil && bdm.EBS.DeleteOnTermination != nil {

View File

@ -7,12 +7,33 @@ import (
"testing" "testing"
"time" "time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSLaunchConfiguration_basic(t *testing.T) {
var conf autoscaling.LaunchConfiguration
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLaunchConfigurationDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSLaunchConfigurationNoNameConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSLaunchConfigurationExists("aws_launch_configuration.bar", &conf),
testAccCheckAWSLaunchConfigurationGeneratedNamePrefix(
"aws_launch_configuration.bar", "terraform-"),
),
},
},
})
}
func TestAccAWSLaunchConfiguration_withBlockDevices(t *testing.T) { func TestAccAWSLaunchConfiguration_withBlockDevices(t *testing.T) {
var conf autoscaling.LaunchConfiguration var conf autoscaling.LaunchConfiguration
@ -60,26 +81,6 @@ func TestAccAWSLaunchConfiguration_withSpotPrice(t *testing.T) {
}) })
} }
func TestAccAWSLaunchConfiguration_withGeneratedName(t *testing.T) {
var conf autoscaling.LaunchConfiguration
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLaunchConfigurationDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSLaunchConfigurationNoNameConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSLaunchConfigurationExists("aws_launch_configuration.bar", &conf),
testAccCheckAWSLaunchConfigurationGeneratedNamePrefix(
"aws_launch_configuration.bar", "terraform-"),
),
},
},
})
}
func testAccCheckAWSLaunchConfigurationGeneratedNamePrefix( func testAccCheckAWSLaunchConfigurationGeneratedNamePrefix(
resource, prefix string) resource.TestCheckFunc { resource, prefix string) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
@ -119,11 +120,11 @@ func testAccCheckAWSLaunchConfigurationDestroy(s *terraform.State) error {
} }
// Verify the error // Verify the error
providerErr, ok := err.(aws.APIError) providerErr, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
if providerErr.Code != "InvalidLaunchConfiguration.NotFound" { if providerErr.Code() != "InvalidLaunchConfiguration.NotFound" {
return err return err
} }
} }

View File

@ -4,8 +4,9 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/elb" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elb"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -90,7 +91,7 @@ func resourceAwsLBCookieStickinessPolicyRead(d *schema.ResourceData, meta interf
getResp, err := elbconn.DescribeLoadBalancerPolicies(request) getResp, err := elbconn.DescribeLoadBalancerPolicies(request)
if err != nil { if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "PolicyNotFound" { if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "PolicyNotFound" {
// The policy is gone. // The policy is gone.
d.SetId("") d.SetId("")
return nil return nil

View File

@ -4,14 +4,14 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/elb" "github.com/aws/aws-sdk-go/service/elb"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAwsLBCookieStickinessPolicy(t *testing.T) { func TestAccAwsLBCookieStickinessPolicy_basic(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,

View File

@ -4,8 +4,8 @@ import (
"fmt" "fmt"
"log" "log"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )

View File

@ -8,7 +8,7 @@ import (
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccAWSMainRouteTableAssociation(t *testing.T) { func TestAccAWSMainRouteTableAssociation_basic(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,

View File

@ -4,11 +4,13 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"log" "log"
"sort"
"strconv" "strconv"
"time" "time"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
@ -30,10 +32,19 @@ func resourceAwsNetworkAcl() *schema.Resource {
Computed: false, Computed: false,
}, },
"subnet_id": &schema.Schema{ "subnet_id": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
ForceNew: true, ForceNew: true,
Computed: false, Computed: false,
Deprecated: "Attribute subnet_id is deprecated on network_acl resources. Use subnet_ids instead",
},
"subnet_ids": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
ConflictsWith: []string{"subnet_id"},
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
}, },
"ingress": &schema.Schema{ "ingress": &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
@ -65,6 +76,14 @@ func resourceAwsNetworkAcl() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"icmp_type": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"icmp_code": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
}, },
}, },
Set: resourceAwsNetworkAclEntryHash, Set: resourceAwsNetworkAclEntryHash,
@ -99,6 +118,14 @@ func resourceAwsNetworkAcl() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"icmp_type": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"icmp_code": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
}, },
}, },
Set: resourceAwsNetworkAclEntryHash, Set: resourceAwsNetworkAclEntryHash,
@ -166,7 +193,16 @@ func resourceAwsNetworkAclRead(d *schema.ResourceData, meta interface{}) error {
} }
d.Set("vpc_id", networkAcl.VPCID) d.Set("vpc_id", networkAcl.VPCID)
d.Set("tags", tagsToMapSDK(networkAcl.Tags)) d.Set("tags", tagsToMap(networkAcl.Tags))
var s []string
for _, a := range networkAcl.Associations {
s = append(s, *a.SubnetID)
}
sort.Strings(s)
if err := d.Set("subnet_ids", s); err != nil {
return err
}
if err := d.Set("ingress", networkAclEntriesToMapList(ingressEntries)); err != nil { if err := d.Set("ingress", networkAclEntriesToMapList(ingressEntries)); err != nil {
return err return err
@ -213,7 +249,62 @@ func resourceAwsNetworkAclUpdate(d *schema.ResourceData, meta interface{}) error
} }
} }
if err := setTagsSDK(conn, d); err != nil { if d.HasChange("subnet_ids") {
o, n := d.GetChange("subnet_ids")
if o == nil {
o = new(schema.Set)
}
if n == nil {
n = new(schema.Set)
}
os := o.(*schema.Set)
ns := n.(*schema.Set)
remove := os.Difference(ns).List()
add := ns.Difference(os).List()
if len(remove) > 0 {
// A Network ACL is required for each subnet. In order to disassociate a
// subnet from this ACL, we must associate it with the default ACL.
defaultAcl, err := getDefaultNetworkAcl(d.Get("vpc_id").(string), conn)
if err != nil {
return fmt.Errorf("Failed to find Default ACL for VPC %s", d.Get("vpc_id").(string))
}
for _, r := range remove {
association, err := findNetworkAclAssociation(r.(string), conn)
if err != nil {
return fmt.Errorf("Failed to find acl association: acl %s with subnet %s: %s", d.Id(), r, err)
}
_, err = conn.ReplaceNetworkACLAssociation(&ec2.ReplaceNetworkACLAssociationInput{
AssociationID: association.NetworkACLAssociationID,
NetworkACLID: defaultAcl.NetworkACLID,
})
if err != nil {
return err
}
}
}
if len(add) > 0 {
for _, a := range add {
association, err := findNetworkAclAssociation(a.(string), conn)
if err != nil {
return fmt.Errorf("Failed to find acl association: acl %s with subnet %s: %s", d.Id(), a, err)
}
_, err = conn.ReplaceNetworkACLAssociation(&ec2.ReplaceNetworkACLAssociationInput{
AssociationID: association.NetworkACLAssociationID,
NetworkACLID: aws.String(d.Id()),
})
if err != nil {
return err
}
}
}
}
if err := setTags(conn, d); err != nil {
return err return err
} else { } else {
d.SetPartial("tags") d.SetPartial("tags")
@ -302,9 +393,10 @@ func updateNetworkAclEntries(d *schema.ResourceData, entryType string, conn *ec2
Protocol: add.Protocol, Protocol: add.Protocol,
RuleAction: add.RuleAction, RuleAction: add.RuleAction,
RuleNumber: add.RuleNumber, RuleNumber: add.RuleNumber,
ICMPTypeCode: add.ICMPTypeCode,
}) })
if connErr != nil { if connErr != nil {
return fmt.Errorf("Error creating %s entry: %s", entryType, err) return fmt.Errorf("Error creating %s entry: %s", entryType, connErr)
} }
} }
return nil return nil
@ -319,25 +411,42 @@ func resourceAwsNetworkAclDelete(d *schema.ResourceData, meta interface{}) error
NetworkACLID: aws.String(d.Id()), NetworkACLID: aws.String(d.Id()),
}) })
if err != nil { if err != nil {
ec2err := err.(aws.APIError) ec2err := err.(awserr.Error)
switch ec2err.Code { switch ec2err.Code() {
case "InvalidNetworkAclID.NotFound": case "InvalidNetworkAclID.NotFound":
return nil return nil
case "DependencyViolation": case "DependencyViolation":
// In case of dependency violation, we remove the association between subnet and network acl. // In case of dependency violation, we remove the association between subnet and network acl.
// This means the subnet is attached to default acl of vpc. // This means the subnet is attached to default acl of vpc.
association, err := findNetworkAclAssociation(d.Get("subnet_id").(string), conn) var associations []*ec2.NetworkACLAssociation
if err != nil { if v, ok := d.GetOk("subnet_id"); ok {
return resource.RetryError{Err: fmt.Errorf("Dependency violation: Cannot delete acl %s: %s", d.Id(), err)}
a, err := findNetworkAclAssociation(v.(string), conn)
if err != nil {
return resource.RetryError{Err: fmt.Errorf("Dependency violation: Cannot find ACL %s: %s", d.Id(), err)}
}
associations = append(associations, a)
} else if v, ok := d.GetOk("subnet_ids"); ok {
ids := v.(*schema.Set).List()
for _, i := range ids {
a, err := findNetworkAclAssociation(i.(string), conn)
if err != nil {
return resource.RetryError{Err: fmt.Errorf("Dependency violation: Cannot delete acl %s: %s", d.Id(), err)}
}
associations = append(associations, a)
}
} }
defaultAcl, err := getDefaultNetworkAcl(d.Get("vpc_id").(string), conn) defaultAcl, err := getDefaultNetworkAcl(d.Get("vpc_id").(string), conn)
if err != nil { if err != nil {
return resource.RetryError{Err: fmt.Errorf("Dependency violation: Cannot delete acl %s: %s", d.Id(), err)} return resource.RetryError{Err: fmt.Errorf("Dependency violation: Cannot delete acl %s: %s", d.Id(), err)}
} }
_, err = conn.ReplaceNetworkACLAssociation(&ec2.ReplaceNetworkACLAssociationInput{
AssociationID: association.NetworkACLAssociationID, for _, a := range associations {
NetworkACLID: defaultAcl.NetworkACLID, _, err = conn.ReplaceNetworkACLAssociation(&ec2.ReplaceNetworkACLAssociationInput{
}) AssociationID: a.NetworkACLAssociationID,
NetworkACLID: defaultAcl.NetworkACLID,
})
}
return resource.RetryError{Err: err} return resource.RetryError{Err: err}
default: default:
// Any other error, we want to quit the retry loop immediately // Any other error, we want to quit the retry loop immediately
@ -374,6 +483,13 @@ func resourceAwsNetworkAclEntryHash(v interface{}) int {
buf.WriteString(fmt.Sprintf("%s-", v.(string))) buf.WriteString(fmt.Sprintf("%s-", v.(string)))
} }
if v, ok := m["icmp_type"]; ok {
buf.WriteString(fmt.Sprintf("%d-", v.(int)))
}
if v, ok := m["icmp_code"]; ok {
buf.WriteString(fmt.Sprintf("%d-", v.(int)))
}
return hashcode.String(buf.String()) return hashcode.String(buf.String())
} }
@ -417,7 +533,7 @@ func findNetworkAclAssociation(subnetId string, conn *ec2.EC2) (networkAclAssoci
} }
} }
} }
return nil, fmt.Errorf("could not find association for subnet %s ", subnetId) return nil, fmt.Errorf("could not find association for subnet: %s ", subnetId)
} }
// networkAclEntriesToMapList turns ingress/egress rules read from AWS into a list // networkAclEntriesToMapList turns ingress/egress rules read from AWS into a list
@ -446,6 +562,11 @@ func networkAclEntriesToMapList(networkAcls []*ec2.NetworkACLEntry) []map[string
acl["to_port"] = *entry.PortRange.To acl["to_port"] = *entry.PortRange.To
} }
if entry.ICMPTypeCode != nil {
acl["icmp_type"] = *entry.ICMPTypeCode.Type
acl["icmp_code"] = *entry.ICMPTypeCode.Code
}
result = append(result, acl) result = append(result, acl)
} }

View File

@ -4,8 +4,9 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/awslabs/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -23,36 +24,36 @@ func TestAccAWSNetworkAcl_EgressAndIngressRules(t *testing.T) {
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckAWSNetworkAclExists("aws_network_acl.bar", &networkAcl), testAccCheckAWSNetworkAclExists("aws_network_acl.bar", &networkAcl),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.bar", "ingress.1216169466.protocol", "6"), "aws_network_acl.bar", "ingress.109047673.protocol", "6"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.bar", "ingress.1216169466.rule_no", "1"), "aws_network_acl.bar", "ingress.109047673.rule_no", "1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.bar", "ingress.1216169466.from_port", "80"), "aws_network_acl.bar", "ingress.109047673.from_port", "80"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.bar", "ingress.1216169466.to_port", "80"), "aws_network_acl.bar", "ingress.109047673.to_port", "80"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.bar", "ingress.1216169466.action", "allow"), "aws_network_acl.bar", "ingress.109047673.action", "allow"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.bar", "ingress.1216169466.cidr_block", "10.3.0.0/18"), "aws_network_acl.bar", "ingress.109047673.cidr_block", "10.3.0.0/18"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.bar", "egress.2634340476.protocol", "6"), "aws_network_acl.bar", "egress.868403673.protocol", "6"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.bar", "egress.2634340476.rule_no", "2"), "aws_network_acl.bar", "egress.868403673.rule_no", "2"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.bar", "egress.2634340476.from_port", "443"), "aws_network_acl.bar", "egress.868403673.from_port", "443"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.bar", "egress.2634340476.to_port", "443"), "aws_network_acl.bar", "egress.868403673.to_port", "443"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.bar", "egress.2634340476.cidr_block", "10.3.0.0/18"), "aws_network_acl.bar", "egress.868403673.cidr_block", "10.3.0.0/18"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.bar", "egress.2634340476.action", "allow"), "aws_network_acl.bar", "egress.868403673.action", "allow"),
), ),
}, },
}, },
}) })
} }
func TestAccAWSNetworkAcl_OnlyIngressRules(t *testing.T) { func TestAccAWSNetworkAcl_OnlyIngressRules_basic(t *testing.T) {
var networkAcl ec2.NetworkACL var networkAcl ec2.NetworkACL
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
@ -66,24 +67,24 @@ func TestAccAWSNetworkAcl_OnlyIngressRules(t *testing.T) {
testAccCheckAWSNetworkAclExists("aws_network_acl.foos", &networkAcl), testAccCheckAWSNetworkAclExists("aws_network_acl.foos", &networkAcl),
// testAccCheckSubnetAssociation("aws_network_acl.foos", "aws_subnet.blob"), // testAccCheckSubnetAssociation("aws_network_acl.foos", "aws_subnet.blob"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.protocol", "6"), "aws_network_acl.foos", "ingress.1451312565.protocol", "6"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.rule_no", "2"), "aws_network_acl.foos", "ingress.1451312565.rule_no", "2"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.from_port", "443"), "aws_network_acl.foos", "ingress.1451312565.from_port", "443"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.to_port", "443"), "aws_network_acl.foos", "ingress.1451312565.to_port", "443"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.action", "deny"), "aws_network_acl.foos", "ingress.1451312565.action", "deny"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.cidr_block", "10.2.0.0/18"), "aws_network_acl.foos", "ingress.1451312565.cidr_block", "10.2.0.0/18"),
), ),
}, },
}, },
}) })
} }
func TestAccAWSNetworkAcl_OnlyIngressRulesChange(t *testing.T) { func TestAccAWSNetworkAcl_OnlyIngressRules_update(t *testing.T) {
var networkAcl ec2.NetworkACL var networkAcl ec2.NetworkACL
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
@ -97,21 +98,21 @@ func TestAccAWSNetworkAcl_OnlyIngressRulesChange(t *testing.T) {
testAccCheckAWSNetworkAclExists("aws_network_acl.foos", &networkAcl), testAccCheckAWSNetworkAclExists("aws_network_acl.foos", &networkAcl),
testIngressRuleLength(&networkAcl, 2), testIngressRuleLength(&networkAcl, 2),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.protocol", "6"), "aws_network_acl.foos", "ingress.2048097841.protocol", "6"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.rule_no", "1"), "aws_network_acl.foos", "ingress.2048097841.rule_no", "1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.from_port", "0"), "aws_network_acl.foos", "ingress.2048097841.from_port", "0"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.to_port", "22"), "aws_network_acl.foos", "ingress.2048097841.to_port", "22"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.action", "deny"), "aws_network_acl.foos", "ingress.2048097841.action", "deny"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.cidr_block", "10.2.0.0/18"), "aws_network_acl.foos", "ingress.1451312565.cidr_block", "10.2.0.0/18"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.from_port", "443"), "aws_network_acl.foos", "ingress.1451312565.from_port", "443"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.rule_no", "2"), "aws_network_acl.foos", "ingress.1451312565.rule_no", "2"),
), ),
}, },
resource.TestStep{ resource.TestStep{
@ -120,17 +121,17 @@ func TestAccAWSNetworkAcl_OnlyIngressRulesChange(t *testing.T) {
testAccCheckAWSNetworkAclExists("aws_network_acl.foos", &networkAcl), testAccCheckAWSNetworkAclExists("aws_network_acl.foos", &networkAcl),
testIngressRuleLength(&networkAcl, 1), testIngressRuleLength(&networkAcl, 1),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.protocol", "6"), "aws_network_acl.foos", "ingress.2048097841.protocol", "6"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.rule_no", "1"), "aws_network_acl.foos", "ingress.2048097841.rule_no", "1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.from_port", "0"), "aws_network_acl.foos", "ingress.2048097841.from_port", "0"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.to_port", "22"), "aws_network_acl.foos", "ingress.2048097841.to_port", "22"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.action", "deny"), "aws_network_acl.foos", "ingress.2048097841.action", "deny"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.cidr_block", "10.2.0.0/18"), "aws_network_acl.foos", "ingress.2048097841.cidr_block", "10.2.0.0/18"),
), ),
}, },
}, },
@ -149,7 +150,7 @@ func TestAccAWSNetworkAcl_OnlyEgressRules(t *testing.T) {
Config: testAccAWSNetworkAclEgressConfig, Config: testAccAWSNetworkAclEgressConfig,
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckAWSNetworkAclExists("aws_network_acl.bond", &networkAcl), testAccCheckAWSNetworkAclExists("aws_network_acl.bond", &networkAcl),
testAccCheckTagsSDK(&networkAcl.Tags, "foo", "bar"), testAccCheckTags(&networkAcl.Tags, "foo", "bar"),
), ),
}, },
}, },
@ -181,6 +182,49 @@ func TestAccAWSNetworkAcl_SubnetChange(t *testing.T) {
} }
func TestAccAWSNetworkAcl_Subnets(t *testing.T) {
var networkAcl ec2.NetworkACL
checkACLSubnets := func(acl *ec2.NetworkACL, count int) resource.TestCheckFunc {
return func(*terraform.State) (err error) {
if count != len(acl.Associations) {
return fmt.Errorf("ACL association count does not match, expected %d, got %d", count, len(acl.Associations))
}
return nil
}
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSNetworkAclDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSNetworkAclSubnet_SubnetIds,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSNetworkAclExists("aws_network_acl.bar", &networkAcl),
testAccCheckSubnetIsAssociatedWithAcl("aws_network_acl.bar", "aws_subnet.one"),
testAccCheckSubnetIsAssociatedWithAcl("aws_network_acl.bar", "aws_subnet.two"),
checkACLSubnets(&networkAcl, 2),
),
},
resource.TestStep{
Config: testAccAWSNetworkAclSubnet_SubnetIdsUpdate,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSNetworkAclExists("aws_network_acl.bar", &networkAcl),
testAccCheckSubnetIsAssociatedWithAcl("aws_network_acl.bar", "aws_subnet.one"),
testAccCheckSubnetIsAssociatedWithAcl("aws_network_acl.bar", "aws_subnet.three"),
testAccCheckSubnetIsAssociatedWithAcl("aws_network_acl.bar", "aws_subnet.four"),
checkACLSubnets(&networkAcl, 3),
),
},
},
})
}
func testAccCheckAWSNetworkAclDestroy(s *terraform.State) error { func testAccCheckAWSNetworkAclDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn conn := testAccProvider.Meta().(*AWSClient).ec2conn
@ -201,12 +245,12 @@ func testAccCheckAWSNetworkAclDestroy(s *terraform.State) error {
return nil return nil
} }
ec2err, ok := err.(aws.APIError) ec2err, ok := err.(awserr.Error)
if !ok { if !ok {
return err return err
} }
// Confirm error code is what we want // Confirm error code is what we want
if ec2err.Code != "InvalidNetworkAclID.NotFound" { if ec2err.Code() != "InvalidNetworkAclID.NotFound" {
return err return err
} }
} }
@ -281,10 +325,6 @@ func testAccCheckSubnetIsAssociatedWithAcl(acl string, sub string) resource.Test
return nil return nil
} }
// r, _ := conn.NetworkACLs([]string{}, ec2.NewFilter())
// fmt.Printf("\n\nall acls\n %#v\n\n", r.NetworkAcls)
// conn.NetworkAcls([]string{}, filter)
return fmt.Errorf("Network Acl %s is not associated with subnet %s", acl, sub) return fmt.Errorf("Network Acl %s is not associated with subnet %s", acl, sub)
} }
} }
@ -494,3 +534,58 @@ resource "aws_network_acl" "bar" {
subnet_id = "${aws_subnet.new.id}" subnet_id = "${aws_subnet.new.id}"
} }
` `
const testAccAWSNetworkAclSubnet_SubnetIds = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
tags {
Name = "acl-subnets-test"
}
}
resource "aws_subnet" "one" {
cidr_block = "10.1.111.0/24"
vpc_id = "${aws_vpc.foo.id}"
}
resource "aws_subnet" "two" {
cidr_block = "10.1.1.0/24"
vpc_id = "${aws_vpc.foo.id}"
}
resource "aws_network_acl" "bar" {
vpc_id = "${aws_vpc.foo.id}"
subnet_ids = ["${aws_subnet.one.id}", "${aws_subnet.two.id}"]
}
`
const testAccAWSNetworkAclSubnet_SubnetIdsUpdate = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
tags {
Name = "acl-subnets-test"
}
}
resource "aws_subnet" "one" {
cidr_block = "10.1.111.0/24"
vpc_id = "${aws_vpc.foo.id}"
}
resource "aws_subnet" "two" {
cidr_block = "10.1.1.0/24"
vpc_id = "${aws_vpc.foo.id}"
}
resource "aws_subnet" "three" {
cidr_block = "10.1.222.0/24"
vpc_id = "${aws_vpc.foo.id}"
}
resource "aws_subnet" "four" {
cidr_block = "10.1.4.0/24"
vpc_id = "${aws_vpc.foo.id}"
}
resource "aws_network_acl" "bar" {
vpc_id = "${aws_vpc.foo.id}"
subnet_ids = [
"${aws_subnet.one.id}",
"${aws_subnet.three.id}",
"${aws_subnet.four.id}",
]
}
`

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