diff --git a/.gitignore b/.gitignore index 0bfaeffb..b635cf30 100644 --- a/.gitignore +++ b/.gitignore @@ -5,13 +5,10 @@ *.tfstate *.tfstate.* -# .tfvars files -*.tfvars - # IDE files .idea *.iml # Build harness files .build-harness -build-harness \ No newline at end of file +build-harness diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b7cf9010..00000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -addons: - apt: - packages: - - git - - make - - curl - -install: - - make init - -script: - - make terraform/install - - make terraform/get-plugins - - make terraform/get-modules - - make terraform/lint - - make terraform/validate \ No newline at end of file diff --git a/README.md b/README.md index 6accb7a1..2bb4606c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Cloud Posse][logo]](https://cpco.io/homepage) -# terraform-aws-s3-bucket [![Build Status](https://travis-ci.org/cloudposse/terraform-aws-s3-bucket.svg?branch=master)](https://travis-ci.org/cloudposse/terraform-aws-s3-bucket) [![Latest Release](https://img.shields.io/github/release/cloudposse/terraform-aws-s3-bucket.svg)](https://github.com/cloudposse/terraform-aws-s3-bucket/releases/latest) [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) +# terraform-aws-s3-bucket [![Codefresh Build Status](https://g.codefresh.io/api/badges/pipeline/cloudposse/terraform-modules%2Fterraform-aws-s3-bucket?type=cf-1)](https://g.codefresh.io/public/accounts/cloudposse/pipelines/5d13993639efa9451b1a2aa4) [![Latest Release](https://img.shields.io/github/release/cloudposse/terraform-aws-s3-bucket.svg)](https://github.com/cloudposse/terraform-aws-s3-bucket/releases/latest) [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) This module creates an S3 bucket with support of versioning, encryption, ACL and bucket object policy. @@ -47,16 +47,21 @@ We literally have [*hundreds of terraform modules*][terraform_modules] that are ## Usage + +**IMPORTANT:** The `master` branch is used in `source` just as an example. In your code, do not pin to `master` because there may be breaking changes between releases. +Instead pin to the release tag (e.g. `?ref=tags/x.y.z`) of one of our [latest releases](https://github.com/cloudposse/terraform-aws-s3-bucket/releases). + + ```hcl module "s3_bucket" { source = "git::https://github.com/cloudposse/terraform-aws-s3-bucket.git?ref=master" - enabled = "${var.enabled}" - user_enabled = "${var.user_enabled}" - versioning_enabled = "${var.versioning_enabled}" - allowed_bucket_actions = "${var.allowed_bucket_actions}" - name = "${var.name}" - stage = "${var.stage}" - namespace = "${var.namespace}" + enabled = true + user_enabled = true + versioning_enabled = false + allowed_bucket_actions = ["s3:GetObject", "s3:ListBucket", "s3:GetBucketLocation"] + name = "app" + stage = "test" + namespace = "eg" } ``` @@ -80,22 +85,22 @@ Available targets: | Name | Description | Type | Default | Required | |------|-------------|:----:|:-----:|:-----:| | acl | The canned ACL to apply. We recommend `private` to avoid exposing sensitive information | string | `private` | no | -| allow_encrypted_uploads_only | Set to `true` to prevent uploads of unencrypted objects to S3 bucket | string | `false` | no | -| allowed_bucket_actions | List of actions the user is permitted to perform on the S3 bucket | list | `` | no | -| attributes | Additional attributes (e.g. `1`) | list | `` | no | +| allow_encrypted_uploads_only | Set to `true` to prevent uploads of unencrypted objects to S3 bucket | bool | `false` | no | +| allowed_bucket_actions | List of actions the user is permitted to perform on the S3 bucket | list(string) | `` | no | +| attributes | Additional attributes (e.g. `1`) | list(string) | `` | no | | delimiter | Delimiter to be used between `namespace`, `stage`, `name` and `attributes` | string | `-` | no | -| enabled | Set to `false` to prevent the module from creating any resources | string | `true` | no | -| force_destroy | A boolean string that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are not recoverable. | string | `false` | no | +| enabled | Set to `false` to prevent the module from creating any resources | bool | `true` | no | +| force_destroy | A boolean string that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are not recoverable | bool | `false` | no | | kms_master_key_id | The AWS KMS master key ID used for the `SSE-KMS` encryption. This can only be used when you set the value of `sse_algorithm` as `aws:kms`. The default aws/s3 AWS KMS master key is used if this element is absent while the `sse_algorithm` is `aws:kms` | string | `` | no | -| name | Name (e.g. `app` or `db`) | string | - | yes | -| namespace | Namespace (e.g. `eg` or `cp`) | string | - | yes | -| policy | A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy. | string | `` | no | -| region | If specified, the AWS region this bucket should reside in. Otherwise, the region used by the callee. | string | `` | no | +| name | Name (e.g. `app` or `cluster`) | string | - | yes | +| namespace | Namespace (e.g. `eg` or `cp`) | string | `` | no | +| policy | A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy | string | `` | no | +| region | If specified, the AWS region this bucket should reside in. Otherwise, the region used by the callee | string | `` | no | | sse_algorithm | The server-side encryption algorithm to use. Valid values are `AES256` and `aws:kms` | string | `AES256` | no | -| stage | Stage (e.g. `prod`, `dev`, `staging`) | string | - | yes | -| tags | Additional tags (e.g. `{ BusinessUnit = "XYZ" }` | map | `` | no | -| user_enabled | Set to `true` to create an S3 user with permission to access the bucket | string | `false` | no | -| versioning_enabled | A state of versioning. Versioning is a means of keeping multiple variants of an object in the same bucket. | string | `false` | no | +| stage | Stage (e.g. `prod`, `dev`, `staging`) | string | `` | no | +| tags | Additional tags (e.g. `{ BusinessUnit = "XYZ" }` | map(string) | `` | no | +| user_enabled | Set to `true` to create an IAM user with permission to access the bucket | bool | `false` | no | +| versioning_enabled | A state of versioning. Versioning is a means of keeping multiple variants of an object in the same bucket | bool | `false` | no | ## Outputs diff --git a/README.yaml b/README.yaml index a3d0d1dd..4965a086 100644 --- a/README.yaml +++ b/README.yaml @@ -33,9 +33,9 @@ github_repo: cloudposse/terraform-aws-s3-bucket # Badges to display badges: - - name: "Build Status" - image: "https://travis-ci.org/cloudposse/terraform-aws-s3-bucket.svg?branch=master" - url: "https://travis-ci.org/cloudposse/terraform-aws-s3-bucket" + - name: "Codefresh Build Status" + image: "https://g.codefresh.io/api/badges/pipeline/cloudposse/terraform-modules%2Fterraform-aws-s3-bucket?type=cf-1" + url: "https://g.codefresh.io/public/accounts/cloudposse/pipelines/5d13993639efa9451b1a2aa4" - name: "Latest Release" image: "https://img.shields.io/github/release/cloudposse/terraform-aws-s3-bucket.svg" url: "https://github.com/cloudposse/terraform-aws-s3-bucket/releases/latest" @@ -68,9 +68,9 @@ related: description: |- This module creates an S3 bucket with support of versioning, encryption, ACL and bucket object policy. If `user_enabled` variable is set to `true`, the module will provision a basic IAM user with permissions to access the bucket. - + This basic IAM system user is suitable for CI/CD systems (_e.g._ TravisCI, CircleCI) or systems which are *external* to AWS that cannot leverage [AWS IAM Instance Profiles](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html). - + We do not recommend creating IAM users this way for any other purpose. # How to use this project @@ -78,13 +78,13 @@ usage: |- ```hcl module "s3_bucket" { source = "git::https://github.com/cloudposse/terraform-aws-s3-bucket.git?ref=master" - enabled = "${var.enabled}" - user_enabled = "${var.user_enabled}" - versioning_enabled = "${var.versioning_enabled}" - allowed_bucket_actions = "${var.allowed_bucket_actions}" - name = "${var.name}" - stage = "${var.stage}" - namespace = "${var.namespace}" + enabled = true + user_enabled = true + versioning_enabled = false + allowed_bucket_actions = ["s3:GetObject", "s3:ListBucket", "s3:GetBucketLocation"] + name = "app" + stage = "test" + namespace = "eg" } ``` diff --git a/codefresh/test.yml b/codefresh/test.yml new file mode 100644 index 00000000..ddd07f97 --- /dev/null +++ b/codefresh/test.yml @@ -0,0 +1,74 @@ +version: '1.0' + +stages: + - Prepare + - Test + +steps: + wait: + title: Wait + stage: Prepare + image: codefresh/cli:latest + commands: + - codefresh get builds --pipeline=${{CF_REPO_NAME}} --status running --limit 1000 -o json | jq --arg id ${{CF_BUILD_ID}} -ser 'flatten|.[-1].id==$id' + retry: + maxAttempts: 10 + delay: 20 + exponentialFactor: 1.1 + + main_clone: + title: "Clone repository" + type: git-clone + stage: Prepare + description: "Initialize" + repo: ${{CF_REPO_OWNER}}/${{CF_REPO_NAME}} + git: CF-default + revision: ${{CF_REVISION}} + + clean_init: + title: Prepare build-harness and test-harness + image: ${{TEST_IMAGE}} + stage: Prepare + commands: + - cf_export PATH="/usr/local/terraform/0.12/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + - make init + - git -C build-harness checkout master + - make -C test/ clean init TEST_HARNESS_BRANCH=master + - make -C test/src clean init + - find . -type d -name '.terraform' | xargs rm -rf + - find . -type f -name 'terraform.tfstate*' -exec rm -f {} \; + + test: + type: "parallel" + title: "Run tests" + description: "Run all tests in parallel" + stage: Test + steps: + test_readme_lint: + title: "Test README.md updated" + stage: "Test" + image: ${{TEST_IMAGE}} + description: Test "readme/lint" + commands: + - make readme/lint + + test_module: + title: Test module with bats + image: ${{TEST_IMAGE}} + stage: Test + commands: + - make -C test/ module + + test_examples_complete: + title: Test "examples/complete" with bats + image: ${{TEST_IMAGE}} + stage: Test + commands: + - make -C test/ examples/complete + + test_examples_complete_terratest: + title: Test "examples/complete" with terratest + image: ${{TEST_IMAGE}} + stage: Test + commands: + - make -C test/src diff --git a/docs/terraform.md b/docs/terraform.md index 3fe001b2..9ca3289a 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -3,22 +3,22 @@ | Name | Description | Type | Default | Required | |------|-------------|:----:|:-----:|:-----:| | acl | The canned ACL to apply. We recommend `private` to avoid exposing sensitive information | string | `private` | no | -| allow_encrypted_uploads_only | Set to `true` to prevent uploads of unencrypted objects to S3 bucket | string | `false` | no | -| allowed_bucket_actions | List of actions the user is permitted to perform on the S3 bucket | list | `` | no | -| attributes | Additional attributes (e.g. `1`) | list | `` | no | +| allow_encrypted_uploads_only | Set to `true` to prevent uploads of unencrypted objects to S3 bucket | bool | `false` | no | +| allowed_bucket_actions | List of actions the user is permitted to perform on the S3 bucket | list(string) | `` | no | +| attributes | Additional attributes (e.g. `1`) | list(string) | `` | no | | delimiter | Delimiter to be used between `namespace`, `stage`, `name` and `attributes` | string | `-` | no | -| enabled | Set to `false` to prevent the module from creating any resources | string | `true` | no | -| force_destroy | A boolean string that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are not recoverable. | string | `false` | no | +| enabled | Set to `false` to prevent the module from creating any resources | bool | `true` | no | +| force_destroy | A boolean string that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are not recoverable | bool | `false` | no | | kms_master_key_id | The AWS KMS master key ID used for the `SSE-KMS` encryption. This can only be used when you set the value of `sse_algorithm` as `aws:kms`. The default aws/s3 AWS KMS master key is used if this element is absent while the `sse_algorithm` is `aws:kms` | string | `` | no | -| name | Name (e.g. `app` or `db`) | string | - | yes | -| namespace | Namespace (e.g. `eg` or `cp`) | string | - | yes | -| policy | A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy. | string | `` | no | -| region | If specified, the AWS region this bucket should reside in. Otherwise, the region used by the callee. | string | `` | no | +| name | Name (e.g. `app` or `cluster`) | string | - | yes | +| namespace | Namespace (e.g. `eg` or `cp`) | string | `` | no | +| policy | A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy | string | `` | no | +| region | If specified, the AWS region this bucket should reside in. Otherwise, the region used by the callee | string | `` | no | | sse_algorithm | The server-side encryption algorithm to use. Valid values are `AES256` and `aws:kms` | string | `AES256` | no | -| stage | Stage (e.g. `prod`, `dev`, `staging`) | string | - | yes | -| tags | Additional tags (e.g. `{ BusinessUnit = "XYZ" }` | map | `` | no | -| user_enabled | Set to `true` to create an S3 user with permission to access the bucket | string | `false` | no | -| versioning_enabled | A state of versioning. Versioning is a means of keeping multiple variants of an object in the same bucket. | string | `false` | no | +| stage | Stage (e.g. `prod`, `dev`, `staging`) | string | `` | no | +| tags | Additional tags (e.g. `{ BusinessUnit = "XYZ" }` | map(string) | `` | no | +| user_enabled | Set to `true` to create an IAM user with permission to access the bucket | bool | `false` | no | +| versioning_enabled | A state of versioning. Versioning is a means of keeping multiple variants of an object in the same bucket | bool | `false` | no | ## Outputs diff --git a/examples/basic/main.tf b/examples/basic/main.tf deleted file mode 100644 index 566b987e..00000000 --- a/examples/basic/main.tf +++ /dev/null @@ -1,9 +0,0 @@ -module "s3_bucket" { - source = "../../" - enabled = "true" - name = "s3-bucket" - stage = "test" - namespace = "example" - versioning_enabled = "true" - user_enabled = "true" -} diff --git a/examples/basic/outputs.tf b/examples/basic/outputs.tf deleted file mode 100644 index 328235dc..00000000 --- a/examples/basic/outputs.tf +++ /dev/null @@ -1,41 +0,0 @@ -output "bucket_domain_name" { - value = "${module.s3_bucket.bucket_domain_name}" - description = "FQDN of bucket" -} - -output "bucket_id" { - value = "${module.s3_bucket.bucket_id}" - description = "Bucket Name (aka ID)" -} - -output "bucket_arn" { - value = "${module.s3_bucket.bucket_arn}" - description = "Bucket ARN" -} - -output "user_name" { - value = "${module.s3_bucket.user_name}" - description = "Normalized IAM user name" -} - -output "user_arn" { - value = "${module.s3_bucket.user_arn}" - description = "The ARN assigned by AWS for the user" -} - -output "user_unique_id" { - value = "${module.s3_bucket.user_unique_id}" - description = "The user unique ID assigned by AWS" -} - -output "access_key_id" { - value = "${module.s3_bucket.access_key_id}" - description = "The access key ID" - sensitive = true -} - -output "secret_access_key" { - value = "${module.s3_bucket.secret_access_key}" - description = "The secret access key. This will be written to the state file in plain-text" - sensitive = true -} diff --git a/examples/complete/fixtures.us-west-1.tfvars b/examples/complete/fixtures.us-west-1.tfvars new file mode 100644 index 00000000..45e1ddea --- /dev/null +++ b/examples/complete/fixtures.us-west-1.tfvars @@ -0,0 +1,17 @@ +region = "us-west-1" + +namespace = "eg" + +stage = "test" + +name = "s3-test" + +acl = "private" + +force_destroy = true + +versioning_enabled = false + +allow_encrypted_uploads_only = true + +allowed_bucket_actions = ["s3:PutObject", "s3:PutObjectAcl", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:GetBucketLocation", "s3:AbortMultipartUpload"] diff --git a/examples/complete/main.tf b/examples/complete/main.tf new file mode 100644 index 00000000..0ff28496 --- /dev/null +++ b/examples/complete/main.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region +} + +module "s3_bucket" { + source = "../../" + + enabled = true + user_enabled = true + region = var.region + namespace = var.namespace + stage = var.stage + name = var.name + acl = var.acl + force_destroy = var.force_destroy + versioning_enabled = var.versioning_enabled + allow_encrypted_uploads_only = var.allow_encrypted_uploads_only + allowed_bucket_actions = var.allowed_bucket_actions +} diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf new file mode 100644 index 00000000..498ab988 --- /dev/null +++ b/examples/complete/outputs.tf @@ -0,0 +1,29 @@ +output "bucket_domain_name" { + value = module.s3_bucket.bucket_domain_name + description = "FQDN of bucket" +} + +output "bucket_id" { + value = module.s3_bucket.bucket_id + description = "Bucket Name (aka ID)" +} + +output "bucket_arn" { + value = module.s3_bucket.bucket_arn + description = "Bucket ARN" +} + +output "user_name" { + value = module.s3_bucket.user_name + description = "Normalized IAM user name" +} + +output "user_arn" { + value = module.s3_bucket.user_arn + description = "The ARN assigned by AWS for the user" +} + +output "user_unique_id" { + value = module.s3_bucket.user_unique_id + description = "The user unique ID assigned by AWS" +} diff --git a/examples/complete/variables.tf b/examples/complete/variables.tf new file mode 100644 index 00000000..a7229579 --- /dev/null +++ b/examples/complete/variables.tf @@ -0,0 +1,45 @@ +variable "region" { + type = string + description = "AWS region" +} + +variable "namespace" { + type = string + description = "Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp'" +} + +variable "stage" { + type = string + description = "Stage (e.g. `prod`, `dev`, `staging`)" +} + +variable "name" { + type = string + description = "The Name of the application or solution (e.g. `bastion` or `portal`)" +} + +variable "acl" { + type = string + description = "The canned ACL to apply. We recommend `private` to avoid exposing sensitive information" +} + +variable "force_destroy" { + type = bool + description = "A boolean string that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are not recoverable" +} + +variable "versioning_enabled" { + type = bool + description = "A state of versioning. Versioning is a means of keeping multiple variants of an object in the same bucket" +} + +variable "allowed_bucket_actions" { + type = list(string) + default = ["s3:PutObject", "s3:PutObjectAcl", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:GetBucketLocation", "s3:AbortMultipartUpload"] + description = "List of actions the user is permitted to perform on the S3 bucket" +} + +variable "allow_encrypted_uploads_only" { + type = bool + description = "Set to `true` to prevent uploads of unencrypted objects to S3 bucket" +} diff --git a/main.tf b/main.tf index 74a2bac6..dc65d1d2 100644 --- a/main.tf +++ b/main.tf @@ -1,24 +1,24 @@ module "default_label" { - source = "git::https://github.com/cloudposse/terraform-null-label.git?ref=tags/0.3.3" - enabled = "${var.enabled}" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" - delimiter = "${var.delimiter}" - attributes = "${var.attributes}" - tags = "${var.tags}" + source = "git::https://github.com/cloudposse/terraform-null-label.git?ref=tags/0.14.1" + enabled = var.enabled + namespace = var.namespace + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags } resource "aws_s3_bucket" "default" { - count = "${var.enabled == "true" ? 1 : 0}" - bucket = "${module.default_label.id}" - acl = "${var.acl}" - region = "${var.region}" - force_destroy = "${var.force_destroy}" - policy = "${var.policy}" + count = var.enabled ? 1 : 0 + bucket = module.default_label.id + acl = var.acl + region = var.region + force_destroy = var.force_destroy + policy = var.policy versioning { - enabled = "${var.versioning_enabled}" + enabled = var.versioning_enabled } # https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-encryption.html @@ -26,35 +26,35 @@ resource "aws_s3_bucket" "default" { server_side_encryption_configuration { rule { apply_server_side_encryption_by_default { - sse_algorithm = "${var.sse_algorithm}" - kms_master_key_id = "${var.kms_master_key_id}" + sse_algorithm = var.sse_algorithm + kms_master_key_id = var.kms_master_key_id } } } - tags = "${module.default_label.tags}" + tags = module.default_label.tags } module "s3_user" { - source = "git::https://github.com/cloudposse/terraform-aws-iam-s3-user.git?ref=tags/0.3.1" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" - attributes = "${var.attributes}" - tags = "${var.tags}" - enabled = "${var.enabled == "true" && var.user_enabled == "true" ? "true" : "false"}" - s3_actions = ["${var.allowed_bucket_actions}"] - s3_resources = ["${join("", aws_s3_bucket.default.*.arn)}/*", "${join("", aws_s3_bucket.default.*.arn)}"] + source = "git::https://github.com/cloudposse/terraform-aws-iam-s3-user.git?ref=tags/0.4.0" + namespace = var.namespace + stage = var.stage + name = var.name + attributes = var.attributes + tags = var.tags + enabled = var.enabled && var.user_enabled ? true : false + s3_actions = var.allowed_bucket_actions + s3_resources = ["${join("", aws_s3_bucket.default.*.arn)}/*", join("", aws_s3_bucket.default.*.arn)] } data "aws_iam_policy_document" "bucket_policy" { - count = "${var.enabled == "true" && var.allow_encrypted_uploads_only == "true" ? 1 : 0}" + count = var.enabled && var.allow_encrypted_uploads_only ? 1 : 0 statement { sid = "DenyIncorrectEncryptionHeader" effect = "Deny" actions = ["s3:PutObject"] - resources = ["arn:aws:s3:::${aws_s3_bucket.default.id}/*"] + resources = ["arn:aws:s3:::${join("", aws_s3_bucket.default.*.id)}/*"] principals { identifiers = ["*"] @@ -63,7 +63,7 @@ data "aws_iam_policy_document" "bucket_policy" { condition { test = "StringNotEquals" - values = ["${var.sse_algorithm}"] + values = [var.sse_algorithm] variable = "s3:x-amz-server-side-encryption" } } @@ -72,7 +72,7 @@ data "aws_iam_policy_document" "bucket_policy" { sid = "DenyUnEncryptedObjectUploads" effect = "Deny" actions = ["s3:PutObject"] - resources = ["arn:aws:s3:::${aws_s3_bucket.default.id}/*"] + resources = ["arn:aws:s3:::${aws_s3_bucket.default[0].id}/*"] principals { identifiers = ["*"] @@ -88,8 +88,7 @@ data "aws_iam_policy_document" "bucket_policy" { } resource "aws_s3_bucket_policy" "default" { - count = "${var.enabled == "true" && var.allow_encrypted_uploads_only == "true" ? 1 : 0}" - bucket = "${join("", aws_s3_bucket.default.*.id)}" - - policy = "${join("", data.aws_iam_policy_document.bucket_policy.*.json)}" + count = var.enabled && var.allow_encrypted_uploads_only ? 1 : 0 + bucket = join("", aws_s3_bucket.default.*.id) + policy = join("", data.aws_iam_policy_document.bucket_policy.*.json) } diff --git a/outputs.tf b/outputs.tf index ef818c80..cd310d1d 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,51 +1,51 @@ output "bucket_domain_name" { - value = "${var.enabled == "true" ? join("", aws_s3_bucket.default.*.bucket_domain_name) : ""}" + value = var.enabled ? join("", aws_s3_bucket.default.*.bucket_domain_name) : "" description = "FQDN of bucket" } output "bucket_id" { - value = "${var.enabled == "true" ? join("", aws_s3_bucket.default.*.id) : ""}" + value = var.enabled ? join("", aws_s3_bucket.default.*.id) : "" description = "Bucket Name (aka ID)" } output "bucket_arn" { - value = "${var.enabled == "true" ? join("", aws_s3_bucket.default.*.arn) : ""}" + value = var.enabled ? join("", aws_s3_bucket.default.*.arn) : "" description = "Bucket ARN" } output "enabled" { - value = "${var.enabled}" + value = var.enabled description = "Is module enabled" } output "user_enabled" { - value = "${var.user_enabled}" + value = var.user_enabled description = "Is user creation enabled" } output "user_name" { - value = "${module.s3_user.user_name}" + value = module.s3_user.user_name description = "Normalized IAM user name" } output "user_arn" { - value = "${module.s3_user.user_arn}" + value = module.s3_user.user_arn description = "The ARN assigned by AWS for the user" } output "user_unique_id" { - value = "${module.s3_user.user_unique_id}" + value = module.s3_user.user_unique_id description = "The user unique ID assigned by AWS" } output "access_key_id" { sensitive = true - value = "${module.s3_user.access_key_id}" + value = module.s3_user.access_key_id description = "The access key ID" } output "secret_access_key" { sensitive = true - value = "${module.s3_user.secret_access_key}" + value = module.s3_user.secret_access_key description = "The secret access key. This will be written to the state file in plain-text" } diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 00000000..442804a2 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +.test-harness diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 00000000..17b2fe74 --- /dev/null +++ b/test/Makefile @@ -0,0 +1,43 @@ +TEST_HARNESS ?= https://github.com/cloudposse/test-harness.git +TEST_HARNESS_BRANCH ?= master +TEST_HARNESS_PATH = $(realpath .test-harness) +BATS_ARGS ?= --tap +BATS_LOG ?= test.log + +# Define a macro to run the tests +define RUN_TESTS +@echo "Running tests in $(1)" +@cd $(1) && bats $(BATS_ARGS) $(addsuffix .bats,$(addprefix $(TEST_HARNESS_PATH)/test/terraform/,$(TESTS))) +endef + +default: all + +-include Makefile.* + +## Provision the test-harnesss +.test-harness: + [ -d $@ ] || git clone --depth=1 -b $(TEST_HARNESS_BRANCH) $(TEST_HARNESS) $@ + +## Initialize the tests +init: .test-harness + +## Install all dependencies (OS specific) +deps:: + @exit 0 + +## Clean up the test harness +clean: + [ "$(TEST_HARNESS_PATH)" == "/" ] || rm -rf $(TEST_HARNESS_PATH) + +## Run all tests +all: module examples/complete + +## Run basic sanity checks against the module itself +module: export TESTS ?= installed lint get-modules module-pinning get-plugins provider-pinning validate terraform-docs input-descriptions output-descriptions +module: deps + $(call RUN_TESTS, ../) + +## Run tests against example +examples/complete: export TESTS ?= installed lint get-modules get-plugins validate +examples/complete: deps + $(call RUN_TESTS, ../$@) diff --git a/test/Makefile.alpine b/test/Makefile.alpine new file mode 100644 index 00000000..7925b186 --- /dev/null +++ b/test/Makefile.alpine @@ -0,0 +1,5 @@ +ifneq (,$(wildcard /sbin/apk)) +## Install all dependencies for alpine +deps:: init + @apk add --update terraform-docs@cloudposse json2hcl@cloudposse +endif diff --git a/test/src/.gitignore b/test/src/.gitignore new file mode 100644 index 00000000..31b0219e --- /dev/null +++ b/test/src/.gitignore @@ -0,0 +1,2 @@ +.gopath +vendor/ diff --git a/test/src/Gopkg.lock b/test/src/Gopkg.lock new file mode 100644 index 00000000..87bb6bd6 --- /dev/null +++ b/test/src/Gopkg.lock @@ -0,0 +1,92 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" + name = "github.com/davecgh/go-spew" + packages = ["spew"] + pruneopts = "UT" + revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" + version = "v1.1.1" + +[[projects]] + digest = "1:75d6042fc66aebc974cc49b0c6c7cc3b9adb5f8130fbfa0dbec0820d990afa25" + name = "github.com/gruntwork-io/terratest" + packages = [ + "modules/collections", + "modules/customerrors", + "modules/files", + "modules/logger", + "modules/retry", + "modules/shell", + "modules/ssh", + "modules/terraform", + ] + pruneopts = "UT" + revision = "892abb2c35878d0808101bbfe6559e931dc2d354" + version = "v0.16.0" + +[[projects]] + digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + pruneopts = "UT" + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + +[[projects]] + digest = "1:5da8ce674952566deae4dbc23d07c85caafc6cfa815b0b3e03e41979cedb8750" + name = "github.com/stretchr/testify" + packages = [ + "assert", + "require", + ] + pruneopts = "UT" + revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053" + version = "v1.3.0" + +[[projects]] + branch = "master" + digest = "1:831470c2758c8b733941144f2803a0ccad0632c5a767415b777ebd296b5f463e" + name = "golang.org/x/crypto" + packages = [ + "curve25519", + "ed25519", + "ed25519/internal/edwards25519", + "internal/chacha20", + "internal/subtle", + "poly1305", + "ssh", + "ssh/agent", + ] + pruneopts = "UT" + revision = "22d7a77e9e5f409e934ed268692e56707cd169e5" + +[[projects]] + branch = "master" + digest = "1:76ee51c3f468493aff39dbacc401e8831fbb765104cbf613b89bef01cf4bad70" + name = "golang.org/x/net" + packages = ["context"] + pruneopts = "UT" + revision = "f3200d17e092c607f615320ecaad13d87ad9a2b3" + +[[projects]] + branch = "master" + digest = "1:181f3fd33e620b958b5ab77da177cf775cdcccd7db82963607875fbd09ae995e" + name = "golang.org/x/sys" + packages = [ + "cpu", + "unix", + ] + pruneopts = "UT" + revision = "9cd6430ef91e39e1a0ec0470cf1321a33ef1b887" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + input-imports = [ + "github.com/gruntwork-io/terratest/modules/terraform", + "github.com/stretchr/testify/assert", + ] + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/test/src/Gopkg.toml b/test/src/Gopkg.toml new file mode 100644 index 00000000..995bac57 --- /dev/null +++ b/test/src/Gopkg.toml @@ -0,0 +1,7 @@ +[[constraint]] + name = "github.com/stretchr/testify" + version = "1.2.2" + +[prune] + go-tests = true + unused-packages = true diff --git a/test/src/Makefile b/test/src/Makefile new file mode 100644 index 00000000..8d318018 --- /dev/null +++ b/test/src/Makefile @@ -0,0 +1,50 @@ +PACKAGE = terraform-aws-s3-bucket +GOEXE ?= /usr/bin/go +GOPATH = $(CURDIR)/.gopath +GOBIN = $(GOPATH)/bin +BASE = $(GOPATH)/src/$(PACKAGE) +PATH := $(PATH):$(GOBIN) + +export TF_DATA_DIR ?= $(CURDIR)/.terraform +export TF_CLI_ARGS_init ?= -get-plugins=true +export GOPATH + +.PHONY: all +## Default target +all: test + +ifneq (,$(wildcard /sbin/apk)) +## Install go, if not installed +$(GOEXE): + apk add --update go +endif + +ifeq ($(shell uname -s),Linux) +## Install all `dep`, if not installed +$(GOBIN)/dep: + @mkdir -p $(GOBIN) + @curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh +endif + +## Prepare the GOPATH +$(BASE): $(GOEXE) + @mkdir -p $(dir $@) + @ln -sf $(CURDIR) $@ + +## Download vendor dependencies to vendor/ +$(BASE)/vendor: $(BASE) $(GOBIN)/dep + cd $(BASE) && dep ensure + +.PHONY : init +## Initialize tests +init: $(BASE)/vendor + +.PHONY : test +## Run tests +test: init + cd $(BASE) && go test -v -timeout 30m -run TestExamplesComplete + +.PHONY : clean +## Clean up files +clean: + rm -rf .gopath/ vendor/ $(TF_DATA_DIR) diff --git a/test/src/examples_complete_test.go b/test/src/examples_complete_test.go new file mode 100644 index 00000000..24fea926 --- /dev/null +++ b/test/src/examples_complete_test.go @@ -0,0 +1,41 @@ +package test + +import ( + "testing" + + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" +) + +// Test the Terraform module in examples/complete using Terratest. +func TestExamplesComplete(t *testing.T) { + t.Parallel() + + terraformOptions := &terraform.Options{ + // The path to where our Terraform code is located + TerraformDir: "../../examples/complete", + Upgrade: true, + // Variables to pass to our Terraform code using -var-file options + VarFiles: []string{"fixtures.us-west-1.tfvars"}, + } + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer terraform.Destroy(t, terraformOptions) + + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors + terraform.InitAndApply(t, terraformOptions) + + // Run `terraform output` to get the value of an output variable + userName := terraform.Output(t, terraformOptions, "user_name") + + expectedUserName := "eg-test-s3-test" + // Verify we're getting back the outputs we expect + assert.Equal(t, expectedUserName, userName) + + // Run `terraform output` to get the value of an output variable + s3BucketId := terraform.Output(t, terraformOptions, "bucket_id") + + expectedS3BucketId := "eg-test-s3-test" + // Verify we're getting back the outputs we expect + assert.Equal(t, expectedS3BucketId, s3BucketId) +} diff --git a/variables.tf b/variables.tf index 958b0f8e..4b8bddb8 100644 --- a/variables.tf +++ b/variables.tf @@ -1,98 +1,100 @@ variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `eg` or `cp`)" + default = "" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" + default = "" } variable "name" { - type = "string" - description = "Name (e.g. `app` or `db`)" + type = string + description = "Name (e.g. `app` or `cluster`)" } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter to be used between `namespace`, `stage`, `name` and `attributes`" } variable "attributes" { - type = "list" + type = list(string) default = [] description = "Additional attributes (e.g. `1`)" } variable "tags" { - type = "map" + type = map(string) default = {} description = "Additional tags (e.g. `{ BusinessUnit = \"XYZ\" }`" } variable "acl" { - type = "string" + type = string default = "private" description = "The canned ACL to apply. We recommend `private` to avoid exposing sensitive information" } variable "policy" { - type = "string" + type = string default = "" - description = "A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy." + description = "A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy" } variable "region" { - type = "string" + type = string default = "" - description = "If specified, the AWS region this bucket should reside in. Otherwise, the region used by the callee." + description = "If specified, the AWS region this bucket should reside in. Otherwise, the region used by the callee" } variable "force_destroy" { - type = "string" - default = "false" - description = "A boolean string that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are not recoverable." + type = bool + default = false + description = "A boolean string that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are not recoverable" } variable "versioning_enabled" { - type = "string" - default = "false" - description = "A state of versioning. Versioning is a means of keeping multiple variants of an object in the same bucket." + type = bool + default = false + description = "A state of versioning. Versioning is a means of keeping multiple variants of an object in the same bucket" } variable "sse_algorithm" { - type = "string" + type = string default = "AES256" description = "The server-side encryption algorithm to use. Valid values are `AES256` and `aws:kms`" } variable "kms_master_key_id" { - type = "string" + type = string default = "" description = "The AWS KMS master key ID used for the `SSE-KMS` encryption. This can only be used when you set the value of `sse_algorithm` as `aws:kms`. The default aws/s3 AWS KMS master key is used if this element is absent while the `sse_algorithm` is `aws:kms`" } variable "enabled" { - type = "string" + type = bool + default = true description = "Set to `false` to prevent the module from creating any resources" - default = "true" } variable "user_enabled" { - type = "string" - default = "false" - description = "Set to `true` to create an S3 user with permission to access the bucket" + type = bool + default = false + description = "Set to `true` to create an IAM user with permission to access the bucket" } variable "allowed_bucket_actions" { - type = "list" + type = list(string) default = ["s3:PutObject", "s3:PutObjectAcl", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:GetBucketLocation", "s3:AbortMultipartUpload"] description = "List of actions the user is permitted to perform on the S3 bucket" } variable "allow_encrypted_uploads_only" { - type = "string" - default = "false" + type = bool + default = false description = "Set to `true` to prevent uploads of unencrypted objects to S3 bucket" } diff --git a/versions.tf b/versions.tf new file mode 100644 index 00000000..a6cb393c --- /dev/null +++ b/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = "~> 0.12.0" + + required_providers { + aws = "~> 2.0" + local = "~> 1.2" + null = "~> 2.0" + } +}