#!/bin/sh set -eu OPERATOR_IMAGE='@@OPERATOR_IMAGE@@' OPERATOR_ID='@@OPERATOR_ID@@' ARCHITECTURE='@@ARCHITECTURE@@' SOURCE_REVISION='@@SOURCE_REVISION@@' ARCHIVE='@@ARCHIVE@@' VARIANT='@@VARIANT@@' PLAN_ID='019f8680-143d-4000-8000-000000000201' REVIEW_ID='019f8680-143d-4000-8000-000000000301' APPLICATION_ID='019f8680-143d-4000-8000-000000000401' AUTOMATION_ID='019f8680-143d-4000-8000-000000000461' AUTOMATION_DECISION_ID='019f8680-143d-7000-8000-000000000471' AUTOMATION_MUTATION_ID='019f8680-143d-4000-8000-000000000481' SECRET_CONFIG_ID='019f8680-143d-4000-8000-000000000491' SECRET_CONFIG_DECISION_ID='019f8680-143d-7000-8000-0000000004a1' SECRET_CONFIG_MUTATION_ID='019f8680-143d-4000-8000-0000000004b1' RUN_HISTORY_PRESERVATION_ID='019f8680-143d-4000-8000-0000000004c1' COMPLETION_ID='019f8680-143d-4000-8000-0000000004d1' fail() { printf '%s\n' "QingLong Local Alpha reconciliation rehearsal failed: $*" >&2 exit 1 } phase() { printf '%s\n' "QingLong Local Alpha reconciliation phase: $1" >&2 } usage() { printf '%s\n' \ 'usage: sh reconciliation-rehearsal.sh prepare edge|standalone /absolute/rehearsal-root /absolute/capture-root /absolute/new/reconciliation-root ' \ ' sh reconciliation-rehearsal.sh review edge|standalone /absolute/rehearsal-root /absolute/capture-root /absolute/reconciliation-root /absolute/review-decisions.ndjson' \ ' sh reconciliation-rehearsal.sh apply-rollback edge|standalone /absolute/rehearsal-root /absolute/capture-root /absolute/reconciliation-root /absolute/automation-decisions.ndjson /absolute/legacy-root' \ ' sh reconciliation-rehearsal.sh apply-plan edge|standalone /absolute/rehearsal-root /absolute/capture-root /absolute/reconciliation-root /absolute/automation-decisions.ndjson /absolute/review-decisions.ndjson /absolute/legacy-root' \ ' sh reconciliation-rehearsal.sh complete edge|standalone /absolute/rehearsal-root /absolute/capture-root /absolute/reconciliation-root /absolute/secret-config-decisions.ndjson /absolute/review-decisions.ndjson /absolute/legacy-root' >&2 exit 2 } safe_absolute_path() { case "$1" in /|*[!A-Za-z0-9_./@-]*|*'/../'*|*'/./'*|*'/..'|*'/.'|*'//'*|*/) return 1 ;; /*) return 0 ;; *) return 1 ;; esac } valid_digest() { [ "${#1}" -eq 64 ] || return 1 case "$1" in *[!0-9a-f]*) return 1 ;; *) return 0 ;; esac } extract_digest() { result_file=$1 field=$2 value=$(sed -n "s/^.*\"$field\":\"\([0-9a-f][0-9a-f]*\)\".*$/\1/p" "$result_file") valid_digest "$value" || fail "$field is missing or invalid in $result_file" printf '%s' "$value" } extract_unsigned() { result_file=$1 field=$2 value=$(sed -n "s/^.*\"$field\":\([0-9][0-9]*\).*$/\1/p" "$result_file") case "$value" in ''|*[!0-9]*) fail "$field is missing or invalid in $result_file" ;; esac printf '%s' "$value" } extract_capture_id() { result_file=$1 value=$(sed -n 's/^.*"captureId":"\([0-9a-f-][0-9a-f-]*\)".*$/\1/p' "$result_file") case "$value" in ????????-????-4???-[89ab]???-????????????) printf '%s' "$value" ;; *) fail "captureId is missing or invalid in $result_file" ;; esac } non_overlapping() { left=$1 right=$2 [ "$left" != "$right" ] || return 1 case "$left/" in "$right"/*) return 1 ;; esac case "$right/" in "$left"/*) return 1 ;; esac return 0 } canonical_directory() { selected=$1 label=$2 safe_absolute_path "$selected" || fail "$label is not a safe canonical absolute path" [ -d "$selected" ] || fail "$label does not exist" [ ! -L "$selected" ] || fail "$label must not be a symbolic link" [ "$(realpath "$selected")" = "$selected" ] || fail "$label is not canonical" } private_decision_file() { decision_file=$1 decision_label=$2 safe_absolute_path "$decision_file" || fail "$decision_label is not a safe canonical absolute path" [ -f "$decision_file" ] || fail "$decision_label does not exist" [ ! -L "$decision_file" ] || fail "$decision_label must not be a symbolic link" [ "$(realpath "$decision_file")" = "$decision_file" ] || fail "$decision_label is not canonical" size=$(stat -c %s "$decision_file") [ "$size" -ge 2 ] && [ "$size" -le 4194304 ] || fail "$decision_label is empty or too large" file_mode=$(stat -c %a "$decision_file") [ "$file_mode" = 400 ] || [ "$file_mode" = 600 ] || fail "$decision_label must have mode 0400 or 0600" decision_parent=${decision_file%/*} [ -n "$decision_parent" ] || decision_parent=/ canonical_directory "$decision_parent" "$decision_label parent" [ "$(stat -c %a "$decision_parent")" = 700 ] || fail "$decision_label parent must have mode 0700" extra_entry=$(find "$decision_parent" -mindepth 1 -maxdepth 1 ! -path "$decision_file" -print -quit) [ -z "$extra_entry" ] || fail "$decision_label parent must contain only the selected decision file" for authority_root in "$rehearsal_root" "$capture_root" "$reconciliation_root"; do non_overlapping "$decision_file" "$authority_root" || fail "$decision_label must be outside all authority roots" non_overlapping "$decision_parent" "$authority_root" || fail "$decision_label parent must be outside all authority roots" done } [ "$#" -ge 1 ] || usage mode=$1 case "$mode" in prepare|review) [ "$#" -eq 6 ] || usage ;; apply-rollback) [ "$#" -eq 7 ] || usage ;; apply-plan|complete) [ "$#" -eq 8 ] || usage ;; *) usage ;; esac profile=$2 rehearsal_root=$3 capture_root=$4 reconciliation_root=$5 phase_input=$6 secondary_input= legacy_root= case "$mode" in apply-rollback) legacy_root=$7 ;; apply-plan|complete) secondary_input=$7; legacy_root=$8 ;; esac case "$profile" in edge|standalone) ;; *) usage ;; esac case "$VARIANT" in headless|console) ;; *) fail 'embedded Trial Kit variant is invalid' ;; esac [ "$(uname -s)" = Linux ] || fail 'reconciliation rehearsal requires a Linux Docker host' for tool in docker sha256sum grep sed stat date realpath find mv rm; do command -v "$tool" >/dev/null 2>&1 || fail "$tool is required" done canonical_directory "$rehearsal_root" 'rehearsal root' canonical_directory "$capture_root" 'capture root' safe_absolute_path "$reconciliation_root" || fail 'reconciliation root is not a safe canonical absolute path' non_overlapping "$rehearsal_root" "$capture_root" || fail 'rehearsal and capture roots overlap' non_overlapping "$rehearsal_root" "$reconciliation_root" || fail 'rehearsal and reconciliation roots overlap' non_overlapping "$capture_root" "$reconciliation_root" || fail 'capture and reconciliation roots overlap' if [ -n "$legacy_root" ]; then canonical_directory "$legacy_root" 'legacy root' for authority_root in "$rehearsal_root" "$capture_root" "$reconciliation_root"; do non_overlapping "$legacy_root" "$authority_root" || fail 'legacy root overlaps an authority root' done fi script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) (CDPATH= cd -- "$script_dir" && sha256sum --check SHA256SUMS) docker info >/dev/null 2>&1 || fail 'docker daemon is unavailable' docker load --input "$script_dir/$ARCHIVE" >/dev/null operator_identity=$(docker image inspect --format '{{.Id}}|{{.Architecture}}|{{.Config.User}}|{{index .Config.Labels "org.opencontainers.image.revision"}}|{{index .Config.Labels "io.qinglong.lifecycle"}}|{{index .Config.Labels "io.qinglong.network"}}' "$OPERATOR_IMAGE") [ "$operator_identity" = "$OPERATOR_ID|$ARCHITECTURE|65532:65532|$SOURCE_REVISION|short-lived|none-by-default" ] || fail 'operator image identity is incompatible' uid=$(id -u) gid=$(id -g) [ "$uid" -eq 0 ] && allow_root_service=true || allow_root_service=false plan_root="$reconciliation_root/plan" review_root="$reconciliation_root/review" diagnostic_root="$reconciliation_root/diagnostics" application_root="$reconciliation_root/application" automation_root="$reconciliation_root/automation" automation_decision_root="$reconciliation_root/automation-decision" automation_apply_root="$reconciliation_root/automation-apply" secret_config_root="$reconciliation_root/secret-config" secret_config_decision_root="$reconciliation_root/secret-config-decision" secret_config_apply_root="$reconciliation_root/secret-config-apply" run_history_root="$reconciliation_root/run-history" completion_root="$reconciliation_root/completion" command_root="$reconciliation_root/commands" result_root="$reconciliation_root/results" target_database="$rehearsal_root/sqlite/qinglong3.sqlite" issuer_keyring="$rehearsal_root/reconciliation-review-issuer.keyring" owner_peppers="$rehearsal_root/owner-peppers" owner_credential="$rehearsal_root/owner-credential.json" secret_keyring="$rehearsal_root/local-secret-keyring.json" decision_parent= secondary_decision_parent= run_deploy() { subcommand=$1 command_file=$2 result_file=$3 input_file=${4:-} set -- docker run --rm --read-only --user "$uid:$gid" --network none \ --cap-drop ALL --security-opt no-new-privileges \ --memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 32 \ --tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \ --mount "type=bind,src=$rehearsal_root,dst=$rehearsal_root" \ --mount "type=bind,src=$capture_root,dst=$capture_root,readonly" \ --mount "type=bind,src=$reconciliation_root,dst=$reconciliation_root" [ -z "$legacy_root" ] || set -- "$@" \ --mount "type=bind,src=$legacy_root,dst=$legacy_root,readonly" [ -z "$decision_parent" ] || set -- "$@" \ --mount "type=bind,src=$decision_parent,dst=$decision_parent,readonly" [ -z "$secondary_decision_parent" ] || set -- "$@" \ --mount "type=bind,src=$secondary_decision_parent,dst=$secondary_decision_parent,readonly" set -- "$@" "$OPERATOR_IMAGE" deploy "$subcommand" \ --command-file "$command_root/$command_file" result_stage="$result_root/.$result_file.$$" [ ! -e "$result_stage" ] || fail 'result staging path already exists' if "$@" >"$result_stage"; then chmod 0600 "$result_stage" mv -f "$result_stage" "$result_root/$result_file" else status=$? rm -f "$result_stage" return "$status" fi } if [ "$mode" = prepare ]; then [ ! -e "$reconciliation_root" ] || fail 'new reconciliation root already exists' reconciliation_parent=${reconciliation_root%/*} [ -n "$reconciliation_parent" ] || reconciliation_parent=/ canonical_directory "$reconciliation_parent" 'reconciliation root parent' case "$phase_input" in none) legacy_timezone_json=null; legacy_timezone_summary=none ;; ''|*[!A-Za-z0-9._+/-]*) fail 'legacy timezone is invalid' ;; *) legacy_timezone_json="\"$phase_input\""; legacy_timezone_summary=$phase_input ;; esac old_umask=$(umask) umask 077 mkdir -m 0700 "$reconciliation_root" for directory in plan review diagnostics application automation automation-decision automation-apply secret-config secret-config-decision secret-config-apply run-history completion commands results; do mkdir -m 0700 "$reconciliation_root/$directory" done umask "$old_umask" capture_result="$rehearsal_root/results/reconciliation-capture-verify.result.json" capture_commit_result="$rehearsal_root/results/reconciliation-capture-commit.result.json" grep -q '"status":"verified"' "$capture_result" || fail 'verified reconciliation capture is missing' capture_id=$(extract_capture_id "$capture_result") capture_bundle_digest=$(extract_digest "$capture_commit_result" bundleDigest) capture_head_digest=$(extract_digest "$capture_result" instanceHeadDigest) prepared_ms=$(($(date +%s) * 1000)) cat >"$command_root/plan-prepare.json" <"$command_root/plan-commit.json" <"$command_root/plan-verify.json" <"$command_root/review-prepare.json" <"$command_root/diagnostic-$stem.json" <"$reconciliation_root/summary.json" <"$command_root/review-commit.json" <"$command_root/review-verify.json" <"$command_root/application-prepare.json" <"$command_root/application-commit.json" <"$command_root/application-verify.json" <"$command_root/automation-plan.json" <"$command_root/automation-verify.json" <"$command_root/automation-decision-prepare.json" <"$reconciliation_root/summary.json" <"$command_root/secret-config-decision-commit.json" <"$command_root/secret-config-decision-verify.json" <"$command_root/secret-config-apply.json" <"$command_root/secret-config-apply-verify.json" <"$command_root/completion.json" <"$command_root/completion-verify.json" <"$reconciliation_root/summary.json" <"$command_root/automation-decision-commit.json" <"$command_root/automation-decision-verify.json" <"$command_root/automation-apply.json" <"$command_root/automation-apply-verify.json" <"$command_root/run-history-preserve.json" <"$command_root/run-history-verify.json" <"$command_root/secret-config-plan.json" <"$command_root/secret-config-verify.json" <"$command_root/secret-config-decision-prepare.json" <"$reconciliation_root/summary.json" <"$command_root/automation-rollback.json" <"$reconciliation_root/summary.json" <