From 0b82c9a8b644b8bf049a0406546d0bbe23d2aa52 Mon Sep 17 00:00:00 2001 From: Andriy Kopachevskyy Date: Thu, 1 Aug 2019 20:20:59 +0300 Subject: [PATCH] Add support for Terraform 0.12 Fixes terraform-google-startup-scripts#25 Upgraded docker_bats to TF 12.4 Updated integration testing scripts to support latest test and lint containers. --- CHANGELOG.md | 5 + Gemfile | 6 +- Makefile | 36 +- README.md | 20 +- build/docker/bats/Dockerfile | 2 +- examples/gsutil/README.md | 19 +- examples/gsutil/main.tf | 37 +- examples/gsutil/outputs.tf | 7 +- examples/gsutil/variables.tf | 1 + examples/gsutil/versions.tf | 18 + examples/simple_example/README.md | 16 +- examples/simple_example/main.tf | 17 +- examples/simple_example/outputs.tf | 7 +- examples/simple_example/variables.tf | 1 + examples/simple_example/versions.tf | 18 + helpers/combine_docfiles.py | 55 -- helpers/terraform_docs | 694 ++++++++++++++++++ helpers/terraform_validate | 23 + main.tf | 27 +- outputs.tf | 3 +- test/ci_integration.sh | 8 +- test/fixtures/gsutil/variables.tf | 3 + test/fixtures/simple_example/main.tf | 6 +- test/fixtures/simple_example/outputs.tf | 4 +- test/fixtures/simple_example/variables.tf | 2 + .../simulated_ci_environment/README.md | 13 +- .../fixtures/simulated_ci_environment/main.tf | 28 +- .../simulated_ci_environment/outputs.tf | 2 +- .../simulated_ci_environment/variables.tf | 8 +- test/make.sh | 67 +- variables.tf | 13 +- versions.tf | 18 + 32 files changed, 978 insertions(+), 206 deletions(-) create mode 100644 examples/gsutil/versions.tf create mode 100644 examples/simple_example/versions.tf delete mode 100755 helpers/combine_docfiles.py create mode 100755 helpers/terraform_docs create mode 100755 helpers/terraform_validate create mode 100644 versions.tf diff --git a/CHANGELOG.md b/CHANGELOG.md index 73af92b..ce18c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.0] - 2019-08-05 + +- Supported version of Terraform is 0.12. [#26] + ## [0.1.0] - 2019-04-02 ### Added @@ -13,3 +17,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [Unreleased]: https://github.com/terraform-google-modules/terraform-google-startup-scripts/compare/v0.1.0...HEAD [0.1.0]: https://github.com/terraform-google-modules/terraform-google-startup-scripts/releases/tag/v0.1.0 +[26]: https://github.com/terraform-google-modules/terraform-google-startup-scripts/pull/26 \ No newline at end of file diff --git a/Gemfile b/Gemfile index 31521a8..9fc49b7 100644 --- a/Gemfile +++ b/Gemfile @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -ruby '2.5.3' +ruby '2.6.3' source 'https://rubygems.org/' do - gem 'kitchen-terraform', '~> 4.1.0' - gem 'retriable', '~> 3.1.2' + gem 'kitchen-terraform', '~> 4.9' + gem 'retriable', '~> 4.0' end diff --git a/Makefile b/Makefile index 763b255..3bd93ce 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,11 @@ SHELL := /usr/bin/env bash # Docker build config variables +CREDENTIALS_PATH ?= /cft/workdir/credentials.json +DOCKER_ORG := gcr.io/cloud-foundation-cicd +DOCKER_TAG_BASE_KITCHEN_TERRAFORM ?= 2.3.0 +DOCKER_REPO_BASE_KITCHEN_TERRAFORM := ${DOCKER_ORG}/cft/kitchen-terraform:${DOCKER_TAG_BASE_KITCHEN_TERRAFORM} + # Use commit ID's because BATS upstream does not tag releases correctly. BUILD_BATS_VERSION ?= 03608115df2071fff4eaaff1605768c275e5f81f BUILD_BATS_ASSERT_VERSION ?= 8200039faf9790c05d9865490c97a0e101b9c80f @@ -25,18 +30,10 @@ BUILD_BATS_SUPPORT_VERSION ?= 004e707638eedd62e0481e8cdc9223ad471f12ee DOCKER_IMAGE_BATS := cftk/bats # DOCKER_TAG_BATS is the image semver and has no correlation to bats versions DOCKER_TAG_BATS ?= 0.6.0 -# Integration Testing Versions -DOCKER_IMAGE_INTEGRATION := cft/kitchen-terraform -# These versions should match the same versions used in the Pipeline definition -BUILD_TERRAFORM_VERSION ?= 0.11.10 -BUILD_CLOUD_SDK_VERSION ?= 216.0.0 -BUILD_PROVIDER_GOOGLE_VERSION ?= 1.19.1 -BUILD_PROVIDER_GSUITE_VERSION ?= 0.1.10 -DOCKER_IMAGE_INTEGRATION_TAG ?= ${BUILD_TERRAFORM_VERSION}_${BUILD_CLOUD_SDK_VERSION}_${BUILD_PROVIDER_GOOGLE_VERSION}_${BUILD_PROVIDER_GSUITE_VERSION} -DOCKER_IMAGE_INTEGRATION_URI ?= gcr.io/cloud-foundation-cicd/${DOCKER_IMAGE_INTEGRATION}:${DOCKER_IMAGE_INTEGRATION_TAG} + # All is the first target in the file so it will get picked up when you just run 'make' on its own -all: check_shell check_python check_golang check_terraform check_docker check_base_files test_check_headers check_headers check_trailing_whitespace generate_docs +all: check_terraform check_shell check_python check_golang check_docker check_base_files test_check_headers check_headers check_trailing_whitespace generate_docs # The .PHONY directive tells make that this isn't a real target and so # the presence of a file named 'check_shell' won't cause this target to stop @@ -153,12 +150,15 @@ test_integration_docker: integration_test_image # Interactive Shell version of integration_test_run .PHONY: docker_run -docker_run: integration_test_image +docker_run: docker run --rm -it \ - --volume $(CURDIR):/terraform-google-startup-scripts \ - --workdir /terraform-google-startup-scripts \ - --env SERVICE_ACCOUNT_JSON \ - --env PROJECT_ID \ - --env REGION \ - ${DOCKER_IMAGE_INTEGRATION_URI} \ - /bin/bash -c 'source test/ci_integration.sh && setup_environment && exec /bin/bash' + -e COMPUTE_ENGINE_SERVICE_ACCOUNT \ + -e PROJECT_ID \ + -e REGION \ + -e ZONES \ + -e SERVICE_ACCOUNT_JSON \ + -e CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE=${CREDENTIALS_PATH} \ + -e GOOGLE_APPLICATION_CREDENTIALS=${CREDENTIALS_PATH} \ + -v "$(CURDIR)":/cft/workdir \ + ${DOCKER_REPO_BASE_KITCHEN_TERRAFORM} \ + /bin/bash -c "source test/ci_integration.sh && setup_environment && exec /bin/bash" diff --git a/README.md b/README.md index 7fba008..a676549 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,11 @@ Use cases are: * Functions to execute commands and provide a consistent output format for the end user and/or machine parsing via logs. +## Compatibility + +This module is meant for use with Terraform 0.12. If you haven't [upgraded](https://www.terraform.io/upgrade-guides/0-12.html) and need a Terraform 0.11.x-compatible version of this module, the last released version intended for Terraform 0.11.x +is [0.1.0](https://registry.terraform.io/modules/terraform-google-modules/startup-scripts/google/0.1.0). + # Usage The library is loaded as the startup script of an instance. It initializes and @@ -160,16 +165,15 @@ ok 3 E_MISSING_MANDATORY_ARG error code is 9 ok 4 E_UNKNOWN_ARG error code is 10 ``` -[^]: (autogen_docs_start) - - + ## Inputs | Name | Description | Type | Default | Required | |------|-------------|:----:|:-----:|:-----:| -| enable_get_from_bucket | If not false, include stdlib::get_from_bucket() prior to executing startup-script-custom. Requires gsutil in the PATH. See also enable_init_gsutil_crcmod_el feature flag. | string | `false` | no | -| enable_init_gsutil_crcmod_el | If not false, include stdlib::init_gsutil_crcmod_el() prior to executing startup-script-custom. Call this function from startup-script-custom to initialize gsutil as per https://cloud.google.com/storage/docs/gsutil/addlhelp/CRC32CandInstallingcrcmod#centos-rhel-and-fedora Intended for CentOS, RHEL and Fedora systems. | string | `false` | no | -| enable_setup_init_script | If true, include stdlib::setup_init_script() prior to executing startup-script-custom. Call this function to load an init script from GCS into /etc/init.d and initialize it with chkconfig. This function depends on stdlib::get_from_bucket, so this input will be set to false if enable_get_from_bucket is false | string | `false` | no | +| enable\_get\_from\_bucket | If not false, include stdlib::get_from_bucket() prior to executing startup-script-custom. Requires gsutil in the PATH. See also enable_init_gsutil_crcmod_el feature flag. | bool | `"false"` | no | +| enable\_init\_gsutil\_crcmod\_el | If not false, include stdlib::init_gsutil_crcmod_el() prior to executing startup-script-custom. Call this function from startup-script-custom to initialize gsutil as per https://cloud.google.com/storage/docs/gsutil/addlhelp/CRC32CandInstallingcrcmod#centos-rhel-and-fedora Intended for CentOS, RHEL and Fedora systems. | bool | `"false"` | no | +| enable\_setup\_init\_script | If not false, include stdlib::setup_init_script() prior to executing startup-script-custom. Call this function to load an init script from GCS into /etc/init.d and initialize it with chkconfig. This function depends on stdlib::get_from_bucket, so this function won't be enabled if enable_get_from_bucket is false. | bool | `"false"` | no | +| enable\_setup\_sudoers | If true, include stdlib::setup_sudoers() prior to executing startup-script-custom. Call this function from startup-script-custom to setup unix usernames in sudoers Comma separated values must be posted to the project metadata key project/attributes/sudoers | bool | `"false"` | no | ## Outputs @@ -177,7 +181,7 @@ ok 4 E_UNKNOWN_ARG error code is 10 |------|-------------| | content | startup-script-stdlib.sh content as a string value. | -[^]: (autogen_docs_end) + [metadata_startup_script]: https://www.terraform.io/docs/providers/google/r/compute_instance.html#metadata_startup_script -[bats]: https://github.com/sstephenson/bats +[bats]: https://github.com/sstephenson/bats \ No newline at end of file diff --git a/build/docker/bats/Dockerfile b/build/docker/bats/Dockerfile index d00e262..285ca44 100644 --- a/build/docker/bats/Dockerfile +++ b/build/docker/bats/Dockerfile @@ -31,7 +31,7 @@ RUN mkdir -p "${APP_BASE_DIR}/home" \ # Terraform WORKDIR ${APP_BASE_DIR} -RUN curl -o terraform.zip https://releases.hashicorp.com/terraform/0.11.11/terraform_0.11.11_linux_amd64.zip +RUN curl -o terraform.zip https://releases.hashicorp.com/terraform/0.12.4/terraform_0.12.4_linux_amd64.zip RUN unzip terraform.zip -d "${APP_BASE_DIR}/bin" # bats diff --git a/examples/gsutil/README.md b/examples/gsutil/README.md index 53999ec..58eaa87 100644 --- a/examples/gsutil/README.md +++ b/examples/gsutil/README.md @@ -14,23 +14,22 @@ functions: 2. `stdlib::get_from_bucket` 3. `stdlib::setup_init_script` -[^]: (autogen_docs_start) - - + ## Inputs | Name | Description | Type | Default | Required | |------|-------------|:----:|:-----:|:-----:| -| project_id | The project_id to deploy the example instance into. (e.g. "simple-sample-project-1234") | string | - | yes | -| region | The region to deploy to | string | - | yes | -| url | The url to fetch in the startup script. This URL is passed via instance metadata to the startup script. (e.g. ifconfig.co/city) | string | `http://ifconfig.co/json` | no | +| message | The content to place in a bucket object message.txt. startup-script-custom fetches this object and validate this message against the content as an end-to-end example of stdlib::get_from_bucket(). | string | `"Hello World! uuid=0afce28a-057b-42cf-a90f-493de3c0666b"` | no | +| project\_id | The project_id to deploy the example instance into. (e.g. "simple-sample-project-1234") | string | n/a | yes | +| region | The region to deploy to | string | n/a | yes | +| service\_account\_email | The service acocunt email to associate with the example instance. Should have storage.buckets.get to use stdlib::get_from_bucket | string | n/a | yes | ## Outputs | Name | Description | |------|-------------| -| nat_ip | Public IP address of the example compute instance. | -| project_id | | -| region | | +| nat\_ip | Public IP address of the example compute instance. | +| project\_id | The project id used when managing resources. | +| region | The region used when managing resources. | -[^]: (autogen_docs_end) + diff --git a/examples/gsutil/main.tf b/examples/gsutil/main.tf index f4fdd4c..5ea504c 100644 --- a/examples/gsutil/main.tf +++ b/examples/gsutil/main.tf @@ -15,9 +15,9 @@ */ provider "google" { - version = "~> 1.20" - project = "${var.project_id}" - region = "${var.region}" + version = "~> 2.9.0" + project = var.project_id + region = var.region zone = "${var.region}-a" } @@ -40,30 +40,30 @@ resource "random_id" "resource_name_suffix" { # Storage bucket used by startup-script-custom and stdlib::get_from_bucket resource "google_storage_bucket" "example" { name = "startup-scripts-${random_id.resource_name_suffix.hex}" - location = "${var.region}" + location = var.region storage_class = "REGIONAL" } resource "google_storage_bucket_object" "message" { name = "message.txt" - content = "${var.message}" - bucket = "${google_storage_bucket.example.name}" + content = var.message + bucket = google_storage_bucket.example.name } resource "google_storage_bucket_object" "init_script_sample" { name = "init_script_sample" - content = "${file("${path.module}/init_scripts/init_script_sample")}" - bucket = "${google_storage_bucket.example.name}" + content = file("${path.module}/init_scripts/init_script_sample") + bucket = google_storage_bucket.example.name } data "template_file" "startup-script-custom" { - template = "${file("${path.module}/templates/startup-script-custom.tpl")}" + template = file("${path.module}/templates/startup-script-custom.tpl") vars = { - bucket = "${google_storage_bucket_object.message.bucket}" - object = "${google_storage_bucket_object.message.name}" - content = "${google_storage_bucket_object.message.content}" - init_script_object = "${google_storage_bucket_object.init_script_sample.name}" + bucket = google_storage_bucket_object.message.bucket + object = google_storage_bucket_object.message.name + content = google_storage_bucket_object.message.content + init_script_object = google_storage_bucket_object.init_script_sample.name } } @@ -73,9 +73,9 @@ resource "google_compute_instance" "example" { machine_type = "f1-micro" can_ip_forward = false - metadata { - startup-script = "${module.startup-scripts.content}" - startup-script-custom = "${data.template_file.startup-script-custom.rendered}" + metadata = { + startup-script = module.startup-scripts.content + startup-script-custom = data.template_file.startup-script-custom.rendered } scheduling { @@ -88,7 +88,7 @@ resource "google_compute_instance" "example" { auto_delete = true initialize_params { - image = "${data.google_compute_image.os.self_link}" + image = data.google_compute_image.os.self_link type = "pd-standard" } } @@ -102,7 +102,8 @@ resource "google_compute_instance" "example" { } service_account { - email = "${var.service_account_email}" + email = var.service_account_email scopes = ["storage-ro"] } } + diff --git a/examples/gsutil/outputs.tf b/examples/gsutil/outputs.tf index de7cd18..63b0a33 100644 --- a/examples/gsutil/outputs.tf +++ b/examples/gsutil/outputs.tf @@ -16,15 +16,16 @@ output "project_id" { description = "The project id used when managing resources." - value = "${var.project_id}" + value = var.project_id } output "region" { description = "The region used when managing resources." - value = "${var.region}" + value = var.region } output "nat_ip" { description = "Public IP address of the example compute instance." - value = "${google_compute_instance.example.network_interface.0.access_config.0.nat_ip}" + value = google_compute_instance.example.network_interface[0].access_config[0].nat_ip } + diff --git a/examples/gsutil/variables.tf b/examples/gsutil/variables.tf index 00127f2..0bb097d 100644 --- a/examples/gsutil/variables.tf +++ b/examples/gsutil/variables.tf @@ -30,3 +30,4 @@ variable "message" { description = "The content to place in a bucket object message.txt. startup-script-custom fetches this object and validate this message against the content as an end-to-end example of stdlib::get_from_bucket()." default = "Hello World! uuid=0afce28a-057b-42cf-a90f-493de3c0666b" } + diff --git a/examples/gsutil/versions.tf b/examples/gsutil/versions.tf new file mode 100644 index 0000000..fca1684 --- /dev/null +++ b/examples/gsutil/versions.tf @@ -0,0 +1,18 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +terraform { + required_version = ">= 0.12" +} diff --git a/examples/simple_example/README.md b/examples/simple_example/README.md index c53dc07..387dc9b 100644 --- a/examples/simple_example/README.md +++ b/examples/simple_example/README.md @@ -133,25 +133,23 @@ $ curl -H Metadata-Flavor:Google http://metadata.google.internal/computeMetadata 100 7268 100 7268 0 0 958k 0 --:--:-- --:--:-- --:--:-- 1013k ``` -[^]: (autogen_docs_start) - - + ## Inputs | Name | Description | Type | Default | Required | |------|-------------|:----:|:-----:|:-----:| -| project_id | The project_id to deploy the example instance into. (e.g. "simple-sample-project-1234") | string | - | yes | -| region | The region to deploy to | string | - | yes | -| url | The url to fetch in the startup script. This URL is passed via instance metadata to the startup script. (e.g. ifconfig.co/city) | string | `http://ifconfig.co/json` | no | +| project\_id | The project_id to deploy the example instance into. (e.g. "simple-sample-project-1234") | string | n/a | yes | +| region | The region to deploy to | string | n/a | yes | +| url | The url to fetch in the startup script. This URL is passed via instance metadata to the startup script. (e.g. ifconfig.co/city) | string | `"http://ifconfig.co/json"` | no | ## Outputs | Name | Description | |------|-------------| -| nat_ip | Public IP address of the example compute instance. | -| project_id | The project id used when managing resources. | +| nat\_ip | Public IP address of the example compute instance. | +| project\_id | The project id used when managing resources. | | region | The region used when managing resources. | -[^]: (autogen_docs_end) + [simple-project]: https://github.com/terraform-google-modules/terraform-google-project-factory/tree/master/examples/simple_project diff --git a/examples/simple_example/main.tf b/examples/simple_example/main.tf index 769285e..94bc1b4 100644 --- a/examples/simple_example/main.tf +++ b/examples/simple_example/main.tf @@ -15,14 +15,14 @@ */ provider "google" { - version = "~> 1.20" - project = "${var.project_id}" - region = "${var.region}" + version = "~> 2.9.0" + project = var.project_id + region = var.region zone = "${var.region}-a" } module "startup-scripts" { - source = "../../" + source = "../../" enable_setup_sudoers = true } @@ -43,9 +43,9 @@ resource "google_compute_instance" "example" { machine_type = "f1-micro" can_ip_forward = false - metadata { - startup-script = "${module.startup-scripts.content}" - startup-script-custom = "${file("${path.module}/files/startup-script-custom")}" + metadata = { + startup-script = module.startup-scripts.content + startup-script-custom = file("${path.module}/files/startup-script-custom") } scheduling { @@ -58,7 +58,7 @@ resource "google_compute_instance" "example" { auto_delete = true initialize_params { - image = "${data.google_compute_image.os.self_link}" + image = data.google_compute_image.os.self_link type = "pd-standard" } } @@ -71,3 +71,4 @@ resource "google_compute_instance" "example" { } } } + diff --git a/examples/simple_example/outputs.tf b/examples/simple_example/outputs.tf index de7cd18..63b0a33 100644 --- a/examples/simple_example/outputs.tf +++ b/examples/simple_example/outputs.tf @@ -16,15 +16,16 @@ output "project_id" { description = "The project id used when managing resources." - value = "${var.project_id}" + value = var.project_id } output "region" { description = "The region used when managing resources." - value = "${var.region}" + value = var.region } output "nat_ip" { description = "Public IP address of the example compute instance." - value = "${google_compute_instance.example.network_interface.0.access_config.0.nat_ip}" + value = google_compute_instance.example.network_interface[0].access_config[0].nat_ip } + diff --git a/examples/simple_example/variables.tf b/examples/simple_example/variables.tf index 8754749..02d347b 100644 --- a/examples/simple_example/variables.tf +++ b/examples/simple_example/variables.tf @@ -26,3 +26,4 @@ variable "url" { description = "The url to fetch in the startup script. This URL is passed via instance metadata to the startup script. (e.g. ifconfig.co/city)" default = "http://ifconfig.co/json" } + diff --git a/examples/simple_example/versions.tf b/examples/simple_example/versions.tf new file mode 100644 index 0000000..fca1684 --- /dev/null +++ b/examples/simple_example/versions.tf @@ -0,0 +1,18 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +terraform { + required_version = ">= 0.12" +} diff --git a/helpers/combine_docfiles.py b/helpers/combine_docfiles.py deleted file mode 100755 index b9874f2..0000000 --- a/helpers/combine_docfiles.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2018 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -''' Combine file from: - * script argument 1 - with content of file from: - * script argument 2 - using the beginning of line separators - hardcoded using regexes in this file: - - We exclude any text using the separate - regex specified here -''' - -import os -import re -import sys - -insert_separator_regex = r'(.*?\[\^\]\:\ \(autogen_docs_start\))(.*?)(\n\[\^\]\:\ \(autogen_docs_end\).*?$)' # noqa: E501 -exclude_separator_regex = r'(.*?)Copyright 20\d\d Google LLC.*?limitations under the License.(.*?)$' # noqa: E501 - -if len(sys.argv) != 3: - sys.exit(1) - -if not os.path.isfile(sys.argv[1]): - sys.exit(0) - -input = open(sys.argv[1], "r").read() -replace_content = open(sys.argv[2], "r").read() - -# Exclude the specified content from the replacement content -groups = re.match( - exclude_separator_regex, - replace_content, - re.DOTALL -).groups(0) -replace_content = groups[0] + groups[1] - -# Find where to put the replacement content, overwrite the input file -groups = re.match(insert_separator_regex, input, re.DOTALL).groups(0) -output = groups[0] + replace_content + groups[2] + "\n" -open(sys.argv[1], "w").write(output) diff --git a/helpers/terraform_docs b/helpers/terraform_docs new file mode 100755 index 0000000..c332309 --- /dev/null +++ b/helpers/terraform_docs @@ -0,0 +1,694 @@ +#!/usr/bin/env bash + +set -e + +main() { + declare argv + argv=$(getopt -o a: --long args: -- "$@") || return + eval "set -- $argv" + + declare args + declare files + + for argv; do + case $argv in + (-a|--args) + shift + args="$1" + shift + ;; + (--) + shift + files="$@" + break + ;; + esac + done + + local hack_terraform_docs=$(terraform version | head -1 | grep -c 0.12) + + if [[ "$hack_terraform_docs" == "1" ]]; then + which awk 2>&1 >/dev/null || ( echo "awk is required for terraform-docs hack to work with Terraform 0.12"; exit 1) + + tmp_file_awk=$(mktemp "${TMPDIR:-/tmp}/terraform-docs-XXXXXXXXXX") + terraform_docs_awk "$tmp_file_awk" + terraform_docs "$tmp_file_awk" "$args" "$files" + rm -f "$tmp_file_awk" + else + terraform_docs "0" "$args" "$files" + fi + +} + +terraform_docs() { + readonly terraform_docs_awk_file="$1" + readonly args="$2" + readonly files="$3" + + declare -a paths + declare -a tfvars_files + + index=0 + + for file_with_path in $files; do + file_with_path="${file_with_path// /__REPLACED__SPACE__}" + + paths[index]=$(dirname "$file_with_path") + + if [[ "$file_with_path" == *".tfvars" ]]; then + tfvars_files+=("$file_with_path") + fi + + ((index+=1)) + done + + readonly tmp_file=$(mktemp) + readonly text_file="README.md" + + for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do + path_uniq="${path_uniq//__REPLACED__SPACE__/ }" + + pushd "$path_uniq" > /dev/null + + if [[ ! -f "$text_file" ]]; then + popd > /dev/null + continue + fi + + if [[ "$terraform_docs_awk_file" == "0" ]]; then + terraform-docs $args md ./ > "$tmp_file" + else + # Can't append extension for mktemp, so renaming instead + tmp_file_docs=$(mktemp "${TMPDIR:-/tmp}/terraform-docs-XXXXXXXXXX") + mv "$tmp_file_docs" "$tmp_file_docs.tf" + tmp_file_docs_tf="$tmp_file_docs.tf" + + awk -f "$terraform_docs_awk_file" ./*.tf > "$tmp_file_docs_tf" + terraform-docs $args md "$tmp_file_docs_tf" > "$tmp_file" + rm -f "$tmp_file_docs_tf" + fi + + # Replace content between markers with the placeholder - https://stackoverflow.com/questions/1212799/how-do-i-extract-lines-between-two-line-delimiters-in-perl#1212834 + perl -i -ne 'if (/BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK/../END OF PRE-COMMIT-TERRAFORM DOCS HOOK/) { print $_ if /BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK/; print "I_WANT_TO_BE_REPLACED\n$_" if /END OF PRE-COMMIT-TERRAFORM DOCS HOOK/;} else { print $_ }' "$text_file" + + # Replace placeholder with the content of the file + perl -i -e 'open(F, "'"$tmp_file"'"); $f = join "", ; while(<>){if (/I_WANT_TO_BE_REPLACED/) {print $f} else {print $_};}' "$text_file" + + rm -f "$tmp_file" + + popd > /dev/null + done +} + +terraform_docs_awk() { + readonly output_file=$1 + + cat <<"EOF" > $output_file +# This script converts Terraform 0.12 variables/outputs to something suitable for `terraform-docs` +# As of terraform-docs v0.6.0, HCL2 is not supported. This script is a *dirty hack* to get around it. +# https://github.com/segmentio/terraform-docs/ +# https://github.com/segmentio/terraform-docs/issues/62 + +# Script was originally found here: https://github.com/cloudposse/build-harness/blob/master/bin/terraform-docs.awk + +{ + if ( $0 ~ /\{/ ) { + braceCnt++ + } + + if ( $0 ~ /\}/ ) { + braceCnt-- + } + + # [START] variable or output block started + if ($0 ~ /^[[:space:]]*(variable|output)[[:space:]][[:space:]]*"(.*?)"/) { + # Normalize the braceCnt (should be 1 now) + braceCnt = 1 + # [CLOSE] "default" block + if (blockDefCnt > 0) { + blockDefCnt = 0 + } + blockCnt++ + print $0 + } + + # [START] multiline default statement started + if (blockCnt > 0) { + if ($0 ~ /^[[:space:]][[:space:]]*(default)[[:space:]][[:space:]]*=/) { + if ($3 ~ "null") { + print " default = \"null\"" + } else { + print $0 + blockDefCnt++ + blockDefStart=1 + } + } + } + + # [PRINT] single line "description" + if (blockCnt > 0) { + if (blockDefCnt == 0) { + if ($0 ~ /^[[:space:]][[:space:]]*description[[:space:]][[:space:]]*=/) { + # [CLOSE] "default" block + if (blockDefCnt > 0) { + blockDefCnt = 0 + } + print $0 + } + } + } + + # [PRINT] single line "type" + if (blockCnt > 0) { + if ($0 ~ /^[[:space:]][[:space:]]*type[[:space:]][[:space:]]*=/ ) { + # [CLOSE] "default" block + if (blockDefCnt > 0) { + blockDefCnt = 0 + } + type=$3 + if (type ~ "object") { + print " type = \"object\"" + } else { + # legacy quoted types: "string", "list", and "map" + if ($3 ~ /^[[:space:]]*"(.*?)"[[:space:]]*$/) { + print " type = " $3 + } else { + print " type = \"" $3 "\"" + } + } + } + } + + # [CLOSE] variable/output block + if (blockCnt > 0) { + if (braceCnt == 0 && blockCnt > 0) { + blockCnt-- + print $0 + } + } + + # [PRINT] Multiline "default" statement + if (blockCnt > 0 && blockDefCnt > 0) { + if (blockDefStart == 1) { + blockDefStart = 0 + } else { + print $0 + } + } +} +EOF + +} + +getopt() { + # pure-getopt, a drop-in replacement for GNU getopt in pure Bash. + # version 1.4.3 + # + # Copyright 2012-2018 Aron Griffis + # + # Permission is hereby granted, free of charge, to any person obtaining + # a copy of this software and associated documentation files (the + # "Software"), to deal in the Software without restriction, including + # without limitation the rights to use, copy, modify, merge, publish, + # distribute, sublicense, and/or sell copies of the Software, and to + # permit persons to whom the Software is furnished to do so, subject to + # the following conditions: + # + # The above copyright notice and this permission notice shall be included + # in all copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + _getopt_main() { + # Returns one of the following statuses: + # 0 success + # 1 error parsing parameters + # 2 error in getopt invocation + # 3 internal error + # 4 reserved for -T + # + # For statuses 0 and 1, generates normalized and shell-quoted + # "options -- parameters" on stdout. + + declare parsed status + declare short long name flags + declare have_short=false + + # Synopsis from getopt man-page: + # + # getopt optstring parameters + # getopt [options] [--] optstring parameters + # getopt [options] -o|--options optstring [options] [--] parameters + # + # The first form can be normalized to the third form which + # _getopt_parse() understands. The second form can be recognized after + # first parse when $short hasn't been set. + + if [[ -n ${GETOPT_COMPATIBLE+isset} || $1 == [^-]* ]]; then + # Enable compatibility mode + flags=c$flags + # Normalize first to third synopsis form + set -- -o "$1" -- "${@:2}" + fi + + # First parse always uses flags=p since getopt always parses its own + # arguments effectively in this mode. + parsed=$(_getopt_parse getopt ahl:n:o:qQs:TuV \ + alternative,help,longoptions:,name:,options:,quiet,quiet-output,shell:,test,version \ + p "$@") + status=$? + if [[ $status != 0 ]]; then + if [[ $status == 1 ]]; then + echo "Try \`getopt --help' for more information." >&2 + # Since this is the first parse, convert status 1 to 2 + status=2 + fi + return $status + fi + eval "set -- $parsed" + + while [[ $# -gt 0 ]]; do + case $1 in + (-a|--alternative) + flags=a$flags ;; + + (-h|--help) + _getopt_help + return 2 # as does GNU getopt + ;; + + (-l|--longoptions) + long="$long${long:+,}$2" + shift ;; + + (-n|--name) + name=$2 + shift ;; + + (-o|--options) + short=$2 + have_short=true + shift ;; + + (-q|--quiet) + flags=q$flags ;; + + (-Q|--quiet-output) + flags=Q$flags ;; + + (-s|--shell) + case $2 in + (sh|bash) + flags=${flags//t/} ;; + (csh|tcsh) + flags=t$flags ;; + (*) + echo 'getopt: unknown shell after -s or --shell argument' >&2 + echo "Try \`getopt --help' for more information." >&2 + return 2 ;; + esac + shift ;; + + (-u|--unquoted) + flags=u$flags ;; + + (-T|--test) + return 4 ;; + + (-V|--version) + echo "pure-getopt 1.4.3" + return 0 ;; + + (--) + shift + break ;; + esac + + shift + done + + if ! $have_short; then + # $short was declared but never set, not even to an empty string. + # This implies the second form in the synopsis. + if [[ $# == 0 ]]; then + echo 'getopt: missing optstring argument' >&2 + echo "Try \`getopt --help' for more information." >&2 + return 2 + fi + short=$1 + have_short=true + shift + fi + + if [[ $short == -* ]]; then + # Leading dash means generate output in place rather than reordering, + # unless we're already in compatibility mode. + [[ $flags == *c* ]] || flags=i$flags + short=${short#?} + elif [[ $short == +* ]]; then + # Leading plus means POSIXLY_CORRECT, unless we're already in + # compatibility mode. + [[ $flags == *c* ]] || flags=p$flags + short=${short#?} + fi + + # This should fire if POSIXLY_CORRECT is in the environment, even if + # it's an empty string. That's the difference between :+ and + + flags=${POSIXLY_CORRECT+p}$flags + + _getopt_parse "${name:-getopt}" "$short" "$long" "$flags" "$@" + } + + _getopt_parse() { + # Inner getopt parser, used for both first parse and second parse. + # Returns 0 for success, 1 for error parsing, 3 for internal error. + # In the case of status 1, still generates stdout with whatever could + # be parsed. + # + # $flags is a string of characters with the following meanings: + # a - alternative parsing mode + # c - GETOPT_COMPATIBLE + # i - generate output in place rather than reordering + # p - POSIXLY_CORRECT + # q - disable error reporting + # Q - disable normal output + # t - quote for csh/tcsh + # u - unquoted output + + declare name="$1" short="$2" long="$3" flags="$4" + shift 4 + + # Split $long on commas, prepend double-dashes, strip colons; + # for use with _getopt_resolve_abbrev + declare -a longarr + _getopt_split longarr "$long" + longarr=( "${longarr[@]/#/--}" ) + longarr=( "${longarr[@]%:}" ) + longarr=( "${longarr[@]%:}" ) + + # Parse and collect options and parameters + declare -a opts params + declare o alt_recycled=false error=0 + + while [[ $# -gt 0 ]]; do + case $1 in + (--) + params=( "${params[@]}" "${@:2}" ) + break ;; + + (--*=*) + o=${1%%=*} + if ! o=$(_getopt_resolve_abbrev "$o" "${longarr[@]}"); then + error=1 + elif [[ ,"$long", == *,"${o#--}"::,* ]]; then + opts=( "${opts[@]}" "$o" "${1#*=}" ) + elif [[ ,"$long", == *,"${o#--}":,* ]]; then + opts=( "${opts[@]}" "$o" "${1#*=}" ) + elif [[ ,"$long", == *,"${o#--}",* ]]; then + if $alt_recycled; then o=${o#-}; fi + _getopt_err "$name: option '$o' doesn't allow an argument" + error=1 + else + echo "getopt: assertion failed (1)" >&2 + return 3 + fi + alt_recycled=false + ;; + + (--?*) + o=$1 + if ! o=$(_getopt_resolve_abbrev "$o" "${longarr[@]}"); then + error=1 + elif [[ ,"$long", == *,"${o#--}",* ]]; then + opts=( "${opts[@]}" "$o" ) + elif [[ ,"$long", == *,"${o#--}::",* ]]; then + opts=( "${opts[@]}" "$o" '' ) + elif [[ ,"$long", == *,"${o#--}:",* ]]; then + if [[ $# -ge 2 ]]; then + shift + opts=( "${opts[@]}" "$o" "$1" ) + else + if $alt_recycled; then o=${o#-}; fi + _getopt_err "$name: option '$o' requires an argument" + error=1 + fi + else + echo "getopt: assertion failed (2)" >&2 + return 3 + fi + alt_recycled=false + ;; + + (-*) + if [[ $flags == *a* ]]; then + # Alternative parsing mode! + # Try to handle as a long option if any of the following apply: + # 1. There's an equals sign in the mix -x=3 or -xy=3 + # 2. There's 2+ letters and an abbreviated long match -xy + # 3. There's a single letter and an exact long match + # 4. There's a single letter and no short match + o=${1::2} # temp for testing #4 + if [[ $1 == *=* || $1 == -?? || \ + ,$long, == *,"${1#-}"[:,]* || \ + ,$short, != *,"${o#-}"[:,]* ]]; then + o=$(_getopt_resolve_abbrev "${1%%=*}" "${longarr[@]}" 2>/dev/null) + case $? in + (0) + # Unambiguous match. Let the long options parser handle + # it, with a flag to get the right error message. + set -- "-$1" "${@:2}" + alt_recycled=true + continue ;; + (1) + # Ambiguous match, generate error and continue. + _getopt_resolve_abbrev "${1%%=*}" "${longarr[@]}" >/dev/null + error=1 + shift + continue ;; + (2) + # No match, fall through to single-character check. + true ;; + (*) + echo "getopt: assertion failed (3)" >&2 + return 3 ;; + esac + fi + fi + + o=${1::2} + if [[ "$short" == *"${o#-}"::* ]]; then + if [[ ${#1} -gt 2 ]]; then + opts=( "${opts[@]}" "$o" "${1:2}" ) + else + opts=( "${opts[@]}" "$o" '' ) + fi + elif [[ "$short" == *"${o#-}":* ]]; then + if [[ ${#1} -gt 2 ]]; then + opts=( "${opts[@]}" "$o" "${1:2}" ) + elif [[ $# -ge 2 ]]; then + shift + opts=( "${opts[@]}" "$o" "$1" ) + else + _getopt_err "$name: option requires an argument -- '${o#-}'" + error=1 + fi + elif [[ "$short" == *"${o#-}"* ]]; then + opts=( "${opts[@]}" "$o" ) + if [[ ${#1} -gt 2 ]]; then + set -- "$o" "-${1:2}" "${@:2}" + fi + else + if [[ $flags == *a* ]]; then + # Alternative parsing mode! Report on the entire failed + # option. GNU includes =value but we omit it for sanity with + # very long values. + _getopt_err "$name: unrecognized option '${1%%=*}'" + else + _getopt_err "$name: invalid option -- '${o#-}'" + if [[ ${#1} -gt 2 ]]; then + set -- "$o" "-${1:2}" "${@:2}" + fi + fi + error=1 + fi ;; + + (*) + # GNU getopt in-place mode (leading dash on short options) + # overrides POSIXLY_CORRECT + if [[ $flags == *i* ]]; then + opts=( "${opts[@]}" "$1" ) + elif [[ $flags == *p* ]]; then + params=( "${params[@]}" "$@" ) + break + else + params=( "${params[@]}" "$1" ) + fi + esac + + shift + done + + if [[ $flags == *Q* ]]; then + true # generate no output + else + echo -n ' ' + if [[ $flags == *[cu]* ]]; then + printf '%s -- %s' "${opts[*]}" "${params[*]}" + else + if [[ $flags == *t* ]]; then + _getopt_quote_csh "${opts[@]}" -- "${params[@]}" + else + _getopt_quote "${opts[@]}" -- "${params[@]}" + fi + fi + echo + fi + + return $error + } + + _getopt_err() { + if [[ $flags != *q* ]]; then + printf '%s\n' "$1" >&2 + fi + } + + _getopt_resolve_abbrev() { + # Resolves an abbrevation from a list of possibilities. + # If the abbreviation is unambiguous, echoes the expansion on stdout + # and returns 0. If the abbreviation is ambiguous, prints a message on + # stderr and returns 1. (For first parse this should convert to exit + # status 2.) If there is no match at all, prints a message on stderr + # and returns 2. + declare a q="$1" + declare -a matches + shift + for a; do + if [[ $q == "$a" ]]; then + # Exact match. Squash any other partial matches. + matches=( "$a" ) + break + elif [[ $flags == *a* && $q == -[^-]* && $a == -"$q" ]]; then + # Exact alternative match. Squash any other partial matches. + matches=( "$a" ) + break + elif [[ $a == "$q"* ]]; then + # Abbreviated match. + matches=( "${matches[@]}" "$a" ) + elif [[ $flags == *a* && $q == -[^-]* && $a == -"$q"* ]]; then + # Abbreviated alternative match. + matches=( "${matches[@]}" "${a#-}" ) + fi + done + case ${#matches[@]} in + (0) + [[ $flags == *q* ]] || \ + printf "$name: unrecognized option %s\\n" >&2 \ + "$(_getopt_quote "$q")" + return 2 ;; + (1) + printf '%s' "${matches[0]}"; return 0 ;; + (*) + [[ $flags == *q* ]] || \ + printf "$name: option %s is ambiguous; possibilities: %s\\n" >&2 \ + "$(_getopt_quote "$q")" "$(_getopt_quote "${matches[@]}")" + return 1 ;; + esac + } + + _getopt_split() { + # Splits $2 at commas to build array specified by $1 + declare IFS=, + eval "$1=( \$2 )" + } + + _getopt_quote() { + # Quotes arguments with single quotes, escaping inner single quotes + declare s space q=\' + for s; do + printf "$space'%s'" "${s//$q/$q\\$q$q}" + space=' ' + done + } + + _getopt_quote_csh() { + # Quotes arguments with single quotes, escaping inner single quotes, + # bangs, backslashes and newlines + declare s i c space + for s; do + echo -n "$space'" + for ((i=0; i<${#s}; i++)); do + c=${s:i:1} + case $c in + (\\|\'|!) + echo -n "'\\$c'" ;; + ($'\n') + echo -n "\\$c" ;; + (*) + echo -n "$c" ;; + esac + done + echo -n \' + space=' ' + done + } + + _getopt_help() { + cat <<-EOT >&2 + + Usage: + getopt + getopt [options] [--] + getopt [options] -o|--options [options] [--] + + Parse command options. + + Options: + -a, --alternative allow long options starting with single - + -l, --longoptions the long options to be recognized + -n, --name the name under which errors are reported + -o, --options the short options to be recognized + -q, --quiet disable error reporting by getopt(3) + -Q, --quiet-output no normal output + -s, --shell set quoting conventions to those of + -T, --test test for getopt(1) version + -u, --unquoted do not quote the output + + -h, --help display this help and exit + -V, --version output version information and exit + + For more details see getopt(1). + EOT + } + + _getopt_version_check() { + if [[ -z $BASH_VERSION ]]; then + echo "getopt: unknown version of bash might not be compatible" >&2 + return 1 + fi + + # This is a lexical comparison that should be sufficient forever. + if [[ $BASH_VERSION < 2.05b ]]; then + echo "getopt: bash $BASH_VERSION might not be compatible" >&2 + return 1 + fi + + return 0 + } + + _getopt_version_check + _getopt_main "$@" + declare status=$? + unset -f _getopt_main _getopt_err _getopt_parse _getopt_quote \ + _getopt_quote_csh _getopt_resolve_abbrev _getopt_split _getopt_help \ + _getopt_version_check + return $status +} + +[[ $BASH_SOURCE != "$0" ]] || main "$@" \ No newline at end of file diff --git a/helpers/terraform_validate b/helpers/terraform_validate new file mode 100755 index 0000000..0c28419 --- /dev/null +++ b/helpers/terraform_validate @@ -0,0 +1,23 @@ +#! /bin/bash +# +# Copyright 2019 Google LLC. This software is provided as-is, without warranty +# or representation for any use or purpose. Your use of it is subject to your +# agreement with Google. +# +# This script initializes modules so that terraform validate as of 0.12 behaves +# as expected and does not issue errors such as: +# +# Error: Module not installed +# +# on test/fixtures/shared_vpc_no_subnets/main.tf line 37: +# 37: module "project-factory" { +# +# This module is not yet installed. Run "terraform init" to install all modules +# required by this configuration. + +# The first and only argument to this script is the directory containing *.tf +# files to validate. This directory is assumed to be a root module. + +cd "$1" +terraform init -backend=false +terraform validate \ No newline at end of file diff --git a/main.tf b/main.tf index b36edb7..a67fd7a 100644 --- a/main.tf +++ b/main.tf @@ -15,23 +15,24 @@ */ locals { - stdlib_head = "${file("${path.module}/files/startup-script-stdlib-head.sh")}" - gsutil_el = "${var.enable_init_gsutil_crcmod_el ? file("${path.module}/files/init_gsutil_crcmod_el.sh") : ""}" - sudoers = "${var.enable_setup_sudoers ? file("${path.module}/files/setup_sudoers.sh") : ""}" - get_from_bucket = "${var.enable_get_from_bucket ? file("${path.module}/files/get_from_bucket.sh") : ""}" - setup_init_script = "${(var.enable_setup_init_script && var.enable_get_from_bucket) ? file("${path.module}/files/setup_init_script.sh") : ""}" - stdlib_body = "${file("${path.module}/files/startup-script-stdlib-body.sh")}" + stdlib_head = file("${path.module}/files/startup-script-stdlib-head.sh") + gsutil_el = var.enable_init_gsutil_crcmod_el ? file("${path.module}/files/init_gsutil_crcmod_el.sh") : "" + sudoers = var.enable_setup_sudoers ? file("${path.module}/files/setup_sudoers.sh") : "" + get_from_bucket = var.enable_get_from_bucket ? file("${path.module}/files/get_from_bucket.sh") : "" + setup_init_script = var.enable_setup_init_script && var.enable_get_from_bucket ? file("${path.module}/files/setup_init_script.sh") : "" + stdlib_body = file("${path.module}/files/startup-script-stdlib-body.sh") # List representing complete content, to be concatenated together. stdlib_list = [ - "${local.stdlib_head}", - "${local.gsutil_el}", - "${local.get_from_bucket}", - "${local.setup_init_script}", - "${local.sudoers}", - "${local.stdlib_body}", + local.stdlib_head, + local.gsutil_el, + local.get_from_bucket, + local.setup_init_script, + local.sudoers, + local.stdlib_body, ] # Final content output to the user - stdlib = "${join("", local.stdlib_list)}" + stdlib = join("", local.stdlib_list) } + diff --git a/outputs.tf b/outputs.tf index eb4eb12..696016d 100644 --- a/outputs.tf +++ b/outputs.tf @@ -16,5 +16,6 @@ output "content" { description = "startup-script-stdlib.sh content as a string value." - value = "${local.stdlib}" + value = local.stdlib } + diff --git a/test/ci_integration.sh b/test/ci_integration.sh index a190d2a..52a428f 100755 --- a/test/ci_integration.sh +++ b/test/ci_integration.sh @@ -24,7 +24,7 @@ DELETE_AT_EXIT="$(mktemp -d)" finish() { echo 'BEGIN: finish() trap handler' >&2 - bundle exec kitchen destroy + kitchen destroy [[ -d "${DELETE_AT_EXIT}" ]] && rm -rf "${DELETE_AT_EXIT}" echo 'END: finish() trap handler' >&2 } @@ -59,9 +59,9 @@ main() { setup_environment set -x # Execute the test lifecycle - bundle exec kitchen create - bundle exec kitchen converge - bundle exec kitchen verify + kitchen create + kitchen converge + kitchen verify } # if script is being executed and not sourced. diff --git a/test/fixtures/gsutil/variables.tf b/test/fixtures/gsutil/variables.tf index 63cdc7b..3829151 100644 --- a/test/fixtures/gsutil/variables.tf +++ b/test/fixtures/gsutil/variables.tf @@ -16,12 +16,15 @@ variable "project_id" { description = "The project_id to deploy the example instance into. (e.g. \"simple-sample-project-1234\")" + type = string } variable "region" { description = "The region to deploy to" + type = string } variable "service_account_email" { description = "The service acocunt email to associate with the example instance. Should have storage.buckets.get to use stdlib::get_from_bucket" + type = string } diff --git a/test/fixtures/simple_example/main.tf b/test/fixtures/simple_example/main.tf index c84f6b1..2f25d34 100644 --- a/test/fixtures/simple_example/main.tf +++ b/test/fixtures/simple_example/main.tf @@ -15,7 +15,7 @@ */ module "example" { - source = "../../../examples/simple_example" - project_id = "${var.project_id}" - region = "${var.region}" + source = "../../../examples/simple_example" + project_id = var.project_id + region = var.region } diff --git a/test/fixtures/simple_example/outputs.tf b/test/fixtures/simple_example/outputs.tf index d7a0f17..740a9e1 100644 --- a/test/fixtures/simple_example/outputs.tf +++ b/test/fixtures/simple_example/outputs.tf @@ -16,10 +16,10 @@ # These outputs are fed to inspec as inputs. output "project_id" { description = "The project id intended for use as an input to the integration test suite" - value = "${var.project_id}" + value = var.project_id } output "region" { description = "The region, intended for use as an input to the integration test suite" - value = "${var.region}" + value = var.region } diff --git a/test/fixtures/simple_example/variables.tf b/test/fixtures/simple_example/variables.tf index 6806da3..56135e7 100644 --- a/test/fixtures/simple_example/variables.tf +++ b/test/fixtures/simple_example/variables.tf @@ -16,8 +16,10 @@ variable "project_id" { description = "The project_id to deploy the example instance into. (e.g. \"simple-sample-project-1234\")" + type = string } variable "region" { description = "The region to deploy to" + type = string } diff --git a/test/fixtures/simulated_ci_environment/README.md b/test/fixtures/simulated_ci_environment/README.md index 978bcaf..87ce34e 100644 --- a/test/fixtures/simulated_ci_environment/README.md +++ b/test/fixtures/simulated_ci_environment/README.md @@ -33,21 +33,20 @@ For example: [^]: (autogen_docs_start) - ## Inputs | Name | Description | Type | Default | Required | |------|-------------|:----:|:-----:|:-----:| -| billing_account | The billing account id associated with the project, e.g. XXXXXX-YYYYYY-ZZZZZZ | string | - | yes | -| folder_id | The numeric folder id to create resources | string | - | yes | -| organization_id | The numeric organization id | string | - | yes | -| project_id | The project_id to deploy the example instance into. (e.g. "simple-sample-project-1234") | string | - | yes | -| region | The region to deploy to | string | - | yes | +| billing\_account | The billing account id associated with the project, e.g. XXXXXX-YYYYYY-ZZZZZZ | string | n/a | yes | +| folder\_id | The numeric folder id to create resources | string | n/a | yes | +| organization\_id | The numeric organization id | string | n/a | yes | +| project\_id | The project_id to deploy the example instance into. (e.g. "simple-sample-project-1234") | string | n/a | yes | +| region | The region to deploy to | string | n/a | yes | ## Outputs | Name | Description | |------|-------------| -| service_account_private_key | The SA KEY JSON content. Store in GOOGLE_CREDENTIALS. This is equivalent to the `phoogle_sa` output in the infra repository | +| service\_account\_private\_key | The SA KEY JSON content. Store in GOOGLE_CREDENTIALS. This is equivalent to the `phoogle_sa` output in the infra repository | [^]: (autogen_docs_end) diff --git a/test/fixtures/simulated_ci_environment/main.tf b/test/fixtures/simulated_ci_environment/main.tf index 01c6268..d26f77f 100644 --- a/test/fixtures/simulated_ci_environment/main.tf +++ b/test/fixtures/simulated_ci_environment/main.tf @@ -15,8 +15,8 @@ */ provider "google" { - project = "${var.project_id}" - region = "${var.region}" + project = var.project_id + region = var.region } locals { @@ -35,32 +35,32 @@ locals { } resource "google_project" "startup_scripts" { - name = "${var.project_id}" - project_id = "${var.project_id}" - folder_id = "${var.folder_id}" - billing_account = "${var.billing_account}" + name = var.project_id + project_id = var.project_id + folder_id = var.folder_id + billing_account = var.billing_account } resource "google_project_service" "startup_scripts" { - project = "${google_project.startup_scripts.id}" - count = "${length(local.required_project_services)}" - service = "${element(local.required_project_services, count.index)}" + project = google_project.startup_scripts.id + count = length(local.required_project_services) + service = local.required_project_services[count.index] } resource "google_service_account" "startup_scripts" { - project = "${google_project.startup_scripts.id}" + project = google_project.startup_scripts.id account_id = "ci-startup-scripts" display_name = "ci-startup-scripts" } resource "google_project_iam_member" "startup_scripts" { depends_on = ["google_project_service.startup_scripts"] - count = "${length(local.required_service_account_project_roles)}" - project = "${google_project.startup_scripts.id}" - role = "${element(local.required_service_account_project_roles, count.index)}" + count = length(local.required_service_account_project_roles) + project = google_project.startup_scripts.id + role = local.required_service_account_project_roles[count.index] member = "serviceAccount:${google_service_account.startup_scripts.email}" } resource "google_service_account_key" "startup_scripts" { - service_account_id = "${google_service_account.startup_scripts.id}" + service_account_id = google_service_account.startup_scripts.id } diff --git a/test/fixtures/simulated_ci_environment/outputs.tf b/test/fixtures/simulated_ci_environment/outputs.tf index c1cba5d..d0f544a 100644 --- a/test/fixtures/simulated_ci_environment/outputs.tf +++ b/test/fixtures/simulated_ci_environment/outputs.tf @@ -15,5 +15,5 @@ */ output "service_account_private_key" { description = "The SA KEY JSON content. Store in GOOGLE_CREDENTIALS. This is equivalent to the `phoogle_sa` output in the infra repository" - value = "${base64decode(google_service_account_key.startup_scripts.private_key)}" + value = base64decode(google_service_account_key.startup_scripts.private_key) } diff --git a/test/fixtures/simulated_ci_environment/variables.tf b/test/fixtures/simulated_ci_environment/variables.tf index ad548b3..2ac0e86 100644 --- a/test/fixtures/simulated_ci_environment/variables.tf +++ b/test/fixtures/simulated_ci_environment/variables.tf @@ -16,23 +16,25 @@ variable "project_id" { description = "The project_id to deploy the example instance into. (e.g. \"simple-sample-project-1234\")" + type = string } variable "region" { description = "The region to deploy to" + type = string } variable "organization_id" { description = "The numeric organization id" - type = "string" + type = string } variable "folder_id" { description = "The numeric folder id to create resources" - type = "string" + type = string } variable "billing_account" { description = "The billing account id associated with the project, e.g. XXXXXX-YYYYYY-ZZZZZZ" - type = "string" + type = string } diff --git a/test/make.sh b/test/make.sh index 3dd874b..fd4ff64 100755 --- a/test/make.sh +++ b/test/make.sh @@ -14,6 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Please note that this file was generated from [terraform-google-module-template](https://github.com/terraform-google-modules/terraform-google-module-template). +# Please make sure to contribute relevant changes upstream! + # Create a temporary directory that's auto-cleaned, even if the process aborts. DELETE_AT_EXIT="$(mktemp -d)" finish() { @@ -32,13 +35,17 @@ maketemp() { find_files() { local pth="$1" shift - find "${pth}" '(' -path '*/.git' -o -path '*/.terraform' ')' \ + find "${pth}" '(' \ + -path '*/.git' \ + -o -path '*/.terraform' \ + -o -path '*/.kitchen' \ + ')' \ -prune -o -type f "$@" } # Compatibility with both GNU and BSD style xargs. compat_xargs() { - local compat=() + local compat=() rval # Test if xargs is GNU or BSD style. GNU xargs will succeed with status 0 # when given --no-run-if-empty and no input on STDIN. BSD xargs will fail and # exit status non-zero If xargs fails, assume it is BSD style and proceed. @@ -47,6 +54,11 @@ compat_xargs() { compat=("--no-run-if-empty") fi xargs "${compat[@]}" "$@" + rval="$?" + if [[ -z "${NOWARN:-}" ]] && [[ "${rval}" -gt 0 ]]; then + echo "Warning: compat_xargs $* failed with exit code ${rval}" >&2 + fi + return "${rval}" } # This function makes sure that the required files for @@ -67,14 +79,28 @@ function docker() { | compat_xargs -0 hadolint } -# This function runs 'terraform validate' against all -# directory paths which contain *.tf files. +# This function runs 'terraform validate' and 'terraform fmt' +# against all directory paths which contain *.tf files. function check_terraform() { - echo "Running terraform validate" + local rval=125 + # fmt is before validate for faster feedback, validate requires terraform + # init which takes time. + echo "Running terraform fmt" find_files . -name "*.tf" -print0 \ | compat_xargs -0 -n1 dirname \ | sort -u \ - | compat_xargs -n1 terraform validate --check-variables=false + | compat_xargs -t -n1 terraform fmt -diff -check=true -write=false + rval="$?" + if [[ "${rval}" -gt 0 ]]; then + echo "Error: terraform fmt failed with exit code ${rval}" >&2 + echo "Check the output for diffs and correct using terraform fmt " >&2 + return "${rval}" + fi + echo "Running terraform validate" + find_files . -not -path "./test/fixtures/shared/*" -name "*.tf" -print0 \ + | compat_xargs -0 -n1 dirname \ + | sort -u \ + | compat_xargs -t -n1 helpers/terraform_validate } # This function runs 'go fmt' and 'go vet' on every file @@ -104,8 +130,11 @@ function check_shell() { # in any files in the project. # There are some exclusions function check_trailing_whitespace() { - echo "The following lines have trailing whitespace" - grep -r '[[:blank:]]$' --exclude-dir=".terraform" --exclude-dir=".kitchen" --exclude="*.png" --exclude="*.pyc" --exclude-dir=".git" . + local rc + echo "Checking for trailing whitespace" + find_files . -print \ + | grep -v -E '\.(pyc|png)$' \ + | NOWARN=1 compat_xargs grep -H -n '[[:blank:]]$' rc=$? if [[ ${rc} -eq 0 ]]; then return 1 @@ -114,16 +143,18 @@ function check_trailing_whitespace() { function generate_docs() { echo "Generating markdown docs with terraform-docs" - local path tmpfile - while read -r path; do - if [[ -e "${path}/README.md" ]]; then - # shellcheck disable=SC2119 - tmpfile="$(maketemp)" - echo "terraform-docs markdown ${path}" - terraform-docs markdown "${path}" > "${tmpfile}" - helpers/combine_docfiles.py "${path}"/README.md "${tmpfile}" + local pth helper_dir rval + helper_dir="$(pwd)/helpers" + while read -r pth; do + if [[ -e "${pth}/README.md" ]]; then + (cd "${pth}" || return 3; "${helper_dir}"/terraform_docs .;) + rval="$?" + if [[ "${rval}" -gt 0 ]]; then + echo "Error: terraform_docs in ${pth} exit code: ${rval}" >&2 + return "${rval}" + fi else - echo "Skipping ${path} because README.md does not exist." + echo "Skipping ${pth} because README.md does not exist." fi done < <(find_files . -name '*.tf' -print0 \ | compat_xargs -0 -n1 dirname \ @@ -135,4 +166,4 @@ function check_headers() { # Use the exclusion behavior of find_files find_files . -type f -print0 \ | compat_xargs -0 python test/verify_boilerplate.py -} +} \ No newline at end of file diff --git a/variables.tf b/variables.tf index 599b84f..8522767 100644 --- a/variables.tf +++ b/variables.tf @@ -15,20 +15,25 @@ */ variable "enable_init_gsutil_crcmod_el" { description = "If not false, include stdlib::init_gsutil_crcmod_el() prior to executing startup-script-custom. Call this function from startup-script-custom to initialize gsutil as per https://cloud.google.com/storage/docs/gsutil/addlhelp/CRC32CandInstallingcrcmod#centos-rhel-and-fedora Intended for CentOS, RHEL and Fedora systems." - default = "false" + type = bool + default = false } variable "enable_get_from_bucket" { description = "If not false, include stdlib::get_from_bucket() prior to executing startup-script-custom. Requires gsutil in the PATH. See also enable_init_gsutil_crcmod_el feature flag." - default = "false" + type = bool + default = false } variable "enable_setup_init_script" { description = "If not false, include stdlib::setup_init_script() prior to executing startup-script-custom. Call this function to load an init script from GCS into /etc/init.d and initialize it with chkconfig. This function depends on stdlib::get_from_bucket, so this function won't be enabled if enable_get_from_bucket is false." - default = "false" + type = bool + default = false } variable "enable_setup_sudoers" { description = "If true, include stdlib::setup_sudoers() prior to executing startup-script-custom. Call this function from startup-script-custom to setup unix usernames in sudoers Comma separated values must be posted to the project metadata key project/attributes/sudoers" - default = "false" + type = bool + default = false } + diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..fca1684 --- /dev/null +++ b/versions.tf @@ -0,0 +1,18 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +terraform { + required_version = ">= 0.12" +}