diff --git a/.github/actions/remote-branch/LICENSE b/.github/actions/remote-branch/LICENSE new file mode 100644 index 00000000000..ade79f7960a --- /dev/null +++ b/.github/actions/remote-branch/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Josiah Siegel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.github/actions/remote-branch/README.md b/.github/actions/remote-branch/README.md new file mode 100644 index 00000000000..af36d8f980a --- /dev/null +++ b/.github/actions/remote-branch/README.md @@ -0,0 +1,106 @@ +# Remote Branch Action + +## Synopsis + +1. Create a branch on a remote repository. +2. [actions/checkout](https://github.com/actions/checkout) determins the active repo. + +## Usage + +### Single repo +```yml +jobs: + create-branch-action: + name: Create branch + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Create branch + uses: CDCgov/prime-reportstream/.github/actions/remote-branch@v1.0.1 + with: + branch: new-branch +``` +### Single alternative repo +```yml +jobs: + create-branch-action: + name: Create branch + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Checkout alt repo + uses: actions/checkout@v4 + with: + sparse-checkout: . + repository: me/alt-repo + token: ${{ secrets.ALT_REPO_TOKEN }} + path: alt-repo + + - name: Create branch on alt repo + uses: CDCgov/prime-reportstream/.github/actions/remote-branch@v1.0.1 + with: + branch: new-branch-alt-repo + path: alt-repo +``` +### Multiple repos +```yml +jobs: + create-branch-action: + name: Create branch + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Checkout second repo + uses: actions/checkout@v4 + with: + sparse-checkout: . + repository: me/second-repo + token: ${{ secrets.SECONDARY_REPO_TOKEN }} + path: second-repo + + - name: Create branch + id: create-branch-action + uses: CDCgov/prime-reportstream/.github/actions/remote-branch@v1.0.1 + with: + branch: new-branch + + - name: Create branch on second repo + id: create-branch-action-second-repo + uses: CDCgov/prime-reportstream/.github/actions/remote-branch@v1.0.1 + with: + branch: new-branch-second-repo + path: second-repo + + - name: Get create branch status + run: echo ${{ steps.create-branch-action.outputs.create-status }} + + - name: Get create branch status on second repo + run: echo ${{ steps.create-branch-action-second-repo.outputs.create-status }} +``` + +## Inputs + +```yml +inputs: + branch: + description: Branch name + required: true + path: + description: Relative path under $GITHUB_WORKSPACE to place the repository + required: false + default: '.' +``` + +## Outputs +```yml +outputs: + create-status: + description: Branch creation status + value: ${{ steps.create-branch.outputs.create_status }} +``` diff --git a/.github/actions/remote-branch/action.yml b/.github/actions/remote-branch/action.yml new file mode 100644 index 00000000000..215d2203407 --- /dev/null +++ b/.github/actions/remote-branch/action.yml @@ -0,0 +1,61 @@ +# action.yml +name: Remote Branch Action +description: Create and manage a remote branch +branding: + icon: 'git-branch' + color: 'blue' +inputs: + branch: + description: Branch name + required: true + path: + description: Relative path under $GITHUB_WORKSPACE to place the repository + required: false + default: '.' +outputs: + create-status: + description: Branch creation status + value: ${{ steps.create-branch.outputs.create_status }} + +runs: + using: "composite" + steps: + + - name: Create branch + id: create-branch + working-directory: ${{ inputs.path }} + shell: bash + run: | + # Assign the arguments to variables + branch_name=${{ inputs.branch }} + + # Create a new branch locally + git checkout -b $branch_name + + # Check if the branch exists on the remote + check_status=$(git ls-remote --heads origin $branch_name | wc -l) + + # Check if the branch does not exist on the remote + if [ $check_status -eq 0 ]; then + # Push the new branch to the remote repository using the token + git push -u origin $branch_name + + # Store the status of the push command + status=$? + + # Check if the push was successful + if [ $status -eq 0 ]; then + # Print a success message + echo "Branch $branch_name created and pushed" + else + # Print an error message + echo "Branch creation failed with status $status" + status="Branch creation failed with status $status" + fi + else + # Print a message that the branch already exists on the remote + echo "Branch $branch_name already exists" + status="Branch $branch_name already exists" + fi + + echo "create_status=$status" >> $GITHUB_OUTPUT diff --git a/.github/actions/terraform-stats/.gitignore b/.github/actions/terraform-stats/.gitignore new file mode 100644 index 00000000000..4d6246fd61a --- /dev/null +++ b/.github/actions/terraform-stats/.gitignore @@ -0,0 +1,2 @@ +terraform/*.git +.github diff --git a/.github/actions/terraform-stats/LICENSE b/.github/actions/terraform-stats/LICENSE new file mode 100644 index 00000000000..1c50494ad20 --- /dev/null +++ b/.github/actions/terraform-stats/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Josiah Siegel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.github/actions/terraform-stats/README.md b/.github/actions/terraform-stats/README.md new file mode 100644 index 00000000000..84522f09306 --- /dev/null +++ b/.github/actions/terraform-stats/README.md @@ -0,0 +1,107 @@ +# Terraform Stats +## Synopsis + +Output the following statistics for the Terraform environment: +1. Terraform version +2. Drift count + * "Drift" refers to changes made outside of Terraform and does not necessary match any resources listed for changes. +3. Resource drifts +4. Change count + * "Change" refers to change actions that Terraform plans to use to move from the prior state to a new state. +5. Change percent + * Percentage of changes to total resources. +6. Resource changes + +## Usage + +```yml +- name: Terraform stats + uses: josiahsiegel/terraform-stats@ + id: stats + with: + terraform-directory: ${{ env.tf-dir }} + terraform-version: 1.1.9 +- name: Get outputs + run: | + echo "terraform-version: ${{ steps.stats.outputs.terraform-version }}" + echo "drift-count: ${{ steps.stats.outputs.drift-count }}" + echo "resource-drifts: ${{ steps.stats.outputs.resource-drifts }}" + echo "change-count: ${{ steps.stats.outputs.change-count }}" + echo "change-percent: ${{ steps.stats.outputs.change-percent }}" + echo "resource-changes: ${{ steps.stats.outputs.resource-changes }}" +``` + +## Workflow summary + +### :construction: Terraform Stats :construction: + +* change-count: 2 +* change-percent: 100 +* resource-changes: +```json +[ + { + "address": "docker_container.nginx", + "changes": [ + "create" + ] + }, + { + "address": "docker_image.nginx", + "changes": [ + "create" + ] + } +] +``` + +## Inputs + +```yml +inputs: + terraform-directory: + description: Terraform commands will run in this location. + required: true + default: "./terraform" + include-no-op: + description: "\"no-op\" refers to the before and after Terraform changes are identical as a value will only be known after apply." + required: true + default: false + add-args: + description: Pass additional arguments to Terraform plan. + required: true + default: "" + upload-plan: + description: Upload plan file. true or false + required: true + default: false + upload-retention-days: + description: Number of days to keep uploaded plan. + required: true + default: 7 + plan-file: + description: Name of plan file. + required: true + default: tf__stats__plan.bin + terraform-version: + description: Specify a specific version of Terraform + required: true + default: latest +``` + +## Outputs +```yml +outputs: + terraform-version: + description: 'Terraform version' + drift-count: + description: 'Count of drifts' + resource-drifts: + description: 'JSON output of resource drifts' + change-count: + description: 'Count of changes' + change-percent: + description: 'Percentage of changes to total resources' + resource-changes: + description: 'JSON output of resource changes' +``` diff --git a/.github/actions/terraform-stats/action.yml b/.github/actions/terraform-stats/action.yml new file mode 100644 index 00000000000..bfe0b53976e --- /dev/null +++ b/.github/actions/terraform-stats/action.yml @@ -0,0 +1,108 @@ +# action.yml +name: 'Generate Terraform statistics' +description: 'Output Terraform stats for drift and pending changes' +branding: + icon: 'bar-chart' + color: 'purple' +inputs: + terraform-directory: + description: Terraform commands will run in this location. + required: true + default: "./terraform" + include-no-op: + description: "\"no-op\" refers to the before and after Terraform changes are identical as a value will only be known after apply." + required: true + default: false + add-args: + description: Pass additional arguments to Terraform plan. + required: true + default: "" + upload-plan: + description: Upload plan file. true or false + required: true + default: false + upload-retention-days: + description: Number of days to keep uploaded plan. + required: true + default: 7 + plan-file: + description: Name of plan file. + required: true + default: tf__stats__plan.bin + terraform-version: + description: Specify a specific version of Terraform + required: true + default: latest + +outputs: + terraform-version: + description: 'Terraform version' + value: ${{ steps.local-action.outputs.terraform-version }} + drift-count: + description: 'Count of drifts' + value: ${{ steps.local-action.outputs.drift-count }} + resource-drifts: + description: 'JSON output of resource drifts' + value: ${{ steps.local-action.outputs.resource-drifts }} + change-count: + description: 'Count of changes' + value: ${{ steps.local-action.outputs.change-count }} + resource-changes: + description: 'JSON output of resource changes' + value: ${{ steps.local-action.outputs.resource-changes }} + change-percent: + description: 'Percentage of changes to total resources' + value: ${{ steps.local-action.outputs.change-percent }} + +runs: + using: "composite" + steps: + - name: Use specific version of Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ inputs.terraform-version }} + terraform_wrapper: false + - name: Run Terraform stats + id: local-action + run: | + ${{ github.action_path }}/lib/tf_stats.sh \ + "${{ inputs.terraform-directory }}" \ + ${{ inputs.include-no-op }} \ + "${{ inputs.add-args }}" \ + "${{ inputs.plan-file }}" + shell: bash + + - name: Upload Artifact + if: inputs.upload-plan == 'true' + uses: actions/upload-artifact@v4.3.3 + with: + name: ${{ inputs.plan-file }} + path: "${{ inputs.terraform-directory }}/${{ inputs.plan-file }}" + retention-days: ${{ inputs.upload-retention-days }} + + - name: Create summary + if: | + steps.local-action.outputs.change-count > 0 || + steps.local-action.outputs.drift-count > 0 + run: | + echo "### :construction: Terraform Stats :construction:" >> $GITHUB_STEP_SUMMARY + if [[ ${{ steps.local-action.outputs.change-count }} > 0 ]]; then + resource_changes=$(echo "${{ steps.local-action.outputs.resource-changes }}" | jq .) + echo " + * change-count: ${{ steps.local-action.outputs.change-count }} + * change-percent: ${{ steps.local-action.outputs.change-percent }} + * resource-changes: + \`\`\`json + $resource_changes + \`\`\`" >> $GITHUB_STEP_SUMMARY + fi + if [[ ${{ steps.local-action.outputs.drift-count }} > 0 ]]; then + resource_drifts=$(echo "${{ steps.local-action.outputs.resource-drifts }}" | jq .) + echo " + * drift-count: ${{ steps.local-action.outputs.drift-count }} + * resource-drift: + \`\`\`json + $resource_drifts + \`\`\`" >> $GITHUB_STEP_SUMMARY + fi + shell: bash diff --git a/.github/actions/terraform-stats/lib/tf_stats.sh b/.github/actions/terraform-stats/lib/tf_stats.sh new file mode 100755 index 00000000000..74eb7431ed9 --- /dev/null +++ b/.github/actions/terraform-stats/lib/tf_stats.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +tf_dir=$1 +#For ["no-op"], the before and +#after values are identical. The "after" value will be incomplete if there +#are values within it that won't be known until after apply. +include_no_op=$2 +add_args=$3 +plan_file=$4 + +# Define a function to run terraform plan with common arguments +tf_plan() { + terraform -chdir=$tf_dir plan $add_args -input=false -no-color -lock-timeout=120s -out=$plan_file "$@" +} + +# Try to run terraform plan and init if needed +if ! tf_plan &>/dev/null; then + terraform -chdir=$tf_dir init >/dev/null || exit 1 + tf_plan >/dev/null || exit 1 +fi + +# Get the plan output in text and json formats +PLAN_TXT=$( terraform -chdir=$tf_dir show -no-color $plan_file ) +PLAN_JSON=$( terraform -chdir=$tf_dir show -no-color -json $plan_file ) + +# Define a function to parse the plan json with jq +parse_plan_json() { + echo $PLAN_JSON | jq "$@" +} + +# Define a function to make output friendly +make_output_friendly() { + local output=$1 + output="${output//'%'/'%25'}" + output="${output//$'\n'/'%0A'}" + output="${output//$'\r'/'%0D'}" + output="${output//'"'/'\"'}" + output="${output//'\\"'/'\\\"'}" + echo $output +} + +# Define a function to write the output to the github output file +write_output() { + local key=$1 + local value=$2 + echo "$key=$(make_output_friendly $value)" >> $GITHUB_OUTPUT +} + +# Get the terraform version from the plan json +VERSION=$(parse_plan_json .terraform_version) + +# Get the resource drift from the plan json +DRIFT=$(parse_plan_json .resource_drift) +DRIFT_COUNT=$(echo $DRIFT | jq length) +DRIFTED_RESOURCES=$(echo $DRIFT | jq -c '[.[] | {address: .address, changes: .change.actions}]') + +# Get the resource changes from the plan json +CHANGES=$(parse_plan_json .resource_changes) +if [[ $include_no_op = true ]]; then + CHANGES_FILTERED=$CHANGES +else + CHANGES_FILTERED=$(echo $CHANGES | jq -c '[.[] | {address: .address, changes: .change.actions} | select( .changes[] != "no-op")]') +fi +CHANGE_COUNT=$(echo $CHANGES_FILTERED | jq length) + +# Get the total resources and percent changed from the plan json +TOTAL_RESOURCES=$(parse_plan_json .planned_values.root_module) +TOTAL_ROOT=$(echo $TOTAL_RESOURCES | jq -c .resources | jq length) +TOTAL_CHILD=$(echo $TOTAL_RESOURCES | jq -c .child_modules | jq -c '[.[]?.resources | length] | add') +TOTAL_COUNT=$(( TOTAL_ROOT + TOTAL_CHILD )) +CHANGE_PERC=$(echo "scale=0 ; $CHANGE_COUNT / $TOTAL_COUNT * 100" | bc) + +# Write the output to the github output file +write_output "terraform-version" "$VERSION" +write_output "change-percent" "$CHANGE_PERC" +write_output "drift-count" "$DRIFT_COUNT" +write_output "change-count" "$CHANGE_COUNT" +write_output "resource-drifts" "$DRIFTED_RESOURCES" +write_output "resource-changes" "$CHANGES_FILTERED" diff --git a/.github/actions/terraform-stats/terraform/main.tf b/.github/actions/terraform-stats/terraform/main.tf new file mode 100644 index 00000000000..57ddbbef9c2 --- /dev/null +++ b/.github/actions/terraform-stats/terraform/main.tf @@ -0,0 +1,24 @@ +terraform { + required_providers { + docker = { + source = "kreuzwerker/docker" + version = "~> 2.13.0" + } + } +} + +provider "docker" {} + +resource "docker_image" "nginx" { + name = "nginx:latest" + keep_locally = false +} + +resource "docker_container" "nginx" { + image = docker_image.nginx.latest + name = "tutorial" + ports { + internal = 80 + external = 8000 + } +} \ No newline at end of file