Skip to content

Commit 721f69d

Browse files
authored
fix(auth): short-circuit ESO reconcile when contract set is empty (#45)
1 parent 58b4742 commit 721f69d

4 files changed

Lines changed: 167 additions & 95 deletions

File tree

docs/platform/consumer/runtime_credentials_eso.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ make auth-reconcile-eso-runtime-secrets
7979
Defaults live in `blueprint/repo.init.env`.
8080
`KEYCLOAK_OPTIONAL_MODULE_RECONCILIATION_ENABLED` gates optional-module Keycloak reconciliation (Workflows/Langfuse) during module deploy flows.
8181
`RUNTIME_CREDENTIALS_REQUIRED` does not disable reconciliation; it only switches reconcile failures from warning mode (`false`) to hard-fail mode (`true`).
82+
If the effective contract set resolves to empty (`contracts=0`), reconciliation exits as a no-op success (`status=noop-empty-contract-set`) and skips source-secret checks.
8283

8384
Additional reconcile knobs:
8485
- `RUNTIME_CREDENTIALS_SOURCE_SECRET_NAME` (default `runtime-credentials-source`)

scripts/bin/platform/auth/reconcile_eso_runtime_secrets.sh

Lines changed: 109 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ external_secret_checked=""
196196
target_secret_checked=""
197197
declare -a ESO_SECRET_CONTRACTS=()
198198

199-
while IFS=$'\t' read -r contract_id contract_module contract_namespace contract_external_secret contract_target_secret contract_target_keys; do
199+
while IFS='|' read -r contract_id contract_module contract_namespace contract_external_secret contract_target_secret contract_target_keys; do
200200
[[ -n "$contract_id" ]] || continue
201201

202202
if [[ -n "$contract_module" ]] && ! is_module_enabled "$contract_module"; then
@@ -208,122 +208,136 @@ while IFS=$'\t' read -r contract_id contract_module contract_namespace contract_
208208
"$contract_external_secret" \
209209
"$contract_target_secret" \
210210
"$contract_target_keys"
211-
done < <(python3 "$runtime_identity_contract_cli" eso-contracts)
211+
done < <(python3 "$runtime_identity_contract_cli" eso-contracts | tr $'\t' '|')
212212

213-
if tooling_is_execution_enabled; then
214-
apply_mode="kubectl-apply-kustomize"
215-
fi
216-
run_kustomize_apply "$ROOT_DIR/infra/gitops/platform/base/security"
217-
218-
source_literals=()
219-
if literal_output="$(parse_literal_pairs "$RUNTIME_CREDENTIALS_SOURCE_SECRET_LITERALS")"; then
220-
if [[ -n "$literal_output" ]]; then
221-
while IFS= read -r literal_pair; do
222-
[[ -n "$literal_pair" ]] || continue
223-
source_literals+=("$literal_pair")
224-
done <<<"$literal_output"
225-
fi
213+
status="success"
214+
if (( ${#ESO_SECRET_CONTRACTS[@]} == 0 )); then
215+
status="noop-empty-contract-set"
216+
apply_mode="skipped-empty-contract-set"
217+
source_seed_mode="skipped-empty-contract-set"
218+
source_secret_present="not-applicable"
219+
crd_status="not-applicable"
220+
external_secret_status="not-applicable"
221+
target_secret_status="not-applicable"
222+
target_missing_keys="none"
223+
external_secret_checked="none"
224+
target_secret_checked="none"
225+
log_info "runtime credential contract set empty; skipping source secret checks"
226226
else
227-
record_reconcile_issue "invalid RUNTIME_CREDENTIALS_SOURCE_SECRET_LITERALS format; expected key=value,key2=value2"
228-
fi
229-
230-
if (( ${#source_literals[@]} > 0 )); then
231-
source_seed_mode="manifest-rendered"
232227
if tooling_is_execution_enabled; then
233-
source_seed_mode="kubectl-apply"
228+
apply_mode="kubectl-apply-kustomize"
234229
fi
235-
apply_optional_module_secret_from_literals \
236-
"$RUNTIME_CREDENTIALS_SOURCE_NAMESPACE" \
237-
"$RUNTIME_CREDENTIALS_SOURCE_SECRET_NAME" \
238-
"${source_literals[@]}"
239-
elif tooling_is_execution_enabled; then
240-
if kubectl -n "$RUNTIME_CREDENTIALS_SOURCE_NAMESPACE" get secret "$RUNTIME_CREDENTIALS_SOURCE_SECRET_NAME" >/dev/null 2>&1; then
241-
source_seed_mode="existing-source-secret"
242-
source_secret_present="true"
230+
run_kustomize_apply "$ROOT_DIR/infra/gitops/platform/base/security"
231+
232+
source_literals=()
233+
if literal_output="$(parse_literal_pairs "$RUNTIME_CREDENTIALS_SOURCE_SECRET_LITERALS")"; then
234+
if [[ -n "$literal_output" ]]; then
235+
while IFS= read -r literal_pair; do
236+
[[ -n "$literal_pair" ]] || continue
237+
source_literals+=("$literal_pair")
238+
done <<<"$literal_output"
239+
fi
243240
else
244-
source_seed_mode="missing-source-secret"
245-
source_secret_present="false"
246-
record_reconcile_issue \
247-
"source secret ${RUNTIME_CREDENTIALS_SOURCE_NAMESPACE}/${RUNTIME_CREDENTIALS_SOURCE_SECRET_NAME} missing and no literals provided"
241+
record_reconcile_issue "invalid RUNTIME_CREDENTIALS_SOURCE_SECRET_LITERALS format; expected key=value,key2=value2"
248242
fi
249-
fi
250243

251-
if tooling_is_execution_enabled; then
252-
require_command kubectl
253-
254-
if wait_for_crd_established "clustersecretstores.external-secrets.io" "$runtime_wait_timeout" \
255-
&& wait_for_crd_established "externalsecrets.external-secrets.io" "$runtime_wait_timeout"; then
256-
crd_status="ready"
257-
else
258-
crd_status="timeout"
259-
record_reconcile_issue "ESO CRDs did not report Established=True within ${runtime_wait_timeout}s"
244+
if (( ${#source_literals[@]} > 0 )); then
245+
source_seed_mode="manifest-rendered"
246+
if tooling_is_execution_enabled; then
247+
source_seed_mode="kubectl-apply"
248+
fi
249+
apply_optional_module_secret_from_literals \
250+
"$RUNTIME_CREDENTIALS_SOURCE_NAMESPACE" \
251+
"$RUNTIME_CREDENTIALS_SOURCE_SECRET_NAME" \
252+
"${source_literals[@]}"
253+
elif tooling_is_execution_enabled; then
254+
if kubectl -n "$RUNTIME_CREDENTIALS_SOURCE_NAMESPACE" get secret "$RUNTIME_CREDENTIALS_SOURCE_SECRET_NAME" >/dev/null 2>&1; then
255+
source_seed_mode="existing-source-secret"
256+
source_secret_present="true"
257+
else
258+
source_seed_mode="missing-source-secret"
259+
source_secret_present="false"
260+
record_reconcile_issue \
261+
"source secret ${RUNTIME_CREDENTIALS_SOURCE_NAMESPACE}/${RUNTIME_CREDENTIALS_SOURCE_SECRET_NAME} missing and no literals provided"
262+
fi
260263
fi
261264

262-
external_secret_status="ready"
263-
target_secret_status="ready"
264-
target_missing_keys="none"
265-
external_secret_checked="none"
266-
target_secret_checked="none"
267-
for contract_entry in "${ESO_SECRET_CONTRACTS[@]}"; do
268-
IFS='|' read -r contract_namespace contract_external_secret contract_target_secret contract_target_keys <<<"$contract_entry"
269-
270-
if wait_for_external_secret_ready \
271-
"$contract_namespace" \
272-
"$contract_external_secret" \
273-
"$runtime_wait_timeout"; then
274-
if [[ "$external_secret_checked" == "none" ]]; then
275-
external_secret_checked="${contract_namespace}/${contract_external_secret}"
276-
else
277-
external_secret_checked+=",${contract_namespace}/${contract_external_secret}"
278-
fi
265+
if tooling_is_execution_enabled; then
266+
require_command kubectl
267+
268+
if wait_for_crd_established "clustersecretstores.external-secrets.io" "$runtime_wait_timeout" \
269+
&& wait_for_crd_established "externalsecrets.external-secrets.io" "$runtime_wait_timeout"; then
270+
crd_status="ready"
279271
else
280-
external_secret_status="timeout"
281-
record_reconcile_issue \
282-
"ExternalSecret ${contract_namespace}/${contract_external_secret} not Ready within ${runtime_wait_timeout}s"
272+
crd_status="timeout"
273+
record_reconcile_issue "ESO CRDs did not report Established=True within ${runtime_wait_timeout}s"
283274
fi
284275

285-
target_check_output="$(verify_target_secret_keys \
286-
"$contract_namespace" \
287-
"$contract_target_secret" \
288-
"$contract_target_keys" || true)"
289-
if [[ "$target_check_output" == "ok" ]]; then
290-
if [[ "$target_secret_checked" == "none" ]]; then
291-
target_secret_checked="${contract_namespace}/${contract_target_secret}"
276+
external_secret_status="ready"
277+
target_secret_status="ready"
278+
target_missing_keys="none"
279+
external_secret_checked="none"
280+
target_secret_checked="none"
281+
for contract_entry in "${ESO_SECRET_CONTRACTS[@]}"; do
282+
IFS='|' read -r contract_namespace contract_external_secret contract_target_secret contract_target_keys <<<"$contract_entry"
283+
284+
if wait_for_external_secret_ready \
285+
"$contract_namespace" \
286+
"$contract_external_secret" \
287+
"$runtime_wait_timeout"; then
288+
if [[ "$external_secret_checked" == "none" ]]; then
289+
external_secret_checked="${contract_namespace}/${contract_external_secret}"
290+
else
291+
external_secret_checked+=",${contract_namespace}/${contract_external_secret}"
292+
fi
292293
else
293-
target_secret_checked+=",${contract_namespace}/${contract_target_secret}"
294+
external_secret_status="timeout"
295+
record_reconcile_issue \
296+
"ExternalSecret ${contract_namespace}/${contract_external_secret} not Ready within ${runtime_wait_timeout}s"
297+
fi
298+
299+
target_check_output="$(verify_target_secret_keys \
300+
"$contract_namespace" \
301+
"$contract_target_secret" \
302+
"$contract_target_keys" || true)"
303+
if [[ "$target_check_output" == "ok" ]]; then
304+
if [[ "$target_secret_checked" == "none" ]]; then
305+
target_secret_checked="${contract_namespace}/${contract_target_secret}"
306+
else
307+
target_secret_checked+=",${contract_namespace}/${contract_target_secret}"
308+
fi
309+
continue
310+
fi
311+
312+
if [[ "$target_check_output" == "__missing_secret__" ]]; then
313+
target_secret_status="missing"
314+
if [[ "$target_missing_keys" == "none" ]]; then
315+
target_missing_keys="${contract_namespace}/${contract_target_secret}:missing"
316+
else
317+
target_missing_keys+=",${contract_namespace}/${contract_target_secret}:missing"
318+
fi
319+
record_reconcile_issue \
320+
"target secret ${contract_namespace}/${contract_target_secret} is missing"
321+
continue
294322
fi
295-
continue
296-
fi
297323

298-
if [[ "$target_check_output" == "__missing_secret__" ]]; then
299-
target_secret_status="missing"
324+
target_secret_status="missing-keys"
300325
if [[ "$target_missing_keys" == "none" ]]; then
301-
target_missing_keys="${contract_namespace}/${contract_target_secret}:missing"
326+
target_missing_keys="${contract_namespace}/${contract_target_secret}:${target_check_output}"
302327
else
303-
target_missing_keys+=",${contract_namespace}/${contract_target_secret}:missing"
328+
target_missing_keys+=",${contract_namespace}/${contract_target_secret}:${target_check_output}"
304329
fi
305330
record_reconcile_issue \
306-
"target secret ${contract_namespace}/${contract_target_secret} is missing"
307-
continue
308-
fi
331+
"target secret ${contract_namespace}/${contract_target_secret} missing key(s): $target_check_output"
332+
done
333+
fi
309334

310-
target_secret_status="missing-keys"
311-
if [[ "$target_missing_keys" == "none" ]]; then
312-
target_missing_keys="${contract_namespace}/${contract_target_secret}:${target_check_output}"
335+
if (( ${#RUNTIME_RECONCILE_ISSUES[@]} > 0 )); then
336+
if [[ "$RUNTIME_CREDENTIALS_REQUIRED_NORMALIZED" == "true" ]]; then
337+
status="failed-required"
313338
else
314-
target_missing_keys+=",${contract_namespace}/${contract_target_secret}:${target_check_output}"
339+
status="warn-and-skip"
315340
fi
316-
record_reconcile_issue \
317-
"target secret ${contract_namespace}/${contract_target_secret} missing key(s): $target_check_output"
318-
done
319-
fi
320-
321-
status="success"
322-
if (( ${#RUNTIME_RECONCILE_ISSUES[@]} > 0 )); then
323-
if [[ "$RUNTIME_CREDENTIALS_REQUIRED_NORMALIZED" == "true" ]]; then
324-
status="failed-required"
325-
else
326-
status="warn-and-skip"
327341
fi
328342
fi
329343

scripts/templates/blueprint/bootstrap/docs/platform/consumer/runtime_credentials_eso.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ make auth-reconcile-eso-runtime-secrets
7979
Defaults live in `blueprint/repo.init.env`.
8080
`KEYCLOAK_OPTIONAL_MODULE_RECONCILIATION_ENABLED` gates optional-module Keycloak reconciliation (Workflows/Langfuse) during module deploy flows.
8181
`RUNTIME_CREDENTIALS_REQUIRED` does not disable reconciliation; it only switches reconcile failures from warning mode (`false`) to hard-fail mode (`true`).
82+
If the effective contract set resolves to empty (`contracts=0`), reconciliation exits as a no-op success (`status=noop-empty-contract-set`) and skips source-secret checks.
8283

8384
Additional reconcile knobs:
8485
- `RUNTIME_CREDENTIALS_SOURCE_SECRET_NAME` (default `runtime-credentials-source`)

tests/infra/test_runtime_credentials_eso.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
from __future__ import annotations
22

3+
import os
4+
from pathlib import Path
5+
import shlex
6+
import sys
7+
import tempfile
38
import unittest
49

510
from tests._shared.helpers import REPO_ROOT, module_flags_env, run_make
@@ -63,6 +68,57 @@ def test_required_mode_fails_on_invalid_literal_contract(self) -> None:
6368
self.assertIn("required=true", state)
6469
self.assertIn("status=failed-required", state)
6570

71+
def test_empty_contract_set_is_noop_without_source_secret_warning(self) -> None:
72+
env = module_flags_env(profile="local-full")
73+
with tempfile.TemporaryDirectory() as tmpdir:
74+
shim_dir = Path(tmpdir)
75+
python_shim = shim_dir / "python3"
76+
python_shim.write_text(
77+
"\n".join(
78+
[
79+
"#!/usr/bin/env bash",
80+
"set -euo pipefail",
81+
f'target_contract_cli="{REPO_ROOT}/scripts/lib/infra/runtime_identity_contract.py"',
82+
'if [[ "${1:-}" == "$target_contract_cli" ]]; then',
83+
' case "${2:-}" in',
84+
" runtime-env-defaults)",
85+
" cat <<'EOF'",
86+
"KEYCLOAK_OPTIONAL_MODULE_RECONCILIATION_ENABLED\ttrue",
87+
"RUNTIME_CREDENTIALS_SOURCE_NAMESPACE\tsecurity",
88+
"RUNTIME_CREDENTIALS_TARGET_NAMESPACE\tapps",
89+
"RUNTIME_CREDENTIALS_ESO_WAIT_TIMEOUT\t180",
90+
"RUNTIME_CREDENTIALS_REQUIRED\tfalse",
91+
"EOF",
92+
" exit 0",
93+
" ;;",
94+
" eso-contracts)",
95+
" exit 0",
96+
" ;;",
97+
" esac",
98+
"fi",
99+
f"exec {shlex.quote(sys.executable)} \"$@\"",
100+
"",
101+
]
102+
),
103+
encoding="utf-8",
104+
)
105+
python_shim.chmod(0o755)
106+
env["PATH"] = f"{shim_dir}:{os.environ.get('PATH', '')}"
107+
108+
result = run_make("auth-reconcile-eso-runtime-secrets", env)
109+
110+
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
111+
combined_output = result.stdout + result.stderr
112+
self.assertIn("runtime credential contract set empty; skipping source secret checks", combined_output)
113+
self.assertNotIn("missing and no literals provided", combined_output)
114+
115+
state_path = REPO_ROOT / "artifacts" / "infra" / "runtime_credentials_eso_reconcile.env"
116+
self.assertTrue(state_path.exists(), msg="runtime credentials state artifact was not created")
117+
state = state_path.read_text(encoding="utf-8")
118+
self.assertIn("status=noop-empty-contract-set", state)
119+
self.assertIn("source_secret_seed_mode=skipped-empty-contract-set", state)
120+
self.assertIn("issue_count=0", state)
121+
66122

67123
if __name__ == "__main__":
68124
unittest.main()

0 commit comments

Comments
 (0)