diff --git a/.github/workflows/test-atmos-pro-enabled.yml b/.github/workflows/test-atmos_pro.yml similarity index 74% rename from .github/workflows/test-atmos-pro-enabled.yml rename to .github/workflows/test-atmos_pro.yml index 323aa8f8..4db22a15 100644 --- a/.github/workflows/test-atmos-pro-enabled.yml +++ b/.github/workflows/test-atmos_pro.yml @@ -1,10 +1,16 @@ -name: "Test - Atmos Pro Enabled" +name: "Atmos Pro Integration Tests" on: - workflow_dispatch: {} - - pull_request: - types: [opened, synchronize, reopened] + workflow_dispatch: + inputs: + ref: + description: "The fully-formed ref of the branch or tag that triggered the workflow run" + required: false + type: string + sha: + description: "The sha of the commit that triggered the workflow run" + required: false + type: string env: AWS_REGION: us-east-2 @@ -29,9 +35,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.ref }} + ref: ${{ inputs.ref }} - name: Setup Atmos Test Config + id: env shell: bash run: | mkdir -p ${{ runner.temp }} @@ -44,18 +51,23 @@ jobs: sed -i -e 's#__PLAN_ROLE__#${{ secrets.TERRAFORM_APPLY_ROLE }}#g' ${{ runner.temp }}/atmos.yaml sed -i -e 's#__APPLY_ROLE__#${{ secrets.TERRAFORM_APPLY_ROLE }}#g' ${{ runner.temp }}/atmos.yaml + echo "seed=${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${{ matrix.platform }}" >> $GITHUB_OUTPUT + cat ${{ runner.temp }}/atmos.yaml - name: Plan Atmos Component - uses: cloudposse/github-action-atmos-terraform-plan@v4 + uses: cloudposse/github-action-atmos-terraform-plan@v5 with: component: "foobar-atmos-pro" stack: "plat-ue2-sandbox" atmos-config-path: ${{ runner.temp }} + sha: ${{ inputs.sha }} + env: + TF_VAR_seed: ${{ steps.env.outputs.seed }} - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.ref }} + ref: ${{ inputs.ref }} - name: Wait 15 sec to wait s3 bucket would be consistent (read after write consistency) shell: bash @@ -69,8 +81,10 @@ jobs: component: "foobar-atmos-pro" stack: "plat-ue2-sandbox" atmos-config-path: ${{ runner.temp }} - atmos-version: ">= 1.174.0" - + sha: ${{ inputs.sha }} + env: + TF_VAR_seed: ${{ steps.env.outputs.seed }} + outputs: result: ${{ steps.current.outputs.status }} diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/test-basic.yml similarity index 70% rename from .github/workflows/integration-tests.yml rename to .github/workflows/test-basic.yml index bb1bca00..cf8c03ce 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/test-basic.yml @@ -1,10 +1,16 @@ name: "Integration Tests" on: - workflow_dispatch: {} - - pull_request: - types: [opened, synchronize, reopened] + workflow_dispatch: + inputs: + ref: + description: "The fully-formed ref of the branch or tag that triggered the workflow run" + required: false + type: string + sha: + description: "The sha of the commit that triggered the workflow run" + required: false + type: string env: AWS_REGION: us-east-2 @@ -26,9 +32,10 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.ref }} + ref: ${{ inputs.ref }} - shell: bash + id: env run: | mkdir -p ${{ runner.temp }} cp ./tests/${{ matrix.platform }}/atmos.yaml ${{ runner.temp }}/atmos.yaml @@ -41,21 +48,28 @@ jobs: sed -i -e 's#__PLAN_ROLE__#${{ secrets.TERRAFORM_APPLY_ROLE }}#g' ${{ runner.temp }}/atmos.yaml sed -i -e 's#__APPLY_ROLE__#${{ secrets.TERRAFORM_APPLY_ROLE }}#g' ${{ runner.temp }}/atmos.yaml + echo "seed=${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${{ matrix.platform }}" >> $GITHUB_OUTPUT + - name: Plan Atmos Component uses: cloudposse/github-action-atmos-terraform-plan@v4 with: component: "foobar" stack: "plat-ue2-sandbox" atmos-config-path: ${{ runner.temp }} + debug: true + sha: ${{ inputs.sha }} + env: + TF_VAR_seed: ${{ steps.env.outputs.seed }} + - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.ref }} + ref: ${{ inputs.ref }} - - name: Wait 15 sec to wait s3 bucket would be consistent (read after write consistency) + - name: Wait 60 sec to wait s3 bucket would be consistent (read after write consistency) shell: bash run: | - sleep 15; + sleep 60; - name: Apply Atmos Component uses: ./ @@ -64,3 +78,7 @@ jobs: stack: "plat-ue2-sandbox" atmos-config-path: ${{ runner.temp }} debug: true + sha: ${{ inputs.sha }} + env: + TF_VAR_seed: ${{ steps.env.outputs.seed }} + \ No newline at end of file diff --git a/.github/workflows/test-plan_diff.yml b/.github/workflows/test-plan_diff.yml new file mode 100644 index 00000000..3b01597e --- /dev/null +++ b/.github/workflows/test-plan_diff.yml @@ -0,0 +1,83 @@ +name: "Integration Tests - Plan diff" + +on: + workflow_dispatch: + inputs: + ref: + description: "The fully-formed ref of the branch or tag that triggered the workflow run" + required: false + type: string + sha: + description: "The sha of the commit that triggered the workflow run" + required: false + type: string + +env: + AWS_REGION: us-east-2 + + +# Permissions required for assuming AWS identity +permissions: + id-token: write + contents: read + +jobs: + test: + runs-on: ubuntu-latest + continue-on-error: true + strategy: + max-parallel: 1 + fail-fast: false # Don't fail fast to avoid locking TF State + matrix: + platform: [terraform, opentofu] + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + + - shell: bash + run: | + mkdir -p ${{ runner.temp }} + cp ./tests/${{ matrix.platform }}/atmos.yaml ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__INFRACOST_ENABLED__#false#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_REGION__#${{ env.AWS_REGION }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_BUCKET__#${{ secrets.TERRAFORM_STATE_BUCKET }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_TABLE__#${{ secrets.TERRAFORM_STATE_TABLE }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_TABLE__#${{ secrets.TERRAFORM_STATE_TABLE }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_ROLE__#${{ secrets.TERRAFORM_STATE_ROLE }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__PLAN_ROLE__#${{ secrets.TERRAFORM_APPLY_ROLE }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__APPLY_ROLE__#${{ secrets.TERRAFORM_APPLY_ROLE }}#g' ${{ runner.temp }}/atmos.yaml + + echo "seed=${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${{ matrix.platform }}" >> $GITHUB_OUTPUT + + - name: Plan Atmos Component + uses: cloudposse/github-action-atmos-terraform-plan@v4 + with: + component: "foobar-plan-diff" + stack: "plat-ue2-sandbox" + atmos-config-path: ${{ runner.temp }} + debug: true + sha: ${{ inputs.sha }} + env: + TF_VAR_seed: ${{ steps.env.outputs.seed }}-a + + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + + - name: Wait 15 sec to wait s3 bucket would be consistent (read after write consistency) + shell: bash + run: | + sleep 15; + + - name: Apply Atmos Component + uses: ./ + with: + component: "foobar-plan-diff" + stack: "plat-ue2-sandbox" + atmos-config-path: ${{ runner.temp }} + debug: true + sha: ${{ inputs.sha }} + env: + TF_VAR_seed: ${{ steps.env.outputs.seed }}-b + \ No newline at end of file diff --git a/.github/workflows/test-plan_fail.yml b/.github/workflows/test-plan_fail.yml new file mode 100644 index 00000000..9998f2b6 --- /dev/null +++ b/.github/workflows/test-plan_fail.yml @@ -0,0 +1,69 @@ +name: "Integration Tests - Plan Fail" + +on: + workflow_dispatch: + inputs: + ref: + description: "The fully-formed ref of the branch or tag that triggered the workflow run" + required: false + type: string + sha: + description: "The sha of the commit that triggered the workflow run" + required: false + type: string + +env: + AWS_REGION: us-east-2 + + +# Permissions required for assuming AWS identity +permissions: + id-token: write + contents: read + +jobs: + test: + runs-on: ubuntu-latest + continue-on-error: true + strategy: + max-parallel: 1 + fail-fast: false # Don't fail fast to avoid locking TF State + matrix: + platform: [terraform, opentofu] + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + + - shell: bash + id: env + run: | + mkdir -p ${{ runner.temp }} + cp ./tests/${{ matrix.platform }}/atmos.yaml ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__INFRACOST_ENABLED__#false#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_REGION__#${{ env.AWS_REGION }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_BUCKET__#${{ secrets.TERRAFORM_STATE_BUCKET }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_TABLE__#${{ secrets.TERRAFORM_STATE_TABLE }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_TABLE__#${{ secrets.TERRAFORM_STATE_TABLE }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_ROLE__#${{ secrets.TERRAFORM_STATE_ROLE }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__PLAN_ROLE__#${{ secrets.TERRAFORM_APPLY_ROLE }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__APPLY_ROLE__#${{ secrets.TERRAFORM_APPLY_ROLE }}#g' ${{ runner.temp }}/atmos.yaml + + echo "seed=${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${{ matrix.platform }}" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + + - name: Apply Atmos Component + uses: ./ + with: + component: "foobar-plan-fail" + stack: "plat-ue2-sandbox" + atmos-config-path: ${{ runner.temp }} + plan-storage: false + debug: true + sha: ${{ inputs.sha }} + env: + TF_VAR_seed: ${{ steps.env.outputs.seed }} + \ No newline at end of file diff --git a/.github/workflows/test-plan_storage_disabled.yml b/.github/workflows/test-plan_storage_disabled.yml new file mode 100644 index 00000000..ccfd2c2a --- /dev/null +++ b/.github/workflows/test-plan_storage_disabled.yml @@ -0,0 +1,68 @@ +name: "Integration Tests - Plan Storage disabled" + +on: + workflow_dispatch: + inputs: + ref: + description: "The fully-formed ref of the branch or tag that triggered the workflow run" + required: false + type: string + sha: + description: "The sha of the commit that triggered the workflow run" + required: false + type: string + +env: + AWS_REGION: us-east-2 + + +# Permissions required for assuming AWS identity +permissions: + id-token: write + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + max-parallel: 1 + fail-fast: false # Don't fail fast to avoid locking TF State + matrix: + platform: [terraform, opentofu] + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + + - shell: bash + id: env + run: | + mkdir -p ${{ runner.temp }} + cp ./tests/${{ matrix.platform }}/atmos.yaml ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__INFRACOST_ENABLED__#false#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_REGION__#${{ env.AWS_REGION }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_BUCKET__#${{ secrets.TERRAFORM_STATE_BUCKET }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_TABLE__#${{ secrets.TERRAFORM_STATE_TABLE }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_TABLE__#${{ secrets.TERRAFORM_STATE_TABLE }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__STORAGE_ROLE__#${{ secrets.TERRAFORM_STATE_ROLE }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__PLAN_ROLE__#${{ secrets.TERRAFORM_APPLY_ROLE }}#g' ${{ runner.temp }}/atmos.yaml + sed -i -e 's#__APPLY_ROLE__#${{ secrets.TERRAFORM_APPLY_ROLE }}#g' ${{ runner.temp }}/atmos.yaml + + echo "seed=${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${{ matrix.platform }}" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + + - name: Apply Atmos Component + uses: ./ + with: + component: "foobar-plan-storage" + stack: "plat-ue2-sandbox" + atmos-config-path: ${{ runner.temp }} + plan-storage: false + debug: true + sha: ${{ inputs.sha }} + env: + TF_VAR_seed: ${{ steps.env.outputs.seed }} + \ No newline at end of file diff --git a/README.yaml b/README.yaml index 5c0bedc9..2e7ec3a2 100644 --- a/README.yaml +++ b/README.yaml @@ -65,7 +65,8 @@ usage: |- ### Config > [!IMPORTANT] - > **Please note!** This GitHub Action only works with `atmos >= 1.158.0`. + > **Please note!** This GitHub Action only works with `atmos >= 1.186.0`. + > If you are using `atmos >= 1.158.0, < 1.186.0` please use `v4` version of this action. > If you are using `atmos >= 1.99.0, < 1.158.0` please use `v3` version of this action. > If you are using `atmos >= 1.63.0, < 1.99.0` please use `v2` version of this action. > If you are using `atmos < 1.63.0` please use `v1` version of this action. @@ -209,12 +210,19 @@ usage: |- runs-on: ubuntu-latest steps: - name: Terraform Apply - uses: cloudposse/github-action-atmos-terraform-apply@v2 + uses: cloudposse/github-action-atmos-terraform-apply@v5 with: component: "foobar" stack: "plat-ue2-sandbox" atmos-config-path: ./rootfs/usr/local/etc/atmos/ ``` + ### Migrating from `v4` to `v5` + + The notable changes in `v5` are: + + - `v5` works only with `atmos >= 1.186.0` + - `v5` uses `atmos terraform plan-diff` to ensure changes to be applied are consistent with the stored (approved by the user) planfile. + ### Migrating from `v3` to `v4` The notable changes in `v4` are: diff --git a/action.yml b/action.yml index 75e08bde..982923fa 100644 --- a/action.yml +++ b/action.yml @@ -22,7 +22,7 @@ inputs: atmos-version: description: The version of atmos to install required: false - default: ">= 1.158.0" + default: ">= 1.186.0" atmos-config-path: description: The path to the atmos.yaml file required: true @@ -190,7 +190,7 @@ runs: tag: ${{ startsWith(fromJson(steps.atmos-settings.outputs.settings).opentofu-version, 'v') && fromJson(steps.atmos-settings.outputs.settings).opentofu-version || format('v{0}', fromJson(steps.atmos-settings.outputs.settings).opentofu-version) }} skip: ${{ fromJson(steps.atmos-settings.outputs.settings).opentofu-version == '' || fromJson(steps.atmos-settings.outputs.settings).opentofu-version == 'null' }} suzuki-shunsuke/tfcmt: v4.14.0 - terraform-docs/terraform-docs: v0.18.0 + terraform-docs/terraform-docs: v0.20.0 - name: Configure Plan AWS Credentials uses: aws-actions/configure-aws-credentials@v4 @@ -238,7 +238,12 @@ runs: COMPONENT_NAME=$(echo "${{ inputs.component }}" | sed 's#/#_#g') COMPONENT_SLUG="$STACK_NAME-$COMPONENT_NAME" COMPONENT_CACHE_KEY=$(basename "${{ fromJson(steps.atmos-settings.outputs.settings).component-path }}") - PLAN_FILE="${COMPONENT_PATH}/$COMPONENT_SLUG-${{ inputs.sha }}.planfile" + PLAN_FILENAME="$COMPONENT_SLUG.planfile" + PLAN_FILE="${COMPONENT_PATH}/${PLAN_FILENAME}" + RETRIEVED_PLAN_FILENAME="$COMPONENT_SLUG-${{ inputs.sha }}.planfile" + RETRIEVED_PLAN_FILE="${COMPONENT_PATH}/${RETRIEVED_PLAN_FILENAME}" + RENEWED_PLAN_FILENAME="new.${RETRIEVED_PLAN_FILENAME}" + RENEWED_PLAN_FILE="${COMPONENT_PATH}/${RENEWED_PLAN_FILENAME}" LOCK_FILE="${COMPONENT_PATH}/.terraform.lock.hcl" echo "stack_name=$STACK_NAME" >> $GITHUB_OUTPUT @@ -246,10 +251,15 @@ runs: echo "component_slug=$COMPONENT_SLUG" >> $GITHUB_OUTPUT echo "component_path=${COMPONENT_PATH}" >> $GITHUB_OUTPUT echo "cache-key=${COMPONENT_CACHE_KEY}" >> $GITHUB_OUTPUT + echo "plan_filename=$PLAN_FILENAME" >> $GITHUB_OUTPUT echo "plan_file=$PLAN_FILE" >> $GITHUB_OUTPUT + echo "retrieved_plan_filename=$RETRIEVED_PLAN_FILENAME" >> $GITHUB_OUTPUT + echo "retrieved_plan_file=$RETRIEVED_PLAN_FILE" >> $GITHUB_OUTPUT + echo "renewed_plan_filename=$RENEWED_PLAN_FILENAME" >> $GITHUB_OUTPUT + echo "renewed_plan_file=$RENEWED_PLAN_FILE" >> $GITHUB_OUTPUT echo "lock_file=$LOCK_FILE" >> $GITHUB_OUTPUT - - name: Configure Plan AWS Credentials + - name: Configure Plan Storage AWS Credentials uses: aws-actions/configure-aws-credentials@v4 if: ${{ ( fromJson(steps.atmos-settings.outputs.settings).plan-repository-type == 's3' || fromJson(steps.atmos-settings.outputs.settings).plan-repository-type == '' || @@ -273,7 +283,7 @@ runs: with: action: getPlan commitSHA: ${{ inputs.sha }} - planPath: ${{ steps.vars.outputs.plan_file }} + planPath: ${{ steps.vars.outputs.retrieved_plan_file }} component: ${{ inputs.component }} stack: ${{ inputs.stack }} planRepositoryType: ${{ fromJson(steps.atmos-settings.outputs.settings).plan-repository-type || 's3' }} @@ -318,8 +328,87 @@ runs: role-session-name: "atmos-terraform-apply-gitops" mask-aws-account-id: "no" - - name: Check Whether Infracost is Enabled + - name: Cache .terraform + id: cache + uses: actions/cache@v4 if: env.ACTIONS_ENABLED == 'true' + with: + path: | + ${{ steps.vars.outputs.component_path }}/.terraform + key: ${{ steps.vars.outputs.cache-key }} + + - name: Plan prepare + if: env.ACTIONS_ENABLED == 'true' + id: plan-diff + shell: bash + run: | + set +e + + # Remove the environment file from the cache to avoid conflicts with workspace select + rm -f ./.terraform/environment + + TERRAFORM_PLAN_OUTPUT_FILE="./terraform-${GITHUB_RUN_ID}-plan-output.txt" + + tfcmt \ + --config "${GITHUB_ACTION_PATH}/config/atmos_plan_summary.yaml" \ + -var "target:${{ inputs.stack }}-${{ inputs.component }}" \ + -var "component:${{ inputs.component }}" \ + -var "stack:${{ inputs.stack }}" \ + -var "job:${{ github.job }}" \ + -var "logoImage:${{ inputs.branding-logo-image }}" \ + -var "logoUrl:${{ inputs.branding-logo-url }}" \ + --output "${{ github.workspace }}/atmos-plan-summary.md" \ + --log-level $([[ "${{ inputs.debug }}" == "true" ]] && echo "DEBUG" || echo "INFO") \ + apply -- \ + atmos terraform plan ${{ inputs.component }} \ + --stack ${{ inputs.stack }} \ + -input=false \ + -no-color \ + &> ${TERRAFORM_PLAN_OUTPUT_FILE} + + PLAN_CHANGED=$? + + cat ${TERRAFORM_PLAN_OUTPUT_FILE} + + if [[ "${PLAN_CHANGED}" != "0" ]]; then + mv ${{ github.workspace }}/atmos-plan-summary.md ${{ github.workspace }}/atmos-apply-summary.md + cat ./atmos-apply-summary.md >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + cp ${{ steps.vars.outputs.plan_file }} ${{ steps.vars.outputs.renewed_plan_file }} + + TERRAFORM_PLAN_DIFF_OUTPUT_FILE="./terraform-${GITHUB_RUN_ID}-plan-diff-output.txt" + + if [[ "${{ inputs.plan-storage }}" == "true" ]]; then + + atmos terraform plan-diff ${{ inputs.component }} \ + --stack ${{ inputs.stack }} \ + -input=false \ + -no-color \ + --orig ${{ steps.vars.outputs.retrieved_plan_filename }} \ + --new ${{ steps.vars.outputs.renewed_plan_filename }} \ + --skip-init \ + > ${TERRAFORM_PLAN_DIFF_OUTPUT_FILE} + + PLAN_CHANGED=$? + fi + + if [[ "${PLAN_CHANGED}" == "0" ]]; then + echo "plan_changed=false" >> $GITHUB_OUTPUT + else + echo "plan_changed=true" >> $GITHUB_OUTPUT + cp ${GITHUB_ACTION_PATH}/config/atmos_plan_diff_github_summary.md ./atmos-apply-summary.md + sed -i "s/{{COMPONENT_NAME}}/${{ inputs.component }}/g" ./atmos-apply-summary.md + sed -i "s/{{STACK_NAME}}/${{ inputs.stack }}/g" ./atmos-apply-summary.md + sed -i -e "/{{DIFF}}/ {r ${TERRAFORM_PLAN_DIFF_OUTPUT_FILE}" -e 'd}' ./atmos-apply-summary.md + cat ./atmos-apply-summary.md >> $GITHUB_STEP_SUMMARY + + exit 1 + fi + + - name: Check Whether Infracost is Enabled + if: env.ACTIONS_ENABLED == 'true' && steps.plan-diff.outputs.plan_changed == 'false' shell: bash run: | if [[ "${{ fromJson(steps.atmos-settings.outputs.settings).enable-infracost }}" == "true" ]]; then @@ -329,21 +418,22 @@ runs: fi - name: Setup Infracost - if: env.INFRACOST_ENABLED == 'true' + if: env.INFRACOST_ENABLED == 'true' && steps.plan-diff.outputs.plan_changed == 'false' uses: infracost/actions/setup@v3 with: api-key: ${{ inputs.infracost-api-key }} - name: Convert PLANFILE to JSON - if: ${{ fromJson(steps.atmos-settings.outputs.settings).enable-infracost == 'true' && steps.atmos-plan.outputs.changes == 'true' }} + if: env.INFRACOST_ENABLED == 'true' || env.DEBUG_ENABLED == 'true' && steps.plan-diff.outputs.plan_changed == 'false' shell: bash working-directory: ${{ steps.vars.outputs.component_path }} run: | - ${{ fromJson(steps.atmos-settings.outputs.settings).command }} show -json "${{ steps.vars.outputs.plan_file }}" > "${{ steps.vars.outputs.plan_file }}.json" + ${{ fromJson(steps.atmos-settings.outputs.settings).command }} show -json "${{ steps.vars.outputs.renewed_plan_file }}" > "${{ steps.vars.outputs.plan_file }}.json" - name: Generate Infracost Diff - if: env.INFRACOST_ENABLED == 'true' + if: env.INFRACOST_ENABLED == 'true' && steps.plan-diff.outputs.plan_changed == 'false' shell: bash + working-directory: ${{ steps.vars.outputs.component_path }} run: | infracost diff \ --path="${{ steps.vars.outputs.plan_file }}.json" \ @@ -357,16 +447,18 @@ runs: --out-file=/tmp/infracost.json - name: Debug Infracost - if: env.INFRACOST_ENABLED == 'true' && env.DEBUG_ENABLED == 'true' + if: env.INFRACOST_ENABLED == 'true' && env.DEBUG_ENABLED == 'true' && steps.plan-diff.outputs.plan_changed == 'false' shell: bash + working-directory: ${{ steps.vars.outputs.component_path }} run: | cat ${{ steps.vars.outputs.plan_file }}.json cat /tmp/infracost.txt cat /tmp/infracost.json - name: Set Infracost Variables - if: env.INFRACOST_ENABLED == 'true' + if: env.INFRACOST_ENABLED == 'true' && steps.plan-diff.outputs.plan_changed == 'false' id: infracost-diff + working-directory: ${{ steps.vars.outputs.component_path }} shell: bash run: | if [[ "${{ fromJson(steps.atmos-settings.outputs.settings).enable-infracost }}" == "true" ]]; then @@ -380,17 +472,8 @@ runs: echo "infracost_details_diff_breakdown=$INFRACOST_DETAILS_DIFF_BREAKDOWN" >> "$GITHUB_OUTPUT" echo "infracost_diff_total_monthly_cost=$INFRACOST_DIFF_TOTAL_MONTHLY_COST" >> "$GITHUB_OUTPUT" - - name: Cache .terraform - id: cache - uses: actions/cache@v4 - if: env.ACTIONS_ENABLED == 'true' - with: - path: | - ${{ steps.vars.outputs.component_path }}/.terraform - key: ${{ steps.vars.outputs.cache-key }} - - name: Terraform Apply - if: env.ACTIONS_ENABLED == 'true' + if: env.ACTIONS_ENABLED == 'true' && steps.plan-diff.outputs.plan_changed == 'false' id: apply shell: bash run: | @@ -414,11 +497,12 @@ runs: --output "${{ github.workspace }}/atmos-apply-summary.md" \ --log-level $([[ "${{ inputs.debug }}" == "true" ]] && echo "DEBUG" || echo "INFO") \ apply -- \ - atmos terraform apply ${{ inputs.component }} \ + atmos terraform deploy ${{ inputs.component }} \ --stack ${{ inputs.stack }} \ - -auto-approve \ -input=false \ -no-color \ + --planfile ${{ steps.vars.outputs.renewed_plan_filename }} \ + --skip-init \ &> ${TERRAFORM_OUTPUT_FILE} TERRAFORM_RESULT=$? @@ -429,8 +513,11 @@ runs: atmos terraform output ${{ inputs.component }} --stack ${{ inputs.stack }} --skip-init -- -json -compact-warnings -no-color | \ grep -v 'Switched to workspace' | \ awk '$1~/^Warnings:$/ {exit} {print}' | \ - grep -v 'WARN detected' 1> output_values.json + grep -v 'WARN detected' 1> ${{ steps.vars.outputs.component_path }}/output_values.json + + cd ${{ steps.vars.outputs.component_path }} terraform-docs -c ${GITHUB_ACTION_PATH}/config/tfdocs-config.yaml --output-file ${{ github.workspace }}/atmos-apply-summary.md ./ + cd - sed -i "s#\`\`#![Sensitive](https://img.shields.io/badge/sensitive-c40000?style=for-the-badge)#g" ${{ github.workspace }}/atmos-apply-summary.md sed -i "s#\`\"#\`#g" ${{ github.workspace }}/atmos-apply-summary.md @@ -438,7 +525,7 @@ runs: sed -i "s#|--#|:-#g" ${{ github.workspace }}/atmos-apply-summary.md cat "${{ github.workspace }}/atmos-apply-summary.md" >> $GITHUB_STEP_SUMMARY - + if [[ "${TERRAFORM_RESULT}" == "0" ]]; then echo "status=succeeded" >> $GITHUB_OUTPUT echo "Terraform apply executed successfully" @@ -449,7 +536,7 @@ runs: # Link to a job that executed this action echo "[Job](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> "${{ github.workspace }}/atmos-apply-summary.md" - + rm -f ${TERRAFORM_OUTPUT_FILE} exit $TERRAFORM_RESULT diff --git a/config/atmos_github_summary.yaml b/config/atmos_github_summary.yaml index 368746e5..ba7ac766 100644 --- a/config/atmos_github_summary.yaml +++ b/config/atmos_github_summary.yaml @@ -26,7 +26,12 @@ templates: {{ end }} result: | + {{- if eq .ExitCode 0 }}
{{if .Result}}{{ .Result }}{{end}} + {{- end }} + {{- if eq .ExitCode 1 }} +
:warning: Error summary + {{- end }}
To reproduce this locally, run:

@@ -60,6 +65,7 @@ templates: {{- end }} terraform: apply: + disable_label: false template: | {{template "apply_title" .}} {{template "branding" .}} diff --git a/config/atmos_plan_diff_github_summary.md b/config/atmos_plan_diff_github_summary.md new file mode 100644 index 00000000..e8de5d94 --- /dev/null +++ b/config/atmos_plan_diff_github_summary.md @@ -0,0 +1,9 @@ +## Apply failed `{{COMPONENT_NAME}}` in `{{STACK_NAME}}` + +
+ [![failed](https://shields.io/badge/APPLY-FAILED-critical?style=for-the-badge)](#user-content-plan-outdated-{{STACK_NAME}}-{{COMPONENT_NAME}}) + +
Terraform Plan outdated +{{DIFF}} + +
diff --git a/config/atmos_plan_summary.yaml b/config/atmos_plan_summary.yaml new file mode 100644 index 00000000..68aa971a --- /dev/null +++ b/config/atmos_plan_summary.yaml @@ -0,0 +1,67 @@ +# tfcmt Configuration used for posting Terraform GitHub Summaries +# https://suzuki-shunsuke.github.io/tfcmt/config +embedded_var_names: [] +ci: + pr: [] + owner: [] + repo: [] + sha: [] + link: [] + vars: {} +templates: + title: | + {{ if eq .ExitCode 1}} + ## Apply Failed for `{{.Vars.component}}` in `{{.Vars.stack}}` + {{ end }} + {{- if eq .ExitCode 0}} + ## Apply Succeeded for `{{.Vars.component}}` in `{{.Vars.stack}}` + {{ end }} + + badge: | + {{ if eq .ExitCode 1}} + [![apply](https://shields.io/badge/APPLY-FAILED-critical?style=for-the-badge)](#user-content-apply-{{.Vars.stack}}-{{.Vars.component}}) + {{ end }} + {{- if eq .ExitCode 0}} + [![apply](https://shields.io/badge/APPLY-SUCCESS-success?style=for-the-badge)](#user-content-apply-{{.Vars.stack}}-{{.Vars.component}}) + {{ end }} + + result: | + {{- if eq .ExitCode 0 }} +
{{if .Result}}{{ .Result }}{{end}} + {{- end }} + {{- if eq .ExitCode 1 }} +
:warning: Error summary + {{- end }} + +
+ To reproduce this locally, run:

+ + ```shell + atmos terraform plan {{.Vars.component}} -s {{.Vars.stack}} + ``` + + {{wrapCode .CombinedOutput}} +
+ + branding: | + {{- if ne .Vars.logoImage "" }} + + {{- end }} +terraform: + apply: + disable_label: false + template: | + {{template "title" .}} + {{template "branding" .}} + {{template "badge" .}} + {{template "result" .}} + +
+ + when_parse_error: + template: | + {{template "title" .}} + +
Result + {{wrapCode .CombinedOutput}} +
diff --git a/tests/opentofu/components/terraform/foobar-fail/context.tf b/tests/opentofu/components/terraform/foobar-fail/context.tf new file mode 100644 index 00000000..5e0ef885 --- /dev/null +++ b/tests/opentofu/components/terraform/foobar-fail/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/tests/opentofu/components/terraform/foobar-fail/main.tf b/tests/opentofu/components/terraform/foobar-fail/main.tf new file mode 100644 index 00000000..d824f549 --- /dev/null +++ b/tests/opentofu/components/terraform/foobar-fail/main.tf @@ -0,0 +1,15 @@ +resource "random_id" "foo" { + keepers = { + # Generate a new id each time we switch to a new seed + seed = "${module.this.id}-${var.example}-${var.seed}" + } + byte_length = 8 +} + + +resource "null_resource" "default" { + count = var.fail ? 1 : 0 + triggers = { + test = random_id.foo["test"] + } +} \ No newline at end of file diff --git a/tests/opentofu/components/terraform/foobar-fail/outputs.tf b/tests/opentofu/components/terraform/foobar-fail/outputs.tf new file mode 100644 index 00000000..055f02d5 --- /dev/null +++ b/tests/opentofu/components/terraform/foobar-fail/outputs.tf @@ -0,0 +1,21 @@ +output "result" { + description = "Test description output" + value = random_id.foo.id +} + +output "sensitive_value" { + description = "Test sensitive description output" + value = random_id.foo.id + sensitive = true +} + + +output "structured_value" { + description = "Test structured output" + value = { + test = "test" + value = { + result = random_id.foo.id + } + } +} \ No newline at end of file diff --git a/tests/opentofu/components/terraform/foobar-fail/variables.tf b/tests/opentofu/components/terraform/foobar-fail/variables.tf new file mode 100644 index 00000000..a7e9c219 --- /dev/null +++ b/tests/opentofu/components/terraform/foobar-fail/variables.tf @@ -0,0 +1,16 @@ +variable "example" { + type = string + description = "testing variable" +} + +variable "seed" { + type = string + description = "testing variable" + default = "seed" +} + +variable "fail" { + type = bool + description = "Fail" + default = false +} diff --git a/tests/opentofu/components/terraform/foobar/main.tf b/tests/opentofu/components/terraform/foobar/main.tf index 5fc39577..2105549b 100644 --- a/tests/opentofu/components/terraform/foobar/main.tf +++ b/tests/opentofu/components/terraform/foobar/main.tf @@ -1,8 +1,17 @@ resource "random_id" "foo" { keepers = { # Generate a new id each time we switch to a new seed - seed = "${module.this.id}-${var.example}" - timestamp = "${timestamp()}" + seed = "${module.this.id}-${var.example}-${var.seed}" } byte_length = 8 } + + +resource "null_resource" "dns_check" { + count = var.fail ? 1 : 0 + + provisioner "local-exec" { + command = "false" + interpreter = ["bash", "-c"] + } +} \ No newline at end of file diff --git a/tests/opentofu/components/terraform/foobar/outputs.tf b/tests/opentofu/components/terraform/foobar/outputs.tf index 055f02d5..7177f0bd 100644 --- a/tests/opentofu/components/terraform/foobar/outputs.tf +++ b/tests/opentofu/components/terraform/foobar/outputs.tf @@ -18,4 +18,4 @@ output "structured_value" { result = random_id.foo.id } } -} \ No newline at end of file +} diff --git a/tests/opentofu/components/terraform/foobar/variables.tf b/tests/opentofu/components/terraform/foobar/variables.tf index d9b2dde4..a7e9c219 100644 --- a/tests/opentofu/components/terraform/foobar/variables.tf +++ b/tests/opentofu/components/terraform/foobar/variables.tf @@ -3,3 +3,14 @@ variable "example" { description = "testing variable" } +variable "seed" { + type = string + description = "testing variable" + default = "seed" +} + +variable "fail" { + type = bool + description = "Fail" + default = false +} diff --git a/tests/opentofu/stacks/catalog/foobar-plan-diff.yaml b/tests/opentofu/stacks/catalog/foobar-plan-diff.yaml new file mode 100644 index 00000000..f518b9cd --- /dev/null +++ b/tests/opentofu/stacks/catalog/foobar-plan-diff.yaml @@ -0,0 +1,11 @@ +components: + terraform: + foobar-plan-diff: + metadata: + component: foobar + settings: + github: + actions_enabled: true + vars: + example: blue + enabled: true diff --git a/tests/opentofu/stacks/catalog/foobar-plan-fail.yaml b/tests/opentofu/stacks/catalog/foobar-plan-fail.yaml new file mode 100644 index 00000000..26d5304e --- /dev/null +++ b/tests/opentofu/stacks/catalog/foobar-plan-fail.yaml @@ -0,0 +1,12 @@ +components: + terraform: + foobar-plan-fail: + metadata: + component: foobar-fail + settings: + github: + actions_enabled: true + vars: + example: blue + enabled: true + fail: true diff --git a/tests/opentofu/stacks/catalog/foobar-plan-storage.yaml b/tests/opentofu/stacks/catalog/foobar-plan-storage.yaml new file mode 100644 index 00000000..9bbf84c7 --- /dev/null +++ b/tests/opentofu/stacks/catalog/foobar-plan-storage.yaml @@ -0,0 +1,11 @@ +components: + terraform: + foobar-plan-storage: + metadata: + component: foobar + settings: + github: + actions_enabled: true + vars: + example: blue + enabled: true diff --git a/tests/opentofu/stacks/orgs/foo/plat/sandbox/plat-ue2-sandbox.yaml b/tests/opentofu/stacks/orgs/foo/plat/sandbox/plat-ue2-sandbox.yaml index 65394b42..5f5e5aba 100644 --- a/tests/opentofu/stacks/orgs/foo/plat/sandbox/plat-ue2-sandbox.yaml +++ b/tests/opentofu/stacks/orgs/foo/plat/sandbox/plat-ue2-sandbox.yaml @@ -1,5 +1,8 @@ import: - catalog/foobar + - catalog/foobar-plan-diff + - catalog/foobar-plan-storage + - catalog/foobar-plan-fail - catalog/foobar-atmos-pro terraform: diff --git a/tests/terraform/components/terraform/foobar-fail/context.tf b/tests/terraform/components/terraform/foobar-fail/context.tf new file mode 100644 index 00000000..5e0ef885 --- /dev/null +++ b/tests/terraform/components/terraform/foobar-fail/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/tests/terraform/components/terraform/foobar-fail/main.tf b/tests/terraform/components/terraform/foobar-fail/main.tf new file mode 100644 index 00000000..d824f549 --- /dev/null +++ b/tests/terraform/components/terraform/foobar-fail/main.tf @@ -0,0 +1,15 @@ +resource "random_id" "foo" { + keepers = { + # Generate a new id each time we switch to a new seed + seed = "${module.this.id}-${var.example}-${var.seed}" + } + byte_length = 8 +} + + +resource "null_resource" "default" { + count = var.fail ? 1 : 0 + triggers = { + test = random_id.foo["test"] + } +} \ No newline at end of file diff --git a/tests/terraform/components/terraform/foobar-fail/outputs.tf b/tests/terraform/components/terraform/foobar-fail/outputs.tf new file mode 100644 index 00000000..055f02d5 --- /dev/null +++ b/tests/terraform/components/terraform/foobar-fail/outputs.tf @@ -0,0 +1,21 @@ +output "result" { + description = "Test description output" + value = random_id.foo.id +} + +output "sensitive_value" { + description = "Test sensitive description output" + value = random_id.foo.id + sensitive = true +} + + +output "structured_value" { + description = "Test structured output" + value = { + test = "test" + value = { + result = random_id.foo.id + } + } +} \ No newline at end of file diff --git a/tests/terraform/components/terraform/foobar-fail/variables.tf b/tests/terraform/components/terraform/foobar-fail/variables.tf new file mode 100644 index 00000000..a7e9c219 --- /dev/null +++ b/tests/terraform/components/terraform/foobar-fail/variables.tf @@ -0,0 +1,16 @@ +variable "example" { + type = string + description = "testing variable" +} + +variable "seed" { + type = string + description = "testing variable" + default = "seed" +} + +variable "fail" { + type = bool + description = "Fail" + default = false +} diff --git a/tests/terraform/components/terraform/foobar/main.tf b/tests/terraform/components/terraform/foobar/main.tf index 5fc39577..2105549b 100644 --- a/tests/terraform/components/terraform/foobar/main.tf +++ b/tests/terraform/components/terraform/foobar/main.tf @@ -1,8 +1,17 @@ resource "random_id" "foo" { keepers = { # Generate a new id each time we switch to a new seed - seed = "${module.this.id}-${var.example}" - timestamp = "${timestamp()}" + seed = "${module.this.id}-${var.example}-${var.seed}" } byte_length = 8 } + + +resource "null_resource" "dns_check" { + count = var.fail ? 1 : 0 + + provisioner "local-exec" { + command = "false" + interpreter = ["bash", "-c"] + } +} \ No newline at end of file diff --git a/tests/terraform/components/terraform/foobar/outputs.tf b/tests/terraform/components/terraform/foobar/outputs.tf index 055f02d5..7177f0bd 100644 --- a/tests/terraform/components/terraform/foobar/outputs.tf +++ b/tests/terraform/components/terraform/foobar/outputs.tf @@ -18,4 +18,4 @@ output "structured_value" { result = random_id.foo.id } } -} \ No newline at end of file +} diff --git a/tests/terraform/components/terraform/foobar/variables.tf b/tests/terraform/components/terraform/foobar/variables.tf index d9b2dde4..a7e9c219 100644 --- a/tests/terraform/components/terraform/foobar/variables.tf +++ b/tests/terraform/components/terraform/foobar/variables.tf @@ -3,3 +3,14 @@ variable "example" { description = "testing variable" } +variable "seed" { + type = string + description = "testing variable" + default = "seed" +} + +variable "fail" { + type = bool + description = "Fail" + default = false +} diff --git a/tests/terraform/stacks/catalog/foobar-plan-diff.yaml b/tests/terraform/stacks/catalog/foobar-plan-diff.yaml new file mode 100644 index 00000000..f518b9cd --- /dev/null +++ b/tests/terraform/stacks/catalog/foobar-plan-diff.yaml @@ -0,0 +1,11 @@ +components: + terraform: + foobar-plan-diff: + metadata: + component: foobar + settings: + github: + actions_enabled: true + vars: + example: blue + enabled: true diff --git a/tests/terraform/stacks/catalog/foobar-plan-fail.yaml b/tests/terraform/stacks/catalog/foobar-plan-fail.yaml new file mode 100644 index 00000000..26d5304e --- /dev/null +++ b/tests/terraform/stacks/catalog/foobar-plan-fail.yaml @@ -0,0 +1,12 @@ +components: + terraform: + foobar-plan-fail: + metadata: + component: foobar-fail + settings: + github: + actions_enabled: true + vars: + example: blue + enabled: true + fail: true diff --git a/tests/terraform/stacks/catalog/foobar-plan-storage.yaml b/tests/terraform/stacks/catalog/foobar-plan-storage.yaml new file mode 100644 index 00000000..9bbf84c7 --- /dev/null +++ b/tests/terraform/stacks/catalog/foobar-plan-storage.yaml @@ -0,0 +1,11 @@ +components: + terraform: + foobar-plan-storage: + metadata: + component: foobar + settings: + github: + actions_enabled: true + vars: + example: blue + enabled: true diff --git a/tests/terraform/stacks/orgs/foo/plat/sandbox/plat-ue2-sandbox.yaml b/tests/terraform/stacks/orgs/foo/plat/sandbox/plat-ue2-sandbox.yaml index 3d552592..aedcd1df 100644 --- a/tests/terraform/stacks/orgs/foo/plat/sandbox/plat-ue2-sandbox.yaml +++ b/tests/terraform/stacks/orgs/foo/plat/sandbox/plat-ue2-sandbox.yaml @@ -1,5 +1,8 @@ import: - catalog/foobar + - catalog/foobar-plan-diff + - catalog/foobar-plan-storage + - catalog/foobar-plan-fail - catalog/foobar-atmos-pro terraform: