mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
fix: make Terraform fixture generation predictable (#29481)
`make gen` could regenerate every Terraform fixture during unrelated work because the installed CLI differed from `version.txt`, or because the provider-version check failed. `make gen` now compares the generator, fixture sources, provider lock, and mise’s Terraform pin against a committed record. Changed inputs trigger regeneration; a different installed executable or checkout timestamps do not. Terraform runs in a temporary directory, and outputs are copied back only after generation succeeds. Shared CI checks the record and conversion behavior without regenerating Terraform fixtures. Fixes CODAGT-1082
This commit is contained in:
@@ -444,7 +444,7 @@ jobs:
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go node pnpm terraform protoc protoc-gen-go"
|
||||
install-args: "go node pnpm protoc protoc-gen-go"
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
uses: ./.github/actions/pnpm-install
|
||||
@@ -458,6 +458,10 @@ jobs:
|
||||
- name: Start PostgreSQL container
|
||||
run: make test-postgres-docker
|
||||
|
||||
# Reject an outdated input record before make gen could invoke Terraform.
|
||||
- name: Check Terraform fixture record
|
||||
run: ./provisioner/terraform/testdata/generate.sh --check
|
||||
|
||||
- name: make gen
|
||||
timeout-minutes: 8
|
||||
run: |
|
||||
|
||||
@@ -1018,7 +1018,7 @@ GEN_FILES := \
|
||||
docs/admin/setup/configuration-reference.md \
|
||||
coderd/apidoc/swagger.json \
|
||||
docs/manifest.json \
|
||||
provisioner/terraform/testdata/version \
|
||||
provisioner/terraform/testdata/generation.sha1 \
|
||||
scripts/metricsdocgen/generated_metrics \
|
||||
site/e2e/provisionerGenerated.ts \
|
||||
examples/examples.gen.json \
|
||||
@@ -1465,21 +1465,24 @@ coderd/notifications/.gen-golden: $(wildcard coderd/notifications/testdata/*/*.g
|
||||
TZ=UTC go test ./coderd/notifications -run="Test.*Golden$$" -update
|
||||
touch "$@"
|
||||
|
||||
provisioner/terraform/testdata/.gen-golden: $(wildcard provisioner/terraform/testdata/*/*.golden) $(wildcard provisioner/terraform/testdata/*/*/*.golden) $(GO_SRC_FILES) $(wildcard provisioner/terraform/*_test.go)
|
||||
# Wait for fixture generation before reading its outputs under make -j.
|
||||
provisioner/terraform/testdata/.gen-golden: provisioner/terraform/testdata/generation.sha1 $(wildcard provisioner/terraform/testdata/resources/*/*.tfplan.* provisioner/terraform/testdata/resources/*/*.tfstate.*) $(wildcard provisioner/terraform/testdata/*/*.golden) $(wildcard provisioner/terraform/testdata/*/*/*.golden) $(GO_SRC_FILES) $(wildcard provisioner/terraform/*_test.go)
|
||||
TZ=UTC go test ./provisioner/terraform -run="Test.*Golden$$" -update
|
||||
touch "$@"
|
||||
|
||||
provisioner/terraform/testdata/version:
|
||||
@tf_match=true; \
|
||||
if [[ "$$(cat provisioner/terraform/testdata/version.txt)" != \
|
||||
"$$(terraform version -json | jq -r '.terraform_version')" ]]; then \
|
||||
tf_match=false; \
|
||||
fi; \
|
||||
if ! $$tf_match || \
|
||||
! ./provisioner/terraform/testdata/generate.sh --check; then \
|
||||
./provisioner/terraform/testdata/generate.sh; \
|
||||
fi
|
||||
.PHONY: provisioner/terraform/testdata/version
|
||||
# Terraform reads ~/.terraformrc unless TF_CLI_CONFIG_FILE selects another file.
|
||||
# After rebuilding a local provider or changing its override, regenerate with:
|
||||
# ./provisioner/terraform/testdata/generate.sh
|
||||
provisioner/terraform/testdata/generation.sha1: FORCE
|
||||
@./provisioner/terraform/testdata/generate.sh --if-needed
|
||||
|
||||
FORCE:
|
||||
.PHONY: FORCE
|
||||
|
||||
# pre-commit runs gen and fmt concurrently; formatting must finish before hashing.
|
||||
ifneq ($(filter fmt,$(MAKECMDGOALS)),)
|
||||
provisioner/terraform/testdata/generation.sha1: | fmt/terraform fmt/shfmt
|
||||
endif
|
||||
|
||||
update-terraform-testdata:
|
||||
./provisioner/terraform/testdata/generate.sh --upgrade
|
||||
|
||||
@@ -189,8 +189,7 @@
|
||||
vendorHash = "sha256-OuQWmZmofdJKq1hvk43RPkILQwAuFzqhmB22Xf6Z3lA=";
|
||||
};
|
||||
|
||||
# Pin to provisioner/terraform/testdata/version.txt for deterministic
|
||||
# `make gen` across platforms.
|
||||
# Keep this pin aligned with mise.toml when updating Terraform.
|
||||
terraform_1_15_5 =
|
||||
let
|
||||
releases = {
|
||||
@@ -314,6 +313,7 @@
|
||||
proto_gen_go_1_30
|
||||
protobuf_23_4
|
||||
ripgrep
|
||||
rsync
|
||||
shellcheck
|
||||
(pinnedPkgs.shfmt)
|
||||
# sqlc
|
||||
|
||||
+170
-51
@@ -7,6 +7,58 @@ scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
cd "$scriptdir/resources"
|
||||
canonical_lock="$(pwd)/.terraform.lock.hcl"
|
||||
manifest="$scriptdir/generation.sha1"
|
||||
|
||||
# kubernetes-metadata requires live infrastructure. duplicate-env-keys has
|
||||
# repeated JSON keys whose UUIDs cannot be preserved by minimize_diff.
|
||||
skip_module() {
|
||||
case "$1" in
|
||||
kubernetes-metadata | duplicate-env-keys) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
terraform_version() {
|
||||
local version
|
||||
version="$(awk -F '"' '
|
||||
/^[[:space:]]*\[/ { tools = ($0 ~ /^[[:space:]]*\[tools\][[:space:]]*(#.*)?$/) }
|
||||
tools && /^[[:space:]]*terraform[[:space:]]*=[[:space:]]*"/ { print $2 }
|
||||
' "$scriptdir/../../../mise.toml")" || return
|
||||
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "ERROR: mise.toml must pin tools.terraform to a release version." >&2
|
||||
return 1
|
||||
fi
|
||||
printf '%s\n' "$version"
|
||||
}
|
||||
|
||||
# Content hashes avoid regeneration after checkout or a local CLI change.
|
||||
fingerprint() {
|
||||
local file module i
|
||||
local lockfile="${1:-.terraform.lock.hcl}"
|
||||
local version
|
||||
version="$(terraform_version)" || return
|
||||
local -a files=(../generate.sh .terraform.lock.hcl)
|
||||
find . -type d \( -name .terraform -o -name .coder \) -prune -o \( -type f -o -type l \) -print0 |
|
||||
LC_ALL=C sort -z | {
|
||||
while IFS= read -r -d '' file; do
|
||||
module="${file#./}"
|
||||
module="${module%%/*}"
|
||||
if skip_module "$module"; then
|
||||
continue
|
||||
fi
|
||||
case "$file" in
|
||||
*.tfplan.json | *.tfstate.json | *.tfplan.dot | *.tfstate.dot | ./.terraform.lock.hcl | */.terraform.lock.hcl | *.tfstate | *.tfstate.* | *.tfplan | *.golden | *.md | */.gitignore) continue ;;
|
||||
esac
|
||||
files+=("$file")
|
||||
done
|
||||
printf 'terraform\0%s\0' "$version"
|
||||
printf '%s\0' "${files[@]}"
|
||||
for i in "${!files[@]}"; do
|
||||
[[ "${files[i]}" != .terraform.lock.hcl ]] || files[i]="$lockfile"
|
||||
done
|
||||
git hash-object --no-filters -- "${files[@]}"
|
||||
} | git hash-object --stdin
|
||||
}
|
||||
|
||||
# These environment variables influence the coder provider.
|
||||
for v in $(env | grep -E '^CODER_' | cut -d= -f1); do
|
||||
@@ -14,11 +66,16 @@ for v in $(env | grep -E '^CODER_' | cut -d= -f1); do
|
||||
done
|
||||
|
||||
generate() {
|
||||
local name="$1"
|
||||
local name="$1" ret
|
||||
|
||||
echo "=== BEGIN: $name"
|
||||
if ((upgrade)); then
|
||||
terraform init -upgrade
|
||||
elif [[ -d "$scriptdir/resources/$name/.terraform/providers" ]]; then
|
||||
# An installed provider directory avoids registry version lookups.
|
||||
if ! terraform init -plugin-dir="$scriptdir/resources/$name/.terraform/providers"; then
|
||||
rm -rf .terraform/providers && terraform init
|
||||
fi
|
||||
else
|
||||
terraform init
|
||||
fi &&
|
||||
@@ -32,16 +89,24 @@ generate() {
|
||||
terraform graph -type=plan >"$name".tfstate.dot
|
||||
ret=$?
|
||||
echo "=== END: $name"
|
||||
if [[ $ret -ne 0 ]]; then
|
||||
return $ret
|
||||
fi
|
||||
return "$ret"
|
||||
}
|
||||
|
||||
minimize_diff() {
|
||||
local name="$1"
|
||||
local f diff status line key value
|
||||
for f in *.tf*.json; do
|
||||
[[ -f "$scriptdir/resources/$name/$f" ]] || continue
|
||||
if diff="$(git diff --no-index -- "$scriptdir/resources/$name/$f" "$f")"; then
|
||||
continue
|
||||
else
|
||||
status=$?
|
||||
((status == 1)) || return "$status"
|
||||
fi
|
||||
declare -A deleted=()
|
||||
declare -a sed_args=()
|
||||
while read -r line; do
|
||||
while IFS= read -r line; do
|
||||
[[ $line =~ \"(terraform_version|id|agent_id|subagent_id|resource_id|token|random|timestamp)\": ]] || continue
|
||||
# Deleted line (previous value).
|
||||
if [[ $line = -\ * ]]; then
|
||||
key="${line#*\"}"
|
||||
@@ -71,11 +136,7 @@ minimize_diff() {
|
||||
sed -i'' "${sed_args[@]}" "$f"
|
||||
fi
|
||||
fi
|
||||
done < <(
|
||||
# Filter out known keys with autogenerated values.
|
||||
git diff -- "$f" |
|
||||
grep -E "\"(terraform_version|id|agent_id|subagent_id|resource_id|token|random|timestamp)\":"
|
||||
)
|
||||
done <<<"$diff"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -89,24 +150,14 @@ extract_provider_version() {
|
||||
}
|
||||
|
||||
run() {
|
||||
d="$1"
|
||||
local d="$1" name out
|
||||
cd "$d"
|
||||
name=$(basename "$(pwd)")
|
||||
name="${PWD##*/}"
|
||||
|
||||
toskip=(
|
||||
# This needs care to update correctly.
|
||||
"kubernetes-metadata"
|
||||
# Multiple resources with duplicate JSON key names (id, agent_id)
|
||||
# cause minimize_diff() to scramble UUIDs. Hand-crafted fixture.
|
||||
"duplicate-env-keys"
|
||||
)
|
||||
for skip in "${toskip[@]}"; do
|
||||
if [[ $name == "$skip" ]]; then
|
||||
echo "== Skipping: $name"
|
||||
touch "$name.tfplan.json" "$name.tfplan.dot" "$name.tfstate.json" "$name.tfstate.dot"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
if skip_module "$name"; then
|
||||
echo "== Skipping hand-maintained fixture: $name"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "== Generating test data for: $name"
|
||||
if ! out="$(generate "$name" 2>&1)"; then
|
||||
@@ -116,14 +167,20 @@ run() {
|
||||
fi
|
||||
if ((minimize)); then
|
||||
echo "== Minimizing diffs for: $name"
|
||||
minimize_diff
|
||||
minimize_diff "$name"
|
||||
fi
|
||||
mkdir -p "$workdir/result/resources/$name"
|
||||
mv "$name".tf{plan,state}.{json,dot} "$workdir/result/resources/$name/"
|
||||
if [[ -d .terraform/providers ]]; then
|
||||
mkdir -p "$workdir/providers/resources/$name"
|
||||
find .terraform/providers -type f -print0 | rsync -r --from0 --files-from=- ./ "$workdir/providers/resources/$name/"
|
||||
fi
|
||||
echo "== Done generating test data for: $name"
|
||||
exit 0
|
||||
return 0
|
||||
}
|
||||
|
||||
if [[ " $* " == *" --help "* || " $* " == *" -h "* ]]; then
|
||||
echo "Usage: $0 [--upgrade] [--check] [--no-minimize] [module1 module2 ...]"
|
||||
echo "Usage: $0 [--upgrade] [--check] [--if-needed] [--no-minimize] [module1 module2 ...]"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -137,17 +194,34 @@ if [[ " $* " == *" --upgrade "* ]]; then
|
||||
upgrade=1
|
||||
fi
|
||||
|
||||
# Verify that the canonical lockfile matches provider-version.txt.
|
||||
if_needed=0
|
||||
if [[ " $* " == *" --if-needed "* ]]; then
|
||||
if_needed=1
|
||||
fi
|
||||
check=0
|
||||
if [[ " $* " == *" --check "* ]]; then
|
||||
check=1
|
||||
fi
|
||||
|
||||
inputs="$(fingerprint)"
|
||||
if ((check)); then
|
||||
expected="$(<"$scriptdir/provider-version.txt")"
|
||||
actual="$(extract_provider_version "$canonical_lock")"
|
||||
if [[ "$expected" == "$actual" ]]; then
|
||||
exit 0
|
||||
else
|
||||
echo "ERROR: provider-version.txt ($expected) does not match lockfile ($actual)"
|
||||
if [[ "$expected" != "$actual" ]]; then
|
||||
echo "ERROR: provider-version.txt ($expected) does not match lockfile ($actual)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if ((check || if_needed)); then
|
||||
if [[ -f "$manifest" && "$(cat "$manifest")" == "$inputs" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
if ((check)); then
|
||||
echo "Terraform fixture inputs changed; run make gen." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "== Terraform fixture inputs changed; regenerating"
|
||||
fi
|
||||
|
||||
# Committed testdata encodes linux/amd64 values from coder_provisioner.
|
||||
# Regenerating elsewhere bakes in the host OS/arch.
|
||||
@@ -160,32 +234,63 @@ if [[ "$(uname)" != "Linux" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
export CHECKPOINT_DISABLE=1
|
||||
# Filter flags from positional args to get directory names.
|
||||
declare -a dirs=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--upgrade | --no-minimize | --check | --help | -h) ;;
|
||||
*) dirs+=("$arg") ;;
|
||||
--upgrade | --no-minimize | --check | --if-needed | --help | -h) ;;
|
||||
*)
|
||||
module="$(cd "$arg" && pwd)"
|
||||
case "$module" in
|
||||
"$scriptdir/resources/"*) dirs+=("${module#"$scriptdir/resources/"}") ;;
|
||||
*)
|
||||
echo "ERROR: module must be within $scriptdir/resources: $arg" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Seed each resource subdirectory with the canonical lockfile.
|
||||
# Keep Terraform's working files and incomplete outputs out of the checkout.
|
||||
workdir="$(mktemp -d "$scriptdir/.generate.XXXXXX")"
|
||||
cleanup() {
|
||||
trap '' HUP INT TERM
|
||||
wait || true
|
||||
rm -rf "$workdir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 129' HUP
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
mkdir -p "$workdir/result/resources" "$workdir/providers"
|
||||
(
|
||||
cd "$scriptdir"
|
||||
tar --exclude='.terraform' --exclude='.coder' --exclude='*.tfstate' --exclude='*.tfstate.*' \
|
||||
--exclude='*.tfplan' -cf - resources generate.sh
|
||||
) | tar -xf - -C "$workdir"
|
||||
cd "$workdir/resources"
|
||||
canonical_lock="$workdir/resources/.terraform.lock.hcl"
|
||||
copied_inputs="$(fingerprint)"
|
||||
if [[ "$copied_inputs" != "$inputs" ]]; then
|
||||
echo "ERROR: fixture inputs changed while copying; rerun generation." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
full=0
|
||||
if [[ ${#dirs[@]} -eq 0 ]]; then
|
||||
full=1
|
||||
dirs=(*/)
|
||||
fi
|
||||
for d in */; do
|
||||
cp "$canonical_lock" "$d/.terraform.lock.hcl"
|
||||
done
|
||||
|
||||
declare -a jobs=()
|
||||
if [[ ${#dirs[@]} -gt 0 ]]; then
|
||||
for d in "${dirs[@]}"; do
|
||||
run "$d" &
|
||||
jobs+=($!)
|
||||
done
|
||||
else
|
||||
for d in */; do
|
||||
run "$d" &
|
||||
jobs+=($!)
|
||||
done
|
||||
fi
|
||||
for d in "${dirs[@]}"; do
|
||||
run "$d" &
|
||||
jobs+=($!)
|
||||
done
|
||||
|
||||
err=0
|
||||
for job in "${jobs[@]}"; do
|
||||
@@ -198,6 +303,7 @@ if [[ $err -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
recorded_inputs="$inputs"
|
||||
# After upgrade, promote the lockfile from a representative directory
|
||||
# back to the canonical location and record the provider version.
|
||||
if ((upgrade)); then
|
||||
@@ -215,10 +321,23 @@ if ((upgrade)); then
|
||||
fi
|
||||
if [[ -n "$src" ]]; then
|
||||
cp "$src" "$canonical_lock"
|
||||
cp "$canonical_lock" "$workdir/result/resources/.terraform.lock.hcl"
|
||||
recorded_inputs="$(cd "$scriptdir/resources" && fingerprint "$canonical_lock")"
|
||||
version="$(extract_provider_version "$canonical_lock")"
|
||||
echo "$version" >"$scriptdir/provider-version.txt"
|
||||
echo "$version" >"$workdir/result/provider-version.txt"
|
||||
echo "== Updated canonical lockfile and provider-version.txt (coder provider $version)"
|
||||
fi
|
||||
fi
|
||||
|
||||
terraform version -json | jq -r '.terraform_version' >../version.txt
|
||||
current_inputs="$(cd "$scriptdir/resources" && fingerprint)"
|
||||
if [[ "$current_inputs" != "$inputs" ]]; then
|
||||
echo "ERROR: fixture inputs changed during generation; rerun generation." >&2
|
||||
exit 1
|
||||
fi
|
||||
rsync -rlt --ignore-existing "$workdir/providers/" "$scriptdir/"
|
||||
rsync -rc --delay-updates "$workdir/result/" "$scriptdir/"
|
||||
if ((full)); then
|
||||
# Record success only after all outputs have been copied.
|
||||
printf '%s\n' "$recorded_inputs" >"$workdir/generation.sha1"
|
||||
rsync -c "$workdir/generation.sha1" "$scriptdir/"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
85e724a1f4c6fd4ace53208053534475f07d9949
|
||||
-1
@@ -1 +0,0 @@
|
||||
1.16.2
|
||||
Reference in New Issue
Block a user