feat(P10a): transactional plane (REQ-075, REQ-079; C-09, C-23)
internal/txn/txn.go: Bundle (desired-state + apply/verify/rollback scripts + signed manifest), RenderBundle (content-addressed txn-id), Stage (SCP to lead), Apply (idempotent + rollback on failure). scripts/orca-pull.sh: C-09 failure contract (idempotent, bounded retry, deterministic, structured syslog) + C-23 (cluster-wide vs ns-scoped --force distinction). internal/cli/txn.go: orca txn apply/list/show/rollback CLI. ---ci--- project: orca phase: 10a milestone: v0.11 status: execute ---/ci---
This commit is contained in:
Executable
+187
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env bash
|
||||
# orca-pull.sh — lead-side transactional applier (P10a, v0.11).
|
||||
# Implements the C-09 failure contract (idempotent re-run, bounded retry,
|
||||
# deterministic state, structured syslog) and the C-23 cluster-wide vs
|
||||
# namespace-scoped distinction.
|
||||
#
|
||||
# Usage:
|
||||
# orca-pull.sh --txn-dir <dir> [--namespace <ns> | --force --i-understand-the-risk | --force --yes]
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = applied (or already-applied no-op)
|
||||
# 1 = apply failure
|
||||
# 2 = verify failure
|
||||
# 3 = rollback failure
|
||||
# 4 = invalid arguments
|
||||
# 5 = already-applied no-op (re-run of a completed txn)
|
||||
#
|
||||
# This script is invoked by the Go-side txn.Apply over SSH on the lead
|
||||
# peer. It wraps apply.sh / verify.sh / rollback.sh in the C-09 contract.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/orca-log.sh
|
||||
. "$SCRIPT_DIR/lib/orca-log.sh"
|
||||
|
||||
# --- exit codes (C-09) ---
|
||||
EXIT_OK=0
|
||||
EXIT_APPLY_FAIL=1
|
||||
EXIT_VERIFY_FAIL=2
|
||||
EXIT_ROLLBACK_FAIL=3
|
||||
EXIT_INVALID_ARGS=4
|
||||
EXIT_ALREADY_APPLIED=5
|
||||
|
||||
# --- bounded retry (C-09) ---
|
||||
MAX_RETRIES=3
|
||||
BACKOFF_SEQ=(1 2 4)
|
||||
|
||||
# --- args ---
|
||||
TXN_DIR=""
|
||||
NAMESPACE=""
|
||||
FORCE=false
|
||||
ACK_RISK=false
|
||||
YES=false
|
||||
|
||||
usage() {
|
||||
cat >&2 <<EOF
|
||||
usage: orca-pull.sh --txn-dir <dir> [--namespace <ns>]
|
||||
[--force --i-understand-the-risk]
|
||||
[--force --yes]
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--txn-dir)
|
||||
[ "$#" -ge 2 ] || { orca_log_error "orca-pull" "-" "failed" "missing --txn-dir value"; usage; exit "$EXIT_INVALID_ARGS"; }
|
||||
TXN_DIR="$2"; shift 2 ;;
|
||||
--namespace)
|
||||
[ "$#" -ge 2 ] || { orca_log_error "orca-pull" "-" "failed" "missing --namespace value"; usage; exit "$EXIT_INVALID_ARGS"; }
|
||||
NAMESPACE="$2"; shift 2 ;;
|
||||
--force)
|
||||
FORCE=true; shift ;;
|
||||
--i-understand-the-risk)
|
||||
ACK_RISK=true; shift ;;
|
||||
--yes)
|
||||
YES=true; shift ;;
|
||||
-h|--help)
|
||||
usage; exit "$EXIT_OK" ;;
|
||||
*)
|
||||
orca_log_error "orca-pull" "-" "failed" "unknown argument: $1"
|
||||
usage; exit "$EXIT_INVALID_ARGS" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- arg validation ---
|
||||
if [ -z "$TXN_DIR" ]; then
|
||||
orca_log_error "orca-pull" "-" "failed" "missing --txn-dir"
|
||||
usage; exit "$EXIT_INVALID_ARGS"
|
||||
fi
|
||||
if [ ! -d "$TXN_DIR" ]; then
|
||||
orca_log_error "orca-pull" "$TXN_DIR" "failed" "txn dir not found"
|
||||
exit "$EXIT_INVALID_ARGS"
|
||||
fi
|
||||
|
||||
# --- C-23 cluster-wide vs namespace-scoped enforcement ---
|
||||
if [ -z "$NAMESPACE" ]; then
|
||||
# Cluster-wide txn: requires --force + (--i-understand-the-risk | --yes).
|
||||
if [ "$FORCE" != "true" ]; then
|
||||
orca_log_error "orca-pull" "$TXN_DIR" "denied" "cluster-wide txn requires --force"
|
||||
echo "error: cluster-wide txn requires --force (C-23)" >&2
|
||||
exit "$EXIT_INVALID_ARGS"
|
||||
fi
|
||||
if [ "$ACK_RISK" != "true" ] && [ "$YES" != "true" ]; then
|
||||
orca_log_error "orca-pull" "$TXN_DIR" "denied" "cluster-wide --force requires --i-understand-the-risk (or --yes)"
|
||||
echo "error: cluster-wide --force requires --i-understand-the-risk (or --yes) (C-23)" >&2
|
||||
exit "$EXIT_INVALID_ARGS"
|
||||
fi
|
||||
else
|
||||
# Namespace-scoped: --force not required. Drift in other namespaces
|
||||
# does not block this txn (C-23). We still honor --force if given
|
||||
# (it's a no-op for ns-scoped).
|
||||
:
|
||||
fi
|
||||
|
||||
# --- locate bundle files ---
|
||||
APPLY="$TXN_DIR/apply.sh"
|
||||
VERIFY="$TXN_DIR/verify.sh"
|
||||
ROLLBACK="$TXN_DIR/rollback.sh"
|
||||
MARKER="$TXN_DIR/.applied"
|
||||
|
||||
for f in "$APPLY" "$VERIFY" "$ROLLBACK"; do
|
||||
if [ ! -f "$f" ]; then
|
||||
orca_log_error "orca-pull" "$TXN_DIR" "failed" "missing bundle file: $f"
|
||||
echo "error: missing $f" >&2
|
||||
exit "$EXIT_INVALID_ARGS"
|
||||
fi
|
||||
done
|
||||
|
||||
# --- idempotency: already-applied is a no-op (C-09) ---
|
||||
if [ -f "$MARKER" ]; then
|
||||
orca_log_info "orca-pull" "$TXN_DIR" "already-applied" ""
|
||||
echo "already-applied"
|
||||
exit "$EXIT_ALREADY_APPLIED"
|
||||
fi
|
||||
|
||||
# --- apply with bounded retry (C-09: 3 attempts, 1s/2s/4s backoff) ---
|
||||
start_ns="$(date +%s%N)"
|
||||
|
||||
run_with_retry() {
|
||||
local script="$1" label="$2"
|
||||
local attempt=0
|
||||
local rc=0
|
||||
while [ "$attempt" -lt "$MAX_RETRIES" ]; do
|
||||
attempt=$((attempt + 1))
|
||||
set +e
|
||||
bash "$script"
|
||||
rc=$?
|
||||
set -e
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ "$attempt" -eq "$MAX_RETRIES" ]; then
|
||||
break
|
||||
fi
|
||||
local wait_s="${BACKOFF_SEQ[$((attempt - 1))]}"
|
||||
orca_log_warn "orca-pull" "$TXN_DIR" "$label-retry" "attempt $attempt failed (rc=$rc), sleeping ${wait_s}s"
|
||||
sleep "$wait_s"
|
||||
done
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
# Apply phase.
|
||||
if ! run_with_retry "$APPLY" "apply"; then
|
||||
apply_rc=$?
|
||||
end_ns="$(date +%s%N)"
|
||||
duration_ms=$(( (end_ns - start_ns) / 1000000 ))
|
||||
orca_log_error "orca-pull" "$TXN_DIR" "apply-failed" "rc=$apply_rc duration_ms=$duration_ms"
|
||||
# Run rollback on apply failure.
|
||||
if bash "$ROLLBACK"; then
|
||||
:
|
||||
else
|
||||
orca_log_error "orca-pull" "$TXN_DIR" "rollback-failed" "rollback after apply failure exited non-zero"
|
||||
exit "$EXIT_ROLLBACK_FAIL"
|
||||
fi
|
||||
exit "$EXIT_APPLY_FAIL"
|
||||
fi
|
||||
|
||||
# Verify phase.
|
||||
if ! run_with_retry "$VERIFY" "verify"; then
|
||||
verify_rc=$?
|
||||
end_ns="$(date +%s%N)"
|
||||
duration_ms=$(( (end_ns - start_ns) / 1000000 ))
|
||||
orca_log_error "orca-pull" "$TXN_DIR" "verify-failed" "rc=$verify_rc duration_ms=$duration_ms"
|
||||
# Run rollback on verify failure.
|
||||
if ! bash "$ROLLBACK"; then
|
||||
orca_log_error "orca-pull" "$TXN_DIR" "rollback-failed" "rollback after verify failure exited non-zero"
|
||||
exit "$EXIT_ROLLBACK_FAIL"
|
||||
fi
|
||||
exit "$EXIT_VERIFY_FAIL"
|
||||
fi
|
||||
|
||||
end_ns="$(date +%s%N)"
|
||||
duration_ms=$(( (end_ns - start_ns) / 1000000 ))
|
||||
orca_log_info "orca-pull" "$TXN_DIR" "applied" "duration_ms=$duration_ms namespace=${NAMESPACE:-cluster-wide}"
|
||||
echo "applied (duration=${duration_ms}ms)"
|
||||
exit "$EXIT_OK"
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bats
|
||||
# Tests for scripts/orca-pull.sh — the C-09 failure contract + C-23
|
||||
# cluster-wide vs namespace-scoped distinction.
|
||||
|
||||
load test_helper
|
||||
|
||||
PULL="$SCRIPTS_DIR/orca-pull.sh"
|
||||
TMP_TXN=""
|
||||
|
||||
setup() {
|
||||
TMP_TXN="$(mktemp -d)"
|
||||
# Minimal apply/verify/rollback scripts that succeed.
|
||||
cat >"$TMP_TXN/apply.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
touch "$TMP_TXN/.applied-marker"
|
||||
exit 0
|
||||
EOF
|
||||
# A real marker the orca-pull.sh checks for.
|
||||
cat >"$TMP_TXN/apply.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
touch "$(dirname "$0")/.applied"
|
||||
exit 0
|
||||
EOF
|
||||
cat >"$TMP_TXN/verify.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
EOF
|
||||
cat >"$TMP_TXN/rollback.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
rm -f "$(dirname "$0")/.applied"
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$TMP_TXN"/*.sh
|
||||
}
|
||||
|
||||
teardown() {
|
||||
[ -n "$TMP_TXN" ] && rm -rf "$TMP_TXN"
|
||||
}
|
||||
|
||||
@test "orca-pull.sh exists and is executable" {
|
||||
[ -f "$PULL" ]
|
||||
[ -x "$PULL" ]
|
||||
}
|
||||
|
||||
@test "orca-pull.sh refuses without --txn-dir (exit 4)" {
|
||||
run "$PULL" --force --i-understand-the-risk
|
||||
assert_status 4 "$status"
|
||||
}
|
||||
|
||||
@test "orca-pull.sh cluster-wide without --force exits 4 (C-23)" {
|
||||
run "$PULL" --txn-dir "$TMP_TXN"
|
||||
assert_status 4 "$status"
|
||||
assert_contains "$output" "requires --force"
|
||||
}
|
||||
|
||||
@test "orca-pull.sh cluster-wide with --force but no ack exits 4 (C-23)" {
|
||||
run "$PULL" --txn-dir "$TMP_TXN" --force
|
||||
assert_status 4 "$status"
|
||||
assert_contains "$output" "--i-understand-the-risk"
|
||||
}
|
||||
|
||||
@test "orca-pull.sh cluster-wide with --force --i-understand-the-risk applies" {
|
||||
run "$PULL" --txn-dir "$TMP_TXN" --force --i-understand-the-risk
|
||||
assert_status 0 "$status"
|
||||
assert_contains "$output" "applied"
|
||||
[ -f "$TMP_TXN/.applied" ]
|
||||
}
|
||||
|
||||
@test "orca-pull.sh namespace-scoped applies without --force (C-23)" {
|
||||
run "$PULL" --txn-dir "$TMP_TXN" --namespace default
|
||||
assert_status 0 "$status"
|
||||
assert_contains "$output" "applied"
|
||||
[ -f "$TMP_TXN/.applied" ]
|
||||
}
|
||||
|
||||
@test "orca-pull.sh idempotent re-run exits 5 (already-applied)" {
|
||||
# First apply.
|
||||
run "$PULL" --txn-dir "$TMP_TXN" --namespace default
|
||||
assert_status 0 "$status"
|
||||
# Re-run: should be a no-op (exit 5).
|
||||
run "$PULL" --txn-dir "$TMP_TXN" --namespace default
|
||||
assert_status 5 "$status"
|
||||
assert_contains "$output" "already-applied"
|
||||
}
|
||||
|
||||
@test "orca-pull.sh apply failure runs rollback and exits 1" {
|
||||
# Make apply.sh fail.
|
||||
cat >"$TMP_TXN/apply.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$TMP_TXN/apply.sh"
|
||||
run "$PULL" --txn-dir "$TMP_TXN" --namespace default
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "orca-pull.sh verify failure runs rollback and exits 2" {
|
||||
# apply succeeds, verify fails.
|
||||
cat >"$TMP_TXN/verify.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$TMP_TXN/verify.sh"
|
||||
run "$PULL" --txn-dir "$TMP_TXN" --namespace default
|
||||
[ "$status" -eq 2 ]
|
||||
}
|
||||
|
||||
@test "orca-pull.sh missing bundle file exits 4" {
|
||||
rm -f "$TMP_TXN/verify.sh"
|
||||
run "$PULL" --txn-dir "$TMP_TXN" --namespace default
|
||||
assert_status 4 "$status"
|
||||
}
|
||||
Reference in New Issue
Block a user