Skip to content

Commit

Permalink
Merge pull request #320 from dflook/binary-plan
Browse files Browse the repository at this point in the history
Add plan_path for plan/apply
  • Loading branch information
dflook authored Jan 9, 2024
2 parents aa00041 + c37333f commit 21fbee5
Show file tree
Hide file tree
Showing 16 changed files with 297 additions and 55 deletions.
118 changes: 118 additions & 0 deletions .github/workflows/test-binary-plan.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: Test terraform-binary-plan

on:
- pull_request

jobs:
missing_plan_path:
runs-on: ubuntu-latest
name: Missing plan
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Apply
uses: ./terraform-apply
id: apply
continue-on-error: true
with:
path: tests/workflows/test-binary-plan
plan_path: hello.tfplan
auto_approve: true

- name: Verify outputs
run: |
if [[ "${{ steps.apply.outcome }}" != "failure" ]]; then
echo "Apply did not fail correctly"
exit 1
fi
apply:
runs-on: ubuntu-latest
name: Apply approved changes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Plan
uses: ./terraform-plan
id: plan
with:
label: test-binary-plan apply
path: tests/workflows/test-binary-plan

- name: Apply
uses: ./terraform-apply
id: first-apply
with:
label: test-binary-plan apply
path: tests/workflows/test-binary-plan
plan_path: ${{ steps.plan.outputs.plan_path }}

auto_approve:
runs-on: ubuntu-latest
name: Apply auto approved changes
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Plan
uses: ./terraform-plan
id: plan
with:
label: test-binary-plan auto_approve
path: tests/workflows/test-binary-plan
add_github_comment: false

- name: Apply
uses: ./terraform-apply
with:
label: test-binary-plan auto_approve
path: tests/workflows/test-binary-plan
plan_path: ${{ steps.plan.outputs.plan_path }}
auto_approve: true

plan_changed:
runs-on: ubuntu-latest
name: Apply should fail if the approved plan has changed
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Plan
uses: ./terraform-plan
id: plan
with:
label: test-binary-plan plan_changed
path: tests/workflows/test-binary-plan

- name: Plan
uses: ./terraform-plan
with:
label: test-binary-plan plan_changed
path: tests/workflows/test-binary-plan

- name: Apply
uses: ./terraform-apply
continue-on-error: true
id: apply
with:
label: test-binary-plan plan_changed
path: tests/workflows/test-binary-plan
plan_path: ${{ steps.plan.outputs.plan_path }}

- name: Verify outputs
run: |
if [[ "${{ steps.apply.outcome }}" != "failure" ]]; then
echo "Apply did not fail correctly"
exit 1
fi
if [[ "${{ steps.apply.outputs.failure-reason }}" != "plan-changed" ]]; then
echo "::error:: failure-reason not set correctly"
exit 1
fi
5 changes: 5 additions & 0 deletions .github/workflows/test-plan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ jobs:
echo "::error:: text_plan_path not set correctly"
exit 1
fi
if ! [[ -f '${{ steps.plan.outputs.plan_path }}' ]]; then
echo "::error:: plan_path not set correctly"
exit 1
fi
if [[ -n "${{ steps.apply.outputs.run_id }}" ]]; then
echo "::error:: run_id should not be set"
Expand Down
5 changes: 3 additions & 2 deletions image/actions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ function detect-terraform-version() {
TERRAFORM_VER_MINOR=$(echo "$TF_VERSION" | cut -d. -f2)
TERRAFORM_VER_PATCH=$(echo "$TF_VERSION" | cut -d. -f3)

terraform version
terraform version > "$STEP_TMP_DIR/terraform_version.stdout"
cat "$STEP_TMP_DIR/terraform_version.stdout"

if terraform version | grep --quiet OpenTofu; then
if grep --quiet OpenTofu "$STEP_TMP_DIR/terraform_version.stdout"; then
export TOOL_PRODUCT_NAME="OpenTofu"
export TOOL_COMMAND_NAME="tofu"
else
Expand Down
107 changes: 62 additions & 45 deletions image/entrypoints/apply.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,49 +77,59 @@ function apply() {

### Generate a plan

plan

if [[ $PLAN_EXIT -eq 1 ]]; then
if grep -q "Saving a generated plan is currently not supported" "$STEP_TMP_DIR/terraform_plan.stderr"; then
set-remote-plan-args
PLAN_OUT=""

if [[ "$INPUT_AUTO_APPROVE" == "true" ]]; then
# The apply will have to generate the plan, so skip doing it now
PLAN_EXIT=2
else
plan
fi
fi
fi

if [[ $PLAN_EXIT -eq 1 ]]; then
cat >&2 "$STEP_TMP_DIR/terraform_plan.stderr"

if lock-info "$STEP_TMP_DIR/terraform_plan.stderr"; then
set_output failure-reason state-locked
fi

update_comment error
exit 1
fi
if [[ "$INPUT_PLAN_PATH" != "" ]]; then
if [[ ! -f "$INPUT_PLAN_PATH" ]]; then
error_log "Plan file '$INPUT_PLAN_PATH' does not exist"
exit 1
fi

if [[ -z "$PLAN_OUT" && "$INPUT_AUTO_APPROVE" == "true" ]]; then
# Since we are doing an auto approved remote apply there is no point in planning beforehand
# No text_plan_path output for this run
:
PLAN_OUT=$(realpath $INPUT_PLAN_PATH)
PLAN_EXIT=2
else
mkdir -p "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR"
cp "$STEP_TMP_DIR/plan.txt" "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.txt"
set_output text_plan_path "$WORKSPACE_TMP_DIR/plan.txt"
fi

if [[ -n "$PLAN_OUT" ]]; then
if (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME show -json "$PLAN_OUT") >"$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.json" 2>"$STEP_TMP_DIR/terraform_show.stderr"; then
set_output json_plan_path "$WORKSPACE_TMP_DIR/plan.json"
else
debug_file "$STEP_TMP_DIR/terraform_show.stderr"
fi
plan

if [[ $PLAN_EXIT -eq 1 ]]; then
if grep -q "Saving a generated plan is currently not supported" "$STEP_TMP_DIR/terraform_plan.stderr"; then
set-remote-plan-args
PLAN_OUT=""

if [[ "$INPUT_AUTO_APPROVE" == "true" ]]; then
# The apply will have to generate the plan, so skip doing it now
PLAN_EXIT=2
else
plan
fi
fi
fi

if [[ $PLAN_EXIT -eq 1 ]]; then
cat >&2 "$STEP_TMP_DIR/terraform_plan.stderr"

if lock-info "$STEP_TMP_DIR/terraform_plan.stderr"; then
set_output failure-reason state-locked
fi

update_comment error
exit 1
fi

if [[ -z "$PLAN_OUT" && "$INPUT_AUTO_APPROVE" == "true" ]]; then
# Since we are doing an auto approved remote apply there is no point in planning beforehand
# No text_plan_path output for this run
:
else
mkdir -p "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR"
cp "$STEP_TMP_DIR/plan.txt" "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.txt"
set_output text_plan_path "$WORKSPACE_TMP_DIR/plan.txt"
fi

if [[ -n "$PLAN_OUT" ]]; then
if (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME show -json "$PLAN_OUT") >"$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.json" 2>"$STEP_TMP_DIR/terraform_show.stderr"; then
set_output json_plan_path "$WORKSPACE_TMP_DIR/plan.json"
else
debug_file "$STEP_TMP_DIR/terraform_show.stderr"
fi
fi
fi

### Apply the plan
Expand All @@ -142,11 +152,18 @@ else
exit 1
fi

if github_pr_comment approved "$STEP_TMP_DIR/plan.txt"; then
apply
if [[ "$INPUT_PLAN_PATH" != "" ]]; then
if github_pr_comment approved-binary "$PLAN_OUT"; then
apply
else
exit 1
fi
else
exit 1
if github_pr_comment approved "$STEP_TMP_DIR/plan.txt"; then
apply
else
exit 1
fi
fi

fi

5 changes: 4 additions & 1 deletion image/entrypoints/plan.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ if [[ "$GITHUB_EVENT_NAME" == "pull_request" || "$GITHUB_EVENT_NAME" == "issue_c
TF_CHANGES=true
fi

if ! TF_CHANGES=$TF_CHANGES STATUS=":memo: Plan generated in $(job_markdown_ref)" github_pr_comment plan <"$STEP_TMP_DIR/plan.txt"; then
if ! PLAN_OUT="$PLAN_OUT" TF_CHANGES=$TF_CHANGES STATUS=":memo: Plan generated in $(job_markdown_ref)" github_pr_comment plan <"$STEP_TMP_DIR/plan.txt"; then
exit 1
fi
fi
Expand Down Expand Up @@ -91,6 +91,9 @@ cp "$STEP_TMP_DIR/plan.txt" "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.txt"
set_output text_plan_path "$WORKSPACE_TMP_DIR/plan.txt"

if [[ -n "$PLAN_OUT" ]]; then
cp "$PLAN_OUT" "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.tfplan"
set_output plan_path "$WORKSPACE_TMP_DIR/plan.tfplan"

if (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME show -json "$PLAN_OUT") >"$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.json" 2>"$STEP_TMP_DIR/terraform_show.stderr"; then
set_output json_plan_path "$WORKSPACE_TMP_DIR/plan.json"
else
Expand Down
34 changes: 33 additions & 1 deletion image/src/github_pr_comment/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from github_pr_comment.backend_fingerprint import fingerprint
from github_pr_comment.cmp import plan_cmp, remove_warnings, remove_unchanged_attributes
from github_pr_comment.comment import find_comment, TerraformComment, update_comment, serialize, deserialize
from github_pr_comment.hash import comment_hash, plan_hash
from github_pr_comment.hash import comment_hash, plan_hash, plan_out_hash
from github_pr_comment.plan_formatting import format_diff
from plan_renderer.outputs import render_outputs
from plan_renderer.variables import render_argument_list, Sensitive
Expand Down Expand Up @@ -382,6 +382,9 @@ def is_approved(proposed_plan: str, comment: TerraformComment) -> bool:
debug('Approving plan based on plan text')
return plan_cmp(proposed_plan, comment.body)

def is_approved_binary_plan(plan_path: str, comment: TerraformComment) -> bool:
return plan_out_hash(plan_path, comment.issue_url) == comment.headers['plan_out_hash']

def truncate(text: str, max_size: int, too_big_message: str) -> str:
lines = []
total_size = 0
Expand Down Expand Up @@ -504,6 +507,12 @@ def main() -> int:

headers = comment.headers.copy()
headers['plan_job_ref'] = job_workflow_ref()

if os.environ.get('PLAN_OUT', ''):
headers['plan_out_hash'] = plan_out_hash(os.environ['PLAN_OUT'], comment.issue_url)
elif 'plan_out_hash' in headers:
del headers['plan_out_hash']

headers['plan_hash'] = plan_hash(body, comment.issue_url)
format_type = os.environ.get('TF_ACTIONS_PLAN_FORMAT', 'diff')
headers['plan_text_format'], plan_text = format_plan_text(body, format_type)
Expand Down Expand Up @@ -565,6 +574,29 @@ def main() -> int:
with open(sys.argv[2], 'w') as f:
f.write(comment.body)

elif sys.argv[1] == 'approved-binary':

if comment.comment_url is None:
sys.stdout.write("Plan not found on PR\n")
sys.stdout.write("Generate the plan first using the dflook/terraform-plan action. Alternatively set the auto_approve input to 'true'\n")
sys.stdout.write("If dflook/terraform-plan was used with add_github_comment set to changes-only, this may mean the plan has since changed to include changes\n")
output('failure-reason', 'plan-changed')
sys.exit(1)

if not is_approved_binary_plan(sys.argv[2], comment):

sys.stdout.write("Not applying the plan - it has changed from the plan on the PR\n")
sys.stdout.write("The plan on the PR must be up to date. Alternatively, set the auto_approve input to 'true' to apply outdated plans\n")
comment = update_comment(github, comment, status=f':x: Plan not applied in {job_markdown_ref()} (Plan has changed)')

if plan_ref := comment.headers.get('plan_job_ref'):
sys.stdout.write(f'\nThis plan is different to the plan generated by the dflook/terraform-plan action in {plan_ref}\n')

output('failure-reason', 'plan-changed')

step_cache['comment'] = serialize(comment)
return 1

elif sys.argv[1] == 'approved':

proposed_plan = remove_warnings(remove_unchanged_attributes(Path(sys.argv[2]).read_text().strip()))
Expand Down
15 changes: 15 additions & 0 deletions image/src/github_pr_comment/hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,18 @@ def plan_hash(plan_text: str, salt: str) -> str:
plan = remove_warnings(remove_unchanged_attributes(plan_text))

return comment_hash(plan.encode(), salt)

def plan_out_hash(plan_path: str, salt: str) -> str:
"""
Compute a sha256 hash of the binary plan file
"""

debug(f'Hashing {plan_path} with salt {salt}')

h = hashlib.sha256(f'dflook/terraform-github-actions/{salt}'.encode())

with open(plan_path, 'rb') as f:
while data := f.read(65536):
h.update(data)

return h.hexdigest()
Loading

0 comments on commit 21fbee5

Please sign in to comment.