diff --git a/scripts/mock_executor.sh b/scripts/mock_executor.sh new file mode 100755 index 0000000..df02770 --- /dev/null +++ b/scripts/mock_executor.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# mock_executor.sh — REQ-06 / D-022 +# +# Reads a contract.yaml, resolves the L2 composition, invokes each L1's +# mock_apply.sh in order, and writes state.json to the current working +# directory. +# +# Input: argv[1] = path to a contract.yaml file. +# Output: +# - stdout: per-L1 progress (echoed from each mock_apply.sh) +# - state.json in cwd: {"l2": "...", "l1s": [...], "contract": {...}} +# Exit: +# 0 if all L1s exit 0; 1 if any L1 exited non-zero (state.json is still +# written with the recorded exit codes). +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "usage: mock_executor.sh " >&2 + exit 1 +fi + +CONTRACT_PATH="$1" + +if [[ ! -f "$CONTRACT_PATH" ]]; then + echo "contract not found: $CONTRACT_PATH" >&2 + exit 1 +fi + +# --- Parse the contract (stack + full contract dict) via python3 + yaml. --- +# Emit stack on line 1 and the full contract JSON on line 2, then read both +# lines into separate bash variables (so the JSON's internal spaces survive). +CONTRACT_PARSED=$(python3 - "$CONTRACT_PATH" <<'PY' +import sys, json, yaml +path = sys.argv[1] +with open(path, "r", encoding="utf-8") as fh: + contract = yaml.safe_load(fh) +if not isinstance(contract, dict): + sys.stderr.write("contract is not a mapping\n") + sys.exit(2) +stack = contract.get("stack", "") +# Use a compact JSON (no spaces) so the single-line contract survives bash +# variable capture cleanly. +print(stack) +print(json.dumps(contract, sort_keys=True, separators=(",", ":"))) +PY +) + +STACK=$(printf '%s\n' "$CONTRACT_PARSED" | sed -n '1p') +CONTRACT_JSON=$(printf '%s\n' "$CONTRACT_PARSED" | sed -n '2p') + +if [[ -z "$STACK" ]]; then + echo "contract missing 'stack' key" >&2 + exit 1 +fi + +# --- Resolve the L2 manifest. --- +L2_MANIFEST="modules/l2/${STACK}/manifest.yaml" +if [[ ! -f "$L2_MANIFEST" ]]; then + echo "L2_NOT_FOUND: ${STACK}" >&2 + exit 1 +fi + +# --- Read the L2's l1s: list (ordered names) via python. --- +L1_NAMES_JSON=$(python3 - "$L2_MANIFEST" <<'PY' +import sys, json, yaml +path = sys.argv[1] +with open(path, "r", encoding="utf-8") as fh: + manifest = yaml.safe_load(fh) +l1s = manifest.get("l1s", []) if isinstance(manifest, dict) else [] +names = [entry.get("name", "") for entry in l1s if isinstance(entry, dict)] +print(json.dumps(names)) +PY +) + +# --- Invoke each L1's mock_apply.sh in order, recording exit codes. --- +# Build the l1s results array in JSON via python, appending as we go. +RESULTS_JSON="[]" + +ALL_OK=0 +while IFS= read -r L1_NAME; do + L1_SCRIPT="modules/l1/${L1_NAME}/mock_apply.sh" + if [[ ! -f "$L1_SCRIPT" ]]; then + echo "L1_NOT_FOUND: ${L1_NAME}" >&2 + exit 1 + fi + + # Capture stdout + exit code. stderr passes through. + L1_OUT=$(bash "$L1_SCRIPT") + L1_RC=$? + + # Echo the L1's stdout so the pipeline sees the progress lines. + printf '%s\n' "$L1_OUT" + + # Record {"name": ..., "applied": true, "exit_code": ...}. + RESULTS_JSON=$(python3 - "$RESULTS_JSON" "$L1_NAME" "$L1_RC" <<'PY' +import sys, json +results = json.loads(sys.argv[1]) +name = sys.argv[2] +rc = int(sys.argv[3]) +results.append({"name": name, "applied": True, "exit_code": rc}) +print(json.dumps(results)) +PY +) + + if [[ $L1_RC -ne 0 ]]; then + ALL_OK=1 + fi +done < <(python3 -c "import sys, json; print('\n'.join(json.loads(sys.argv[1])))" "$L1_NAMES_JSON") + +# --- Write state.json to the current working directory (D-022). --- +python3 - "$RESULTS_JSON" "$STACK" "$CONTRACT_JSON" <<'PY' +import sys, json +results = json.loads(sys.argv[1]) +stack = sys.argv[2] +contract = json.loads(sys.argv[3]) +state = { + "l2": stack, + "l1s": results, + "contract": contract, +} +with open("state.json", "w", encoding="utf-8") as fh: + json.dump(state, fh, indent=2) + fh.write("\n") +PY + +exit "$ALL_OK" \ No newline at end of file