Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
607 lines
21 KiB
Bash
Executable File
607 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
readonly SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
readonly DEFAULT_SOURCE_ROOT="$(CDPATH= cd -- "${SCRIPT_DIR}/../.." && pwd)"
|
|
readonly TARGET="Administrator@192.168.0.99"
|
|
readonly SOURCE_HOST_IPV4="192.168.0.110"
|
|
readonly GITEA_ORIGIN_HTTPS="https://gitea.wooo.work/wooo/awoooi.git"
|
|
readonly GITEA_ORIGIN_HTTP_INTERNAL="http://192.168.0.110:3000/wooo/awoooi.git"
|
|
readonly GITEA_ORIGIN_SSH_INTERNAL="ssh://git@192.168.0.110:2222/wooo/awoooi.git"
|
|
readonly RECEIVER_RELATIVE="scripts/reboot-recovery/agent99-remote-atomic-deploy-receiver.ps1"
|
|
readonly PREFLIGHT_RELATIVE="scripts/reboot-recovery/agent99-live-preflight.ps1"
|
|
readonly WRAPPER_RELATIVE="scripts/reboot-recovery/deploy-agent99-via-windows99-ssh.sh"
|
|
readonly REMOTE_STDIN_SENTINEL="__AWOOOI_AGENT99_REMOTE_BOOTSTRAP_END_V1__"
|
|
|
|
RUNTIME_FILES=(
|
|
"agent99-bootstrap.ps1"
|
|
"agent99-contract-check.ps1"
|
|
"agent99-control-plane.ps1"
|
|
"agent99-deploy.ps1"
|
|
"agent99-register-tasks.ps1"
|
|
"agent99-sre-alert-inbox.ps1"
|
|
"agent99-sre-alert-relay.ps1"
|
|
"agent99-submit-request.ps1"
|
|
"agent99-synthetic-tests.ps1"
|
|
"agent99-telegram-inbox.ps1"
|
|
"agent99-windows-update-policy.ps1"
|
|
"host-reboot-scorecard.ps1"
|
|
"agent99.config.example.json"
|
|
"agent99.config.99.example.json"
|
|
)
|
|
|
|
MODE="check"
|
|
MODE_SELECTION="default"
|
|
SOURCE_ROOT="${DEFAULT_SOURCE_ROOT}"
|
|
SOURCE_REVISION=""
|
|
TRACE_ID=""
|
|
RUN_ID=""
|
|
WORK_ITEM_ID=""
|
|
IDENTITY_FILE=""
|
|
KNOWN_HOSTS_FILE="${HOME:-}/.ssh/known_hosts"
|
|
CONNECT_TIMEOUT_SECONDS="8"
|
|
REMOTE_EXECUTION_TIMEOUT_SECONDS="180"
|
|
|
|
usage() {
|
|
printf '%s\n' \
|
|
"Usage: $0 [--check|--apply] [options]" \
|
|
"" \
|
|
"Default is a remote no-write check/plan against fixed target ${TARGET}." \
|
|
"Apply requires --apply plus --trace-id, --run-id, and --work-item-id." \
|
|
"" \
|
|
"Options:" \
|
|
" --check No-write transport and envelope check (default)." \
|
|
" --apply Stage, ValidateOnly, bounded promote, and verify." \
|
|
" --trace-id ID Controlled-apply trace identity." \
|
|
" --run-id ID Controlled-apply run identity." \
|
|
" --work-item-id ID Controlled-apply work-item identity." \
|
|
" --source-root PATH Checkout root (default: repository root)." \
|
|
" --source-revision SHA Exact 40-character Git commit (default: HEAD)." \
|
|
" --identity-file PATH Existing SSH identity; wrapper never reads or prints its contents." \
|
|
" --known-hosts-file PATH Pinned known_hosts path (default: ~/.ssh/known_hosts)." \
|
|
" --connect-timeout SECONDS SSH connect timeout from 1 through 30 (default: 8)." \
|
|
" -h, --help Show this help."
|
|
}
|
|
|
|
fail() {
|
|
printf 'agent99_transport_error=%s\n' "$1" >&2
|
|
exit 64
|
|
}
|
|
|
|
require_value() {
|
|
[[ $# -ge 2 && -n "$2" ]] || fail "missing_option_value"
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--check)
|
|
[[ "${MODE_SELECTION}" != "apply" ]] || fail "conflicting_mode_flags"
|
|
MODE="check"
|
|
MODE_SELECTION="check"
|
|
shift
|
|
;;
|
|
--apply)
|
|
[[ "${MODE_SELECTION}" != "check" ]] || fail "conflicting_mode_flags"
|
|
MODE="apply"
|
|
MODE_SELECTION="apply"
|
|
shift
|
|
;;
|
|
--trace-id)
|
|
require_value "$@"
|
|
TRACE_ID="$2"
|
|
shift 2
|
|
;;
|
|
--run-id)
|
|
require_value "$@"
|
|
RUN_ID="$2"
|
|
shift 2
|
|
;;
|
|
--work-item-id)
|
|
require_value "$@"
|
|
WORK_ITEM_ID="$2"
|
|
shift 2
|
|
;;
|
|
--source-root)
|
|
require_value "$@"
|
|
SOURCE_ROOT="$2"
|
|
shift 2
|
|
;;
|
|
--source-revision)
|
|
require_value "$@"
|
|
SOURCE_REVISION="$2"
|
|
shift 2
|
|
;;
|
|
--identity-file)
|
|
require_value "$@"
|
|
IDENTITY_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--known-hosts-file)
|
|
require_value "$@"
|
|
KNOWN_HOSTS_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--connect-timeout)
|
|
require_value "$@"
|
|
CONNECT_TIMEOUT_SECONDS="$2"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
fail "unsupported_option"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ "${CONNECT_TIMEOUT_SECONDS}" =~ ^[0-9]+$ ]] || fail "invalid_connect_timeout"
|
|
(( CONNECT_TIMEOUT_SECONDS >= 1 && CONNECT_TIMEOUT_SECONDS <= 30 )) || fail "invalid_connect_timeout"
|
|
|
|
if [[ "${MODE}" == "apply" ]]; then
|
|
REMOTE_EXECUTION_TIMEOUT_SECONDS="2400"
|
|
readonly SAFE_ID_PATTERN='^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$'
|
|
[[ "${TRACE_ID}" =~ ${SAFE_ID_PATTERN} ]] || fail "apply_trace_id_missing_or_invalid"
|
|
[[ "${RUN_ID}" =~ ${SAFE_ID_PATTERN} ]] || fail "apply_run_id_missing_or_invalid"
|
|
[[ "${WORK_ITEM_ID}" =~ ${SAFE_ID_PATTERN} ]] || fail "apply_work_item_id_missing_or_invalid"
|
|
fi
|
|
|
|
for dependency in git python3 ssh scp mktemp hostname timeout; do
|
|
command -v "${dependency}" >/dev/null 2>&1 || fail "required_command_missing_${dependency}"
|
|
done
|
|
|
|
SOURCE_HOST_VERIFIED=0
|
|
read -r -a LOCAL_ADDRESSES <<<"$(hostname -I 2>/dev/null || true)"
|
|
for local_address in "${LOCAL_ADDRESSES[@]}"; do
|
|
if [[ "${local_address%%/*}" == "${SOURCE_HOST_IPV4}" ]]; then
|
|
SOURCE_HOST_VERIFIED=1
|
|
break
|
|
fi
|
|
done
|
|
[[ "${SOURCE_HOST_VERIFIED}" == "1" ]] || fail "source_host_is_not_110"
|
|
|
|
[[ -d "${SOURCE_ROOT}" ]] || fail "source_root_missing"
|
|
SOURCE_ROOT="$(CDPATH= cd -- "${SOURCE_ROOT}" && pwd)"
|
|
ORIGIN_URL="$(git -C "${SOURCE_ROOT}" remote get-url origin 2>/dev/null)" || fail "origin_remote_missing"
|
|
case "${ORIGIN_URL}" in
|
|
"${GITEA_ORIGIN_HTTPS}"|"${GITEA_ORIGIN_HTTP_INTERNAL}"|"${GITEA_ORIGIN_SSH_INTERNAL}") ;;
|
|
*) fail "origin_is_not_fixed_internal_gitea" ;;
|
|
esac
|
|
git -C "${SOURCE_ROOT}" fetch --quiet --no-tags origin \
|
|
'+refs/heads/main:refs/remotes/origin/main' || fail "fresh_origin_main_fetch_failed"
|
|
ORIGIN_MAIN_REVISION="$(git -C "${SOURCE_ROOT}" rev-parse --verify 'refs/remotes/origin/main^{commit}' 2>/dev/null)" \
|
|
|| fail "fresh_origin_main_revision_unavailable"
|
|
ORIGIN_MAIN_REVISION="$(printf '%s' "${ORIGIN_MAIN_REVISION}" | tr '[:upper:]' '[:lower:]')"
|
|
[[ -f "${KNOWN_HOSTS_FILE}" ]] || fail "known_hosts_file_missing"
|
|
if [[ -n "${IDENTITY_FILE}" ]]; then
|
|
[[ -f "${IDENTITY_FILE}" ]] || fail "identity_file_missing"
|
|
fi
|
|
|
|
if [[ -z "${SOURCE_REVISION}" ]]; then
|
|
SOURCE_REVISION="${ORIGIN_MAIN_REVISION}"
|
|
fi
|
|
SOURCE_REVISION="$(printf '%s' "${SOURCE_REVISION}" | tr '[:upper:]' '[:lower:]')"
|
|
[[ "${SOURCE_REVISION}" =~ ^[0-9a-f]{40}$ ]] || fail "source_revision_must_be_full_commit_sha"
|
|
git -C "${SOURCE_ROOT}" cat-file -e "${SOURCE_REVISION}^{commit}" 2>/dev/null || fail "source_revision_not_found"
|
|
[[ "${SOURCE_REVISION}" == "${ORIGIN_MAIN_REVISION}" ]] || fail "source_revision_is_not_fresh_origin_main"
|
|
|
|
SOURCE_BOUND_FILES=(
|
|
"${RUNTIME_FILES[@]}"
|
|
"${PREFLIGHT_RELATIVE}"
|
|
"${RECEIVER_RELATIVE}"
|
|
"${WRAPPER_RELATIVE}"
|
|
)
|
|
for relative_path in "${SOURCE_BOUND_FILES[@]}"; do
|
|
[[ -f "${SOURCE_ROOT}/${relative_path}" ]] || fail "source_bound_file_missing"
|
|
git -C "${SOURCE_ROOT}" ls-files --error-unmatch -- "${relative_path}" >/dev/null 2>&1 || fail "source_bound_file_untracked"
|
|
done
|
|
git -C "${SOURCE_ROOT}" diff --quiet "${SOURCE_REVISION}" -- "${SOURCE_BOUND_FILES[@]}" || fail "source_bundle_differs_from_revision"
|
|
|
|
umask 077
|
|
WORK_DIR="$(mktemp -d /tmp/awoooi-agent99-transport.XXXXXXXX)"
|
|
cleanup() {
|
|
rm -rf -- "${WORK_DIR}"
|
|
}
|
|
trap cleanup EXIT HUP INT TERM
|
|
|
|
ENVELOPE_PATH="${WORK_DIR}/envelope.json"
|
|
BOOTSTRAP_PATH="${WORK_DIR}/remote-bootstrap.ps1"
|
|
BOOTSTRAP_PAYLOAD_PATH="${WORK_DIR}/remote-bootstrap.ps1.gz.b64"
|
|
|
|
python3 - \
|
|
"${SOURCE_ROOT}" \
|
|
"${SOURCE_REVISION}" \
|
|
"${MODE}" \
|
|
"${TRACE_ID}" \
|
|
"${RUN_ID}" \
|
|
"${WORK_ITEM_ID}" \
|
|
"${ENVELOPE_PATH}" \
|
|
"${RUNTIME_FILES[@]}" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
source_root = Path(sys.argv[1])
|
|
source_revision, mode = sys.argv[2:4]
|
|
trace_id, run_id, work_item_id = sys.argv[4:7]
|
|
output_path = Path(sys.argv[7])
|
|
runtime_names = sys.argv[8:]
|
|
|
|
if len(runtime_names) != 14 or len(set(runtime_names)) != 14:
|
|
raise SystemExit("fixed_runtime_file_contract_failed")
|
|
|
|
files: list[dict[str, str]] = []
|
|
manifest_rows: list[str] = []
|
|
for name in runtime_names:
|
|
if Path(name).name != name:
|
|
raise SystemExit("runtime_filename_not_flat")
|
|
content = (source_root / name).read_bytes()
|
|
digest = hashlib.sha256(content).hexdigest()
|
|
files.append(
|
|
{
|
|
"name": name,
|
|
"sha256": digest,
|
|
"contentBase64": base64.b64encode(content).decode("ascii"),
|
|
}
|
|
)
|
|
manifest_rows.append(f"{name}\t{digest}\n")
|
|
|
|
manifest_sha256 = hashlib.sha256("".join(manifest_rows).encode("utf-8")).hexdigest()
|
|
preflight_path = source_root / "scripts/reboot-recovery/agent99-live-preflight.ps1"
|
|
preflight_content = preflight_path.read_bytes()
|
|
|
|
envelope = {
|
|
"schemaVersion": "agent99_remote_atomic_deploy_envelope_v1",
|
|
"sourceHostAlias": "110",
|
|
"targetHostAlias": "99",
|
|
"transport": "110_to_99_ssh_publickey",
|
|
"transportPayloadMode": (
|
|
"ephemeral_file_delete_before_receiver"
|
|
if mode == "apply"
|
|
else "control_command_no_stdin"
|
|
),
|
|
"transportPayloadContainsSecrets": False,
|
|
"mode": mode,
|
|
"traceId": trace_id,
|
|
"runId": run_id,
|
|
"workItemId": work_item_id,
|
|
"sourceRevision": source_revision,
|
|
"expectedRuntimeFileCount": 14,
|
|
"manifestSha256": manifest_sha256,
|
|
"files": files,
|
|
"livePreflight": {
|
|
"name": preflight_path.name,
|
|
"sha256": hashlib.sha256(preflight_content).hexdigest(),
|
|
"contentBase64": base64.b64encode(preflight_content).decode("ascii"),
|
|
},
|
|
}
|
|
output_path.write_text(
|
|
json.dumps(envelope, separators=(",", ":"), ensure_ascii=True),
|
|
encoding="utf-8",
|
|
)
|
|
PY
|
|
|
|
python3 - \
|
|
"${SOURCE_ROOT}/${RECEIVER_RELATIVE}" \
|
|
"${ENVELOPE_PATH}" \
|
|
"${BOOTSTRAP_PATH}" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
receiver_path, envelope_path, output_path = map(Path, sys.argv[1:4])
|
|
receiver_base64 = base64.b64encode(receiver_path.read_bytes()).decode("ascii")
|
|
envelope_base64 = base64.b64encode(envelope_path.read_bytes()).decode("ascii")
|
|
envelope_mode = json.loads(envelope_path.read_text(encoding="utf-8"))["mode"]
|
|
transport_delete_switch = (
|
|
" -TransportPayloadDeletedBeforeReceiver"
|
|
if envelope_mode == "apply"
|
|
else ""
|
|
)
|
|
bootstrap = (
|
|
"$ErrorActionPreference = 'Stop'\r\n"
|
|
f"$receiverText = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('{receiver_base64}'))\r\n"
|
|
f"$envelopeText = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('{envelope_base64}'))\r\n"
|
|
"& ([ScriptBlock]::Create($receiverText)) -EnvelopeJson $envelopeText"
|
|
f"{transport_delete_switch}\r\n"
|
|
)
|
|
output_path.write_text(bootstrap, encoding="utf-8")
|
|
PY
|
|
|
|
python3 - \
|
|
"${BOOTSTRAP_PATH}" \
|
|
"${BOOTSTRAP_PAYLOAD_PATH}" \
|
|
"${REMOTE_STDIN_SENTINEL}" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import gzip
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
bootstrap_path, output_path = map(Path, sys.argv[1:3])
|
|
sentinel = sys.argv[3]
|
|
compressed = gzip.compress(
|
|
bootstrap_path.read_bytes(),
|
|
compresslevel=9,
|
|
mtime=0,
|
|
)
|
|
payload = base64.b64encode(compressed).decode("ascii")
|
|
line_width = 65536
|
|
payload_lines = [
|
|
payload[offset : offset + line_width]
|
|
for offset in range(0, len(payload), line_width)
|
|
]
|
|
if not payload_lines or len(payload_lines) > 8:
|
|
raise SystemExit("remote_bootstrap_payload_line_contract_failed")
|
|
output_path.write_text(
|
|
"\n".join(payload_lines) + "\n" + sentinel + "\n",
|
|
encoding="ascii",
|
|
)
|
|
PY
|
|
|
|
TRANSPORT_TOKEN="$(python3 - "${BOOTSTRAP_PAYLOAD_PATH}" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
print(hashlib.sha256(Path(sys.argv[1]).read_bytes()).hexdigest()[:20])
|
|
PY
|
|
)"
|
|
readonly TRANSPORT_TOKEN
|
|
readonly REMOTE_TRANSPORT_DIR_WINDOWS='C:\Wooo\Agent99\deploy\transport-inbox'
|
|
readonly REMOTE_TRANSPORT_PATH_WINDOWS="${REMOTE_TRANSPORT_DIR_WINDOWS}\\agent99-bootstrap-${TRANSPORT_TOKEN}.b64"
|
|
readonly REMOTE_TRANSPORT_PATH_SCP="C:/Wooo/Agent99/deploy/transport-inbox/agent99-bootstrap-${TRANSPORT_TOKEN}.b64"
|
|
|
|
SSH_OPTIONS=(
|
|
-T
|
|
-o BatchMode=yes
|
|
-o PreferredAuthentications=publickey
|
|
-o PubkeyAuthentication=yes
|
|
-o PasswordAuthentication=no
|
|
-o KbdInteractiveAuthentication=no
|
|
-o NumberOfPasswordPrompts=0
|
|
-o StrictHostKeyChecking=yes
|
|
-o "UserKnownHostsFile=${KNOWN_HOSTS_FILE}"
|
|
-o "ConnectTimeout=${CONNECT_TIMEOUT_SECONDS}"
|
|
-o ConnectionAttempts=1
|
|
-o ServerAliveInterval=5
|
|
-o ServerAliveCountMax=2
|
|
-o LogLevel=ERROR
|
|
-o IdentitiesOnly=yes
|
|
)
|
|
if [[ -n "${IDENTITY_FILE}" ]]; then
|
|
SSH_OPTIONS+=( -i "${IDENTITY_FILE}" )
|
|
fi
|
|
|
|
SCP_OPTIONS=(
|
|
-q
|
|
-o BatchMode=yes
|
|
-o PreferredAuthentications=publickey
|
|
-o PubkeyAuthentication=yes
|
|
-o PasswordAuthentication=no
|
|
-o KbdInteractiveAuthentication=no
|
|
-o NumberOfPasswordPrompts=0
|
|
-o StrictHostKeyChecking=yes
|
|
-o "UserKnownHostsFile=${KNOWN_HOSTS_FILE}"
|
|
-o "ConnectTimeout=${CONNECT_TIMEOUT_SECONDS}"
|
|
-o ConnectionAttempts=1
|
|
-o ServerAliveInterval=5
|
|
-o ServerAliveCountMax=2
|
|
-o LogLevel=ERROR
|
|
-o IdentitiesOnly=yes
|
|
)
|
|
if [[ -n "${IDENTITY_FILE}" ]]; then
|
|
SCP_OPTIONS+=( -i "${IDENTITY_FILE}" )
|
|
fi
|
|
|
|
readonly REMOTE_CHECK_COMMAND="$(python3 - "${ENVELOPE_PATH}" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
envelope = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
source_revision = envelope["sourceRevision"]
|
|
manifest_sha256 = envelope["manifestSha256"]
|
|
stage_identity = f"check||||{source_revision}"
|
|
stage_token = hashlib.sha256(stage_identity.encode("utf-8")).hexdigest()[:20]
|
|
script = r'''$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue'
|
|
$a='C:\Wooo\Agent99';$b=Join-Path $a 'bin';$p=Join-Path $a 'state\runtime-manifest.json'
|
|
$r=[ordered]@{exists=$false;sourceRevision='';runtimeMatched=$false;recordedRuntimeMatched=$false;fileCount=0;mismatchCount=-1;recordedMismatchCount=-1;parseError=''}
|
|
if(Test-Path -LiteralPath $p -PathType Leaf){try{$m=Get-Content -LiteralPath $p -Raw|ConvertFrom-Json;$rows=@($m.files);$seen=@{};$bad=0;foreach($x in $rows){$n=[string]$x.name;if(!$n-or[IO.Path]::GetFileName($n)-ne$n-or$seen.ContainsKey($n)){$bad++;continue};$seen[$n]=1;$q=Join-Path $b $n;$h=if(Test-Path -LiteralPath $q -PathType Leaf){(Get-FileHash -LiteralPath $q -Algorithm SHA256).Hash.ToLowerInvariant()}else{'missing'};if(([string]$x.runtimeSha256).ToLowerInvariant()-ne$h){$bad++}};$r.exists=$true;$r.sourceRevision=[string]$m.sourceRevision;$r.recordedRuntimeMatched=[bool]$m.runtimeMatched;$r.fileCount=[int]$m.fileCount;$r.mismatchCount=$bad;$r.recordedMismatchCount=[int]$m.mismatchCount;$r.runtimeMatched=[bool]($m.runtimeMatched-and$m.fileCount-eq 14-and$m.mismatchCount-eq 0-and$rows.Count-eq 14-and$seen.Count-eq 14-and$bad-eq 0)}catch{$r.exists=$true;$r.parseError=$_.Exception.GetType().Name}}
|
|
$c=[ordered]@{executed=$false;ok=$false;exitCode=-1;errorType=''}
|
|
try{& powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File (Join-Path $b 'agent99-contract-check.ps1') -SourceRoot $b 2>$null|Out-Null;$c.executed=$true;$c.exitCode=$LASTEXITCODE;$c.ok=[bool]($c.exitCode-eq 0)}catch{$c.executed=$true;$c.errorType=$_.Exception.GetType().Name}
|
|
$z=[ordered]@{};foreach($n in 'remoteWritePerformed,liveRuntimeWritePerformed,livePromotionAttempted,livePromotionPerformed,secretValueRead,privateKeyValueRead,tokenValueRead,environmentSecretRead,uiInteraction,vmPowerChange,hostReboot,serviceRestart,scheduledTaskRestart,scheduledTaskModification'.Split(',')){$z[$n]=$false}
|
|
$ready=[bool]($c.executed-and$c.ok-and$c.exitCode-eq 0)
|
|
$status=if($ready){'check_ready'}else{'check_failed_no_apply'}
|
|
$next=if($ready){'rerun_with_apply_and_trace_run_work_item_after_source_commit_and_transport_check'}else{'repair_runtime_contract_before_apply'}
|
|
[ordered]@{schemaVersion='agent99_remote_atomic_deploy_receipt_v1';status=$status;mode='check';sourceRevision='__SOURCE_REVISION__';expectedRuntimeFileCount=14;manifestSha256='__MANIFEST_SHA256__';plannedStagingPath='C:\Wooo\Agent99\deploy\remote-source-__STAGE_TOKEN__';runtimeManifest=[pscustomobject]$r;runtimeContract=[pscustomobject]$c;livePreflight=[ordered]@{executed=$false;reason='apply_only_after_validate_only'};transportPayloadMode='control_command_no_stdin';transientTransportPayloadStored=$false;transientTransportPayloadDeletedBeforeReceiver=$false;transportPayloadContainsSecrets=$false;operationBoundaries=$z;nextSafeAction=$next}|ConvertTo-Json -Compress -Depth 8
|
|
if(-not$ready){exit 65}'''
|
|
script = (
|
|
script.replace("__SOURCE_REVISION__", source_revision)
|
|
.replace("__MANIFEST_SHA256__", manifest_sha256)
|
|
.replace("__STAGE_TOKEN__", stage_token)
|
|
)
|
|
encoded = base64.b64encode(script.encode("utf-16le")).decode("ascii")
|
|
print(
|
|
"powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass "
|
|
f"-EncodedCommand {encoded}"
|
|
)
|
|
PY
|
|
)"
|
|
|
|
readonly REMOTE_PREPARE_COMMAND="$(python3 - \
|
|
"${REMOTE_TRANSPORT_DIR_WINDOWS}" \
|
|
"${REMOTE_TRANSPORT_PATH_WINDOWS}" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import sys
|
|
|
|
directory = sys.argv[1].replace("'", "''")
|
|
path = sys.argv[2].replace("'", "''")
|
|
script = rf'''$ErrorActionPreference = "Stop"
|
|
$directory = '{directory}'
|
|
$payloadPath = '{path}'
|
|
New-Item -ItemType Directory -Force -Path $directory | Out-Null
|
|
if (Test-Path -LiteralPath $payloadPath) {{
|
|
Remove-Item -LiteralPath $payloadPath -Force
|
|
}}
|
|
if (Test-Path -LiteralPath $payloadPath) {{
|
|
throw "remote_transport_stale_payload_cleanup_failed"
|
|
}}'''
|
|
encoded = base64.b64encode(script.encode("utf-16le")).decode("ascii")
|
|
print(
|
|
"powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass "
|
|
f"-EncodedCommand {encoded}"
|
|
)
|
|
PY
|
|
)"
|
|
|
|
readonly REMOTE_FILE_COMMAND="$(python3 - \
|
|
"${REMOTE_TRANSPORT_PATH_WINDOWS}" \
|
|
"${REMOTE_STDIN_SENTINEL}" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import sys
|
|
|
|
path = sys.argv[1].replace("'", "''")
|
|
sentinel = sys.argv[2].replace("'", "''")
|
|
decoder = rf'''$ErrorActionPreference = "Stop"
|
|
$payloadPath = '{path}'
|
|
$sentinel = '{sentinel}'
|
|
$allLines = @()
|
|
try {{
|
|
$payloadFile = Get-Item -LiteralPath $payloadPath -ErrorAction Stop
|
|
if ($payloadFile.Length -le 0 -or $payloadFile.Length -gt 1048576) {{
|
|
throw "remote_bootstrap_file_size_invalid"
|
|
}}
|
|
$allLines = @([IO.File]::ReadAllLines($payloadPath, [Text.Encoding]::ASCII))
|
|
}} finally {{
|
|
if (Test-Path -LiteralPath $payloadPath) {{
|
|
Remove-Item -LiteralPath $payloadPath -Force -ErrorAction SilentlyContinue
|
|
}}
|
|
}}
|
|
if (Test-Path -LiteralPath $payloadPath) {{
|
|
throw "remote_bootstrap_file_delete_failed"
|
|
}}
|
|
if ($allLines.Count -lt 2 -or $allLines[-1] -cne $sentinel) {{
|
|
throw "remote_bootstrap_sentinel_missing"
|
|
}}
|
|
$payloadLines = @($allLines[0..($allLines.Count - 2)])
|
|
$charCount = [int](($payloadLines | Measure-Object -Property Length -Sum).Sum) + (2 * $payloadLines.Count)
|
|
if ($payloadLines.Count -gt 8 -or $charCount -gt 1048576) {{
|
|
throw "remote_bootstrap_input_limit_exceeded"
|
|
}}
|
|
$payload = [string]::Join("", $payloadLines)
|
|
$compressed = [Convert]::FromBase64String(($payload -replace '\s', ''))
|
|
$inputStream = New-Object IO.MemoryStream(,$compressed)
|
|
$gzip = New-Object IO.Compression.GzipStream($inputStream, [IO.Compression.CompressionMode]::Decompress)
|
|
$reader = New-Object IO.StreamReader($gzip, [Text.Encoding]::UTF8)
|
|
try {{
|
|
$inputText = $reader.ReadToEnd()
|
|
}} finally {{
|
|
$reader.Dispose()
|
|
$gzip.Dispose()
|
|
$inputStream.Dispose()
|
|
}}
|
|
& ([ScriptBlock]::Create($inputText))'''
|
|
encoded = base64.b64encode(decoder.encode("utf-16le")).decode("ascii")
|
|
print(
|
|
"powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass "
|
|
f"-EncodedCommand {encoded}"
|
|
)
|
|
PY
|
|
)"
|
|
|
|
readonly REMOTE_CLEANUP_COMMAND="$(python3 - \
|
|
"${REMOTE_TRANSPORT_PATH_WINDOWS}" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import sys
|
|
|
|
path = sys.argv[1].replace("'", "''")
|
|
script = rf'''$ErrorActionPreference = "Stop"
|
|
$payloadPath = '{path}'
|
|
if (Test-Path -LiteralPath $payloadPath) {{
|
|
Remove-Item -LiteralPath $payloadPath -Force
|
|
}}
|
|
if (Test-Path -LiteralPath $payloadPath) {{
|
|
throw "remote_transport_payload_cleanup_failed"
|
|
}}'''
|
|
encoded = base64.b64encode(script.encode("utf-16le")).decode("ascii")
|
|
print(
|
|
"powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass "
|
|
f"-EncodedCommand {encoded}"
|
|
)
|
|
PY
|
|
)"
|
|
|
|
cleanup_remote_payload() {
|
|
timeout --signal=TERM --kill-after=5 30s \
|
|
ssh "${SSH_OPTIONS[@]}" "${TARGET}" "${REMOTE_CLEANUP_COMMAND}" </dev/null \
|
|
>/dev/null 2>&1
|
|
}
|
|
|
|
if [[ "${MODE}" == "check" ]]; then
|
|
set +e
|
|
timeout --signal=TERM --kill-after=10 "${REMOTE_EXECUTION_TIMEOUT_SECONDS}s" \
|
|
ssh "${SSH_OPTIONS[@]}" "${TARGET}" "${REMOTE_CHECK_COMMAND}" </dev/null
|
|
status=$?
|
|
set -e
|
|
exit "${status}"
|
|
fi
|
|
|
|
set +e
|
|
timeout --signal=TERM --kill-after=5 30s \
|
|
ssh "${SSH_OPTIONS[@]}" "${TARGET}" "${REMOTE_PREPARE_COMMAND}" </dev/null
|
|
prepare_status=$?
|
|
set -e
|
|
if [[ "${prepare_status}" -ne 0 ]]; then
|
|
printf 'agent99_transport_error=ephemeral_payload_prepare_failed\n' >&2
|
|
exit "${prepare_status}"
|
|
fi
|
|
|
|
set +e
|
|
timeout --signal=TERM --kill-after=10 120s \
|
|
scp "${SCP_OPTIONS[@]}" "${BOOTSTRAP_PAYLOAD_PATH}" \
|
|
"${TARGET}:${REMOTE_TRANSPORT_PATH_SCP}"
|
|
copy_status=$?
|
|
set -e
|
|
if [[ "${copy_status}" -ne 0 ]]; then
|
|
cleanup_remote_payload || true
|
|
printf 'agent99_transport_error=ephemeral_payload_copy_failed\n' >&2
|
|
exit "${copy_status}"
|
|
fi
|
|
|
|
set +e
|
|
timeout --signal=TERM --kill-after=10 "${REMOTE_EXECUTION_TIMEOUT_SECONDS}s" \
|
|
ssh "${SSH_OPTIONS[@]}" "${TARGET}" "${REMOTE_FILE_COMMAND}" </dev/null
|
|
status=$?
|
|
cleanup_remote_payload
|
|
cleanup_status=$?
|
|
set -e
|
|
if [[ "${cleanup_status}" -ne 0 ]]; then
|
|
printf 'agent99_transport_error=ephemeral_payload_cleanup_verifier_failed\n' >&2
|
|
if [[ "${status}" -eq 0 ]]; then
|
|
exit 74
|
|
fi
|
|
fi
|
|
exit "${status}"
|