diff --git a/.circleci/config.yml b/.circleci/config.yml index 55affbf..0a35a0e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,72 +1,96 @@ env: &env environment: - GRUNTWORK_INSTALLER_VERSION: v0.0.36 - MODULE_CI_VERSION: v0.46.0 - GO_VERSION: 1.21.1 GO111MODULE: auto - + GRUNTWORK_INSTALLER_VERSION: v0.0.36 + MODULE_CI_VERSION: v0.54.0 + TERRATEST_LOG_PARSER_VERSION: v0.37.0 + GOLANG_VERSION: 1.21.1 defaults: &defaults machine: enabled: true image: ubuntu-2004:2022.10.1 <<: *env - +run_precommit: &run_precommit + # Fail the build if the pre-commit hooks don't pass. Note: if you run $ pre-commit install locally within this repo, these hooks will + # execute automatically every time before you commit, ensuring the build never fails at this step! + name: run pre-commit hooks + command: | + pre-commit install + pre-commit run --all-files install_gruntwork_utils: &install_gruntwork_utils name: install gruntwork utils command: | curl -Ls https://raw.githubusercontent.com/gruntwork-io/gruntwork-installer/master/bootstrap-gruntwork-installer.sh | bash /dev/stdin --version "${GRUNTWORK_INSTALLER_VERSION}" gruntwork-install --module-name "gruntwork-module-circleci-helpers" --repo "https://github.com/gruntwork-io/terraform-aws-ci" --tag "${MODULE_CI_VERSION}" - gruntwork-install --module-name "kubernetes-circleci-helpers" --repo "https://github.com/gruntwork-io/terraform-aws-ci" --tag "${MODULE_CI_VERSION}" - - echo "Installing Go version $GO_VERSION" - curl -O --silent --location --fail --show-error "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz" - sudo rm -rf /usr/local/go - sudo tar -C /usr/local -xzf "go${GO_VERSION}.linux-amd64.tar.gz" - sudo ln -s /usr/local/go/bin/go /usr/bin/go - echo "The installed version of Go is now $(go version)" - -version: 2 + gruntwork-install --module-name "git-helpers" --repo "https://github.com/gruntwork-io/terraform-aws-ci" --tag "${MODULE_CI_VERSION}" + gruntwork-install --binary-name "terratest_log_parser" --repo "https://github.com/gruntwork-io/terratest" --tag "${TERRATEST_LOG_PARSER_VERSION}" + configure-environment-for-gruntwork-module \ + --mise-version "NONE" \ + --terraform-version "NONE" \ + --terragrunt-version "NONE" \ + --packer-version "NONE" \ + --go-version ${GOLANG_VERSION} +version: 2.1 +# --------------------------------------------------------------------------------------------------------------------- +# REUSABLE STEPS +# --------------------------------------------------------------------------------------------------------------------- +commands: + store_results: + description: Store test results for easy viewing. + steps: + - run: + command: terratest_log_parser --testlog /tmp/logs/all.log --outputdir /tmp/logs + when: always + - store_artifacts: + path: /tmp/logs + - store_test_results: + path: /tmp/logs +#---------------------------------------------------------------------------------------------------------------------- +# BUILD JOBS +#---------------------------------------------------------------------------------------------------------------------- jobs: - setup: + precommit: <<: *env docker: - - image: cimg/python:3.10.2 - + - image: 087285199408.dkr.ecr.us-east-1.amazonaws.com/circle-ci-test-image-base:go1.21.9-tf1.5-tg39.1-pck1.8-ci54.0 steps: - checkout - - # Install gruntwork utilities + # Fail the build if the pre-commit hooks don't pass. Note: if you run pre-commit install locally, these hooks will + # execute automatically every time before you commit, ensuring the build never fails at this step! - run: - <<: *install_gruntwork_utils - - - persist_to_workspace: - root: /home/circleci - paths: - - project - + <<: *run_precommit tests: <<: *defaults steps: + - checkout - attach_workspace: at: /home/circleci - - run: <<: *install_gruntwork_utils - - run: | - run-go-tests --path test --timeout 60m --packages . | (tee /tmp/logs/all.log || true) - + - run: + name: Run tests + command: | + mkdir -p /tmp/logs + run-go-tests \ + --path test \ + --timeout 60m \ + --packages . \ + | (tee /tmp/logs/all.log || true) + - store_results +#---------------------------------------------------------------------------------------------------------------------- +# WORKFLOWS +#---------------------------------------------------------------------------------------------------------------------- workflows: version: 2 build-and-test: jobs: - - setup: + - precommit: context: - AWS__PHXDEVOPS__circle-ci-test - GITHUB__PAT__gruntwork-ci filters: tags: only: /^v.*/ - - tests: context: - AWS__PHXDEVOPS__circle-ci-test @@ -75,8 +99,7 @@ workflows: - SLACK__WEBHOOK__refarch-deployer-test - SLACK__CHANNEL__test-workflow-approvals requires: - - setup + - precommit filters: tags: only: /^v.*/ - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..83cd2d8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/gruntwork-io/pre-commit + rev: v0.1.23 + hooks: + - id: terraform-fmt + - id: goimports diff --git a/Dockerfile b/Dockerfile index 76c2023..afe93b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,14 @@ # Dockerfile used in execution of Github Action -FROM gruntwork/terragrunt:0.0.2 -MAINTAINER Gruntwork +FROM gruntwork/terragrunt:0.1.0 +LABEL maintainer "Gruntwork " + +ENV MISE_CONFIG_DIR=~/.config/mise +ENV MISE_STATE_DIR=~/.local/state/mise +ENV MISE_DATA_DIR=~/.local/share/mise +ENV MISE_CACHE_DIR=~/.cache/mise +ENV ASDF_HASHICORP_TERRAFORM_VERSION_FILE=.terraform-version + +ENV PATH="~/.local/share/mise/shims:~/mise:${PATH}" COPY ["./src/main.sh", "/action/main.sh"] diff --git a/README.md b/README.md index 839a50f..865ceba 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,15 @@ A GitHub Action for installing and running Terragrunt Supported GitHub action inputs: -| Input Name | Description | Required | Example values | -|:---------------|:------------------------------------------------------------------|:--------:|:--------------:| -| tf_version | Terraform version to be used in Action execution | `true` | 1.4.6 | -| tg_version | Terragrunt version to be user in Action execution | `true` | 0.50.8 | -| tg_dir | Directory in which Terragrunt will be invoked | `true` | work | -| tg_command | Terragrunt command to execute | `true` | plan/apply | -| tg_comment | Add comment to Pull request with execution output | `false` | 0/1 | -| tg_add_approve | Automatically add "-auto-approve" to commands, enabled by default | `false` | 0/1 | +| Input Name | Description | Required | Example values | +|:---------------|:------------------------------------------------------------------|:-----------------------------------------:|:--------------:| +| tf_version | Terraform version to be used in Action execution | `true` if `tofu_version` is not supplied | 1.4.6 | +| tofu_version | OpenTofu version to be used in Action execution | `true` if `tf_version` is not supplied | 1.6.0 | +| tg_version | Terragrunt version to be user in Action execution | `true` | 0.50.8 | +| tg_dir | Directory in which Terragrunt will be invoked | `true` | work | +| tg_command | Terragrunt command to execute | `true` | plan/apply | +| tg_comment | Add comment to Pull request with execution output | `false` | 0/1 | +| tg_add_approve | Automatically add "-auto-approve" to commands, enabled by default | `false` | 0/1 | ## Environment Variables diff --git a/action.yml b/action.yml index 765fc59..0a2c5b4 100644 --- a/action.yml +++ b/action.yml @@ -12,7 +12,8 @@ inputs: required: true tf_version: description: 'Terraform version to install.' - required: true + tofu_version: + description: 'OpenTofu version to install.' tg_command: description: 'Terragrunt command to execute.' required: true diff --git a/src/main.sh b/src/main.sh index e12316d..1cfd9a6 100755 --- a/src/main.sh +++ b/src/main.sh @@ -27,13 +27,24 @@ function clean_multiline_text { } # install and switch particular terraform version +function install_tofu { + local -r version="$1" + if [[ "${version}" == "none" ]]; then + return + fi + log "Installing OpenTofu version ${version}" + mise install -y opentofu@"${version}" + mise use -g opentofu@"${version}" +} + function install_terraform { local -r version="$1" if [[ "${version}" == "none" ]]; then return fi - tfenv install "${version}" - tfenv use "${version}" + log "Installing Terraform version ${version}" + mise install terraform@"${version}" + mise use -g terraform@"${version}" } # install passed terragrunt version @@ -42,7 +53,9 @@ function install_terragrunt { if [[ "${version}" == "none" ]]; then return fi - TG_VERSION="${version}" tgswitch + log "Installing Terragrunt version ${version}" + mise install -y terragrunt@"${version}" + mise use -g terragrunt@"${version}" } # run terragrunt commands in specified directory @@ -139,13 +152,19 @@ function main { trap 'log "Finished Terragrunt Action execution"' EXIT local -r tf_version=${INPUT_TF_VERSION} local -r tg_version=${INPUT_TG_VERSION} + local -r tofu_version=${INPUT_TOFU_VERSION} local -r tg_command=${INPUT_TG_COMMAND} local -r tg_comment=${INPUT_TG_COMMENT:-0} local -r tg_add_approve=${INPUT_TG_ADD_APPROVE:-1} local -r tg_dir=${INPUT_TG_DIR:-.} - if [[ -z "${tf_version}" ]]; then - log "tf_version is not set" + if [[ (-z "${tf_version}") && (-z "${tofu_version}")]]; then + log "One of tf_version or tofu_version must be set" + exit 1 + fi + + if [[ (-n "${tf_version}") && (-n "${tofu_version}")]]; then + log "Only one of tf_version and tofu_version may be set" exit 1 fi @@ -163,11 +182,26 @@ function main { trap 'setup_permissions $tg_dir ' EXIT setup_pre_exec - install_terraform "${tf_version}" + if [[ -n "${tf_version}" ]]; then + install_terraform "${tf_version}" + fi + if [[ -n "${tofu_version}" ]]; then + if [[ "${tg_version}" < 0.52.0 ]]; then + log "Terragrunt version ${tg_version} is incompatible with OpenTofu. Terragrunt version 0.52.0 or greater must be specified in order to use OpenTofu." + exit 1 + fi + install_tofu "${tofu_version}" + fi + install_terragrunt "${tg_version}" # add auto approve for apply and destroy commands local tg_arg_and_commands="${tg_command}" + if [[ -n "${tofu_version}" ]]; then + log "Using OpenTofu" + export TERRAGRUNT_TFPATH=tofu + fi + if [[ "$tg_command" == "apply"* || "$tg_command" == "destroy"* || "$tg_command" == "run-all apply"* || "$tg_command" == "run-all destroy"* ]]; then export TERRAGRUNT_NON_INTERACTIVE=true export TF_INPUT=false diff --git a/terragrunt/Dockerfile b/terragrunt/Dockerfile index 60066fb..c5f533d 100644 --- a/terragrunt/Dockerfile +++ b/terragrunt/Dockerfile @@ -1,10 +1,9 @@ # Container to run Terragrunt and Terraform -# Contains inside TFenv and TGSwitch to allow users to install custom Terraform and Terragrunt versions +# Contains inside mise to allow users to install custom Terraform and Terragrunt versions FROM ubuntu:22.04 -MAINTAINER Gruntwork +LABEL maintainer "Gruntwork " -ARG TF_ENV_VERSION=v3.0.0 -ARG TGSWITCH_VERSION=0.6.0 +ARG MISE_VERSION_INSTALL=v2024.4.0 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y \ @@ -17,28 +16,29 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # Create runner user -RUN addgroup --system --gid 127 docker -RUN useradd --system -u 1001 -g 127 -ms /bin/bash runner -RUN usermod -aG sudo runner && echo 'runner ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers +RUN addgroup --system --gid 127 docker \ + && useradd --system -u 1001 -g 127 -ms /bin/bash runner \ + && usermod -aG sudo runner && echo 'runner ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers USER runner +WORKDIR /home/runner RUN mkdir -p /home/runner/.ssh COPY ./known_hosts /home/runner/.ssh/known_hosts -# clone tfenv -RUN git clone --depth=1 --branch ${TF_ENV_VERSION} https://github.com/tfutils/tfenv.git ~/.tfenv -RUN echo 'export PATH="${HOME}/.tfenv/bin:${PATH}"' >> ~/.bash_profile +# install mise +RUN mkdir -p "${HOME}/mise" \ + && wget -q "https://github.com/jdx/mise/releases/download/${MISE_VERSION_INSTALL}/mise-${MISE_VERSION_INSTALL}-linux-x64" -O "/${HOME}/mise/mise" \ + && chmod u+x "${HOME}/mise/mise" -# install tgswitch -RUN mkdir -p "${HOME}/tgswitch" -RUN wget -q https://github.com/warrensbox/tgswitch/releases/download/${TGSWITCH_VERSION}/tgswitch_${TGSWITCH_VERSION}_linux_amd64.tar.gz -O /tmp/tgswitch_${TGSWITCH_VERSION}_linux_amd64.tar.gz -RUN tar -xzf /tmp/tgswitch_${TGSWITCH_VERSION}_linux_amd64.tar.gz -C ${HOME}/tgswitch -RUN chmod u+x ${HOME}/tgswitch/tgswitch -RUN rm -rf /tmp/tgswitch_${TGSWITCH_VERSION}_linux_amd64.tar.gz +ENV MISE_CONFIG_DIR=~/.config/mise +ENV MISE_STATE_DIR=~/.local/state/mise +ENV MISE_DATA_DIR=~/.local/share/mise +ENV MISE_CACHE_DIR=~/.cache/mise +ENV ASDF_HASHICORP_TERRAFORM_VERSION_FILE=.terraform-version # Running action as runner user # https://docs.github.com/en/actions/creating-actions/dockerfile-support-for-github-actions#user -ENV PATH="/home/runner/.tfenv/bin:/home/runner/tgswitch:/home/runner/bin:${PATH}" +ENV PATH="~/.local/share/mise/shims:~/mise:${PATH}" ENV TF_INPUT=false ENV TF_IN_AUTOMATION=1 diff --git a/terragrunt/README.md b/terragrunt/README.md index d948b87..b825f78 100644 --- a/terragrunt/README.md +++ b/terragrunt/README.md @@ -1,17 +1,16 @@ # Docker image to run terragrunt -Docker image with TGEnv and TGSwitch installed inside, which can be used to install and run Terragrunt. +Docker image with [`mise`](https://mise.jdx.dev/) installed inside, which can be used to install and run Terragrunt. Example usage: ``` -tfenv install "1.4.6" -tfenv use "1.4.6" -TG_VERSION="0.46.3" tgswitch +mise use terraform@1.4.6 +mise use opentofu@1.6.2 +mise use terragrunt@0.46.3 terragrunt ... ``` ## References -* https://github.com/tfutils/tfenv -* https://github.com/warrensbox/tgswitch \ No newline at end of file +* https://mise.jdx.dev/ \ No newline at end of file diff --git a/test/action.go b/test/action.go index 2718abe..f1c6fa0 100644 --- a/test/action.go +++ b/test/action.go @@ -1,9 +1,10 @@ package test import ( + "testing" + "github.com/gruntwork-io/terratest/modules/docker" "github.com/gruntwork-io/terratest/modules/random" - "testing" ) func buildActionImage(t *testing.T) string { diff --git a/test/action_container_test.go b/test/action_container_test.go index e1104dd..04ec77e 100644 --- a/test/action_container_test.go +++ b/test/action_container_test.go @@ -9,7 +9,7 @@ import ( func TestActionContainerIsBuilt(t *testing.T) { tag := buildActionImage(t) - + opts := &docker.RunOptions{Entrypoint: "/bin/bash", Command: []string{"-c", "ls /action"}} output := docker.Run(t, tag, opts) assert.Equal(t, "main.sh", output) diff --git a/test/action_run_test.go b/test/action_run_test.go index f4f932e..87a27eb 100644 --- a/test/action_run_test.go +++ b/test/action_run_test.go @@ -1,77 +1,109 @@ package test import ( - "github.com/gruntwork-io/terratest/modules/files" - "github.com/stretchr/testify/require" "os" "path/filepath" "testing" + "github.com/gruntwork-io/terratest/modules/files" + "github.com/stretchr/testify/require" + "github.com/gruntwork-io/terratest/modules/docker" "github.com/stretchr/testify/assert" ) -func TestActionIsExecuted(t *testing.T) { +func TestTerragruntAction(t *testing.T) { t.Parallel() tag := buildActionImage(t) + + testCases := []struct { + iac_name string + iac_type string + iac_version string + tg_version string + }{ + {"Terraform", "TF", "1.4.6", "0.46.3"}, + {"OpenTofu", "TOFU", "1.6.0", "0.53.3"}, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.iac_name, func(t *testing.T) { + t.Parallel() + t.Run("testActionIsExecuted", func(t *testing.T) { + t.Parallel() + testActionIsExecuted(t, tc.iac_type, tc.iac_name, tc.iac_version, tc.tg_version, tag) + }) + t.Run("testOutputPlanIsUsedInApply", func(t *testing.T) { + t.Parallel() + testOutputPlanIsUsedInApply(t, tc.iac_type, tc.iac_name, tc.iac_version, tc.tg_version, tag) + }) + t.Run("testRunAllIsExecute", func(t *testing.T) { + t.Parallel() + testRunAllIsExecuted(t, tc.iac_type, tc.iac_name, tc.iac_version, tc.tg_version, tag) + }) + t.Run("testAutoApproveDelete", func(t *testing.T) { + t.Parallel() + testAutoApproveDelete(t, tc.iac_type, tc.iac_name, tc.iac_version, tc.tg_version, tag) + }) + }) + } +} + +func testActionIsExecuted(t *testing.T, iac_type string, iac_name string, iac_version string, tg_version string, tag string) { fixturePath := prepareFixture(t, "fixture-action-execution") - output := runAction(t, tag, fixturePath, "plan") - assert.Contains(t, output, "You can apply this plan to save these new output values to the Terraform") + outputTF := runAction(t, tag, fixturePath, iac_type, iac_version, tg_version, "plan") + assert.Contains(t, outputTF, "You can apply this plan to save these new output values to the "+iac_name) } -func TestOutputPlanIsUsedInApply(t *testing.T) { - t.Parallel() - tag := buildActionImage(t) +func testOutputPlanIsUsedInApply(t *testing.T, iac_type string, iac_name string, iac_version string, tg_version string, tag string) { fixturePath := prepareFixture(t, "fixture-dependencies-project") - output := runAction(t, tag, fixturePath, "run-all plan -out=plan.out") + output := runAction(t, tag, fixturePath, iac_type, iac_version, tg_version, "run-all plan -out=plan.out") assert.Contains(t, output, "1 to add, 0 to change, 0 to destroy") - output = runAction(t, tag, fixturePath, "run-all apply plan.out") + output = runAction(t, tag, fixturePath, iac_type, iac_version, tg_version, "run-all apply plan.out") assert.Contains(t, output, "1 added, 0 changed, 0 destroyed") } -func TestRunAllIsExecuted(t *testing.T) { - t.Parallel() - tag := buildActionImage(t) +func testRunAllIsExecuted(t *testing.T, iac_type string, iac_name string, iac_version string, tg_version string, tag string) { fixturePath := prepareFixture(t, "fixture-dependencies-project") - output := runAction(t, tag, fixturePath, "run-all plan") + output := runAction(t, tag, fixturePath, iac_type, iac_version, tg_version, "run-all plan") assert.Contains(t, output, "1 to add, 0 to change, 0 to destroy") - output = runAction(t, tag, fixturePath, "run-all apply") + output = runAction(t, tag, fixturePath, iac_type, iac_version, tg_version, "run-all apply") assert.Contains(t, output, "1 to add, 0 to change, 0 to destroy") - output = runAction(t, tag, fixturePath, "run-all destroy") + output = runAction(t, tag, fixturePath, iac_type, iac_version, tg_version, "run-all destroy") assert.Contains(t, output, "0 to add, 0 to change, 1 to destroy") assert.Contains(t, output, "Destroy complete! Resources: 1 destroyed") } -func TestAutoApproveDelete(t *testing.T) { - t.Parallel() - tag := buildActionImage(t) +func testAutoApproveDelete(t *testing.T, iac_type string, iac_name string, iac_version string, tg_version string, tag string) { fixturePath := prepareFixture(t, "fixture-dependencies-project") - output := runAction(t, tag, fixturePath, "run-all plan -out=plan.out") + output := runAction(t, tag, fixturePath, iac_type, iac_version, tg_version, "run-all plan -out=plan.out") assert.Contains(t, output, "1 to add, 0 to change, 0 to destroy") - output = runAction(t, tag, fixturePath, "run-all apply plan.out") + output = runAction(t, tag, fixturePath, iac_type, iac_version, tg_version, "run-all apply plan.out") assert.Contains(t, output, "1 added, 0 changed, 0 destroyed") // run destroy with auto-approve - output = runAction(t, tag, fixturePath, "run-all plan -destroy -out=destroy.out") + output = runAction(t, tag, fixturePath, iac_type, iac_version, tg_version, "run-all plan -destroy -out=destroy.out") assert.Contains(t, output, "0 to add, 0 to change, 1 to destroy") - output = runAction(t, tag, fixturePath, "run-all apply -destroy destroy.out") + output = runAction(t, tag, fixturePath, iac_type, iac_version, tg_version, "run-all apply -destroy destroy.out") assert.Contains(t, output, "Resources: 0 added, 0 changed, 1 destroyed") } -func runAction(t *testing.T, tag, fixturePath, command string) string { +func runAction(t *testing.T, tag, fixturePath, iac_type string, iac_version string, tg_version string, command string) string { opts := &docker.RunOptions{ EnvironmentVariables: []string{ - "INPUT_TF_VERSION=1.4.6", - "INPUT_TG_VERSION=0.46.3", + "INPUT_" + iac_type + "_VERSION=" + iac_version, + "INPUT_TG_VERSION=" + tg_version, "INPUT_TG_COMMAND=" + command, "INPUT_TG_DIR=/github/workspace/code", "GITHUB_OUTPUT=/tmp/logs",