Merge branch 'master' of github.com:reverbdotcom/terraform into adding-rds-snapshots

* 'master' of github.com:reverbdotcom/terraform: (524 commits)
  docs: tweaks to RELEASING
  Minor change to docs
  Update CHANGELOG.md
  Update DynamoDB example docs to remove non-key attributes; update test to remove non-key attribute from attribute set to prevent infinite planning loops
  Update CHANGELOG.md
  use /usr/bin/env bash
  provider/aws: fix go vet
  provider/aws: ignore providers with Meta nil
  update CHANGELOG
  provider/aws: Code cleanups for Spot Requests
  provider/aws: fix db_subnet acc test
  Fixing the tests
  Fixes issue #2568
  Update CHANGELOG.md
  Update CHANGELOG.md
  fixes typo
  Fixed void Azure network config bug.
  provider/aws: ecs task definition is deregistered correctly
  provider/azure: fixup storage service test
  provider/docker: [tests] change images
  ...
This commit is contained in:
Adam Enger 2015-06-30 15:26:23 -05:00
commit ca8736d29f
513 changed files with 26683 additions and 1959 deletions

View File

@ -1,17 +1,184 @@
## 0.5.3 (Unreleased)
## 0.6.0 (Unreleased)
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: New `force_destroy` parameter for S3 buckets, to destroy
* 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]
@ -122,7 +289,7 @@ IMPROVEMENTS:
* **New resource: `google_dns_record_set`**
* **Migrate to upstream AWS SDK:** Migrate the AWS provider to
[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
migrated back to the upstream version.
* core: Improve error message on diff mismatch [GH-1501]
@ -150,7 +317,7 @@ IMPROVEMENTS:
* provider/aws: `aws_network_acl` improved validation for network ACL ports
and protocols [GH-1798] [GH-1808]
* 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_zone` exports nameservers [GH-1525]
* provider/aws: `aws_s3_bucket` website support [GH-1738]
@ -307,7 +474,7 @@ FEATURES:
* **Math operations** in interpolations. You can now do things like
`${count.index+1}`. [GH-1068]
* **New AWS SDK:** Move to `aws-sdk-go` (hashicorp/aws-sdk-go),
a fork of the offical `awslabs` repo. We forked for stability while
a fork of the official `awslabs` repo. We forked for stability while
`awslabs` refactored the library, and will move back to the officially
supported version in the next release.
@ -336,7 +503,7 @@ IMPROVEMENTS:
* providers/aws: Improve dependency violation error handling, when deleting
Internet Gateways or Auto Scaling groups [GH-1325].
* provider/aws: Add non-destructive updates to AWS RDS. You can now upgrade
`egine_version`, `parameter_group_name`, and `multi_az` without forcing
`engine_version`, `parameter_group_name`, and `multi_az` without forcing
a new database to be created.[GH-1341]
* providers/aws: Full support for block device mappings on instances and
launch configurations [GH-1045, GH-1364]

View File

@ -15,6 +15,10 @@ dev: generate
quickdev: generate
@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: generate
TF_ACC= go test $(TEST) $(TESTARGS) -timeout=30s -parallel=4
@ -23,10 +27,11 @@ test: generate
# testacc runs acceptance tests
testacc: generate
@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; \
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: generate
@ -61,7 +66,8 @@ vet:
@go tool vet $(VETARGS) . ; if [ $$? -eq 1 ]; then \
echo ""; \
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
# 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.
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/)
- [Mercurial](http://mercurial.selenic.com/)
@ -74,7 +74,7 @@ To run the acceptance tests, invoke `make testacc`:
```sh
$ make testacc TEST=./builtin/providers/aws TESTARGS='-run=Vpc'
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
2015/02/10 14:11:17 [INFO] Test: Using us-west-2 as test region
[...]

83
RELEASING.md Normal file
View File

@ -0,0 +1,83 @@
# 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
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"
$script = <<SCRIPT
# Install Go and prerequisites
apt-get -qq update
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
SRCROOT="/opt/go"
SRCPATH="/opt/gopath"
# Setup the GOPATH
mkdir -p /opt/gopath
cat <<EOF >/etc/profile.d/gopath.sh
export GOPATH="/opt/gopath"
export PATH="/opt/go/bin:\$GOPATH/bin:\$PATH"
# Get the ARCH
ARCH=`uname -m | sed 's|i686|386|' | sed 's|x86_64|amd64|'`
# Install Prereq Packages
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
# Make sure the GOPATH is usable by vagrant
chown -R vagrant:vagrant /opt/go
chown -R vagrant:vagrant /opt/gopath
sudo mv /tmp/gopath.sh /etc/profile.d/gopath.sh
sudo chmod 0755 /etc/profile.d/gopath.sh
source /etc/profile.d/gopath.sh
SCRIPT
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
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|
config.vm.provider "p" do |v|
v.vmx["memsize"] = "2048"
v.vmx["numvcpus"] = "2"
v.vmx["cpuid.coresPerSocket"] = "1"
config.vm.provider p do |v|
v.vmx["memsize"] = "4096"
v.vmx["numvcpus"] = "4"
v.vmx['cpuid.coresPerSocket'] = '2'
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

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

View File

@ -5,7 +5,7 @@ import (
"reflect"
"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/terraform"
)

View File

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

View File

@ -5,8 +5,8 @@ import (
"net"
"strconv"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
)
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))),
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)
}
return entries, nil
@ -60,6 +72,9 @@ func flattenNetworkAclEntries(list []*ec2.NetworkACLEntry) []map[string]interfac
func protocolIntegers() map[string]int {
var protocolIntegers = make(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,
"tcp": 6,
"icmp": 1,

View File

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

View File

@ -1,6 +1,11 @@
package aws
import (
"net"
"sync"
"time"
"github.com/awslabs/aws-sdk-go/aws/credentials"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
"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 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{
Schema: map[string]*schema.Schema{
"access_key": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
"AWS_ACCESS_KEY",
"AWS_ACCESS_KEY_ID",
}, nil),
DefaultFunc: func() (interface{}, error) {
return getCredDefault(nil, func() string {
return credVal.AccessKeyID
})
},
Description: descriptions["access_key"],
},
"secret_key": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
"AWS_SECRET_KEY",
"AWS_SECRET_ACCESS_KEY",
}, nil),
DefaultFunc: func() (interface{}, error) {
return getCredDefault(nil, func() string {
return credVal.SecretAccessKey
})
},
Description: descriptions["secret_key"],
},
"token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
"AWS_SESSION_TOKEN",
"AWS_SECURITY_TOKEN",
}, ""),
DefaultFunc: func() (interface{}, error) {
return getCredDefault("", func() string {
return credVal.SessionToken
})
},
Description: descriptions["token"],
},
@ -85,45 +150,65 @@ func Provider() terraform.ResourceProvider {
ResourcesMap: map[string]*schema.Resource{
"aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(),
"aws_autoscaling_group": resourceAwsAutoscalingGroup(),
"aws_autoscaling_notification": resourceAwsAutoscalingNotification(),
"aws_autoscaling_policy": resourceAwsAutoscalingPolicy(),
"aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(),
"aws_customer_gateway": resourceAwsCustomerGateway(),
"aws_db_instance": resourceAwsDbInstance(),
"aws_db_parameter_group": resourceAwsDbParameterGroup(),
"aws_db_security_group": resourceAwsDbSecurityGroup(),
"aws_db_subnet_group": resourceAwsDbSubnetGroup(),
"aws_dynamodb_table": resourceAwsDynamoDbTable(),
"aws_ebs_volume": resourceAwsEbsVolume(),
"aws_ecs_cluster": resourceAwsEcsCluster(),
"aws_ecs_service": resourceAwsEcsService(),
"aws_ecs_task_definition": resourceAwsEcsTaskDefinition(),
"aws_eip": resourceAwsEip(),
"aws_elasticache_cluster": resourceAwsElasticacheCluster(),
"aws_elasticache_parameter_group": resourceAwsElasticacheParameterGroup(),
"aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(),
"aws_elasticache_subnet_group": resourceAwsElasticacheSubnetGroup(),
"aws_elb": resourceAwsElb(),
"aws_flow_log": resourceAwsFlowLog(),
"aws_iam_access_key": resourceAwsIamAccessKey(),
"aws_iam_group_policy": resourceAwsIamGroupPolicy(),
"aws_iam_group": resourceAwsIamGroup(),
"aws_iam_group_membership": resourceAwsIamGroupMembership(),
"aws_iam_instance_profile": resourceAwsIamInstanceProfile(),
"aws_iam_policy": resourceAwsIamPolicy(),
"aws_iam_policy_attachment": resourceAwsIamPolicyAttachment(),
"aws_iam_role_policy": resourceAwsIamRolePolicy(),
"aws_iam_role": resourceAwsIamRole(),
"aws_iam_server_certificate": resourceAwsIAMServerCertificate(),
"aws_iam_user_policy": resourceAwsIamUserPolicy(),
"aws_iam_user": resourceAwsIamUser(),
"aws_instance": resourceAwsInstance(),
"aws_internet_gateway": resourceAwsInternetGateway(),
"aws_key_pair": resourceAwsKeyPair(),
"aws_kinesis_stream": resourceAwsKinesisStream(),
"aws_lambda_function": resourceAwsLambdaFunction(),
"aws_launch_configuration": resourceAwsLaunchConfiguration(),
"aws_lb_cookie_stickiness_policy": resourceAwsLBCookieStickinessPolicy(),
"aws_main_route_table_association": resourceAwsMainRouteTableAssociation(),
"aws_network_acl": resourceAwsNetworkAcl(),
"aws_network_interface": resourceAwsNetworkInterface(),
"aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(),
"aws_route53_delegation_set": resourceAwsRoute53DelegationSet(),
"aws_route53_record": resourceAwsRoute53Record(),
"aws_route53_zone_association": resourceAwsRoute53ZoneAssociation(),
"aws_route53_zone": resourceAwsRoute53Zone(),
"aws_route_table_association": resourceAwsRouteTableAssociation(),
"aws_route53_health_check": resourceAwsRoute53HealthCheck(),
"aws_route_table": resourceAwsRouteTable(),
"aws_route_table_association": resourceAwsRouteTableAssociation(),
"aws_s3_bucket": resourceAwsS3Bucket(),
"aws_security_group": resourceAwsSecurityGroup(),
"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_volume_attachment": resourceAwsVolumeAttachment(),
"aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(),
"aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(),
"aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(),

View File

@ -4,9 +4,9 @@ import (
"fmt"
"strings"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/elb"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elb"
"github.com/hashicorp/terraform/helper/schema"
)

View File

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

View File

@ -9,10 +9,10 @@ import (
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/autoscaling"
"github.com/awslabs/aws-sdk-go/service/elb"
"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/aws/aws-sdk-go/service/elb"
)
func resourceAwsAutoscalingGroup() *schema.Resource {
@ -27,6 +27,15 @@ func resourceAwsAutoscalingGroup() *schema.Resource {
Type: schema.TypeString,
Required: 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{
@ -59,7 +68,6 @@ func resourceAwsAutoscalingGroup() *schema.Resource {
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
"force_delete": &schema.Schema{
@ -79,7 +87,6 @@ func resourceAwsAutoscalingGroup() *schema.Resource {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"availability_zones": &schema.Schema{
@ -93,7 +100,6 @@ func resourceAwsAutoscalingGroup() *schema.Resource {
"load_balancers": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
@ -221,6 +227,10 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{})
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") {
opts.DesiredCapacity = aws.Long(int64(d.Get("desired_capacity").(int)))
}
@ -241,6 +251,11 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{})
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 err := setAutoscalingTags(conn, d); err != nil {
return err
} else {
@ -254,6 +269,42 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{})
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)
}
@ -323,7 +374,7 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{})
func getAwsAutoscalingGroup(
d *schema.ResourceData,
meta interface{}) (*autoscaling.AutoScalingGroup, error) {
meta interface{}) (*autoscaling.Group, error) {
conn := meta.(*AWSClient).autoscalingconn
describeOpts := autoscaling.DescribeAutoScalingGroupsInput{
@ -404,7 +455,7 @@ func waitForASGCapacity(d *schema.ResourceData, meta interface{}) error {
}
wantELB := d.Get("min_elb_capacity").(int)
log.Printf("[DEBUG] Wanting for capacity: %d ASG, %d ELB", wantASG, wantELB)
log.Printf("[DEBUG] Waiting for capacity: %d ASG, %d ELB", wantASG, wantELB)
return resource.Retry(waitForASGCapacityTimeout, func() error {
g, err := getAwsAutoscalingGroup(d, meta)
@ -458,7 +509,7 @@ func waitForASGCapacity(d *schema.ResourceData, meta interface{}) error {
return nil
}
return fmt.Errorf("Still need to wait for more healthy instances.")
return fmt.Errorf("Still need to wait for more healthy instances. This could mean instances failed to launch. See Scaling History for more information.")
})
}
@ -466,7 +517,7 @@ func waitForASGCapacity(d *schema.ResourceData, meta interface{}) error {
// provided ASG.
//
// Nested like: lbName -> instanceId -> instanceState
func getLBInstanceStates(g *autoscaling.AutoScalingGroup, meta interface{}) (map[string]map[string]string, error) {
func getLBInstanceStates(g *autoscaling.Group, meta interface{}) (map[string]map[string]string, error) {
lbInstanceStates := make(map[string]map[string]string)
elbconn := meta.(*AWSClient).elbconn

View File

@ -6,15 +6,15 @@ import (
"strings"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/autoscaling"
"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/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSAutoScalingGroup_basic(t *testing.T) {
var group autoscaling.AutoScalingGroup
var group autoscaling.Group
var lc autoscaling.LaunchConfiguration
resource.Test(t, resource.TestCase{
@ -68,7 +68,7 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) {
}
func TestAccAWSAutoScalingGroup_tags(t *testing.T) {
var group autoscaling.AutoScalingGroup
var group autoscaling.Group
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@ -102,7 +102,7 @@ func TestAccAWSAutoScalingGroup_tags(t *testing.T) {
}
func TestAccAWSAutoScalingGroup_WithLoadBalancer(t *testing.T) {
var group autoscaling.AutoScalingGroup
var group autoscaling.Group
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@ -154,7 +154,7 @@ func testAccCheckAWSAutoScalingGroupDestroy(s *terraform.State) error {
return nil
}
func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.AutoScalingGroup) resource.TestCheckFunc {
func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.Group) resource.TestCheckFunc {
return func(s *terraform.State) error {
if *group.AvailabilityZones[0] != "us-west-2a" {
return fmt.Errorf("Bad availability_zones: %#v", group.AvailabilityZones[0])
@ -207,7 +207,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 {
if *group.LoadBalancerNames[0] != "foobar-terraform-test" {
return fmt.Errorf("Bad load_balancers: %#v", group.LoadBalancerNames[0])
@ -217,7 +217,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 {
rs, ok := s.RootModule().Resources[n]
if !ok {
@ -266,7 +266,7 @@ func testLaunchConfigurationName(n string, lc *autoscaling.LaunchConfiguration)
}
func testAccCheckAWSAutoScalingGroupHealthyCapacity(
g *autoscaling.AutoScalingGroup, exp int) resource.TestCheckFunc {
g *autoscaling.Group, exp int) resource.TestCheckFunc {
return func(s *terraform.State) error {
healthy := 0
for _, i := range g.Instances {
@ -343,9 +343,42 @@ resource "aws_autoscaling_group" "bar" {
`
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" {
name = "foobar-terraform-test"
availability_zones = ["us-west-2a"]
subnets = ["${aws_subnet.foo.id}"]
security_groups = ["${aws_security_group.foo.id}"]
listener {
instance_port = 80
@ -361,6 +394,8 @@ resource "aws_elb" "bar" {
interval = 5
timeout = 2
}
depends_on = ["aws_internet_gateway.gw"]
}
resource "aws_launch_configuration" "foobar" {
@ -368,16 +403,18 @@ resource "aws_launch_configuration" "foobar" {
// 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" {
availability_zones = ["us-west-2a"]
availability_zones = ["${aws_subnet.foo.availability_zone}"]
vpc_zone_identifier = ["${aws_subnet.foo.id}"]
name = "foobar3-terraform-test"
max_size = 2
min_size = 2
health_check_grace_period = 300
health_check_type = "ELB"
min_elb_capacity = 1
min_elb_capacity = 2
force_delete = true
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,9 +5,9 @@ import (
"log"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"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"

View File

@ -4,14 +4,14 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2"
"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 TestAccCustomerGateway(t *testing.T) {
func TestAccCustomerGateway_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,

View File

@ -3,13 +3,14 @@ package aws
import (
"fmt"
"log"
"regexp"
"strings"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"github.com/awslabs/aws-sdk-go/service/rds"
"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/aws/aws-sdk-go/service/rds"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
@ -73,6 +74,26 @@ func resourceAwsDbInstance() *schema.Resource {
Type: schema.TypeString,
Required: 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"))
}
return
},
},
"instance_class": &schema.Schema{
@ -153,6 +174,20 @@ func resourceAwsDbInstance() *schema.Resource {
"final_snapshot_identifier": &schema.Schema{
Type: schema.TypeString,
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{

View File

@ -9,12 +9,12 @@ import (
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/aws"
"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
resource.Test(t, resource.TestCase{

View File

@ -11,9 +11,9 @@ import (
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/rds"
)
func resourceAwsDbParameterGroup() *schema.Resource {

View File

@ -4,14 +4,14 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/aws"
"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/terraform"
)
func TestAccAWSDBParameterGroup(t *testing.T) {
func TestAccAWSDBParameterGroup_basic(t *testing.T) {
var v rds.DBParameterGroup
resource.Test(t, resource.TestCase{

View File

@ -6,9 +6,9 @@ import (
"log"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/aws"
"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/multierror"
"github.com/hashicorp/terraform/helper/resource"

View File

@ -4,14 +4,14 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/aws"
"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/terraform"
)
func TestAccAWSDBSecurityGroup(t *testing.T) {
func TestAccAWSDBSecurityGroup_basic(t *testing.T) {
var v rds.DBSecurityGroup
resource.Test(t, resource.TestCase{

View File

@ -3,12 +3,13 @@ package aws
import (
"fmt"
"log"
"regexp"
"strings"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/aws"
"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/schema"
)
@ -24,6 +25,22 @@ func resourceAwsDbSubnetGroup() *schema.Resource {
Type: schema.TypeString,
ForceNew: 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 and hyphens 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{

View File

@ -7,12 +7,12 @@ import (
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/aws"
"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
testCheck := func(*terraform.State) error {

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,11 +2,14 @@ package aws
import (
"fmt"
"log"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"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"
)
@ -14,6 +17,7 @@ func resourceAwsEbsVolume() *schema.Resource {
return &schema.Resource{
Create: resourceAwsEbsVolumeCreate,
Read: resourceAwsEbsVolumeRead,
Update: resourceAWSEbsVolumeUpdate,
Delete: resourceAwsEbsVolumeDelete,
Schema: map[string]*schema.Schema{
@ -58,6 +62,7 @@ func resourceAwsEbsVolume() *schema.Resource {
Computed: true,
ForceNew: true,
},
"tags": tagsSchema(),
},
}
}
@ -91,9 +96,69 @@ func resourceAwsEbsVolumeCreate(d *schema.ResourceData, meta interface{}) error
if err != nil {
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)
}
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 {
conn := meta.(*AWSClient).ec2conn
@ -149,6 +214,9 @@ func readVolume(d *schema.ResourceData, volume *ec2.Volume) error {
if volume.VolumeType != nil {
d.Set("type", *volume.VolumeType)
}
if volume.Tags != nil {
d.Set("tags", tagsToMap(volume.Tags))
}
return nil
}

View File

@ -1,26 +1,88 @@
package aws
import (
"fmt"
"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 TestAccAWSEBSVolume(t *testing.T) {
func TestAccAWSEBSVolume_basic(t *testing.T) {
var v ec2.Volume
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
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 = `
resource "aws_ebs_volume" "test" {
availability_zone = "us-west-2a"
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,80 @@
package aws
import (
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecs"
"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: %#v", 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())
// TODO: Handle ClientException: The Cluster cannot be deleted while Container Instances are active.
// TODO: Handle ClientException: The Cluster cannot be deleted while Services are active.
out, err := conn.DeleteCluster(&ecs.DeleteClusterInput{
Cluster: aws.String(d.Id()),
})
log.Printf("[DEBUG] ECS cluster %s deleted: %#v", d.Id(), out)
return 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,316 @@
package aws
import (
"bytes"
"fmt"
"log"
"regexp"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"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,
},
"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))),
}
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: %#v", loadBalancers)
input.LoadBalancers = loadBalancers
}
if v, ok := d.GetOk("iam_role"); ok {
input.Role = aws.String(v.(string))
}
log.Printf("[DEBUG] Creating ECS service: %#v", 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
}
service := out.Services[0]
log.Printf("[DEBUG] Received ECS service %#v", 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 %#v", 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 %#v", 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,276 @@
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 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"),
),
},
},
})
}
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
}
`

View File

@ -0,0 +1,179 @@
package aws
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"fmt"
"log"
"github.com/aws/aws-sdk-go/aws"
"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: %#v", input)
out, err := conn.RegisterTaskDefinition(&input)
if err != nil {
return err
}
taskDefinition := *out.TaskDefinition
log.Printf("[DEBUG] ECS task definition registered: %#v (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 %#v", 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,9 +6,9 @@ import (
"strings"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"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"
)
@ -222,6 +222,17 @@ func resourceAwsEipDelete(d *schema.ResourceData, meta interface{}) error {
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 {
return err
}

View File

@ -5,14 +5,14 @@ import (
"strings"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"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/terraform"
)
func TestAccAWSEIP_normal(t *testing.T) {
func TestAccAWSEIP_basic(t *testing.T) {
var conf ec2.Address
resource.Test(t, resource.TestCase{

View File

@ -7,10 +7,11 @@ import (
"strings"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/elasticache"
"github.com/awslabs/aws-sdk-go/service/iam"
"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/elasticache"
"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"
@ -32,7 +33,6 @@ func resourceAwsElasticacheCluster() *schema.Resource {
"engine": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"node_type": &schema.Schema{
Type: schema.TypeString,
@ -42,25 +42,21 @@ func resourceAwsElasticacheCluster() *schema.Resource {
"num_cache_nodes": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: true,
},
"parameter_group_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"port": &schema.Schema{
Type: schema.TypeInt,
Default: 11211,
Optional: true,
Required: true,
ForceNew: true,
},
"engine_version": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"subnet_group_name": &schema.Schema{
Type: schema.TypeString,
@ -82,7 +78,6 @@ func resourceAwsElasticacheCluster() *schema.Resource {
Type: schema.TypeSet,
Optional: true,
Computed: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: func(v interface{}) int {
return hashcode.String(v.(string))
@ -111,6 +106,15 @@ func resourceAwsElasticacheCluster() *schema.Resource {
},
"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,
},
},
}
}
@ -123,7 +127,7 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{
numNodes := int64(d.Get("num_cache_nodes").(int)) // 2
engine := d.Get("engine").(string) // memcached
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)
securityNameSet := d.Get("security_group_names").(*schema.Set)
securityIdSet := d.Get("security_group_ids").(*schema.Set)
@ -159,7 +163,7 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{
stateConf := &resource.StateChangeConf{
Pending: pending,
Target: "available",
Refresh: CacheClusterStateRefreshFunc(conn, d.Id(), "available", pending),
Refresh: cacheClusterStateRefreshFunc(conn, d.Id(), "available", pending),
Timeout: 10 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
@ -241,6 +245,59 @@ func resourceAwsElasticacheClusterUpdate(d *schema.ResourceData, meta interface{
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("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)
}
@ -253,7 +310,7 @@ func setCacheNodeData(d *schema.ResourceData, c *elasticache.CacheCluster) error
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: %#v", node)
return fmt.Errorf("Unexpected nil pointer in: %s", awsutil.StringValue(node))
}
cacheNodeData = append(cacheNodeData, map[string]interface{}{
"id": *node.CacheNodeID,
@ -289,7 +346,7 @@ func resourceAwsElasticacheClusterDelete(d *schema.ResourceData, meta interface{
stateConf := &resource.StateChangeConf{
Pending: []string{"creating", "available", "deleting", "incompatible-parameters", "incompatible-network", "restore-failed"},
Target: "",
Refresh: CacheClusterStateRefreshFunc(conn, d.Id(), "", []string{}),
Refresh: cacheClusterStateRefreshFunc(conn, d.Id(), "", []string{}),
Timeout: 10 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
@ -305,10 +362,11 @@ func resourceAwsElasticacheClusterDelete(d *schema.ResourceData, meta interface{
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) {
resp, err := conn.DescribeCacheClusters(&elasticache.DescribeCacheClustersInput{
CacheClusterID: aws.String(clusterID),
CacheClusterID: aws.String(clusterID),
ShowCacheNodeInfo: aws.Boolean(true),
})
if err != nil {
apierr := err.(awserr.Error)
@ -336,6 +394,18 @@ func CacheClusterStateRefreshFunc(conn *elasticache.ElastiCache, clusterID, give
// return given state if it's not in pending
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
}
log.Printf("[DEBUG] current status: %v", *c.CacheClusterStatus)

View File

@ -6,8 +6,8 @@ import (
"testing"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/elasticache"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/elasticache"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
@ -32,6 +32,7 @@ func TestAccAWSElasticacheCluster_basic(t *testing.T) {
}
func TestAccAWSElasticacheCluster_vpc(t *testing.T) {
var csg elasticache.CacheSubnetGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
@ -40,7 +41,7 @@ func TestAccAWSElasticacheCluster_vpc(t *testing.T) {
resource.TestStep{
Config: testAccAWSElasticacheClusterInVPCConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSElasticacheSubnetGroupExists("aws_elasticache_subnet_group.bar"),
testAccCheckAWSElasticacheSubnetGroupExists("aws_elasticache_subnet_group.bar", &csg),
testAccCheckAWSElasticacheClusterExists("aws_elasticache_cluster.bar"),
),
},
@ -95,7 +96,7 @@ func genRandInt() int {
}
var testAccAWSElasticacheClusterConfig = fmt.Sprintf(`
provider "aws" {
provider "aws" {
region = "us-east-1"
}
resource "aws_security_group" "bar" {
@ -120,6 +121,7 @@ resource "aws_elasticache_cluster" "bar" {
engine = "memcached"
node_type = "cache.m1.small"
num_cache_nodes = 1
port = 11211
parameter_group_name = "default.memcached1.4"
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,9 +5,9 @@ import (
"log"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/elasticache"
"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/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"

View File

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

View File

@ -5,9 +5,9 @@ import (
"log"
"time"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/elasticache"
"github.com/hashicorp/aws-sdk-go/aws"
"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/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
@ -17,13 +17,13 @@ func resourceAwsElasticacheSubnetGroup() *schema.Resource {
return &schema.Resource{
Create: resourceAwsElasticacheSubnetGroupCreate,
Read: resourceAwsElasticacheSubnetGroupRead,
Update: resourceAwsElasticacheSubnetGroupUpdate,
Delete: resourceAwsElasticacheSubnetGroupDelete,
Schema: map[string]*schema.Schema{
"description": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
@ -32,9 +32,7 @@ func resourceAwsElasticacheSubnetGroup() *schema.Resource {
},
"subnet_ids": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
ForceNew: true,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: func(v interface{}) int {
return hashcode.String(v.(string))
@ -110,6 +108,29 @@ func resourceAwsElasticacheSubnetGroupRead(d *schema.ResourceData, meta interfac
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 {
conn := meta.(*AWSClient).elasticacheconn

View File

@ -4,22 +4,56 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/elasticache"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/elasticache"
"github.com/hashicorp/terraform/helper/resource"
"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{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSElasticacheSubnetGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSElasticacheSubnetGroupConfig,
Config: config,
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
}
func testAccCheckAWSElasticacheSubnetGroupExists(n string) resource.TestCheckFunc {
func testAccCheckAWSElasticacheSubnetGroupExists(n string, csg *elasticache.CacheSubnetGroup) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
@ -58,17 +92,47 @@ func testAccCheckAWSElasticacheSubnetGroupExists(n string) resource.TestCheckFun
}
conn := testAccProvider.Meta().(*AWSClient).elasticacheconn
_, err := conn.DescribeCacheSubnetGroups(&elasticache.DescribeCacheSubnetGroupsInput{
resp, err := conn.DescribeCacheSubnetGroups(&elasticache.DescribeCacheSubnetGroupsInput{
CacheSubnetGroupName: aws.String(rs.Primary.ID),
})
if err != nil {
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
}
}
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" {
cidr_block = "192.168.0.0/16"
tags {
@ -90,4 +154,63 @@ resource "aws_elasticache_subnet_group" "bar" {
description = "tf-test-cache-subnet-group-descr"
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,10 +4,12 @@ import (
"bytes"
"fmt"
"log"
"regexp"
"strings"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/elb"
"github.com/aws/aws-sdk-go/aws"
"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/schema"
)
@ -24,6 +26,26 @@ func resourceAwsElb() *schema.Resource {
Type: schema.TypeString,
Required: 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 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{
@ -541,9 +563,11 @@ func resourceAwsElbListenerHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
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("%s-", m["lb_protocol"].(string)))
buf.WriteString(fmt.Sprintf("%s-",
strings.ToLower(m["lb_protocol"].(string))))
if v, ok := m["ssl_certificate_id"]; ok {
buf.WriteString(fmt.Sprintf("%s-", v.(string)))

View File

@ -7,9 +7,9 @@ import (
"sort"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/elb"
"github.com/aws/aws-sdk-go/aws"
"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/terraform"
)
@ -362,6 +362,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 {
conn := testAccProvider.Meta().(*AWSClient).elbconn
@ -513,7 +546,8 @@ resource "aws_elb" "bar" {
instance_port = 8000
instance_protocol = "http"
lb_port = 80
lb_protocol = "http"
// Protocol should be case insensitive
lb_protocol = "HttP"
}
tags {

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,9 +3,9 @@ package aws
import (
"fmt"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"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"
)

View File

@ -4,14 +4,14 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"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/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSAccessKey_normal(t *testing.T) {
func TestAccAWSAccessKey_basic(t *testing.T) {
var conf iam.AccessKeyMetadata
resource.Test(t, resource.TestCase{

View File

@ -3,9 +3,9 @@ package aws
import (
"fmt"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"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"
)

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,9 +5,9 @@ import (
"net/url"
"strings"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"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"
)

View File

@ -4,13 +4,13 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam"
"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 TestAccAWSIAMGroupPolicy(t *testing.T) {
func TestAccAWSIAMGroupPolicy_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,

View File

@ -4,14 +4,14 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"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/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSIAMGroup_normal(t *testing.T) {
func TestAccAWSIAMGroup_basic(t *testing.T) {
var conf iam.GetGroupOutput
resource.Test(t, resource.TestCase{

View File

@ -2,10 +2,11 @@ package aws
import (
"fmt"
"regexp"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"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"
)
@ -34,6 +35,19 @@ func resourceAwsIamInstanceProfile() *schema.Resource {
Type: schema.TypeString,
Required: 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{
Type: schema.TypeString,

View File

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

View File

@ -3,9 +3,9 @@ package aws
import (
"fmt"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"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"
)
@ -59,7 +59,7 @@ func resourceAwsIamPolicyCreate(d *schema.ResourceData, meta interface{}) error
response, err := iamconn.CreatePolicy(request)
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)
@ -78,7 +78,7 @@ func resourceAwsIamPolicyRead(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
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)
@ -101,7 +101,7 @@ func resourceAwsIamPolicyUpdate(d *schema.ResourceData, meta interface{}) error
}
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
}
@ -187,7 +187,7 @@ func iamPolicyDeleteVersion(arn, versionID string, iamconn *iam.IAM) error {
_, err := iamconn.DeletePolicyVersion(request)
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
}
@ -199,7 +199,7 @@ func iamPolicyListVersions(arn string, iamconn *iam.IAM) ([]*iam.PolicyVersion,
response, err := iamconn.ListPolicyVersions(request)
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
}

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,10 +2,11 @@ package aws
import (
"fmt"
"regexp"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"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"
)
@ -31,6 +32,19 @@ func resourceAwsIamRole() *schema.Resource {
Type: schema.TypeString,
Required: 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{
Type: schema.TypeString,
@ -102,6 +116,27 @@ func resourceAwsIamRoleReadResult(d *schema.ResourceData, role *iam.Role) error
func resourceAwsIamRoleDelete(d *schema.ResourceData, meta interface{}) error {
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{
RoleName: aws.String(d.Id()),
}

View File

@ -3,11 +3,12 @@ package aws
import (
"fmt"
"net/url"
"regexp"
"strings"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"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"
)
@ -30,6 +31,19 @@ func resourceAwsIamRolePolicy() *schema.Resource {
Type: schema.TypeString,
Required: 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{
Type: schema.TypeString,

View File

@ -4,13 +4,13 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam"
"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 TestAccAWSIAMRolePolicy(t *testing.T) {
func TestAccAWSIAMRolePolicy_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,

View File

@ -4,14 +4,14 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"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/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSRole_normal(t *testing.T) {
func TestAccAWSRole_basic(t *testing.T) {
var conf iam.GetRoleOutput
resource.Test(t, resource.TestCase{
@ -30,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 {
iamconn := testAccProvider.Meta().(*AWSClient).iamconn
@ -105,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\"]}]}"
}
`
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,9 +3,9 @@ package aws
import (
"fmt"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"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"
)

View File

@ -5,9 +5,9 @@ import (
"net/url"
"strings"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"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"
)

View File

@ -4,13 +4,13 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/iam"
"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 TestAccAWSIAMUserPolicy(t *testing.T) {
func TestAccAWSIAMUserPolicy_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,

View File

@ -4,14 +4,14 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/iam"
"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/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSUser_normal(t *testing.T) {
func TestAccAWSUser_basic(t *testing.T) {
var conf iam.GetUserOutput
resource.Test(t, resource.TestCase{

View File

@ -10,9 +10,10 @@ import (
"strings"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"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/ec2"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
@ -37,8 +38,8 @@ func resourceAwsInstance() *schema.Resource {
"associate_public_ip_address": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
Optional: true,
},
"availability_zone": &schema.Schema{
@ -85,6 +86,7 @@ func resourceAwsInstance() *schema.Resource {
"source_dest_check": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"user_data": &schema.Schema{
@ -146,6 +148,11 @@ func resourceAwsInstance() *schema.Resource {
Optional: true,
},
"monitoring": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"iam_instance_profile": &schema.Schema{
Type: schema.TypeString,
ForceNew: true,
@ -312,211 +319,34 @@ func resourceAwsInstance() *schema.Resource {
func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
// Figure out user data
userData := ""
if v := d.Get("user_data"); v != nil {
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)),
instanceOpts, err := buildAwsInstanceOpts(d, meta)
if err != nil {
return err
}
// Build the creation struct
runOpts := &ec2.RunInstancesInput{
ImageID: aws.String(d.Get("ami").(string)),
Placement: placement,
InstanceType: aws.String(d.Get("instance_type").(string)),
BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
DisableAPITermination: instanceOpts.DisableAPITermination,
EBSOptimized: instanceOpts.EBSOptimized,
Monitoring: instanceOpts.Monitoring,
IAMInstanceProfile: instanceOpts.IAMInstanceProfile,
ImageID: instanceOpts.ImageID,
InstanceType: instanceOpts.InstanceType,
KeyName: instanceOpts.KeyName,
MaxCount: aws.Long(int64(1)),
MinCount: aws.Long(int64(1)),
UserData: aws.String(userData),
EBSOptimized: aws.Boolean(d.Get("ebs_optimized").(bool)),
DisableAPITermination: aws.Boolean(d.Get("disable_api_termination").(bool)),
IAMInstanceProfile: iam,
}
associatePublicIPAddress := false
if v := d.Get("associate_public_ip_address"); v != nil {
associatePublicIPAddress = v.(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"); 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 {
if dn == nil {
return 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 err
}
}
}
if len(blockDevices) > 0 {
runOpts.BlockDeviceMappings = blockDevices
NetworkInterfaces: instanceOpts.NetworkInterfaces,
Placement: instanceOpts.Placement,
PrivateIPAddress: instanceOpts.PrivateIPAddress,
SecurityGroupIDs: instanceOpts.SecurityGroupIDs,
SecurityGroups: instanceOpts.SecurityGroups,
SubnetID: instanceOpts.SubnetID,
UserData: instanceOpts.UserData64,
}
// Create the instance
log.Printf("[DEBUG] Run configuration: %#v", runOpts)
var err error
log.Printf("[DEBUG] Run configuration: %s", awsutil.StringValue(runOpts))
var runResp *ec2.Reservation
for i := 0; i < 5; i++ {
@ -627,6 +457,8 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
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("public_dns", instance.PublicDNSName)
d.Set("public_ip", instance.PublicIPAddress)
@ -638,6 +470,12 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("subnet_id", instance.SubnetID)
}
d.Set("ebs_optimized", instance.EBSOptimized)
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
@ -715,8 +553,8 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
if d.HasChange("vpc_security_group_ids") {
var groups []*string
if v := d.Get("vpc_security_group_ids"); v != nil {
for _, v := range v.(*schema.Set).List() {
if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 {
for _, v := range v.List() {
groups = append(groups, aws.String(v.(string)))
}
}
@ -752,32 +590,8 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
log.Printf("[INFO] Terminating instance: %s", d.Id())
req := &ec2.TerminateInstancesInput{
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)
if err := awsTerminateInstance(conn, d.Id()); err != nil {
return err
}
d.SetId("")
@ -909,16 +723,305 @@ func fetchRootDeviceName(ami string, conn *ec2.EC2) (*string, error) {
}
log.Printf("[DEBUG] Describing AMI %q to get root block device name", ami)
req := &ec2.DescribeImagesInput{ImageIDs: []*string{aws.String(ami)}}
if res, err := conn.DescribeImages(req); err == nil {
if len(res.Images) == 1 {
return res.Images[0].RootDeviceName, nil
} else if len(res.Images) == 0 {
return nil, nil
} else {
return nil, fmt.Errorf("Expected 1 AMI for ID: %s, got: %#v", ami, res.Images)
}
} else {
res, err := conn.DescribeImages(&ec2.DescribeImagesInput{
ImageIDs: []*string{aws.String(ami)},
})
if err != nil {
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,15 +5,16 @@ import (
"reflect"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"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/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSInstance_normal(t *testing.T) {
func TestAccAWSInstance_basic(t *testing.T) {
var v ec2.Instance
var vol *ec2.Volume
@ -456,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 {
return testAccCheckInstanceDestroyWithProvider(s, testAccProvider)
}
@ -524,6 +579,11 @@ func testAccCheckInstanceExistsWithProviders(n string, i *ec2.Instance, provider
return fmt.Errorf("No ID is set")
}
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
resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIDs: []*string{aws.String(rs.Primary.ID)},
@ -653,7 +713,6 @@ resource "aws_instance" "foo" {
ami = "ami-4fccb37f"
instance_type = "m1.small"
subnet_id = "${aws_subnet.foo.id}"
source_dest_check = true
}
`
@ -890,3 +949,41 @@ resource "aws_eip" "foo_eip" {
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,9 +5,9 @@ import (
"log"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"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"
)

View File

@ -4,9 +4,9 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"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/terraform"
)

View File

@ -6,9 +6,9 @@ import (
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
)
func resourceAwsKeyPair() *schema.Resource {

View File

@ -5,14 +5,14 @@ import (
"strings"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"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/terraform"
)
func TestAccAWSKeyPair_normal(t *testing.T) {
func TestAccAWSKeyPair_basic(t *testing.T) {
var conf ec2.KeyPairInfo
resource.Test(t, resource.TestCase{

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,10 +9,10 @@ import (
"log"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/autoscaling"
"github.com/awslabs/aws-sdk-go/service/ec2"
"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/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
@ -30,6 +30,15 @@ func resourceAwsLaunchConfiguration() *schema.Resource {
Optional: true,
Computed: 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{
@ -106,6 +115,13 @@ func resourceAwsLaunchConfiguration() *schema.Resource {
ForceNew: true,
},
"enable_monitoring": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Computed: true,
ForceNew: true,
},
"ebs_block_device": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
@ -256,6 +272,12 @@ func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface
createLaunchConfigurationOpts.UserData = aws.String(userData)
}
if v, ok := d.GetOk("enable_monitoring"); ok {
createLaunchConfigurationOpts.InstanceMonitoring = &autoscaling.InstanceMonitoring{
Enabled: aws.Boolean(v.(bool)),
}
}
if v, ok := d.GetOk("iam_instance_profile"); ok {
createLaunchConfigurationOpts.IAMInstanceProfile = aws.String(v.(string))
}
@ -373,7 +395,24 @@ func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface
log.Printf(
"[DEBUG] autoscaling create launch configuration: %#v", 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 {
return fmt.Errorf("Error creating launch configuration: %s", err)
}
@ -423,6 +462,7 @@ func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{}
d.Set("iam_instance_profile", lc.IAMInstanceProfile)
d.Set("ebs_optimized", lc.EBSOptimized)
d.Set("spot_price", lc.SpotPrice)
d.Set("enable_monitoring", lc.InstanceMonitoring.Enabled)
d.Set("security_groups", lc.SecurityGroups)
if err := readLCBlockDevices(d, lc, ec2conn); err != nil {

View File

@ -7,13 +7,33 @@ import (
"testing"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/autoscaling"
"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/resource"
"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) {
var conf autoscaling.LaunchConfiguration
@ -61,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(
resource, prefix string) resource.TestCheckFunc {
return func(s *terraform.State) error {

View File

@ -4,9 +4,9 @@ import (
"fmt"
"strings"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/elb"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elb"
"github.com/hashicorp/terraform/helper/schema"
)

View File

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

View File

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

View File

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

View File

@ -8,9 +8,9 @@ import (
"strconv"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"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/resource"
"github.com/hashicorp/terraform/helper/schema"
@ -76,6 +76,14 @@ func resourceAwsNetworkAcl() *schema.Resource {
Type: schema.TypeString,
Optional: true,
},
"icmp_type": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"icmp_code": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
},
},
Set: resourceAwsNetworkAclEntryHash,
@ -110,6 +118,14 @@ func resourceAwsNetworkAcl() *schema.Resource {
Type: schema.TypeString,
Optional: true,
},
"icmp_type": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"icmp_code": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
},
},
Set: resourceAwsNetworkAclEntryHash,
@ -377,9 +393,10 @@ func updateNetworkAclEntries(d *schema.ResourceData, entryType string, conn *ec2
Protocol: add.Protocol,
RuleAction: add.RuleAction,
RuleNumber: add.RuleNumber,
ICMPTypeCode: add.ICMPTypeCode,
})
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
@ -466,6 +483,13 @@ func resourceAwsNetworkAclEntryHash(v interface{}) int {
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())
}
@ -538,6 +562,11 @@ func networkAclEntriesToMapList(networkAcls []*ec2.NetworkACLEntry) []map[string
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)
}

View File

@ -4,9 +4,9 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"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/terraform"
)
@ -24,29 +24,29 @@ func TestAccAWSNetworkAcl_EgressAndIngressRules(t *testing.T) {
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSNetworkAclExists("aws_network_acl.bar", &networkAcl),
resource.TestCheckResourceAttr(
"aws_network_acl.bar", "ingress.1216169466.protocol", "6"),
"aws_network_acl.bar", "ingress.109047673.protocol", "6"),
resource.TestCheckResourceAttr(
"aws_network_acl.bar", "ingress.1216169466.rule_no", "1"),
"aws_network_acl.bar", "ingress.109047673.rule_no", "1"),
resource.TestCheckResourceAttr(
"aws_network_acl.bar", "ingress.1216169466.from_port", "80"),
"aws_network_acl.bar", "ingress.109047673.from_port", "80"),
resource.TestCheckResourceAttr(
"aws_network_acl.bar", "ingress.1216169466.to_port", "80"),
"aws_network_acl.bar", "ingress.109047673.to_port", "80"),
resource.TestCheckResourceAttr(
"aws_network_acl.bar", "ingress.1216169466.action", "allow"),
"aws_network_acl.bar", "ingress.109047673.action", "allow"),
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(
"aws_network_acl.bar", "egress.2634340476.protocol", "6"),
"aws_network_acl.bar", "egress.868403673.protocol", "6"),
resource.TestCheckResourceAttr(
"aws_network_acl.bar", "egress.2634340476.rule_no", "2"),
"aws_network_acl.bar", "egress.868403673.rule_no", "2"),
resource.TestCheckResourceAttr(
"aws_network_acl.bar", "egress.2634340476.from_port", "443"),
"aws_network_acl.bar", "egress.868403673.from_port", "443"),
resource.TestCheckResourceAttr(
"aws_network_acl.bar", "egress.2634340476.to_port", "443"),
"aws_network_acl.bar", "egress.868403673.to_port", "443"),
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(
"aws_network_acl.bar", "egress.2634340476.action", "allow"),
"aws_network_acl.bar", "egress.868403673.action", "allow"),
),
},
},
@ -67,17 +67,17 @@ func TestAccAWSNetworkAcl_OnlyIngressRules_basic(t *testing.T) {
testAccCheckAWSNetworkAclExists("aws_network_acl.foos", &networkAcl),
// testAccCheckSubnetAssociation("aws_network_acl.foos", "aws_subnet.blob"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.protocol", "6"),
"aws_network_acl.foos", "ingress.1451312565.protocol", "6"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.rule_no", "2"),
"aws_network_acl.foos", "ingress.1451312565.rule_no", "2"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.from_port", "443"),
"aws_network_acl.foos", "ingress.1451312565.from_port", "443"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.to_port", "443"),
"aws_network_acl.foos", "ingress.1451312565.to_port", "443"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.action", "deny"),
"aws_network_acl.foos", "ingress.1451312565.action", "deny"),
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"),
),
},
},
@ -98,21 +98,21 @@ func TestAccAWSNetworkAcl_OnlyIngressRules_update(t *testing.T) {
testAccCheckAWSNetworkAclExists("aws_network_acl.foos", &networkAcl),
testIngressRuleLength(&networkAcl, 2),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.protocol", "6"),
"aws_network_acl.foos", "ingress.2048097841.protocol", "6"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.rule_no", "1"),
"aws_network_acl.foos", "ingress.2048097841.rule_no", "1"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.from_port", "0"),
"aws_network_acl.foos", "ingress.2048097841.from_port", "0"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.to_port", "22"),
"aws_network_acl.foos", "ingress.2048097841.to_port", "22"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.action", "deny"),
"aws_network_acl.foos", "ingress.2048097841.action", "deny"),
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(
"aws_network_acl.foos", "ingress.3264550475.from_port", "443"),
"aws_network_acl.foos", "ingress.1451312565.from_port", "443"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.3264550475.rule_no", "2"),
"aws_network_acl.foos", "ingress.1451312565.rule_no", "2"),
),
},
resource.TestStep{
@ -121,17 +121,17 @@ func TestAccAWSNetworkAcl_OnlyIngressRules_update(t *testing.T) {
testAccCheckAWSNetworkAclExists("aws_network_acl.foos", &networkAcl),
testIngressRuleLength(&networkAcl, 1),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.protocol", "6"),
"aws_network_acl.foos", "ingress.2048097841.protocol", "6"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.rule_no", "1"),
"aws_network_acl.foos", "ingress.2048097841.rule_no", "1"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.from_port", "0"),
"aws_network_acl.foos", "ingress.2048097841.from_port", "0"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.to_port", "22"),
"aws_network_acl.foos", "ingress.2048097841.to_port", "22"),
resource.TestCheckResourceAttr(
"aws_network_acl.foos", "ingress.2824900805.action", "deny"),
"aws_network_acl.foos", "ingress.2048097841.action", "deny"),
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"),
),
},
},

View File

@ -7,9 +7,9 @@ import (
"strconv"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"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/resource"
"github.com/hashicorp/terraform/helper/schema"
@ -78,9 +78,17 @@ func resourceAwsNetworkInterfaceCreate(d *schema.ResourceData, meta interface{})
conn := meta.(*AWSClient).ec2conn
request := &ec2.CreateNetworkInterfaceInput{
Groups: expandStringList(d.Get("security_groups").(*schema.Set).List()),
SubnetID: aws.String(d.Get("subnet_id").(string)),
PrivateIPAddresses: expandPrivateIPAddesses(d.Get("private_ips").(*schema.Set).List()),
SubnetID: aws.String(d.Get("subnet_id").(string)),
}
security_groups := d.Get("security_groups").(*schema.Set).List()
if len(security_groups) != 0 {
request.Groups = expandStringList(security_groups)
}
private_ips := d.Get("private_ips").(*schema.Set).List()
if len(private_ips) != 0 {
request.PrivateIPAddresses = expandPrivateIPAddesses(private_ips)
}
log.Printf("[DEBUG] Creating network interface")

View File

@ -4,9 +4,9 @@ import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/aws/awserr"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"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/terraform"
)

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