From 37523015fa204a5e098d1187a911581db3a04890 Mon Sep 17 00:00:00 2001 From: pp Date: Thu, 29 Aug 2019 15:54:50 +0300 Subject: [PATCH] Update testing to use Cloud Build * Based on PR #43 * Fix #49 * README.md - some syntax errors fixed * Splitted lint-test (Fix #51) * Improved test fixtures --- CONTRIBUTING.md | 217 ++++++++++++++ LICENSE | 3 +- Makefile | 161 +++------- README.md | 100 +------ build/int.cloudbuild.yaml | 41 +++ build/lint.cloudbuild.yaml | 47 +++ examples/folder/README.md | 3 +- examples/service_account/README.md | 1 + helpers/init_centos.sh | 121 -------- helpers/init_debian.sh | 120 -------- test/.gitignore | 1 + test/boilerplate/boilerplate.Dockerfile.txt | 13 - test/boilerplate/boilerplate.Makefile.txt | 13 - test/boilerplate/boilerplate.go.txt | 15 - test/boilerplate/boilerplate.py.txt | 13 - test/boilerplate/boilerplate.sh.txt | 13 - test/boilerplate/boilerplate.xml.txt | 15 - test/boilerplate/boilerplate.yaml.txt | 13 - test/ci_integration.sh | 94 ------ test/fixtures/full/base/main.tf | 52 +--- test/fixtures/full/base/outputs.tf | 11 +- test/fixtures/full/base/variables.tf | 19 +- test/fixtures/full/iam.tf | 95 +++--- test/fixtures/full/main.tf | 23 +- test/fixtures/full/outputs.tf | 13 +- test/fixtures/full/variables.tf | 20 +- test/integration/full/controls/gcloud.rb | 21 +- test/integration/full/inspec.yml | 5 +- test/make.sh | 154 ---------- test/setup/.gitignore | 2 + test/setup/iam.tf | 78 +++++ test/setup/main.tf | 65 ++++ test/setup/make_source.sh | 39 +++ test/setup/outputs.tf | 46 +++ .../boilerplate.tf.txt => setup/variables.tf} | 11 + .../versions.tf} | 23 +- test/terraform_validate | 24 -- test/test_verify_boilerplate.py | 136 --------- test/verify_boilerplate.py | 279 ------------------ 39 files changed, 759 insertions(+), 1361 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 build/int.cloudbuild.yaml create mode 100644 build/lint.cloudbuild.yaml delete mode 100755 helpers/init_centos.sh delete mode 100755 helpers/init_debian.sh create mode 100644 test/.gitignore delete mode 100644 test/boilerplate/boilerplate.Dockerfile.txt delete mode 100644 test/boilerplate/boilerplate.Makefile.txt delete mode 100644 test/boilerplate/boilerplate.go.txt delete mode 100644 test/boilerplate/boilerplate.py.txt delete mode 100644 test/boilerplate/boilerplate.sh.txt delete mode 100644 test/boilerplate/boilerplate.xml.txt delete mode 100644 test/boilerplate/boilerplate.yaml.txt delete mode 100755 test/ci_integration.sh delete mode 100755 test/make.sh create mode 100644 test/setup/.gitignore create mode 100644 test/setup/iam.tf create mode 100644 test/setup/main.tf create mode 100755 test/setup/make_source.sh create mode 100644 test/setup/outputs.tf rename test/{boilerplate/boilerplate.tf.txt => setup/variables.tf} (69%) rename test/{fixtures/full/terraform.tfvars.example => setup/versions.tf} (72%) delete mode 100755 test/terraform_validate delete mode 100755 test/test_verify_boilerplate.py delete mode 100755 test/verify_boilerplate.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6dbcc5db --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,217 @@ +# Contributing + +This document provides guidelines for contributing to the module. + +## File structure + +The project has the following folders and files: + +- /: root folder. +- /examples: examples for using this module. +- /scripts: Scripts for specific tasks on module (see Infrastructure section on this file). +- /test: Folders with files for testing the module (see Testing section on this file). +- /main.tf: main file for this module, contains all the variables for operate the module. +- /modules: modules to manage the IAM bindings for each resource type. +- /variables.tf: all the variables for the module. +- /output.tf: the outputs of the module. +- /readme.MD: this file. + +## Testing and documentation generation + +### Requirements +- [Docker Engine][docker-engine] +- [Google Cloud SDK][google-cloud-sdk] +- [make] + +### Integration testing +##### Terraform integration tests + +Integration tests are used to verify the behaviour of the root module, +submodules, and example modules. Additions, changes, and fixes should +be accompanied with tests. + +The integration tests are run using [Kitchen][kitchen], +[Kitchen-Terraform][kitchen-terraform], and [InSpec][inspec]. These +tools are packaged within a Docker image for convenience. + +The general strategy for these tests is to verify the behaviour of the +[example modules](./examples/), thus ensuring that the root module, +submodules, and example modules are all functionally correct. + +### Test Environment +The easiest way to test the module is in an isolated test project. The setup for such a project is defined in [test/setup](./test/setup/) directory. + +To use this setup, you need a service account with Project Creator access on a folder. Export the Service Account credentials to your environment like so: + +``` +export SERVICE_ACCOUNT_JSON=$(< credentials.json) +``` + +You will also need to set a few environment variables: +``` +export TF_VAR_org_id="your_org_id" +export TF_VAR_folder_id="your_folder_id" +export TF_VAR_billing_account="your_billing_account_id" +``` + +With these settings in place, you can prepare a test project using Docker: +``` +make docker_test_prepare +``` + +### Noninteractive Execution + +Run `make docker_test_integration` to test all of the example modules +noninteractively, using the prepared test project. + +### Interactive Execution + +1. Run `make docker_run` to start the testing Docker container in + interactive mode. + +1. Run `kitchen_do create ` to initialize the working + directory for an example module. + +1. Run `kitchen_do converge ` to apply the example module. + +1. Run `kitchen_do verify ` to test the example module. + +1. Run `kitchen_do destroy ` to destroy the example module + state. + +### Autogeneration of documentation from .tf files + +Run +``` +make generate_docs +``` + +### Lint testing + +Lint testing is also performed within a Docker container containing all the +dependencies required for lint tests. Execute those tests by running `make +docker_test_lint` from the root of the repository. + +Successful output looks similar to the following: + +``` +Checking for documentation generation +Checking for trailing whitespace +Checking for missing newline at end of file +Running shellcheck +Checking file headers +Running flake8 +Running terraform fmt +terraform fmt -diff -check=true -write=false . +terraform fmt -diff -check=true -write=false ./examples/folder +terraform fmt -diff -check=true -write=false ./examples/kms_crypto_key +terraform fmt -diff -check=true -write=false ./examples/kms_key_ring +terraform fmt -diff -check=true -write=false ./examples/organization +terraform fmt -diff -check=true -write=false ./examples/project +terraform fmt -diff -check=true -write=false ./examples/pubsub_subscription +terraform fmt -diff -check=true -write=false ./examples/pubsub_topic +terraform fmt -diff -check=true -write=false ./examples/service_account +terraform fmt -diff -check=true -write=false ./examples/stackdriver_agent_roles +terraform fmt -diff -check=true -write=false ./examples/storage_bucket +terraform fmt -diff -check=true -write=false ./examples/subnet +terraform fmt -diff -check=true -write=false ./modules/folders_iam +terraform fmt -diff -check=true -write=false ./modules/kms_crypto_keys_iam +terraform fmt -diff -check=true -write=false ./modules/kms_key_rings_iam +terraform fmt -diff -check=true -write=false ./modules/organizations_iam +terraform fmt -diff -check=true -write=false ./modules/projects_iam +terraform fmt -diff -check=true -write=false ./modules/pubsub_subscriptions_iam +terraform fmt -diff -check=true -write=false ./modules/pubsub_topics_iam +terraform fmt -diff -check=true -write=false ./modules/service_accounts_iam +terraform fmt -diff -check=true -write=false ./modules/storage_buckets_iam +terraform fmt -diff -check=true -write=false ./modules/subnets_iam +terraform fmt -diff -check=true -write=false ./test/fixtures/full +terraform fmt -diff -check=true -write=false ./test/fixtures/full/base +terraform fmt -diff -check=true -write=false ./test/setup +Running terraform validate +terraform_validate . +Success! The configuration is valid. + +terraform_validate ./examples/folder +Success! The configuration is valid. + +terraform_validate ./examples/kms_crypto_key +Success! The configuration is valid. + +terraform_validate ./examples/kms_key_ring +Success! The configuration is valid. + +terraform_validate ./examples/organization +Success! The configuration is valid. + +terraform_validate ./examples/project +Success! The configuration is valid. + +terraform_validate ./examples/pubsub_subscription +Success! The configuration is valid. + +terraform_validate ./examples/pubsub_topic +Success! The configuration is valid. + +terraform_validate ./examples/service_account +Success! The configuration is valid. + +terraform_validate ./examples/stackdriver_agent_roles +Success! The configuration is valid. + +terraform_validate ./examples/storage_bucket +Success! The configuration is valid. + +terraform_validate ./examples/subnet +Success! The configuration is valid. + +terraform_validate ./modules/folders_iam +Success! The configuration is valid. + +terraform_validate ./modules/kms_crypto_keys_iam +Success! The configuration is valid. + +terraform_validate ./modules/kms_key_rings_iam +Success! The configuration is valid. + +terraform_validate ./modules/organizations_iam +Success! The configuration is valid. + +terraform_validate ./modules/projects_iam +Success! The configuration is valid. + +terraform_validate ./modules/pubsub_subscriptions_iam +Success! The configuration is valid. + +terraform_validate ./modules/pubsub_topics_iam +Success! The configuration is valid. + +terraform_validate ./modules/service_accounts_iam +Success! The configuration is valid. + +terraform_validate ./modules/storage_buckets_iam +Success! The configuration is valid. + +terraform_validate ./modules/subnets_iam +Success! The configuration is valid. + +terraform_validate ./test/fixtures/full +Success! The configuration is valid. + +terraform_validate ./test/fixtures/full/base +Success! The configuration is valid. + +terraform_validate ./test/setup +Success! The configuration is valid. +``` + +[docker-engine]: https://www.docker.com/products/docker-engine +[flake8]: http://flake8.pycqa.org/en/latest/ +[gofmt]: https://golang.org/cmd/gofmt/ +[hadolint]: https://github.com/hadolint/hadolint +[inspec]: https://inspec.io/ +[kitchen-terraform]: https://github.com/newcontext-oss/kitchen-terraform +[kitchen]: https://kitchen.ci/ +[make]: https://en.wikipedia.org/wiki/Make_(software) +[shellcheck]: https://www.shellcheck.net/ +[terraform-docs]: https://github.com/segmentio/terraform-docs +[terraform]: https://terraform.io/ diff --git a/LICENSE b/LICENSE index 7a4a3ea2..6b0b1270 100644 --- a/LICENSE +++ b/LICENSE @@ -199,4 +199,5 @@ 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. \ No newline at end of file + limitations under the License. + diff --git a/Makefile b/Makefile index 5c8dc801..f3626423 100644 --- a/Makefile +++ b/Makefile @@ -18,132 +18,69 @@ # Make will use bash instead of sh 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} +DOCKER_TAG_VERSION_DEVELOPER_TOOLS := 0.1.0 +DOCKER_IMAGE_DEVELOPER_TOOLS := cft/developer-tools +REGISTRY_URL := gcr.io/cloud-foundation-cicd -# All is the first target in the file so it will get picked up when you just run 'make' on its own -.PHONY: all -all: check generate_docs - -# Run all available linters -.PHONY: check -check: check_shell check_python check_golang check_terraform check_base_files test_check_headers check_headers check_trailing_whitespace - -# 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 -# working -.PHONY: check_shell -check_shell: - @source test/make.sh && check_shell - -.PHONY: check_python -check_python: - @source test/make.sh && check_python - -.PHONY: check_golang -check_golang: - @source test/make.sh && golang - -.PHONY: check_terraform -check_terraform: - @source test/make.sh && check_terraform - -.PHONY: check_docker -check_docker: - @source test/make.sh && docker - -.PHONY: check_base_files -check_base_files: - @source test/make.sh && basefiles - -.PHONY: check_trailing_whitespace -check_trailing_whitespace: - @source test/make.sh && check_trailing_whitespace - -.PHONY: test_check_headers -test_check_headers: - @echo "Testing the validity of the header check" - @python test/test_verify_boilerplate.py - -.PHONY: check_headers -check_headers: - @source test/make.sh && check_headers - -# Integration tests -.PHONY: test_integration -test_integration: - test/ci_integration.sh - -.PHONY: generate_docs -generate_docs: - @source test/make.sh && generate_docs - -# Versioning -.PHONY: version -version: - @source helpers/version-repo.sh - -# Run docker +# Enter docker container for local development .PHONY: docker_run docker_run: docker run --rm -it \ - -e PROJECT_ID \ -e SERVICE_ACCOUNT_JSON \ - -e GOOGLE_APPLICATION_CREDENTIALS=${CREDENTIALS_PATH} \ - -v $(CURDIR):/cft/workdir \ - ${DOCKER_REPO_BASE_KITCHEN_TERRAFORM} \ - /bin/bash -c "cd /cft/workdir && source test/ci_integration.sh && setup_environment && exec /bin/bash" + -v $(CURDIR):/workspace \ + $(REGISTRY_URL)/${DOCKER_IMAGE_DEVELOPER_TOOLS}:${DOCKER_TAG_VERSION_DEVELOPER_TOOLS} \ + /bin/bash -.PHONY: docker_create -docker_create: +# Execute prepare tests within the docker container +.PHONY: docker_test_prepare +docker_test_prepare: docker run --rm -it \ - -e PROJECT_ID \ -e SERVICE_ACCOUNT_JSON \ - -e GOOGLE_APPLICATION_CREDENTIALS=${CREDENTIALS_PATH} \ - -v $(CURDIR):/cft/workdir \ - ${DOCKER_REPO_BASE_KITCHEN_TERRAFORM} \ - /bin/bash -c "cd /cft/workdir && source test/ci_integration.sh && setup_environment && kitchen create" - -.PHONY: docker_converge -docker_converge: + -e TF_VAR_org_id \ + -e TF_VAR_folder_id \ + -e TF_VAR_billing_account \ + -v $(CURDIR):/workspace \ + $(REGISTRY_URL)/${DOCKER_IMAGE_DEVELOPER_TOOLS}:${DOCKER_TAG_VERSION_DEVELOPER_TOOLS} \ + /usr/local/bin/execute_with_credentials.sh prepare_environment + +# Clean up test environment within the docker container +.PHONY: docker_test_cleanup +docker_test_cleanup: docker run --rm -it \ - -e PROJECT_ID \ -e SERVICE_ACCOUNT_JSON \ - -e GOOGLE_APPLICATION_CREDENTIALS=${CREDENTIALS_PATH} \ - -v $(CURDIR):/cft/workdir \ - ${DOCKER_REPO_BASE_KITCHEN_TERRAFORM} \ - /bin/bash -c "cd /cft/workdir && source test/ci_integration.sh && setup_environment && kitchen converge" - -.PHONY: docker_verify -docker_verify: + -e TF_VAR_org_id \ + -e TF_VAR_folder_id \ + -e TF_VAR_billing_account \ + -v $(CURDIR):/workspace \ + $(REGISTRY_URL)/${DOCKER_IMAGE_DEVELOPER_TOOLS}:${DOCKER_TAG_VERSION_DEVELOPER_TOOLS} \ + /usr/local/bin/execute_with_credentials.sh cleanup_environment + +# Execute integration tests within the docker container +.PHONY: docker_test_integration +docker_test_integration: docker run --rm -it \ - -e PROJECT_ID \ -e SERVICE_ACCOUNT_JSON \ - -e GOOGLE_APPLICATION_CREDENTIALS=${CREDENTIALS_PATH} \ - -v $(CURDIR):/cft/workdir \ - ${DOCKER_REPO_BASE_KITCHEN_TERRAFORM} \ - /bin/bash -c "cd /cft/workdir && source test/ci_integration.sh && setup_environment && kitchen verify" + -v $(CURDIR):/workspace \ + $(REGISTRY_URL)/${DOCKER_IMAGE_DEVELOPER_TOOLS}:${DOCKER_TAG_VERSION_DEVELOPER_TOOLS} \ + /usr/local/bin/test_integration.sh -.PHONY: docker_destroy -docker_destroy: +# Execute lint tests within the docker container +.PHONY: docker_test_lint +docker_test_lint: docker run --rm -it \ - -e PROJECT_ID \ - -e SERVICE_ACCOUNT_JSON \ - -e GOOGLE_APPLICATION_CREDENTIALS=${CREDENTIALS_PATH} \ - -v $(CURDIR):/cft/workdir \ - ${DOCKER_REPO_BASE_KITCHEN_TERRAFORM} \ - /bin/bash -c "cd /cft/workdir && source test/ci_integration.sh && setup_environment && kitchen destroy" + -v $(CURDIR):/workspace \ + $(REGISTRY_URL)/${DOCKER_IMAGE_DEVELOPER_TOOLS}:${DOCKER_TAG_VERSION_DEVELOPER_TOOLS} \ + /usr/local/bin/test_lint.sh -.PHONY: test_integration_docker -test_integration_docker: +# Generate documentation +.PHONY: docker_generate_docs +docker_generate_docs: docker run --rm -it \ - -e PROJECT_ID \ - -e SERVICE_ACCOUNT_JSON \ - -e GOOGLE_APPLICATION_CREDENTIALS=${CREDENTIALS_PATH} \ - -v $(CURDIR):/cft/workdir \ - ${DOCKER_REPO_BASE_KITCHEN_TERRAFORM} \ - /bin/bash -c "cd /cft/workdir && source test/ci_integration.sh && setup_environment && make test_integration" + -v $(CURDIR):/workspace \ + $(REGISTRY_URL)/${DOCKER_IMAGE_DEVELOPER_TOOLS}:${DOCKER_TAG_VERSION_DEVELOPER_TOOLS} \ + /bin/bash -c 'source /usr/local/bin/task_helper_functions.sh && generate_docs' + +# Alias for backwards compatibility +.PHONY: generate_docs +generate_docs: docker_generate_docs diff --git a/README.md b/README.md index 48452b60..40beca0d 100644 --- a/README.md +++ b/README.md @@ -282,20 +282,6 @@ You can choose the following resource types for apply the IAM bindings: Set the specified variable on the module call to choose the resources to affect. Remember to set the `mode` [variable](#variables) and give enough [permissions](#permissions) to manage the selected resource as well. -## File structure - -The project has the following folders and files: - -- /: root folder. -- /examples: examples for using this module. -- /scripts: Scripts for specific tasks on module (see Infrastructure section on this file). -- /test: Folders with files for testing the module (see Testing section on this file). -- /main.tf: main file for this module, contains all the variables for operate the module. -- /modules: modules to manage the IAM bindings for each resource type. -- /variables.tf: all the variables for the module. -- /output.tf: the outputs of the module. -- /readme.MD: this file. - ## Requirements ### Terraform plugins @@ -330,7 +316,7 @@ In order to execute this module you must have a Service Account with an appropri - Project compute admin: Full control of Compute Engine resources. - Project compute network admin: Full control of Compute Engine networking resources. - Project custom: Add compute.subnetworks.getIamPolicy and - compute.subnetworks..setIamPolicy permissions. + compute.subnetworks.setIamPolicy permissions. - Storage bucket: - Storage Admin: Full control of GCS resources. - Storage Legacy Bucket Owner: Read and write access to existing @@ -382,89 +368,5 @@ The script will do: - Compile the terraform-provider-google plugin - Move the terraform-provider-google to the right location -## Development - -### Requirements - -- [docker](https://www.docker.com/) v18.XX - -### Integration Tests - -Integration tests are run though -[test-kitchen](https://github.com/test-kitchen/test-kitchen), -[kitchen-terraform](https://github.com/newcontext-oss/kitchen-terraform), and -[InSpec](https://github.com/inspec/inspec). - -#### Test Setup - -1. Configure the test fixtures - ``` - cp test/fixtures/full/terraform.tfvars.example test/fixtures/full/terraform.tfvars - # Edit copied var file. - ``` -2. Download a Service Account key with the necessary [permissions](#permissions) - and put it in the module's root directory with the name `credentials.json`. -3. Run the testing container in interactive mode. - ``` - make docker_run - ``` - - The module root directory will be loaded into the Docker container at `/cftk/workdir/`. -4. Run kitchen-terraform to test the infrastructure. - - 1. `kitchen create` creates Terraform state. - 2. `kitchen converge` creates the underlying resources to later attach bindings to. - 3. `mv test/fixtures/full/iam.tf.mv test/fixtures/full/iam.tf` activate bindings. - 4. `kitchen create` re-init terraform plugins. - 5. `kitchen converge` apply IAM bindings. - 6. `kitchen verify` tests the created infrastructure. - 7. `kitchen destroy` remove all test fixtures. - -NOTE: Steps 3-5 are needed because the IAM terraform resources rely on computed values from the resources that are to have the bindings applied. - -Alternatively, you can simply run `make test_integration_docker` to run all the -test steps non-interactively. - -### Autogeneration of documentation from .tf files - -Run -``` -make generate_docs -``` - -### Linting - -The makefile in this project will lint or sometimes just format any shell, -Python, golang, Terraform, or Dockerfiles. The linters will only be run if -the makefile finds files with the appropriate file extension. - -All of the linter checks are in the default make target, so you just have to -run - -``` -make -s -``` - -The -s is for 'silent'. Successful output looks like this - -``` -Running shellcheck -Running flake8 -Running gofmt -Running terraform validate -Running hadolint on Dockerfiles -Test passed - Verified all file Apache 2 headers -``` - -The linters -are as follows: -* Shell - shellcheck. Can be found in homebrew -* Python - flake8. Can be installed with 'pip install flake8' -* Golang - gofmt. gofmt comes with the standard golang installation. golang -is a compiled language so there is no standard linter. -* Terraform - terraform has a built-in linter in the 'terraform validate' -command. -* Dockerfiles - hadolint. Can be found in homebrew - [v1.1.1]: https://registry.terraform.io/modules/terraform-google-modules/iam/google/1.1.1 [terraform-0.12-upgrade]: https://www.terraform.io/upgrade-guides/0-12.html diff --git a/build/int.cloudbuild.yaml b/build/int.cloudbuild.yaml new file mode 100644 index 00000000..acdecf83 --- /dev/null +++ b/build/int.cloudbuild.yaml @@ -0,0 +1,41 @@ +# Copyright 2019 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. + +timeout: 3600s +steps: +- id: prepare + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'source /usr/local/bin/task_helper_functions.sh && prepare_environment'] + env: + - 'TF_VAR_org_id=$_ORG_ID' + - 'TF_VAR_folder_id=$_FOLDER_ID' + - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' +- id: create + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'source /usr/local/bin/task_helper_functions.sh && kitchen_do create'] +- id: converge + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'source /usr/local/bin/task_helper_functions.sh && kitchen_do converge'] +- id: verify + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'source /usr/local/bin/task_helper_functions.sh && kitchen_do verify'] +- id: destroy + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'source /usr/local/bin/task_helper_functions.sh && kitchen_do destroy'] +tags: +- 'ci' +- 'integration' +substitutions: + _DOCKER_IMAGE_DEVELOPER_TOOLS: 'cft/developer-tools' + _DOCKER_TAG_VERSION_DEVELOPER_TOOLS: '0.1.0' diff --git a/build/lint.cloudbuild.yaml b/build/lint.cloudbuild.yaml new file mode 100644 index 00000000..dec06927 --- /dev/null +++ b/build/lint.cloudbuild.yaml @@ -0,0 +1,47 @@ +# Copyright 2019 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. + +steps: +- id: 'lint-pull-image' + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' +- id: 'lint-documentation' + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'source /usr/local/bin/task_helper_functions.sh && check_documentation'] + waitFor: ['lint-pull-image'] +- id: 'lint-whitespace' + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'source /usr/local/bin/task_helper_functions.sh && check_whitespace'] + waitFor: ['lint-pull-image'] +- id: 'lint-shell' + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'source /usr/local/bin/task_helper_functions.sh && check_shell'] + waitFor: ['lint-pull-image'] +- id: 'lint-headers' + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'source /usr/local/bin/task_helper_functions.sh && check_headers'] + waitFor: ['lint-pull-image'] +- id: 'lint-python' + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'source /usr/local/bin/task_helper_functions.sh && check_python'] + waitFor: ['lint-pull-image'] +- id: 'lint-terraform' + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'source /usr/local/bin/task_helper_functions.sh && check_terraform'] + waitFor: ['lint-pull-image'] +tags: +- 'ci' +- 'lint' +substitutions: + _DOCKER_IMAGE_DEVELOPER_TOOLS: 'cft/developer-tools' + _DOCKER_TAG_VERSION_DEVELOPER_TOOLS: '0.1.0' diff --git a/examples/folder/README.md b/examples/folder/README.md index ae093780..f4f6c954 100644 --- a/examples/folder/README.md +++ b/examples/folder/README.md @@ -9,4 +9,5 @@ | sa\_email | Email for Service Account to receive roles (Ex. default-sa@example-project-id.iam.gserviceaccount.com) | string | n/a | yes | | user\_email | Email for group to receive roles (Ex. user@example.com) | string | n/a | yes | - \ No newline at end of file + + diff --git a/examples/service_account/README.md b/examples/service_account/README.md index ce4b30f1..2381b143 100644 --- a/examples/service_account/README.md +++ b/examples/service_account/README.md @@ -3,6 +3,7 @@ | Name | Description | Type | Default | Required | |------|-------------|:----:|:-----:|:-----:| +| credentials\_file\_path | Path to the service account | string | n/a | yes | | group\_email | Email for group to receive roles (ex. group@example.com) | string | n/a | yes | | sa\_email | Email for Service Account to receive roles (Ex. default-sa@example-project-id.iam.gserviceaccount.com) | string | n/a | yes | | service\_account\_one | First service Account to add the IAM policies/bindings | string | n/a | yes | diff --git a/helpers/init_centos.sh b/helpers/init_centos.sh deleted file mode 100755 index d71597f0..00000000 --- a/helpers/init_centos.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/bash -# Copyright 2019 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. - - -# ####################################### # -# VARIABLES FOR SETUP THE ENVIRONMENT # -# Please modify the variables below # -# according to your system # -# ####################################### # - -# Where almost all the downloads are performed -HOME=$HOME - -# Arquitecture of your linux dist. (for terraform) -LINUXARQ="linux_amd64" -# Terraform installation path -TERRAFORM_HOME="$HOME/terraform" -# Terraform version -TERRAFORM_VERSION="0.10.8" -# Terraform plugins path -TERRAFORM_PLUGINS_PATH="$HOME/.terraform.d/plugins" - -# URL to bats repo -BATS_REPO="https://github.com/sstephenson/bats.git" -# Folder to install bats -BATS_HOME="/opt/bats" - -# ####################################### # -# Basic tool installation # -# ####################################### # -sudo yum -y install wget -sudo yum -y install curl -sudo yum -y install git -sudo yum -y install unzip -sudo yum -y install epel-release -sudo yum -y install epel-release -sudo yum -y install jq -sudo rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm - -# ####################################### # -# Terraform Installation # -# ####################################### # -sudo mkdir -p "$TERRAFORM_HOME" -cd "$TERRAFORM_HOME" || exit -#shellcheck disable=SC2153 -yes | sudo wget "https://releases.hashicorp.com/terraform/$TERRAFORM_VERSION/terraform_$TERRAFORM_VERSION_$LINUXARQ.zip" -yes | sudo unzip "terraform_$TERRAFORM_VERSION_$LINUXARQ.zip*" -sudo rm -f terraform_"$TERRAFORM_VERSION"_"$LINUXARQ".zip* - -export TERRAFORM_HOME=$TERRAFORM_HOME - -# ####################################### # -# Environment variables setup # -# ####################################### # - -export PATH="$PATH:$TERRAFORM_HOME" - -sudo touch /etc/profile.d/environment.sh -sudo chown "$(whoami)" /etc/profile.d/environment.sh -cat < diff --git a/test/boilerplate/boilerplate.yaml.txt b/test/boilerplate/boilerplate.yaml.txt deleted file mode 100644 index 086a24e6..00000000 --- a/test/boilerplate/boilerplate.yaml.txt +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2019 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. diff --git a/test/ci_integration.sh b/test/ci_integration.sh deleted file mode 100755 index b5dc0100..00000000 --- a/test/ci_integration.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2019 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. - -# Always clean up. -DELETE_AT_EXIT="$(mktemp -d)" -finish() { - local rv=$? - echo 'BEGIN: finish() trap handler' >&2 - if [[ "${rv}" -ne 0 ]]; then - echo 'BEGIN: .kitchen/logs/kitchen.log' - cat .kitchen/logs/kitchen.log - echo 'END: .kitchen/logs/kitchen.log' - echo 'BEGIN: kitchen diagnose --all' - kitchen diagnose --all - echo 'END: kitchen diagnose --all' - fi - kitchen destroy "$SUITE" - [[ -d "${DELETE_AT_EXIT}" ]] && rm -rf "${DELETE_AT_EXIT}" - echo 'END: finish() trap handler' >&2 - exit "${rv}" -} - -# Map the input parameters provided by Concourse CI, or whatever mechanism is -# running the tests to Terraform input variables. Also setup credentials for -# use with kitchen-terraform, inspec, and gcloud. -setup_environment() { - local tmpfile - tmpfile="$(mktemp)" - echo "${SERVICE_ACCOUNT_JSON}" > "${tmpfile}" - - # gcloud variables - export CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE="${tmpfile}" - # Application default credentials (Terraform google provider and inspec-gcp) - export GOOGLE_APPLICATION_CREDENTIALS="${tmpfile}" - - # Terraform variables - export TF_VAR_fixture_project_id="${PROJECT_ID}" - export TF_VAR_billing_account="${BILLING_ACCOUNT_ID}" - export TF_VAR_org_id="${ORG_ID}" - export TF_VAR_parent_id="${FOLDER_ID}" - export TF_VAR_mode="additive" - export TF_VAR_location=${REGION:-"us-central1"} -} - -main() { - export SUITE="${SUITE:-}" - - set -eu - # Setup trap handler to auto-cleanup - export TMPDIR="${DELETE_AT_EXIT}" - trap finish EXIT - - # Setup environment variables - setup_environment - - set -x - - # Execute the test lifecycle - if [[ -f "test/fixtures/full/iam.tf" ]]; then - mv "test/fixtures/full/iam.tf" "test/fixtures/full/iam.tf.mv" - fi - - kitchen create "$SUITE" - kitchen converge "$SUITE" || true - # First converge fails, give delay before the 2-nd attempt, - # to allow activation of the api being finished - sleep 30 - kitchen converge "$SUITE" - - # add test fixtures and apply them (for the details see the module's readme page) - cp "test/fixtures/full/iam.tf.mv" "test/fixtures/full/iam.tf" - kitchen create "$SUITE" - kitchen converge "$SUITE" - kitchen verify "$SUITE" -} - -# if script is being executed and not sourced. -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - main "$@" -fi - diff --git a/test/fixtures/full/base/main.tf b/test/fixtures/full/base/main.tf index 3789e99f..ca05a449 100644 --- a/test/fixtures/full/base/main.tf +++ b/test/fixtures/full/base/main.tf @@ -17,7 +17,7 @@ locals { n = 2 prefix = "test-iam" - location = var.location + location = var.base_location subnet_cdir = ["192.168.0.0/24", "192.168.10.0/24"] } @@ -27,7 +27,7 @@ resource "google_folder" "test" { count = local.n display_name = "${local.prefix}-folder-${count.index}-${random_id.test[count.index].hex}" - parent = var.parent_id + parent = var.base_parent_id } # Projects @@ -42,10 +42,9 @@ resource "google_project" "test" { count = local.n project_id = "${local.prefix}-prj-${count.index}-${random_id.test[count.index].hex}" - folder_id = var.parent_id - billing_account = var.billing_account - - name = "Test IAM Project ${count.index}" + folder_id = var.base_parent_id + name = "Test IAM Project ${count.index}" + billing_account = var.base_billing_account } # Service Accounts @@ -53,7 +52,7 @@ resource "google_project" "test" { resource "google_service_account" "test" { count = local.n - project = google_project.test[0].project_id + project = var.base_project_id account_id = "${local.prefix}-svcacct-${count.index}-${random_id.test[count.index].hex}" } @@ -63,29 +62,17 @@ resource "google_service_account" "test" { resource "google_storage_bucket" "test" { count = local.n - project = google_project.test[0].project_id + project = var.base_project_id name = "${local.prefix}-bkt-${count.index}-${random_id.test[count.index].hex}" } # KMS -resource "google_project_service" "kms" { - count = local.n - - project = google_project.test[count.index].project_id - service = "cloudkms.googleapis.com" -} - - - resource "google_kms_key_ring" "test" { count = local.n - depends_on = [google_project_service.kms] - - project = google_project.test[0].project_id - + project = var.base_project_id name = "${local.prefix}-keyrng-${count.index}-${random_id.test[count.index].hex}" location = local.location } @@ -103,15 +90,14 @@ resource "google_kms_crypto_key" "test" { resource "google_pubsub_topic" "test" { count = local.n - project = var.fixture_project_id - - name = "${local.prefix}-tpc-${count.index}-${random_id.test[count.index].hex}" + project = var.base_project_id + name = "${local.prefix}-tpc-${count.index}-${random_id.test[count.index].hex}" } resource "google_pubsub_subscription" "test" { count = local.n - project = var.fixture_project_id + project = var.base_project_id topic = google_pubsub_topic.test[count.index].name name = "${local.prefix}-sub-${count.index}-${random_id.test[count.index].hex}" @@ -119,27 +105,13 @@ resource "google_pubsub_subscription" "test" { # Subnets -resource "google_project_service" "subnet" { - project = google_project.test[0].project_id - service = "compute.googleapis.com" -} - resource "google_compute_subnetwork" "test" { count = local.n - depends_on = [google_project_service.subnet] - - project = google_project.test[0].project_id + project = var.base_project_id region = local.location name = "${local.prefix}-snet-${count.index}-${random_id.test[count.index].hex}" ip_cidr_range = local.subnet_cdir[count.index] network = "default" } -# Members - -resource "google_service_account" "member" { - count = local.n - project = google_project.test[0].project_id - account_id = "${local.prefix}-member-${count.index}-${random_id.test[count.index].hex}" -} diff --git a/test/fixtures/full/base/outputs.tf b/test/fixtures/full/base/outputs.tf index 1513a2f6..4644bb7e 100644 --- a/test/fixtures/full/base/outputs.tf +++ b/test/fixtures/full/base/outputs.tf @@ -33,11 +33,6 @@ output "service_accounts" { description = "Service accounts created for bindings." } -output "members" { - value = google_service_account.member.*.email - description = "Members created for binding with roles." -} - output "buckets" { value = google_storage_bucket.test.*.name description = "Storage buckets created for bindings." @@ -72,3 +67,9 @@ output "region" { value = local.location description = "Subnetworks region created for bindings." } + +output "bindings_number" { + value = local.n + description = "Number of each type of bindings, created for the test fixture." +} + diff --git a/test/fixtures/full/base/variables.tf b/test/fixtures/full/base/variables.tf index af7badfd..7d5a7e4d 100644 --- a/test/fixtures/full/base/variables.tf +++ b/test/fixtures/full/base/variables.tf @@ -14,22 +14,19 @@ * limitations under the License. */ -variable "parent_id" { - type = string +variable "base_billing_account" { + description = "The billing account id associated with the project, e.g. XXXXXX-YYYYYY-ZZZZZZ" +} + +variable "base_parent_id" { description = "Folder to create resources in, e.g. folders/12345678" } -variable "billing_account" { - type = string - description = "Billing account to associate created projects with." +variable "base_location" { + description = "Region for subnetwork tests." } -variable "fixture_project_id" { - type = string +variable "base_project_id" { description = "Project ID of the test fixture project. Used to avoid timing issues with recently created projects." } -variable "location" { - type = string - description = "Region for subnetwork tests." -} diff --git a/test/fixtures/full/iam.tf b/test/fixtures/full/iam.tf index cf55e56d..b06b13ef 100644 --- a/test/fixtures/full/iam.tf +++ b/test/fixtures/full/iam.tf @@ -15,11 +15,13 @@ */ module "iam_binding_project" { - source = "../../../modules/projects_iam" - mode = var.mode - projects = module.base.projects + source = "../../../modules/projects_iam" + mode = var.mode + projects = module.base.projects + projects_num = module.base.bindings_number - bindings = local.project_bindings + bindings = local.project_bindings + bindings_num = module.base.bindings_number } ## TODO(jmccune): Disabled as per discussion with Aaron. Re-enable post 0.12 @@ -32,11 +34,13 @@ module "iam_binding_project" { # } module "iam_binding_folder" { - source = "../../../modules/folders_iam" - mode = var.mode - folders = module.base.folders + source = "../../../modules/folders_iam" + mode = var.mode + folders = module.base.folders + folders_num = module.base.bindings_number - bindings = local.basic_bindings + bindings = local.folder_bindings + bindings_num = module.base.bindings_number } module "iam_binding_subnet" { @@ -45,57 +49,72 @@ module "iam_binding_subnet" { project = module.base.projects[0] subnets_region = module.base.region subnets = module.base.subnets + subnets_num = module.base.bindings_number - bindings = local.basic_bindings + bindings = local.basic_bindings + bindings_num = module.base.bindings_number } module "iam_binding_service_account" { - source = "../../../modules/service_accounts_iam" - mode = var.mode - service_accounts = module.base.service_accounts - project = module.base.projects[0] + source = "../../../modules/service_accounts_iam" + mode = var.mode + service_accounts = module.base.service_accounts + project = module.base.projects[0] + service_accounts_num = module.base.bindings_number - bindings = local.basic_bindings + bindings = local.basic_bindings + bindings_num = module.base.bindings_number } module "iam_binding_storage_bucket" { - source = "../../../modules/storage_buckets_iam" - mode = var.mode - storage_buckets = module.base.buckets + source = "../../../modules/storage_buckets_iam" + mode = var.mode + storage_buckets = module.base.buckets + storage_buckets_num = module.base.bindings_number - bindings = local.bucket_bindings + bindings = local.bucket_bindings + bindings_num = module.base.bindings_number } module "iam_binding_kms_crypto_key" { - source = "../../../modules/kms_crypto_keys_iam" - mode = var.mode - kms_crypto_keys = module.base.keys - bindings = local.basic_bindings + source = "../../../modules/kms_crypto_keys_iam" + mode = var.mode + kms_crypto_keys = module.base.keys + kms_crypto_keys_num = module.base.bindings_number + + bindings = local.basic_bindings + bindings_num = module.base.bindings_number } module "iam_binding_kms_key_ring" { - source = "../../../modules/kms_key_rings_iam" - mode = var.mode - kms_key_rings = module.base.key_rings + source = "../../../modules/kms_key_rings_iam" + mode = var.mode + kms_key_rings = module.base.key_rings + kms_key_rings_num = module.base.bindings_number - bindings = local.basic_bindings + bindings = local.basic_bindings + bindings_num = module.base.bindings_number } module "iam_binding_pubsub_subscription" { - source = "../../../modules/pubsub_subscriptions_iam" - mode = var.mode - pubsub_subscriptions = module.base.subscriptions - project = var.fixture_project_id - - bindings = local.basic_bindings + source = "../../../modules/pubsub_subscriptions_iam" + mode = var.mode + pubsub_subscriptions = module.base.subscriptions + project = var.project_id + pubsub_subscriptions_num = module.base.bindings_number + + bindings = local.basic_bindings + bindings_num = module.base.bindings_number } module "iam_binding_pubsub_topic" { - source = "../../../modules/pubsub_topics_iam" - mode = var.mode - pubsub_topics = module.base.topics - project = var.fixture_project_id - - bindings = local.basic_bindings + source = "../../../modules/pubsub_topics_iam" + mode = var.mode + pubsub_topics = module.base.topics + project = var.project_id + pubsub_topics_num = module.base.bindings_number + + bindings = local.basic_bindings + bindings_num = module.base.bindings_number } diff --git a/test/fixtures/full/main.tf b/test/fixtures/full/main.tf index 22e2289a..a6287442 100644 --- a/test/fixtures/full/main.tf +++ b/test/fixtures/full/main.tf @@ -17,16 +17,18 @@ locals { basic_roles = ["roles/owner", "roles/editor"] org_roles = ["roles/owner", "roles/iam.organizationRoleViewer"] + folder_roles = ["roles/resourcemanager.folderViewer", "roles/resourcemanager.folderMover"] project_roles = ["roles/iam.securityReviewer", "roles/iam.roleViewer"] bucket_roles = ["roles/storage.legacyObjectReader", "roles/storage.legacyBucketReader"] + members = [ var.member1, var.member2 ] member_group_0 = [ - "serviceAccount:${module.base.members[0]}", - "serviceAccount:${module.base.members[1]}", + "serviceAccount:${var.member1}", + "serviceAccount:${var.member2}", ] member_group_1 = [ - "serviceAccount:${module.base.members[0]}", + "serviceAccount:${var.member2}", ] basic_bindings = "${map( @@ -39,6 +41,11 @@ locals { local.org_roles[1], local.member_group_1, )}" + folder_bindings = "${map( + local.folder_roles[0], local.member_group_0, + local.folder_roles[1], local.member_group_1, + )}" + project_bindings = "${map( local.project_roles[0], local.member_group_0, local.project_roles[1], local.member_group_1, @@ -60,9 +67,9 @@ provider "google-beta" { } module "base" { - source = "./base" - billing_account = var.billing_account - parent_id = var.parent_id - fixture_project_id = var.fixture_project_id - location = var.location + source = "./base" + base_billing_account = var.billing_account + base_parent_id = var.folder_id + base_location = var.location + base_project_id = var.project_id } diff --git a/test/fixtures/full/outputs.tf b/test/fixtures/full/outputs.tf index 9a96babc..dafd2e87 100644 --- a/test/fixtures/full/outputs.tf +++ b/test/fixtures/full/outputs.tf @@ -26,6 +26,11 @@ output "org_roles" { description = "Roles to be assigned to organizations." } +output "folder_roles" { + value = local.folder_roles + description = "Roles to be assigned to folders." +} + output "project_roles" { value = local.project_roles description = "Roles to be assigned to projects." @@ -76,7 +81,7 @@ output "service_accounts" { } output "members" { - value = module.base.members + value = local.members description = "Members created for binding with roles." } @@ -105,7 +110,7 @@ output "subscriptions" { description = "Pubsub subscriptions created for bindings." } -output "fixture_project_id" { - value = var.fixture_project_id - description = "Project ID of the test fixture project. Used to avoid timing issues with recently created projects." +output "project_id" { + value = var.project_id + description = "Project ID of the test fixture project. Used to avoid timing issues with recently created projects." } diff --git a/test/fixtures/full/variables.tf b/test/fixtures/full/variables.tf index e142466c..14a742ca 100644 --- a/test/fixtures/full/variables.tf +++ b/test/fixtures/full/variables.tf @@ -17,9 +17,10 @@ variable "mode" { type = string description = "Mode of IAM management ('authoritative' OR 'additive')." + default = "additive" } -variable "parent_id" { +variable "folder_id" { type = string description = "Folder to create resources in, e.g. folders/12345678" } @@ -29,12 +30,23 @@ variable "billing_account" { description = "Billing account to associate created projects with." } -variable "fixture_project_id" { +variable "location" { + type = string + description = "Region for subnetwork tests." + default = "us-central1" +} + +variable "project_id" { type = string description = "Project ID of the test fixture project. Used to avoid timing issues with recently created projects." } -variable "location" { +variable "member1" { type = string - description = "Region for subnetwork tests." + description = "Member created for binding with roles." +} + +variable "member2" { + type = string + description = "Member created for binding with roles." } diff --git a/test/integration/full/controls/gcloud.rb b/test/integration/full/controls/gcloud.rb index dc41ddcd..5802c562 100644 --- a/test/integration/full/controls/gcloud.rb +++ b/test/integration/full/controls/gcloud.rb @@ -4,7 +4,7 @@ # 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 +# 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, @@ -13,7 +13,7 @@ # limitations under the License. # Fixture project as managed in cloud-foundation-toolkit/infra -fixture_project_id = attribute('fixture_project_id') +project_id = attribute('project_id') # Resource pairs (arrays of length = 2) folders = attribute('folders') @@ -29,6 +29,7 @@ # Role pairs (arrays of length = 2) basic_roles = attribute('basic_roles') +folder_roles = attribute('folder_roles') org_roles = attribute('org_roles') project_roles = attribute('project_roles') bucket_roles = attribute('bucket_roles') @@ -64,14 +65,14 @@ def assert_bindings(name, cmd, expected_role, expected_members) assert_bindings( 'folder-0', "gcloud beta resource-manager folders get-iam-policy #{folders[0]} --format='json(bindings)'", - basic_roles[0], + folder_roles[0], member_groups[0], ) assert_bindings( 'folder-1', "gcloud beta resource-manager folders get-iam-policy #{folders[1]} --format='json(bindings)'", - basic_roles[1], + folder_roles[1], member_groups[1], ) @@ -79,14 +80,14 @@ def assert_bindings(name, cmd, expected_role, expected_members) assert_bindings( 'subnet-0', - "gcloud beta compute networks subnets get-iam-policy #{subnets[0]} --project='#{projects[0]}' --region='#{region}' --format='json(bindings)'", + "gcloud beta compute networks subnets get-iam-policy #{subnets[0]} --project='#{project_id}' --region='#{region}' --format='json(bindings)'", basic_roles[0], member_groups[0], ) assert_bindings( 'subnet-1', - "gcloud beta compute networks subnets get-iam-policy #{subnets[1]} --project='#{projects[0]}' --region='#{region}' --format='json(bindings)'", + "gcloud beta compute networks subnets get-iam-policy #{subnets[1]} --project='#{project_id}' --region='#{region}' --format='json(bindings)'", basic_roles[1], member_groups[1], ) @@ -181,14 +182,14 @@ def split_key(k) assert_bindings( 'topic-0', - "gcloud beta pubsub topics get-iam-policy #{topics[0]} --project='#{fixture_project_id}' --format='json(bindings)'", + "gcloud beta pubsub topics get-iam-policy #{topics[0]} --project='#{project_id}' --format='json(bindings)'", basic_roles[0], member_groups[0], ) assert_bindings( 'topic-1', - "gcloud beta pubsub topics get-iam-policy #{topics[1]} --project='#{fixture_project_id}' --format='json(bindings)'", + "gcloud beta pubsub topics get-iam-policy #{topics[1]} --project='#{project_id}' --format='json(bindings)'", basic_roles[1], member_groups[1], ) @@ -197,14 +198,14 @@ def split_key(k) assert_bindings( 'subscription-0', - "gcloud beta pubsub subscriptions get-iam-policy #{subscriptions[0]} --project='#{fixture_project_id}' --format='json(bindings)'", + "gcloud beta pubsub subscriptions get-iam-policy #{subscriptions[0]} --project='#{project_id}' --format='json(bindings)'", basic_roles[0], member_groups[0], ) assert_bindings( 'subscription-1', - "gcloud beta pubsub subscriptions get-iam-policy #{subscriptions[1]} --project='#{fixture_project_id}' --format='json(bindings)'", + "gcloud beta pubsub subscriptions get-iam-policy #{subscriptions[1]} --project='#{project_id}' --format='json(bindings)'", basic_roles[1], member_groups[1], ) diff --git a/test/integration/full/inspec.yml b/test/integration/full/inspec.yml index c40af7e3..ad3783dc 100644 --- a/test/integration/full/inspec.yml +++ b/test/integration/full/inspec.yml @@ -59,9 +59,12 @@ attributes: - name: member_group_1 required: true type: array - - name: fixture_project_id + - name: project_id required: true type: string - name: region required: true type: string + - name: folder_roles + required: true + type: array diff --git a/test/make.sh b/test/make.sh deleted file mode 100755 index 4a7a60ab..00000000 --- a/test/make.sh +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2019 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. - -# 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() { - [[ -d "${DELETE_AT_EXIT}" ]] && rm -rf "${DELETE_AT_EXIT}" -} -trap finish EXIT -# Create a temporary file in the auto-cleaned up directory while avoiding -# overwriting TMPDIR for other processes. -# shellcheck disable=SC2120 # (Arguments may be passed, e.g. maketemp -d) -maketemp() { - TMPDIR="${DELETE_AT_EXIT}" mktemp "$@" -} - -# find_files is a helper to exclude .git directories and match only regular -# files to avoid double-processing symlinks. -find_files() { - local pth="$1" - shift - # Note: Take care to use -print or -print0 when using this function, - # otherwise excluded directories will be included in the output. - 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=() - # 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. - # stderr is silently redirected to avoid console log spam. - if xargs --no-run-if-empty /dev/null; then - compat=("--no-run-if-empty") - fi - xargs "${compat[@]}" "$@" -} - -# This function makes sure that the required files for -# releasing to OSS are present -function basefiles() { - local fn required_files="LICENSE README.md" - echo "Checking for required files ${required_files}" - for fn in ${required_files}; do - test -f "${fn}" || echo "Missing required file ${fn}" - done -} - -# This function runs 'terraform validate' and 'terraform fmt' -# against all directory paths which contain *.tf files. -function check_terraform() { - echo "Running terraform validate" - #shellcheck disable=SC2156 - find . -name "*.tf" -not -path "./test/fixtures/shared/*" -not -path "./test/fixtures/all_examples/*" -exec bash -c 'cd $(dirname "{}") && terraform init && terraform validate ' \; - echo "Running terraform fmt" - find_files . -name "*.tf" -exec terraform fmt -check=true -write=false {} \; -} - -# This function runs 'go fmt' and 'go vet' on every file -# that ends in '.go' -function golang() { - echo "Running go fmt and go vet" - find_files . -name "*.go" -print0 | compat_xargs -0 -n1 go fmt - find_files . -name "*.go" -print0 | compat_xargs -0 -n1 go vet -} - -# This function runs the flake8 linter on every file -# ending in '.py' -function check_python() { - echo "Running flake8" - find_files . -name "*.py" -print0 | compat_xargs -0 flake8 - return 0 -} - -# This function runs the shellcheck linter on every -# file ending in '.sh' -function check_shell() { - echo "Running shellcheck" - find_files . -name "*.sh" -print0 | compat_xargs -0 shellcheck -x -} - -# This function makes sure that there is no trailing whitespace -# in any files in the project. -# There are some exclusions -function check_trailing_whitespace() { - local rc - echo "Checking for trailing whitespace" - find_files . -print \ - | grep -v -E '\.(pyc|png)$' \ - | compat_xargs grep -H -n '[[:blank:]]$' - rc=$? - if [[ ${rc} -eq 0 ]]; then - return 1 - fi -} - -function generate_docs() { - echo "Generating markdown docs with terraform-docs" - local path - while read -r path; do - if [[ -e "${path}/README.md" ]]; then - # script seem to be designed to work into current directory - cd "${path}" && echo "Working in ${path} ..." - terraform_docs.sh . && echo Success! || echo "Warning! Exit code: ${?}" - # shellcheck disable=SC2164 - cd - >/dev/null - else - echo "Skipping ${path} because README.md does not exist." - fi - done < <(find_files . -name '*.tf' -print0 \ - | compat_xargs -0 -n1 dirname \ - | sort -u) -} - -function prepare_test_variables() { - echo "Preparing terraform.tfvars files for integration tests" - #shellcheck disable=2044 - for i in $(find ./test/fixtures -type f -name terraform.tfvars.sample); do - destination=${i/%.sample/} - if [ ! -f "${destination}" ]; then - cp "${i}" "${destination}" - echo "${destination} has been created. Please edit it to reflect your GCP configuration." - fi - done -} - -function check_headers() { - echo "Checking file headers" - # Use the exclusion behavior of find_files - find_files . -type f -print0 \ - | compat_xargs -0 python test/verify_boilerplate.py -} - diff --git a/test/setup/.gitignore b/test/setup/.gitignore new file mode 100644 index 00000000..0e515f83 --- /dev/null +++ b/test/setup/.gitignore @@ -0,0 +1,2 @@ +terraform.tfvars +source.sh diff --git a/test/setup/iam.tf b/test/setup/iam.tf new file mode 100644 index 00000000..463735f0 --- /dev/null +++ b/test/setup/iam.tf @@ -0,0 +1,78 @@ +/** + * Copyright 2019 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. + */ + +locals { + int_required_proj_roles = [ + "roles/owner", + "roles/resourcemanager.projectIamAdmin", + "roles/iam.serviceAccountAdmin", + "roles/compute.admin", + "roles/compute.networkAdmin", + "roles/compute.storageAdmin", + "roles/pubsub.admin", + "roles/cloudkms.admin", + "roles/storage.admin", + "roles/composer.worker", + ] + + int_required_folder_roles = [ + "roles/resourcemanager.projectCreator", + "roles/resourcemanager.folderAdmin", + "roles/resourcemanager.folderIamAdmin", + "roles/owner", + "roles/billing.projectManager", + "roles/composer.worker", + ] + + int_required_ba_roles = [ + "roles/billing.user", + ] +} + +resource "google_service_account" "int_test" { + project = module.iam-project.project_id + account_id = "iam-int-test" + display_name = "iam-int-test" +} + +resource "google_project_iam_member" "int_test_project" { + count = length(local.int_required_proj_roles) + + project = module.iam-project.project_id + role = local.int_required_proj_roles[count.index] + member = "serviceAccount:${google_service_account.int_test.email}" +} + +resource "google_folder_iam_member" "int_test_folder" { + count = length(local.int_required_folder_roles) + + folder = google_folder.ci-iam-folder.name + role = local.int_required_folder_roles[count.index] + member = "serviceAccount:${google_service_account.int_test.email}" +} + +resource "google_billing_account_iam_member" "int_test_ba" { + count = length(local.int_required_ba_roles) + + billing_account_id = var.billing_account + role = local.int_required_ba_roles[count.index] + member = "serviceAccount:${google_service_account.int_test.email}" +} + +resource "google_service_account_key" "int_test" { + service_account_id = google_service_account.int_test.id +} + diff --git a/test/setup/main.tf b/test/setup/main.tf new file mode 100644 index 00000000..226bc142 --- /dev/null +++ b/test/setup/main.tf @@ -0,0 +1,65 @@ +/** + * Copyright 2019 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. + */ + +# Random folder name suffix + +resource "random_id" "folder-rand" { + byte_length = 2 +} + +resource "google_folder" "ci-iam-folder" { + display_name = "ci-tests-iam-folder-${random_id.folder-rand.hex}" + parent = "folders/${var.folder_id}" +} + +module "iam-project" { + source = "terraform-google-modules/project-factory/google" + version = "~> 3.0" + + name = "ci-iam" + random_project_id = true + org_id = var.org_id + folder_id = var.folder_id + billing_account = var.billing_account + auto_create_network = true + + activate_apis = [ + "admin.googleapis.com", + "appengine.googleapis.com", + "cloudbilling.googleapis.com", + "cloudresourcemanager.googleapis.com", + "compute.googleapis.com", + "iam.googleapis.com", + "iamcredentials.googleapis.com", + "oslogin.googleapis.com", + "serviceusage.googleapis.com", + "cloudkms.googleapis.com", + "pubsub.googleapis.com", + "storage-api.googleapis.com", + "servicenetworking.googleapis.com", + "storage-component.googleapis.com", + "iap.googleapis.com", + ] +} + +# Members + +resource "google_service_account" "member" { + count = 2 + project = module.iam-project.project_id + account_id = "ci-iam-member-${count.index}-${random_id.folder-rand.hex}" +} + diff --git a/test/setup/make_source.sh b/test/setup/make_source.sh new file mode 100755 index 00000000..8ff83703 --- /dev/null +++ b/test/setup/make_source.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +# 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. + +echo "#!/usr/bin/env bash" > ../source.sh + +project_id=$(terraform output project_id) +echo "export TF_VAR_project_id='$project_id'" >> ../source.sh + +sa_json=$(terraform output sa_key) +# shellcheck disable=SC2086 +echo "export SERVICE_ACCOUNT_JSON='$(echo $sa_json | base64 --decode)'" >> ../source.sh + +folder_id=$(terraform output folder_id) +echo "export TF_VAR_folder_id='$folder_id'" >> ../source.sh + +billing_account=$(terraform output billing_account) +echo "export TF_VAR_billing_account='$billing_account'" >> ../source.sh + +org_id=$(terraform output org_id) +echo "export TF_VAR_org_id='$org_id'" >> ../source.sh + +member1=$(terraform output member1) +echo "export TF_VAR_member1='$member1'" >> ../source.sh + +member2=$(terraform output member2) +echo "export TF_VAR_member2='$member2'" >> ../source.sh diff --git a/test/setup/outputs.tf b/test/setup/outputs.tf new file mode 100644 index 00000000..055c72a7 --- /dev/null +++ b/test/setup/outputs.tf @@ -0,0 +1,46 @@ +/** + * Copyright 2019 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. + */ + +output "project_id" { + value = module.iam-project.project_id +} + +output "sa_key" { + value = google_service_account_key.int_test.private_key + sensitive = true +} + +output "folder_id" { + value = google_folder.ci-iam-folder.id +} + +output "org_id" { + value = var.org_id +} + +output "billing_account" { + value = var.billing_account +} + +output "member1" { + value = google_service_account.member[0].email + description = "Members created for binding with roles." +} + +output "member2" { + value = google_service_account.member[1].email + description = "Members created for binding with roles." +} diff --git a/test/boilerplate/boilerplate.tf.txt b/test/setup/variables.tf similarity index 69% rename from test/boilerplate/boilerplate.tf.txt rename to test/setup/variables.tf index 83185dca..6d80b898 100644 --- a/test/boilerplate/boilerplate.tf.txt +++ b/test/setup/variables.tf @@ -13,3 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +variable "org_id" { + description = "The numeric organization id" +} + +variable "folder_id" { + description = "The folder to deploy in" +} + +variable "billing_account" { + description = "The billing account id associated with the project, e.g. XXXXXX-YYYYYY-ZZZZZZ" +} diff --git a/test/fixtures/full/terraform.tfvars.example b/test/setup/versions.tf similarity index 72% rename from test/fixtures/full/terraform.tfvars.example rename to test/setup/versions.tf index 90af68bd..753832a9 100644 --- a/test/fixtures/full/terraform.tfvars.example +++ b/test/setup/versions.tf @@ -14,7 +14,22 @@ * limitations under the License. */ -org_id = "000000000000" -billing_account = "00A0A0-00A000-0AA00A" -mode = "additive" -location = "us-central1" +terraform { + required_version = ">= 0.12" +} + +provider "google" { + version = "~> 2.13.0" +} + +provider "google-beta" { + version = "~> 2.13.0" +} + + +provider "null" { + version = "~> 2.1" +} +provider "random" { + version = "~> 2.2" +} diff --git a/test/terraform_validate b/test/terraform_validate deleted file mode 100755 index 29ff6a53..00000000 --- a/test/terraform_validate +++ /dev/null @@ -1,24 +0,0 @@ -#! /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. - -set -eu -cd "${1}" -terraform init -backend=false >/dev/null -terraform validate diff --git a/test/test_verify_boilerplate.py b/test/test_verify_boilerplate.py deleted file mode 100755 index 1fa3686e..00000000 --- a/test/test_verify_boilerplate.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2019 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. - -''' A simple test for the verify_boilerplate python script. -This will create a set of test files, both valid and invalid, -and confirm that the has_valid_header call returns the correct -value. - -It also checks the number of files that are found by the -get_files call. -''' -from copy import deepcopy -from tempfile import mkdtemp -from shutil import rmtree -import unittest -from verify_boilerplate import has_valid_header, get_refs, get_regexs, \ - get_args, get_files - - -class AllTestCase(unittest.TestCase): - """ - All of the setup, teardown, and tests are contained in this - class. - """ - - def write_file(self, filename, content, expected): - """ - A utility method that creates test files, and adds them to - the cases that will be tested. - - Args: - filename: (string) the file name (path) to be created. - content: (list of strings) the contents of the file. - expected: (boolean) True if the header is expected to be valid, - false if not. - """ - - file = open(filename, 'w+') - for line in content: - file.write(line + "\n") - file.close() - self.cases[filename] = expected - - def create_test_files(self, tmp_path, extension, header): - """ - Creates 2 test files for .tf, .xml, .go, etc and one for - Dockerfile, and Makefile. - - The reason for the difference is that Makefile and Dockerfile - don't have an extension. These would be substantially more - difficult to create negative test cases, unless the files - were written, deleted, and re-written. - - Args: - tmp_path: (string) the path in which to create the files - extension: (string) the file extension - header: (list of strings) the header/boilerplate content - """ - - content = "\n...blah \ncould be code or could be garbage\n" - special_cases = ["Dockerfile", "Makefile"] - header_template = deepcopy(header) - valid_filename = tmp_path + extension - valid_content = header_template.append(content) - if extension not in special_cases: - # Invalid test cases for non-*file files (.tf|.py|.sh|.yaml|.xml..) - invalid_header = [] - for line in header_template: - if "2019" in line: - invalid_header.append(line.replace('2019', 'YEAR')) - else: - invalid_header.append(line) - invalid_header.append(content) - invalid_content = invalid_header - invalid_filename = tmp_path + "invalid." + extension - self.write_file(invalid_filename, invalid_content, False) - valid_filename = tmp_path + "testfile." + extension - - valid_content = header_template - self.write_file(valid_filename, valid_content, True) - - def setUp(self): - """ - Set initial counts and values, and initializes the setup of the - test files. - """ - self.cases = {} - self.tmp_path = mkdtemp() + "/" - self.my_args = get_args() - self.my_refs = get_refs(self.my_args) - self.my_regex = get_regexs() - self.prexisting_file_count = len( - get_files(self.my_refs.keys(), self.my_args)) - for key in self.my_refs: - self.create_test_files(self.tmp_path, key, - self.my_refs.get(key)) - - def tearDown(self): - """ Delete the test directory. """ - rmtree(self.tmp_path) - - def test_files_headers(self): - """ - Confirms that the expected output of has_valid_header is correct. - """ - for case in self.cases: - if self.cases[case]: - self.assertTrue(has_valid_header(case, self.my_refs, - self.my_regex)) - else: - self.assertFalse(has_valid_header(case, self.my_refs, - self.my_regex)) - - def test_invalid_count(self): - """ - Test that the initial files found isn't zero, indicating - a problem with the code. - """ - self.assertFalse(self.prexisting_file_count == 0) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/verify_boilerplate.py b/test/verify_boilerplate.py deleted file mode 100755 index 06c24da5..00000000 --- a/test/verify_boilerplate.py +++ /dev/null @@ -1,279 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2019 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. -# Verifies that all source files contain the necessary copyright boilerplate -# snippet. -# This is based on existing work -# https://github.com/kubernetes/test-infra/blob/master/hack -# /verify_boilerplate.py -from __future__ import print_function -import argparse -import glob -import os -import re -import sys - - -def get_args(): - """Parses command line arguments. - - Configures and runs argparse.ArgumentParser to extract command line - arguments. - - Returns: - An argparse.Namespace containing the arguments parsed from the - command line - """ - parser = argparse.ArgumentParser() - parser.add_argument("filenames", - help="list of files to check, " - "all files if unspecified", - nargs='*') - rootdir = os.path.dirname(__file__) + "/../" - rootdir = os.path.abspath(rootdir) - parser.add_argument( - "--rootdir", - default=rootdir, - help="root directory to examine") - - default_boilerplate_dir = os.path.join(rootdir, "test/boilerplate") - parser.add_argument("--boilerplate-dir", default=default_boilerplate_dir) - return parser.parse_args() - - -def get_refs(ARGS): - """Converts the directory of boilerplate files into a map keyed by file - extension. - - Reads each boilerplate file's contents into an array, then adds that array - to a map keyed by the file extension. - - Returns: - A map of boilerplate lines, keyed by file extension. For example, - boilerplate.py.txt would result in the k,v pair {".py": py_lines} where - py_lines is an array containing each line of the file. - """ - refs = {} - - # Find and iterate over the absolute path for each boilerplate template - for path in glob.glob(os.path.join( - ARGS.boilerplate_dir, - "boilerplate.*.txt")): - extension = os.path.basename(path).split(".")[1] - ref_file = open(path, 'r') - ref = ref_file.read().splitlines() - ref_file.close() - refs[extension] = ref - return refs - - -# pylint: disable=too-many-locals -def has_valid_header(filename, refs, regexs): - """Test whether a file has the correct boilerplate header. - - Tests each file against the boilerplate stored in refs for that file type - (based on extension), or by the entire filename (eg Dockerfile, Makefile). - Some heuristics are applied to remove build tags and shebangs, but little - variance in header formatting is tolerated. - - Args: - filename: A string containing the name of the file to test - refs: A map of boilerplate headers, keyed by file extension - regexs: a map of compiled regex objects used in verifying boilerplate - - Returns: - True if the file has the correct boilerplate header, otherwise returns - False. - """ - try: - with open(filename, 'r') as fp: # pylint: disable=invalid-name - data = fp.read() - except IOError: - return False - basename = os.path.basename(filename) - extension = get_file_extension(filename) - if extension: - ref = refs[extension] - else: - ref = refs[basename] - # remove build tags from the top of Go files - if extension == "go": - con = regexs["go_build_constraints"] - (data, found) = con.subn("", data, 1) - # remove shebang - elif extension == "sh" or extension == "py": - she = regexs["shebang"] - (data, found) = she.subn("", data, 1) - data = data.splitlines() - # if our test file is smaller than the reference it surely fails! - if len(ref) > len(data): - return False - # trim our file to the same number of lines as the reference file - data = data[:len(ref)] - year = regexs["year"] - for datum in data: - if year.search(datum): - return False - - # if we don't match the reference at this point, fail - if ref != data: - return False - return True - - -def get_file_extension(filename): - """Extracts the extension part of a filename. - - Identifies the extension as everything after the last period in filename. - - Args: - filename: string containing the filename - - Returns: - A string containing the extension in lowercase - """ - return os.path.splitext(filename)[1].split(".")[-1].lower() - - -# These directories will be omitted from header checks -SKIPPED_DIRS = [ - 'Godeps', 'third_party', '_gopath', '_output', - '.git', 'vendor', '__init__.py', 'node_modules' -] - - -def normalize_files(files): - """Extracts the files that require boilerplate checking from the files - argument. - - A new list will be built. Each path from the original files argument will - be added unless it is within one of SKIPPED_DIRS. All relative paths will - be converted to absolute paths by prepending the root_dir path parsed from - the command line, or its default value. - - Args: - files: a list of file path strings - - Returns: - A modified copy of the files list where any any path in a skipped - directory is removed, and all paths have been made absolute. - """ - newfiles = [] - for pathname in files: - if any(x in pathname for x in SKIPPED_DIRS): - continue - newfiles.append(pathname) - for idx, pathname in enumerate(newfiles): - if not os.path.isabs(pathname): - newfiles[idx] = os.path.join(ARGS.rootdir, pathname) - return newfiles - - -def get_files(extensions, ARGS): - """Generates a list of paths whose boilerplate should be verified. - - If a list of file names has been provided on the command line, it will be - treated as the initial set to search. Otherwise, all paths within rootdir - will be discovered and used as the initial set. - - Once the initial set of files is identified, it is normalized via - normalize_files() and further stripped of any file name whose extension is - not in extensions. - - Args: - extensions: a list of file extensions indicating which file types - should have their boilerplate verified - - Returns: - A list of absolute file paths - """ - files = [] - if ARGS.filenames: - files = ARGS.filenames - else: - for root, dirs, walkfiles in os.walk(ARGS.rootdir): - # don't visit certain dirs. This is just a performance improvement - # as we would prune these later in normalize_files(). But doing it - # cuts down the amount of filesystem walking we do and cuts down - # the size of the file list - for dpath in SKIPPED_DIRS: - if dpath in dirs: - dirs.remove(dpath) - for name in walkfiles: - pathname = os.path.join(root, name) - files.append(pathname) - files = normalize_files(files) - outfiles = [] - for pathname in files: - basename = os.path.basename(pathname) - extension = get_file_extension(pathname) - if extension in extensions or basename in extensions: - outfiles.append(pathname) - return outfiles - - -def get_regexs(): - """Builds a map of regular expressions used in boilerplate validation. - - There are two scenarios where these regexes are used. The first is in - validating the date referenced is the boilerplate, by ensuring it is an - acceptable year. The second is in identifying non-boilerplate elements, - like shebangs and compiler hints that should be ignored when validating - headers. - - Returns: - A map of compiled regular expression objects, keyed by mnemonic. - """ - regexs = {} - # Search for "YEAR" which exists in the boilerplate, but shouldn't in the - # real thing - regexs["year"] = re.compile('YEAR') - # dates can be 2014, 2015, 2016, 2017, 2018, or 2019 company holder names - # can be anything - regexs["date"] = re.compile('(2014|2015|2016|2017|2018|2019)') - # strip // +build \n\n build constraints - regexs["go_build_constraints"] = re.compile(r"^(// \+build.*\n)+\n", - re.MULTILINE) - # strip #!.* from shell/python scripts - regexs["shebang"] = re.compile(r"^(#!.*\n)\n*", re.MULTILINE) - return regexs - - -def main(args): - """Identifies and verifies files that should have the desired boilerplate. - - Retrieves the lists of files to be validated and tests each one in turn. - If all files contain correct boilerplate, this function terminates - normally. Otherwise it prints the name of each non-conforming file and - exists with a non-zero status code. - """ - regexs = get_regexs() - refs = get_refs(args) - filenames = get_files(refs.keys(), args) - nonconforming_files = [] - for filename in filenames: - if not has_valid_header(filename, refs, regexs): - nonconforming_files.append(filename) - if nonconforming_files: - print('%d files have incorrect boilerplate headers:' % len( - nonconforming_files)) - for filename in sorted(nonconforming_files): - print(os.path.relpath(filename, args.rootdir)) - sys.exit(1) - - -if __name__ == "__main__": - ARGS = get_args() - main(ARGS)