Skip to content

Commit 23ebd73

Browse files
[minor] Write exact GCP smoke namespaces
Encode complete GitHub run IDs once for fresh and upgrade writers while retaining the dual cleanup reader and existing non-GCP naming.
1 parent d518627 commit 23ebd73

12 files changed

Lines changed: 318 additions & 50 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ $(CLOUD_COMPOSE_CI_BIN): $(GO_MODULE_FILES) $(GO_SOURCES)
110110
@mkdir -p "$(dir $(CLOUD_COMPOSE_CI_BIN))"
111111
go build -trimpath -o "$(CLOUD_COMPOSE_CI_BIN)" ./cmd/cloud-compose-ci
112112

113-
gcp-upgrade-smoke-contract:
113+
gcp-upgrade-smoke-contract: cloud-compose-ci
114114
bash ci/gcp-upgrade-smoke-contract.sh
115115

116116
artifact-install-contract:

ci/cloud-smoke.sh

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,8 @@ target_workdir() {
554554
}
555555

556556
target_var_args() {
557-
local root="$1" key_path="$2" target="$3" public_key provider template
557+
local root="$1" key_path="$2" target="$3" run_id="$4" run_namespace="$5"
558+
local public_key provider template
558559
local source_ref source_sha256 source_cache_key checksum_dir checksum_file archive_tmp
559560

560561
provider="$(target_provider "$target")"
@@ -603,8 +604,11 @@ target_var_args() {
603604
fi
604605
printf '%s\0%s\0' "-var" "cloud_compose_source_sha256=${source_sha256}"
605606
fi
606-
if grep -q 'variable "smoke_run_id"' "$root/variables.tf" && [[ -n "$(smoke_run_id)" ]]; then
607-
printf '%s\0%s\0' "-var" "smoke_run_id=$(smoke_run_id)"
607+
if grep -q 'variable "smoke_run_id"' "$root/variables.tf" && [[ -n "$run_id" ]]; then
608+
printf '%s\0%s\0' "-var" "smoke_run_id=${run_id}"
609+
fi
610+
if grep -q 'variable "smoke_run_namespace"' "$root/variables.tf" && [[ -n "$run_namespace" ]]; then
611+
printf '%s\0%s\0' "-var" "smoke_run_namespace=${run_namespace}"
608612
fi
609613
if grep -q 'variable "gcp_project_id"' "$root/variables.tf"; then
610614
printf '%s\0%s\0' "-var" "gcp_project_id=${GCLOUD_PROJECT:-}"
@@ -846,7 +850,7 @@ run_target() (
846850
set -euo pipefail
847851

848852
local target="$1"
849-
local root workdir key_path home_dir output_json public_key run_id
853+
local root workdir key_path home_dir output_json public_key run_id run_namespace
850854
local -a auto_args var_args
851855

852856
root="$(target_root "$target")"
@@ -861,10 +865,10 @@ run_target() (
861865

862866
ensure_key "$key_path"
863867
run_id="$(smoke_run_id)"
864-
# Validate GCP cleanup ownership before Terraform can create resources. The
865-
# reader phase intentionally keeps writing the legacy resource namespace.
866-
gcp_run_namespace "$target" "$run_id" >/dev/null
867-
mapfile -d '' -t var_args < <(target_var_args "$root" "$key_path" "$target")
868+
# Compute outside process substitution so an invalid hosted run ID aborts
869+
# before Terraform can create resources under an undiscoverable namespace.
870+
run_namespace="$(gcp_run_namespace "$target" "$run_id")"
871+
mapfile -d '' -t var_args < <(target_var_args "$root" "$key_path" "$target" "$run_id" "$run_namespace")
868872

869873
auto_args=()
870874
if [[ -n "${GITHUB_ACTIONS:-}" || "${CLOUD_COMPOSE_SMOKE_AUTO_APPROVE:-}" == "true" ]]; then
@@ -951,15 +955,20 @@ destroy_target() (
951955
set -euo pipefail
952956

953957
local target="$1"
954-
local root workdir key_path destroy_status destroy_timeout cleanup_status
958+
local root workdir key_path destroy_status destroy_timeout cleanup_status run_id run_namespace
955959
local -a auto_args var_args
956960

957961
root="$(target_root "$target")"
958962
target_env "$target"
959963

960964
workdir="$(target_workdir "$target")"
961965
key_path="$workdir/id_ed25519"
962-
mapfile -d '' -t var_args < <(target_var_args "$root" "$key_path" "$target")
966+
run_id="$(smoke_run_id)"
967+
run_namespace=""
968+
if [[ -n "$run_id" ]]; then
969+
run_namespace="$(gcp_run_namespace "$target" "$run_id")"
970+
fi
971+
mapfile -d '' -t var_args < <(target_var_args "$root" "$key_path" "$target" "$run_id" "$run_namespace")
963972

964973
auto_args=()
965974
if [[ -n "${GITHUB_ACTIONS:-}" || "${CLOUD_COMPOSE_SMOKE_AUTO_APPROVE:-}" == "true" ]]; then
@@ -986,7 +995,7 @@ destroy_target() (
986995
fi
987996

988997
cleanup_status=0
989-
provider_tag_cleanup "$target" "$(smoke_run_id)" || cleanup_status=$?
998+
provider_tag_cleanup "$target" "$run_id" || cleanup_status=$?
990999

9911000
if [[ "$destroy_status" -ne 0 && "$cleanup_status" -eq 0 ]]; then
9921001
echo "Provider tag cleanup completed for ${target} after Terraform destroy failed"

ci/gcp-upgrade-smoke-contract.sh

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ grep -Fq 'CLOUD_COMPOSE_SMOKE_RUN_ID must match GITHUB_RUN_ID in GitHub Actions'
5050
fail "hosted cleanup ownership is not bound to the actual GitHub run id"
5151
grep -Fq 'CLOUD_COMPOSE_SMOKE_RUN_ID must be set explicitly outside GitHub Actions' "$script" ||
5252
fail "manual upgrade runs can accidentally reuse an implicit cleanup scope"
53+
grep -Fq '"$runner" gcp namespace --run-id "$run_id"' "$script" ||
54+
fail "upgrade runner does not use the shared exact run-namespace codec"
55+
grep -Fq 'name="cc-g-wp-${run_namespace}-up"' "$script" ||
56+
fail "upgrade resource names do not use the exact run namespace"
57+
grep -Fq 'cloud-compose-gcp-upgrade-${run_namespace}' "$script" ||
58+
fail "upgrade working directories do not use the exact run namespace"
59+
if grep -Fq 'sanitize_run_fragment' "$script"; then
60+
fail "upgrade runner still truncates run IDs to the legacy namespace"
61+
fi
5362
grep -Fq '/mnt/disks/data/.cloud-compose-upgrade-sentinel' "$script" ||
5463
fail "upgrade runner omits the persistent data-disk sentinel"
5564
grep -Fq '/mnt/disks/volumes/.cloud-compose-upgrade-sentinel' "$script" ||
@@ -214,7 +223,7 @@ cleanup_log="$tmp/cleanup.log"
214223
set +e
215224
GITHUB_ACTIONS='' \
216225
GITHUB_RUN_ID='' \
217-
CLOUD_COMPOSE_SMOKE_RUN_ID=contract-run \
226+
CLOUD_COMPOSE_SMOKE_RUN_ID=123456789 \
218227
CLOUD_COMPOSE_UPGRADE_CURRENT_REF=HEAD \
219228
GCLOUD_PROJECT=contract-project \
220229
GCLOUD_NETWORK_PROJECT_ID=contract-project \

ci/gcp-upgrade-smoke.sh

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,15 @@ fail() {
7373
return 1
7474
}
7575

76-
sanitize_run_fragment() {
77-
local value="$1"
76+
exact_run_namespace() {
77+
local run_id="$1" runner
7878

79-
value="$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | cut -c1-8)"
80-
if [[ -z "$value" ]]; then
81-
fail "the smoke run id did not contain a usable name fragment"
79+
runner="${CLOUD_COMPOSE_CI_BIN:-$repo_root/.bin/cloud-compose-ci}"
80+
if [[ ! -x "$runner" ]]; then
81+
fail "compiled CI runner is missing at ${runner}; run 'make cloud-compose-ci' first"
8282
return 1
8383
fi
84-
printf '%s\n' "$value"
84+
"$runner" gcp namespace --run-id "$run_id"
8585
}
8686

8787
upgrade_run_id() {
@@ -832,7 +832,7 @@ run_upgrade() (
832832
require_env GCLOUD_POWER_START_ROLE
833833
require_env GCLOUD_POWER_SUSPEND_ROLE
834834

835-
local requested_base current_ref current_sha base_sha run_id run_fragment name
835+
local requested_base current_ref current_sha base_sha run_id run_namespace name
836836
local work_root old_source new_source state_path old_data new_data key_path public_key
837837
local runner_ipv4 runner_cidr region zone plan_file plan_json nonce
838838
local old_root new_root old_output new_output old_home new_home
@@ -850,11 +850,11 @@ run_upgrade() (
850850
[[ "$current_sha" =~ ^[0-9a-f]{40}$ ]] || fail "current upgrade ref did not resolve to a full commit"
851851

852852
run_id="$(upgrade_run_id)"
853-
run_fragment="$(sanitize_run_fragment "$run_id")"
854-
name="cc-g-wp-${run_fragment}-up"
853+
run_namespace="$(exact_run_namespace "$run_id")"
854+
name="cc-g-wp-${run_namespace}-up"
855855
export CLOUD_COMPOSE_SMOKE_RUN_ID="$run_id"
856856

857-
work_root="${CLOUD_COMPOSE_GCP_UPGRADE_WORKDIR:-${RUNNER_TEMP:-/tmp}/cloud-compose-gcp-upgrade-${run_fragment}}"
857+
work_root="${CLOUD_COMPOSE_GCP_UPGRADE_WORKDIR:-${RUNNER_TEMP:-/tmp}/cloud-compose-gcp-upgrade-${run_namespace}}"
858858
[[ "$work_root" == /* ]] || fail "CLOUD_COMPOSE_GCP_UPGRADE_WORKDIR must be an absolute path"
859859
old_source="$work_root/source-0.10.2"
860860
new_source="$work_root/source-current"
@@ -1008,13 +1008,13 @@ destroy_upgrade() {
10081008
require_env GCLOUD_NETWORK_NAME
10091009
require_env GCLOUD_SUBNETWORK_NAME
10101010

1011-
local current_sha run_id run_fragment work_root old_source new_source state_path old_data new_data
1011+
local current_sha run_id run_namespace work_root old_source new_source state_path old_data new_data
10121012

10131013
current_sha="$(git -C "$repo_root" rev-parse --verify "${CLOUD_COMPOSE_UPGRADE_CURRENT_REF:-HEAD}^{commit}")"
10141014
run_id="$(upgrade_run_id)"
1015-
run_fragment="$(sanitize_run_fragment "$run_id")"
1015+
run_namespace="$(exact_run_namespace "$run_id")"
10161016
export CLOUD_COMPOSE_SMOKE_RUN_ID="$run_id"
1017-
work_root="${CLOUD_COMPOSE_GCP_UPGRADE_WORKDIR:-${RUNNER_TEMP:-/tmp}/cloud-compose-gcp-upgrade-${run_fragment}}"
1017+
work_root="${CLOUD_COMPOSE_GCP_UPGRADE_WORKDIR:-${RUNNER_TEMP:-/tmp}/cloud-compose-gcp-upgrade-${run_namespace}}"
10181018
[[ "$work_root" == /* ]] || fail "CLOUD_COMPOSE_GCP_UPGRADE_WORKDIR must be an absolute path"
10191019
old_source="$work_root/source-0.10.2"
10201020
new_source="$work_root/source-current"

docs/runtime-contracts.md

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -756,20 +756,26 @@ workflow, including cases where cancellation prevented the in-job destroy from
756756
finishing. Never allow pull-request branches in a cleanup environment's
757757
deployment policy.
758758

759-
GCP run namespaces require a staged compatibility change because the privileged
760-
fallback always executes the default branch. The compiled runner recognizes the
761-
legacy first-eight-character namespace and the reserved exact namespace that
762-
encodes a canonical numeric run ID as nine fixed-width base36 characters. That
763-
width preserves both separators and the random suffix for every supported GCP
764-
template within the 21-character resource-name limit. In this reader phase the
765-
fresh GCP driver calls the pure `cloud-compose-ci gcp namespace` command before
766-
Terraform argument construction to require a non-empty canonical run ID no
767-
larger than 44 bits, while deliberately continuing to write legacy names. Manual GCP
768-
smoke applies must therefore set `CLOUD_COMPOSE_SMOKE_RUN_ID`; non-GCP smoke
769-
naming and invocation remain unchanged. Merge the
770-
dual-reader before changing the smoke Terraform writer. Retain the legacy reader
771-
until all branches and resources created by the old writer have expired; never
772-
introduce a new writer that the trusted fallback cannot discover.
759+
GCP smoke writers encode the complete canonical numeric GitHub Actions run ID as
760+
a fixed-width, nine-character lowercase base36 namespace. The fresh and upgrade
761+
drivers obtain that value from the compiled `cloud-compose-ci gcp namespace`
762+
command before Terraform can create resources. A manual fresh GCP smoke apply
763+
must set `CLOUD_COMPOSE_SMOKE_RUN_ID`; an empty, malformed, noncanonical, or
764+
greater-than-44-bit value fails before apply. Non-GCP smoke invocation remains
765+
unchanged. The namespace width preserves both
766+
separators and at least one random suffix character for every supported GCP
767+
template within the 21-character resource-name limit. The original decimal run
768+
ID remains the cleanup input and the source of compatibility tags; DigitalOcean
769+
and Linode naming and invocation are unchanged. Manual GCP smoke applies must
770+
set `CLOUD_COMPOSE_SMOKE_RUN_ID`; the smoke context validates that the supplied
771+
namespace decodes to that same canonical, at-most-44-bit run ID.
772+
773+
The privileged fallback always executes the default branch, so its compiled
774+
runner remains a dual reader: it queries both the legacy first-eight-character
775+
namespace and the exact namespace for a run. Retain the legacy reader until all
776+
branches and resources created by the old writer have expired. A malformed,
777+
noncanonical, or greater-than-44-bit run ID fails before apply rather than
778+
creating a namespace that the trusted fallback cannot discover.
773779

774780
Use separate provider identities for smoke and fallback cleanup:
775781

internal/contracttest/cloud_smoke_cleanup_test.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func TestCloudSmokeGCPApplyRequiresCanonicalRunID(t *testing.T) {
101101
driverPath := filepath.Join(root, "ci/cloud-smoke.sh")
102102
driver := readRepositoryFile(t, root, "ci/cloud-smoke.sh")
103103

104-
validation := `gcp_run_namespace "$target" "$run_id" >/dev/null`
104+
validation := `run_namespace="$(gcp_run_namespace "$target" "$run_id")"`
105105
validationIndex := strings.Index(driver, validation)
106106
argumentsIndex := strings.Index(driver, `mapfile -d '' -t var_args < <(target_var_args`)
107107
applyIndex := strings.Index(driver, `terraform -chdir="$root" apply`)
@@ -138,6 +138,44 @@ func TestCloudSmokeGCPApplyRequiresCanonicalRunID(t *testing.T) {
138138
}
139139
}
140140

141+
func TestCloudSmokeGCPWriterUsesExactRunNamespace(t *testing.T) {
142+
t.Parallel()
143+
root := repositoryRoot(t)
144+
driver := readRepositoryFile(t, root, "ci/cloud-smoke.sh")
145+
gcpVariables := readRepositoryFile(t, root, "tests/smoke/gcp/variables.tf")
146+
gcpFixture := readRepositoryFile(t, root, "tests/smoke/gcp/main.tf")
147+
contextVariables := readRepositoryFile(t, root, "tests/smoke/modules/context/variables.tf")
148+
contextFixture := readRepositoryFile(t, root, "tests/smoke/modules/context/main.tf")
149+
150+
for label, marker := range map[string]string{
151+
"compiled namespace command": `"$runner" gcp namespace --run-id "$run_id"`,
152+
"captured raw run ID": `run_id="$(smoke_run_id)"`,
153+
"captured exact namespace": `run_namespace="$(gcp_run_namespace "$target" "$run_id")"`,
154+
"raw Terraform input": `"smoke_run_id=${run_id}"`,
155+
"exact Terraform input": `"smoke_run_namespace=${run_namespace}"`,
156+
"raw cleanup ownership": `provider_tag_cleanup "$target" "$run_id"`,
157+
} {
158+
requireContains(t, driver, marker, label)
159+
}
160+
161+
namespaceIndex := strings.Index(driver, `run_namespace="$(gcp_run_namespace "$target" "$run_id")"`)
162+
argumentsIndex := strings.Index(driver, `mapfile -d '' -t var_args < <(target_var_args`)
163+
if namespaceIndex < 0 || argumentsIndex < 0 || namespaceIndex >= argumentsIndex {
164+
t.Fatal("fresh GCP smoke does not validate its exact namespace before Terraform argument process substitution")
165+
}
166+
167+
for label, text := range map[string]string{
168+
"GCP root variables": gcpVariables,
169+
"context variables": contextVariables,
170+
"GCP context plumbing": gcpFixture,
171+
"context naming decision": contextFixture,
172+
} {
173+
requireContains(t, text, "smoke_run_namespace", label)
174+
}
175+
requireContains(t, contextFixture, `local.cloud_provider == "gcp" ? var.smoke_run_namespace : ""`, "GCP-only exact namespace selection")
176+
requireContains(t, contextFixture, `local.smoke_run_id != "" ? "gha-run-${local.smoke_run_id}" : ""`, "legacy run-id cleanup tag")
177+
}
178+
141179
func TestCloudSmokeGCPWrapperFailsClosedWithoutRunID(t *testing.T) {
142180
t.Parallel()
143181
root := repositoryRoot(t)

internal/gcpcleanup/cleanup.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ type failureList struct {
104104
errors []error
105105
}
106106

107-
// NameFilter returns the legacy anchored resource-name filter shared with the current smoke Terraform fixture.
107+
// NameFilter returns the legacy anchored resource-name filter retained for
108+
// compatibility with smoke resources created before exact namespaces.
108109
func NameFilter(target, runID string) (string, error) {
109110
prefix, err := targetNamePrefix(target)
110111
if err != nil {

tests/smoke/gcp/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ module "context" {
2727
ssh_public_key = var.ssh_public_key
2828
operator_ssh_public_keys = var.operator_ssh_public_keys
2929
smoke_run_id = var.smoke_run_id
30+
smoke_run_namespace = var.smoke_run_namespace
3031
docker_compose_branch = var.docker_compose_branch
3132
ingress_port = var.ingress_port
3233
}

tests/smoke/gcp/variables.tf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,17 @@ variable "smoke_run_id" {
7575
description = "Optional GitHub Actions run id used to tag and name disposable smoke-test resources."
7676
}
7777

78+
variable "smoke_run_namespace" {
79+
type = string
80+
default = ""
81+
description = "Optional output of cloud-compose-ci gcp namespace used in GCP disposable resource names."
82+
83+
validation {
84+
condition = var.smoke_run_namespace == "" || can(regex("^[0-9a-z]{9}$", var.smoke_run_namespace))
85+
error_message = "smoke_run_namespace must be empty or exactly nine lowercase base36 characters."
86+
}
87+
}
88+
7889
variable "docker_compose_branch" {
7990
type = string
8091
default = "main"

0 commit comments

Comments
 (0)