Skip to content

Commit 41789e8

Browse files
committed
Add plan_path for plan/apply
1 parent aa00041 commit 41789e8

File tree

16 files changed

+297
-55
lines changed

16 files changed

+297
-55
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
name: Test terraform-binary-plan
2+
3+
on:
4+
- pull_request
5+
6+
jobs:
7+
missing_plan_path:
8+
runs-on: ubuntu-latest
9+
name: Missing plan
10+
steps:
11+
- name: Checkout
12+
uses: actions/checkout@v3
13+
14+
- name: Apply
15+
uses: ./terraform-apply
16+
id: apply
17+
continue-on-error: true
18+
with:
19+
path: tests/workflows/test-binary-plan
20+
plan_path: hello.tfplan
21+
auto_approve: true
22+
23+
- name: Verify outputs
24+
run: |
25+
if [[ "${{ steps.apply.outcome }}" != "failure" ]]; then
26+
echo "Apply did not fail correctly"
27+
exit 1
28+
fi
29+
30+
apply:
31+
runs-on: ubuntu-latest
32+
name: Apply approved changes
33+
env:
34+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35+
steps:
36+
- name: Checkout
37+
uses: actions/checkout@v3
38+
39+
- name: Plan
40+
uses: ./terraform-plan
41+
id: plan
42+
with:
43+
label: test-binary-plan apply
44+
path: tests/workflows/test-binary-plan
45+
46+
- name: Apply
47+
uses: ./terraform-apply
48+
id: first-apply
49+
with:
50+
label: test-binary-plan apply
51+
path: tests/workflows/test-binary-plan
52+
plan_path: ${{ steps.plan.outputs.plan_path }}
53+
54+
auto_approve:
55+
runs-on: ubuntu-latest
56+
name: Apply auto approved changes
57+
steps:
58+
- name: Checkout
59+
uses: actions/checkout@v3
60+
61+
- name: Plan
62+
uses: ./terraform-plan
63+
id: plan
64+
with:
65+
label: test-binary-plan auto_approve
66+
path: tests/workflows/test-binary-plan
67+
add_github_comment: false
68+
69+
- name: Apply
70+
uses: ./terraform-apply
71+
with:
72+
label: test-binary-plan auto_approve
73+
path: tests/workflows/test-binary-plan
74+
plan_path: ${{ steps.plan.outputs.plan_path }}
75+
auto_approve: true
76+
77+
plan_changed:
78+
runs-on: ubuntu-latest
79+
name: Apply should fail if the approved plan has changed
80+
env:
81+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
82+
steps:
83+
- name: Checkout
84+
uses: actions/checkout@v3
85+
86+
- name: Plan
87+
uses: ./terraform-plan
88+
id: plan
89+
with:
90+
label: test-binary-plan plan_changed
91+
path: tests/workflows/test-binary-plan
92+
93+
- name: Plan
94+
uses: ./terraform-plan
95+
with:
96+
label: test-binary-plan plan_changed
97+
path: tests/workflows/test-binary-plan
98+
99+
- name: Apply
100+
uses: ./terraform-apply
101+
continue-on-error: true
102+
id: apply
103+
with:
104+
label: test-binary-plan plan_changed
105+
path: tests/workflows/test-binary-plan
106+
plan_path: ${{ steps.plan.outputs.plan_path }}
107+
108+
- name: Verify outputs
109+
run: |
110+
if [[ "${{ steps.apply.outcome }}" != "failure" ]]; then
111+
echo "Apply did not fail correctly"
112+
exit 1
113+
fi
114+
115+
if [[ "${{ steps.apply.outputs.failure-reason }}" != "plan-changed" ]]; then
116+
echo "::error:: failure-reason not set correctly"
117+
exit 1
118+
fi

.github/workflows/test-plan.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ jobs:
3838
echo "::error:: text_plan_path not set correctly"
3939
exit 1
4040
fi
41+
42+
if ! [[ -f '${{ steps.plan.outputs.plan_path }}' ]]; then
43+
echo "::error:: plan_path not set correctly"
44+
exit 1
45+
fi
4146
4247
if [[ -n "${{ steps.apply.outputs.run_id }}" ]]; then
4348
echo "::error:: run_id should not be set"

image/actions.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ function detect-terraform-version() {
5151
TERRAFORM_VER_MINOR=$(echo "$TF_VERSION" | cut -d. -f2)
5252
TERRAFORM_VER_PATCH=$(echo "$TF_VERSION" | cut -d. -f3)
5353

54-
terraform version
54+
terraform version > "$STEP_TMP_DIR/terraform_version.stdout"
55+
cat "$STEP_TMP_DIR/terraform_version.stdout"
5556

56-
if terraform version | grep --quiet OpenTofu; then
57+
if grep --quiet OpenTofu "$STEP_TMP_DIR/terraform_version.stdout"; then
5758
export TOOL_PRODUCT_NAME="OpenTofu"
5859
export TOOL_COMMAND_NAME="tofu"
5960
else

image/entrypoints/apply.sh

Lines changed: 62 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -77,49 +77,59 @@ function apply() {
7777

7878
### Generate a plan
7979

80-
plan
81-
82-
if [[ $PLAN_EXIT -eq 1 ]]; then
83-
if grep -q "Saving a generated plan is currently not supported" "$STEP_TMP_DIR/terraform_plan.stderr"; then
84-
set-remote-plan-args
85-
PLAN_OUT=""
86-
87-
if [[ "$INPUT_AUTO_APPROVE" == "true" ]]; then
88-
# The apply will have to generate the plan, so skip doing it now
89-
PLAN_EXIT=2
90-
else
91-
plan
92-
fi
93-
fi
94-
fi
95-
96-
if [[ $PLAN_EXIT -eq 1 ]]; then
97-
cat >&2 "$STEP_TMP_DIR/terraform_plan.stderr"
98-
99-
if lock-info "$STEP_TMP_DIR/terraform_plan.stderr"; then
100-
set_output failure-reason state-locked
101-
fi
102-
103-
update_comment error
104-
exit 1
105-
fi
80+
if [[ "$INPUT_PLAN_PATH" != "" ]]; then
81+
if [[ ! -f "$INPUT_PLAN_PATH" ]]; then
82+
echo "Plan file '$INPUT_PLAN_PATH' does not exist"
83+
exit 1
84+
fi
10685

107-
if [[ -z "$PLAN_OUT" && "$INPUT_AUTO_APPROVE" == "true" ]]; then
108-
# Since we are doing an auto approved remote apply there is no point in planning beforehand
109-
# No text_plan_path output for this run
110-
:
86+
PLAN_OUT=$(realpath $INPUT_PLAN_PATH)
87+
PLAN_EXIT=2
11188
else
112-
mkdir -p "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR"
113-
cp "$STEP_TMP_DIR/plan.txt" "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.txt"
114-
set_output text_plan_path "$WORKSPACE_TMP_DIR/plan.txt"
115-
fi
116-
117-
if [[ -n "$PLAN_OUT" ]]; then
118-
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
119-
set_output json_plan_path "$WORKSPACE_TMP_DIR/plan.json"
120-
else
121-
debug_file "$STEP_TMP_DIR/terraform_show.stderr"
122-
fi
89+
plan
90+
91+
if [[ $PLAN_EXIT -eq 1 ]]; then
92+
if grep -q "Saving a generated plan is currently not supported" "$STEP_TMP_DIR/terraform_plan.stderr"; then
93+
set-remote-plan-args
94+
PLAN_OUT=""
95+
96+
if [[ "$INPUT_AUTO_APPROVE" == "true" ]]; then
97+
# The apply will have to generate the plan, so skip doing it now
98+
PLAN_EXIT=2
99+
else
100+
plan
101+
fi
102+
fi
103+
fi
104+
105+
if [[ $PLAN_EXIT -eq 1 ]]; then
106+
cat >&2 "$STEP_TMP_DIR/terraform_plan.stderr"
107+
108+
if lock-info "$STEP_TMP_DIR/terraform_plan.stderr"; then
109+
set_output failure-reason state-locked
110+
fi
111+
112+
update_comment error
113+
exit 1
114+
fi
115+
116+
if [[ -z "$PLAN_OUT" && "$INPUT_AUTO_APPROVE" == "true" ]]; then
117+
# Since we are doing an auto approved remote apply there is no point in planning beforehand
118+
# No text_plan_path output for this run
119+
:
120+
else
121+
mkdir -p "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR"
122+
cp "$STEP_TMP_DIR/plan.txt" "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.txt"
123+
set_output text_plan_path "$WORKSPACE_TMP_DIR/plan.txt"
124+
fi
125+
126+
if [[ -n "$PLAN_OUT" ]]; then
127+
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
128+
set_output json_plan_path "$WORKSPACE_TMP_DIR/plan.json"
129+
else
130+
debug_file "$STEP_TMP_DIR/terraform_show.stderr"
131+
fi
132+
fi
123133
fi
124134

125135
### Apply the plan
@@ -142,11 +152,18 @@ else
142152
exit 1
143153
fi
144154

145-
if github_pr_comment approved "$STEP_TMP_DIR/plan.txt"; then
146-
apply
155+
if [[ "$INPUT_PLAN_PATH" != "" ]]; then
156+
if github_pr_comment approved-binary "$PLAN_OUT"; then
157+
apply
158+
else
159+
exit 1
160+
fi
147161
else
148-
exit 1
162+
if github_pr_comment approved "$STEP_TMP_DIR/plan.txt"; then
163+
apply
164+
else
165+
exit 1
166+
fi
149167
fi
150168

151169
fi
152-

image/entrypoints/plan.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ if [[ "$GITHUB_EVENT_NAME" == "pull_request" || "$GITHUB_EVENT_NAME" == "issue_c
6060
TF_CHANGES=true
6161
fi
6262

63-
if ! TF_CHANGES=$TF_CHANGES STATUS=":memo: Plan generated in $(job_markdown_ref)" github_pr_comment plan <"$STEP_TMP_DIR/plan.txt"; then
63+
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
6464
exit 1
6565
fi
6666
fi
@@ -91,6 +91,9 @@ cp "$STEP_TMP_DIR/plan.txt" "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.txt"
9191
set_output text_plan_path "$WORKSPACE_TMP_DIR/plan.txt"
9292

9393
if [[ -n "$PLAN_OUT" ]]; then
94+
cp "$PLAN_OUT" "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.tfplan"
95+
set_output plan_path "$WORKSPACE_TMP_DIR/plan.tfplan"
96+
9497
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
9598
set_output json_plan_path "$WORKSPACE_TMP_DIR/plan.json"
9699
else

image/src/github_pr_comment/__main__.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from github_pr_comment.backend_fingerprint import fingerprint
2121
from github_pr_comment.cmp import plan_cmp, remove_warnings, remove_unchanged_attributes
2222
from github_pr_comment.comment import find_comment, TerraformComment, update_comment, serialize, deserialize
23-
from github_pr_comment.hash import comment_hash, plan_hash
23+
from github_pr_comment.hash import comment_hash, plan_hash, plan_out_hash
2424
from github_pr_comment.plan_formatting import format_diff
2525
from plan_renderer.outputs import render_outputs
2626
from plan_renderer.variables import render_argument_list, Sensitive
@@ -382,6 +382,9 @@ def is_approved(proposed_plan: str, comment: TerraformComment) -> bool:
382382
debug('Approving plan based on plan text')
383383
return plan_cmp(proposed_plan, comment.body)
384384

385+
def is_approved_binary_plan(plan_path: str, comment: TerraformComment) -> bool:
386+
return plan_out_hash(plan_path, comment.issue_url) == comment.headers['plan_out_hash']
387+
385388
def truncate(text: str, max_size: int, too_big_message: str) -> str:
386389
lines = []
387390
total_size = 0
@@ -504,6 +507,12 @@ def main() -> int:
504507

505508
headers = comment.headers.copy()
506509
headers['plan_job_ref'] = job_workflow_ref()
510+
511+
if os.environ.get('PLAN_OUT', ''):
512+
headers['plan_out_hash'] = plan_out_hash(os.environ['PLAN_OUT'], comment.issue_url)
513+
elif 'plan_out_hash' in headers:
514+
del headers['plan_out_hash']
515+
507516
headers['plan_hash'] = plan_hash(body, comment.issue_url)
508517
format_type = os.environ.get('TF_ACTIONS_PLAN_FORMAT', 'diff')
509518
headers['plan_text_format'], plan_text = format_plan_text(body, format_type)
@@ -565,6 +574,29 @@ def main() -> int:
565574
with open(sys.argv[2], 'w') as f:
566575
f.write(comment.body)
567576

577+
elif sys.argv[1] == 'approved-binary':
578+
579+
if comment.comment_url is None:
580+
sys.stdout.write("Plan not found on PR\n")
581+
sys.stdout.write("Generate the plan first using the dflook/terraform-plan action. Alternatively set the auto_approve input to 'true'\n")
582+
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")
583+
output('failure-reason', 'plan-changed')
584+
sys.exit(1)
585+
586+
if not is_approved_binary_plan(sys.argv[2], comment):
587+
588+
sys.stdout.write("Not applying the plan - it has changed from the plan on the PR\n")
589+
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")
590+
comment = update_comment(github, comment, status=f':x: Plan not applied in {job_markdown_ref()} (Plan has changed)')
591+
592+
if plan_ref := comment.headers.get('plan_job_ref'):
593+
sys.stdout.write(f'\nThis plan is different to the plan generated by the dflook/terraform-plan action in {plan_ref}\n')
594+
595+
output('failure-reason', 'plan-changed')
596+
597+
step_cache['comment'] = serialize(comment)
598+
return 1
599+
568600
elif sys.argv[1] == 'approved':
569601

570602
proposed_plan = remove_warnings(remove_unchanged_attributes(Path(sys.argv[2]).read_text().strip()))

image/src/github_pr_comment/hash.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,18 @@ def plan_hash(plan_text: str, salt: str) -> str:
2323
plan = remove_warnings(remove_unchanged_attributes(plan_text))
2424

2525
return comment_hash(plan.encode(), salt)
26+
27+
def plan_out_hash(plan_path: str, salt: str) -> str:
28+
"""
29+
Compute a sha256 hash of the binary plan file
30+
"""
31+
32+
debug(f'Hashing {plan_path} with salt {salt}')
33+
34+
h = hashlib.sha256(f'dflook/terraform-github-actions/{salt}'.encode())
35+
36+
with open(plan_path, 'rb') as f:
37+
while data := f.read(65536):
38+
h.update(data)
39+
40+
return h.hexdigest()

0 commit comments

Comments
 (0)