direxio-deployer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +92 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/README_zh.md +218 -0
- package/SKILL.md +722 -0
- package/agents/README.md +25 -0
- package/agents/openai.yaml +12 -0
- package/bin/direxio-deployer.mjs +375 -0
- package/package.json +28 -0
- package/references/agent-targets.md +128 -0
- package/references/architecture.md +44 -0
- package/references/bug-history.md +78 -0
- package/references/deployment-lessons.md +218 -0
- package/references/deployment-optimization-audit.md +317 -0
- package/references/deployment-workflow.md +341 -0
- package/references/iam-policy.json +52 -0
- package/references/runtime-wiring.md +209 -0
- package/references/state-machine.md +46 -0
- package/references/token-refresh.md +81 -0
- package/references/tooling.md +106 -0
- package/references/troubleshooting.md +26 -0
- package/references/user-journey.md +75 -0
- package/references/verification-recovery.md +84 -0
- package/references/voip-turn-runbook.md +154 -0
- package/references/windows-deployment-notes.md +119 -0
- package/scripts/aws-credentials.sh +195 -0
- package/scripts/cloud-init/Caddyfile +48 -0
- package/scripts/cloud-init/docker-compose.yml +125 -0
- package/scripts/cloud-init/init-tokens.sh +238 -0
- package/scripts/cloud-init/user-data.yaml +40 -0
- package/scripts/destroy.ps1 +77 -0
- package/scripts/destroy.sh +589 -0
- package/scripts/lib/aws.sh +73 -0
- package/scripts/lib/domain.sh +175 -0
- package/scripts/lib/operation_report.sh +240 -0
- package/scripts/lib/ops.sh +230 -0
- package/scripts/lib/paths.sh +35 -0
- package/scripts/lib/state.sh +137 -0
- package/scripts/mcp-tools-list.mjs +95 -0
- package/scripts/orchestrate.ps1 +112 -0
- package/scripts/orchestrate.sh +1126 -0
- package/scripts/phases/s0_prereq_aws.sh +39 -0
- package/scripts/phases/s1_preflight.sh +72 -0
- package/scripts/phases/s2_domain.sh +103 -0
- package/scripts/phases/s3_provision.sh +421 -0
- package/scripts/phases/s4_bootstrap_stack.sh +38 -0
- package/scripts/phases/s5_init_tokens.sh +118 -0
- package/scripts/phases/s6_wire_local.sh +1435 -0
- package/scripts/phases/s7_verify_e2e.sh +136 -0
- package/scripts/pricing-estimate.sh +256 -0
- package/scripts/render/render-userdata.sh +86 -0
- package/scripts/reset-app-data.sh +40 -0
- package/scripts/update.sh +30 -0
- package/tests/aws_credentials_test.sh +139 -0
- package/tests/connect_daemon_runtime_check_test.sh +120 -0
- package/tests/default_paths_test.sh +58 -0
- package/tests/destroy_local_bridge_test.sh +154 -0
- package/tests/destroy_root_identity_test.sh +91 -0
- package/tests/destroy_route53_zone_test.sh +80 -0
- package/tests/domain_authoritative_dns_test.sh +49 -0
- package/tests/mcp_doctor_runtime_check_test.sh +86 -0
- package/tests/mcp_smoke_runtime_check_test.sh +121 -0
- package/tests/mcp_tools_runtime_check_test.sh +123 -0
- package/tests/npm_skill_distribution_test.sh +95 -0
- package/tests/operation_report_test.sh +258 -0
- package/tests/orchestrate_status_recovery_test.sh +91 -0
- package/tests/phase_timeout_test.sh +88 -0
- package/tests/pricing_estimate_test.sh +159 -0
- package/tests/render_userdata_remote_nodes_test.sh +40 -0
- package/tests/root_volume_tracking_test.sh +41 -0
- package/tests/route53_overwrite_guard_test.sh +86 -0
- package/tests/route53_zone_auto_create_test.sh +66 -0
- package/tests/runtime_summary_check_test.sh +203 -0
- package/tests/s6_wire_local_test.sh +405 -0
- package/tests/skill_structure_test.sh +298 -0
- package/tests/update_reset_ops_test.sh +230 -0
- package/tests/user_confirmation_gates_test.sh +152 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(ValueFromRemainingArguments = $true)]
|
|
3
|
+
[string[]] $DestroyArgs
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
|
|
8
|
+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
9
|
+
$RepoRoot = Split-Path -Parent $ScriptDir
|
|
10
|
+
|
|
11
|
+
function Find-GitBash {
|
|
12
|
+
$candidates = @(
|
|
13
|
+
"$env:ProgramFiles\Git\bin\bash.exe",
|
|
14
|
+
"$env:ProgramFiles\Git\usr\bin\bash.exe",
|
|
15
|
+
"$env:LOCALAPPDATA\Programs\Git\bin\bash.exe",
|
|
16
|
+
"$env:LOCALAPPDATA\Programs\Git\usr\bin\bash.exe"
|
|
17
|
+
)
|
|
18
|
+
foreach ($candidate in $candidates) {
|
|
19
|
+
if ($candidate -and (Test-Path $candidate)) {
|
|
20
|
+
return $candidate
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw "Git Bash was not found. Install Git for Windows or run scripts/destroy.sh from a POSIX shell."
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ConvertTo-GitBashPath([string] $Path) {
|
|
27
|
+
$normalized = $Path.Replace('\', '/')
|
|
28
|
+
if ($normalized -match '^/[A-Za-z]/') {
|
|
29
|
+
return $normalized
|
|
30
|
+
}
|
|
31
|
+
if ($normalized -match '^/mnt/[A-Za-z]/') {
|
|
32
|
+
$drive = $normalized.Substring(5, 1).ToLowerInvariant()
|
|
33
|
+
$rest = $normalized.Substring(6)
|
|
34
|
+
return "/$drive$rest"
|
|
35
|
+
}
|
|
36
|
+
$full = [System.IO.Path]::GetFullPath($Path)
|
|
37
|
+
$drive = $full.Substring(0, 1).ToLowerInvariant()
|
|
38
|
+
$rest = $full.Substring(2).Replace('\', '/')
|
|
39
|
+
return "/$drive$rest"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function Convert-ArgumentForGitBash([string] $Value) {
|
|
43
|
+
if ($Value -match '^[A-Za-z]:[\\/]') {
|
|
44
|
+
return ConvertTo-GitBashPath $Value
|
|
45
|
+
}
|
|
46
|
+
if ($Value -match '^\.{1,2}[\\/]') {
|
|
47
|
+
return ConvertTo-GitBashPath (Join-Path (Get-Location).ProviderPath $Value)
|
|
48
|
+
}
|
|
49
|
+
return $Value
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function Quote-BashArg([string] $Value) {
|
|
53
|
+
return "'" + ($Value -replace "'", "'\''") + "'"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
$bash = Find-GitBash
|
|
57
|
+
|
|
58
|
+
$windowsDirexioHome = if ($env:DIREXIO_HOME -and ($env:DIREXIO_HOME -notmatch '^/[A-Za-z]/' -and $env:DIREXIO_HOME -notmatch '^/mnt/[A-Za-z]/')) {
|
|
59
|
+
$env:DIREXIO_HOME
|
|
60
|
+
} else {
|
|
61
|
+
Join-Path $env:USERPROFILE '.direxio'
|
|
62
|
+
}
|
|
63
|
+
$env:DIREXIO_WINDOWS_HOME = $windowsDirexioHome
|
|
64
|
+
$env:DIREXIO_HOME = ConvertTo-GitBashPath $windowsDirexioHome
|
|
65
|
+
$env:DIREXIO_LOCAL_PATH_STYLE = 'windows'
|
|
66
|
+
|
|
67
|
+
if ($env:P2P_WORKDIR) {
|
|
68
|
+
$env:P2P_WORKDIR_WINDOWS = $env:P2P_WORKDIR
|
|
69
|
+
$env:P2P_WORKDIR = ConvertTo-GitBashPath $env:P2P_WORKDIR
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
$repoRootForBash = ConvertTo-GitBashPath $RepoRoot
|
|
73
|
+
$quotedArgs = ($DestroyArgs | ForEach-Object { Quote-BashArg (Convert-ArgumentForGitBash $_) }) -join ' '
|
|
74
|
+
$command = "cd $(Quote-BashArg $repoRootForBash) && ./scripts/destroy.sh $quotedArgs"
|
|
75
|
+
|
|
76
|
+
& $bash -lc $command
|
|
77
|
+
exit $LASTEXITCODE
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# destroy.sh - remove AWS resources recorded by deployment state.
|
|
3
|
+
#
|
|
4
|
+
# Source:
|
|
5
|
+
# 1. $P2P_WORKDIR/state.json written by orchestrate.sh; by default
|
|
6
|
+
# DOMAIN=__DOMAIN__ maps to ~/.direxio/nodes/<service_id>/state.json.
|
|
7
|
+
# 2. explicit argument: bash destroy.sh /path/to/state.json
|
|
8
|
+
#
|
|
9
|
+
# Order: terminate instance -> release EIP -> delete security group -> delete key pair
|
|
10
|
+
# -> remove the corresponding local service directory.
|
|
11
|
+
# Each cloud step is tolerant of already-removed resources.
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
|
|
14
|
+
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
15
|
+
# shellcheck disable=SC1090
|
|
16
|
+
source "$HERE/lib/paths.sh"
|
|
17
|
+
# shellcheck disable=SC1090
|
|
18
|
+
source "$HERE/lib/aws.sh"
|
|
19
|
+
# shellcheck disable=SC1090
|
|
20
|
+
source "$HERE/lib/operation_report.sh"
|
|
21
|
+
P2P_WORKDIR=$(direxio_default_workdir)
|
|
22
|
+
|
|
23
|
+
log() { echo -e "\033[33m[destroy]\033[0m $*"; }
|
|
24
|
+
|
|
25
|
+
destroy_now() {
|
|
26
|
+
date -u +%Y-%m-%dT%H:%M:%SZ
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
destroy_evidence_set() {
|
|
30
|
+
local key=$1 status=$2 detail=${3:-} tmp
|
|
31
|
+
tmp="$SRC.tmp.destroy.$$"
|
|
32
|
+
if jq --arg key "$key" \
|
|
33
|
+
--arg status "$status" \
|
|
34
|
+
--arg detail "$detail" \
|
|
35
|
+
--arg checked_at "$(destroy_now)" \
|
|
36
|
+
'.destroy_evidence[$key] = {
|
|
37
|
+
status: $status,
|
|
38
|
+
detail: $detail,
|
|
39
|
+
checked_at: $checked_at
|
|
40
|
+
}' "$SRC" > "$tmp"; then
|
|
41
|
+
mv "$tmp" "$SRC"
|
|
42
|
+
else
|
|
43
|
+
rm -f "$tmp"
|
|
44
|
+
log " (failed to record destroy evidence for $key)"
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
route53_a_record_present() {
|
|
49
|
+
local zone_id=$1 domain=$2 public_ip=$3 rrsets present
|
|
50
|
+
rrsets=$(aws route53 list-resource-record-sets --hosted-zone-id "$zone_id" --output json 2>/dev/null) || return 2
|
|
51
|
+
present=$(printf '%s\n' "$rrsets" | jq -r --arg name "$domain." --arg ip "$public_ip" '
|
|
52
|
+
any(.ResourceRecordSets[]?;
|
|
53
|
+
.Name == $name
|
|
54
|
+
and .Type == "A"
|
|
55
|
+
and any(.ResourceRecords[]?; .Value == $ip)
|
|
56
|
+
)
|
|
57
|
+
' 2>/dev/null) || return 2
|
|
58
|
+
[ "$present" = "true" ]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
verify_route53_a_record_deleted() {
|
|
62
|
+
local zone_id=$1 domain=$2 public_ip=$3 rc
|
|
63
|
+
if [ -z "$zone_id" ] || [ -z "$domain" ] || [ -z "$public_ip" ]; then
|
|
64
|
+
destroy_evidence_set route53_a_record skipped "missing hosted zone, domain, or public IP"
|
|
65
|
+
return 0
|
|
66
|
+
fi
|
|
67
|
+
route53_a_record_present "$zone_id" "$domain" "$public_ip"
|
|
68
|
+
rc=$?
|
|
69
|
+
case "$rc" in
|
|
70
|
+
0) destroy_evidence_set route53_a_record still_present "$domain still resolves to recorded IP $public_ip in hosted zone $zone_id" ;;
|
|
71
|
+
1) destroy_evidence_set route53_a_record deleted "$domain A $public_ip is absent from hosted zone $zone_id" ;;
|
|
72
|
+
*) destroy_evidence_set route53_a_record unknown "could not verify hosted zone $zone_id record state" ;;
|
|
73
|
+
esac
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
verify_route53_hosted_zone_deleted() {
|
|
77
|
+
local zone_id=$1 out
|
|
78
|
+
if [ -z "$zone_id" ]; then
|
|
79
|
+
destroy_evidence_set route53_hosted_zone skipped "missing hosted zone id"
|
|
80
|
+
return 0
|
|
81
|
+
fi
|
|
82
|
+
if out=$(aws route53 get-hosted-zone --id "$zone_id" --output json 2>/dev/null); then
|
|
83
|
+
if [ -n "$out" ]; then
|
|
84
|
+
destroy_evidence_set route53_hosted_zone still_present "hosted zone $zone_id still exists"
|
|
85
|
+
else
|
|
86
|
+
destroy_evidence_set route53_hosted_zone unknown "empty get-hosted-zone response for $zone_id"
|
|
87
|
+
fi
|
|
88
|
+
else
|
|
89
|
+
destroy_evidence_set route53_hosted_zone deleted "hosted zone $zone_id is absent"
|
|
90
|
+
fi
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
verify_ec2_instance_terminated() {
|
|
94
|
+
local instance_id=$1 state
|
|
95
|
+
if [ -z "$instance_id" ]; then
|
|
96
|
+
destroy_evidence_set ec2_instance skipped "missing instance id"
|
|
97
|
+
return 0
|
|
98
|
+
fi
|
|
99
|
+
if state=$(aws ec2 describe-instances --instance-ids "$instance_id" \
|
|
100
|
+
--query 'Reservations[0].Instances[0].State.Name' --output text 2>/dev/null); then
|
|
101
|
+
case "$state" in
|
|
102
|
+
terminated) destroy_evidence_set ec2_instance terminated "instance $instance_id state is terminated" ;;
|
|
103
|
+
""|"None") destroy_evidence_set ec2_instance unknown "instance $instance_id returned no state" ;;
|
|
104
|
+
*) destroy_evidence_set ec2_instance "$state" "instance $instance_id state is $state" ;;
|
|
105
|
+
esac
|
|
106
|
+
else
|
|
107
|
+
destroy_evidence_set ec2_instance not_found "instance $instance_id was not returned by EC2"
|
|
108
|
+
fi
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
verify_ebs_root_volume_deleted() {
|
|
112
|
+
local volume_id=$1 state
|
|
113
|
+
if [ -z "$volume_id" ]; then
|
|
114
|
+
destroy_evidence_set ebs_root_volume skipped "missing root volume id"
|
|
115
|
+
return 0
|
|
116
|
+
fi
|
|
117
|
+
if state=$(aws ec2 describe-volumes --volume-ids "$volume_id" \
|
|
118
|
+
--query 'Volumes[0].State' --output text 2>/dev/null); then
|
|
119
|
+
case "$state" in
|
|
120
|
+
""|"None") destroy_evidence_set ebs_root_volume deleted "root volume $volume_id is absent" ;;
|
|
121
|
+
deleted) destroy_evidence_set ebs_root_volume deleted "root volume $volume_id state is deleted" ;;
|
|
122
|
+
*) destroy_evidence_set ebs_root_volume "$state" "root volume $volume_id state is $state" ;;
|
|
123
|
+
esac
|
|
124
|
+
else
|
|
125
|
+
destroy_evidence_set ebs_root_volume deleted "root volume $volume_id is absent"
|
|
126
|
+
fi
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
verify_elastic_ip_released() {
|
|
130
|
+
local allocation_id=$1 out
|
|
131
|
+
if [ -z "$allocation_id" ]; then
|
|
132
|
+
destroy_evidence_set elastic_ip skipped "missing allocation id"
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
if out=$(aws ec2 describe-addresses --allocation-ids "$allocation_id" \
|
|
136
|
+
--query 'Addresses[0].AllocationId' --output text 2>/dev/null); then
|
|
137
|
+
case "$out" in
|
|
138
|
+
""|"None") destroy_evidence_set elastic_ip released "allocation $allocation_id is absent" ;;
|
|
139
|
+
*) destroy_evidence_set elastic_ip still_allocated "allocation $allocation_id still exists" ;;
|
|
140
|
+
esac
|
|
141
|
+
else
|
|
142
|
+
destroy_evidence_set elastic_ip released "allocation $allocation_id is absent"
|
|
143
|
+
fi
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
verify_security_group_deleted() {
|
|
147
|
+
local group_id=$1 out
|
|
148
|
+
if [ -z "$group_id" ]; then
|
|
149
|
+
destroy_evidence_set security_group skipped "missing security group id"
|
|
150
|
+
return 0
|
|
151
|
+
fi
|
|
152
|
+
if out=$(aws ec2 describe-security-groups --group-ids "$group_id" \
|
|
153
|
+
--query 'SecurityGroups[0].GroupId' --output text 2>/dev/null); then
|
|
154
|
+
case "$out" in
|
|
155
|
+
""|"None") destroy_evidence_set security_group deleted "security group $group_id is absent" ;;
|
|
156
|
+
*) destroy_evidence_set security_group still_present "security group $group_id still exists" ;;
|
|
157
|
+
esac
|
|
158
|
+
else
|
|
159
|
+
destroy_evidence_set security_group deleted "security group $group_id is absent"
|
|
160
|
+
fi
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
verify_key_pair_deleted() {
|
|
164
|
+
local key_name=$1 out
|
|
165
|
+
if [ -z "$key_name" ]; then
|
|
166
|
+
destroy_evidence_set key_pair skipped "missing key pair name"
|
|
167
|
+
return 0
|
|
168
|
+
fi
|
|
169
|
+
if out=$(aws ec2 describe-key-pairs --key-names "$key_name" \
|
|
170
|
+
--query 'KeyPairs[0].KeyName' --output text 2>/dev/null); then
|
|
171
|
+
case "$out" in
|
|
172
|
+
""|"None") destroy_evidence_set key_pair deleted "key pair $key_name is absent" ;;
|
|
173
|
+
*) destroy_evidence_set key_pair still_present "key pair $key_name still exists" ;;
|
|
174
|
+
esac
|
|
175
|
+
else
|
|
176
|
+
destroy_evidence_set key_pair deleted "key pair $key_name is absent"
|
|
177
|
+
fi
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Resolve source and load INSTANCE_ID/EIP_ID/SG_ID/KEY_NAME/KEY_FILE/REGION.
|
|
181
|
+
SRC=${1:-}
|
|
182
|
+
if [ -z "$SRC" ]; then
|
|
183
|
+
if [ -f "$P2P_WORKDIR/state.json" ]; then SRC="$P2P_WORKDIR/state.json"
|
|
184
|
+
else echo "state.json not found; set DOMAIN=<service domain> or P2P_WORKDIR=<service dir> to destroy a specific deployment."; exit 1
|
|
185
|
+
fi
|
|
186
|
+
fi
|
|
187
|
+
[ -f "$SRC" ] || { echo "$SRC not found."; exit 1; }
|
|
188
|
+
P2P_ROOT=$(cd "${DIREXIO_HOME:-$HOME/.direxio}" 2>/dev/null && pwd -P || printf '%s' "${DIREXIO_HOME:-$HOME/.direxio}")
|
|
189
|
+
|
|
190
|
+
command -v jq >/dev/null 2>&1 || { echo "jq is required to parse state.json."; exit 1; }
|
|
191
|
+
REGION=$(jq -r '.region // empty' "$SRC")
|
|
192
|
+
INSTANCE_ID=$(jq -r '.resources.instance_id // empty' "$SRC")
|
|
193
|
+
ROOT_VOLUME_ID=$(jq -r '.resources.root_volume_id // empty' "$SRC")
|
|
194
|
+
EIP_ID=$(jq -r '.resources.eip_id // empty' "$SRC")
|
|
195
|
+
SG_ID=$(jq -r '.resources.sg_id // empty' "$SRC")
|
|
196
|
+
KEY_NAME=$(jq -r '.resources.key_name // empty' "$SRC")
|
|
197
|
+
KEY_FILE=$(jq -r '.resources.key_file // empty' "$SRC")
|
|
198
|
+
DOMAIN_MODE=$(jq -r '.domain_mode // empty' "$SRC")
|
|
199
|
+
DOMAIN=$(jq -r '.domain // empty' "$SRC")
|
|
200
|
+
AS_URL=$(jq -r '.as_url // empty' "$SRC")
|
|
201
|
+
PUBLIC_IP=$(jq -r '.resources.public_ip // empty' "$SRC")
|
|
202
|
+
ROUTE53_ZONE_ID=$(jq -r '.resources.route53_zone_id // empty' "$SRC")
|
|
203
|
+
ROUTE53_ZONE_NAME=$(jq -r '.resources.route53_zone_name // empty' "$SRC")
|
|
204
|
+
ROUTE53_ZONE_CREATED_BY_DEPLOYER=$(jq -r '.resources.route53_zone_created_by_deployer // empty' "$SRC")
|
|
205
|
+
CC_CONNECT_CONFIG=$(jq -r '.cc_connect_config // empty' "$SRC")
|
|
206
|
+
CC_CONNECT_BINARY=$(jq -r '.cc_connect_binary // empty' "$SRC")
|
|
207
|
+
CC_CONNECT_RUNTIME_DIR=$(jq -r '.cc_connect_runtime_dir // empty' "$SRC")
|
|
208
|
+
AGENT_SERVICE_DIR=$(jq -r '.agent_service_dir // empty' "$SRC")
|
|
209
|
+
AGENT_SERVICE_ID=$(jq -r '.agent_service_id // empty' "$SRC")
|
|
210
|
+
|
|
211
|
+
export NO_PROXY="*"; export no_proxy="*"
|
|
212
|
+
unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy 2>/dev/null || true
|
|
213
|
+
[ -n "${REGION:-${AWS_DEFAULT_REGION:-}}" ] || {
|
|
214
|
+
echo "Region is missing. Add .region to state.json or set AWS_DEFAULT_REGION, then retry."
|
|
215
|
+
exit 1
|
|
216
|
+
}
|
|
217
|
+
export AWS_DEFAULT_REGION=${REGION:-${AWS_DEFAULT_REGION:-}}
|
|
218
|
+
|
|
219
|
+
log "source = $SRC (region=$AWS_DEFAULT_REGION)"
|
|
220
|
+
|
|
221
|
+
AWS_IDENTITY_ARN=$(aws_identity_arn)
|
|
222
|
+
if [ -z "$AWS_IDENTITY_ARN" ] || [ "$AWS_IDENTITY_ARN" = "None" ]; then
|
|
223
|
+
echo "AWS credentials are required before destroy can remove cloud resources or local state."
|
|
224
|
+
exit 1
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
find_route53_zone() {
|
|
228
|
+
local domain=$1 best_id="" best_name="" best_len=0 id name clean len
|
|
229
|
+
while IFS=$'\t' read -r id name; do
|
|
230
|
+
id=${id%$'\r'}
|
|
231
|
+
name=${name%$'\r'}
|
|
232
|
+
clean=${name%.}
|
|
233
|
+
case "$domain" in
|
|
234
|
+
"$clean"|*."$clean")
|
|
235
|
+
len=${#clean}
|
|
236
|
+
if [ "$len" -gt "$best_len" ]; then
|
|
237
|
+
best_id=${id#/hostedzone/}
|
|
238
|
+
best_name=$clean
|
|
239
|
+
best_len=$len
|
|
240
|
+
fi
|
|
241
|
+
;;
|
|
242
|
+
esac
|
|
243
|
+
done < <(aws route53 list-hosted-zones --output json 2>/dev/null | jq -r '.HostedZones[] | [.Id, .Name] | @tsv')
|
|
244
|
+
[ -n "$best_id" ] && printf '%s\t%s\n' "$best_id" "$best_name"
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
delete_route53_record() {
|
|
248
|
+
local domain=$1 public_ip=$2 zone zone_id zone_name change_file change_json change_id
|
|
249
|
+
[ -n "$domain" ] && [ -n "$public_ip" ] || return 0
|
|
250
|
+
zone_id=${ROUTE53_ZONE_ID:-}
|
|
251
|
+
zone_name=${ROUTE53_ZONE_NAME:-}
|
|
252
|
+
if [ -z "$zone_id" ]; then
|
|
253
|
+
zone=$(find_route53_zone "$domain")
|
|
254
|
+
zone_id=$(printf '%s' "$zone" | cut -f1)
|
|
255
|
+
zone_name=$(printf '%s' "$zone" | cut -f2)
|
|
256
|
+
fi
|
|
257
|
+
if [ -z "$zone_id" ]; then
|
|
258
|
+
log "Route53 hosted zone not found for $domain; leaving DNS record untouched"
|
|
259
|
+
destroy_evidence_set route53_a_record skipped "hosted zone not found for $domain"
|
|
260
|
+
return 0
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
log "deleting Route53 A record $domain -> $public_ip (zone=$zone_name) ..."
|
|
264
|
+
change_file=$(mktemp)
|
|
265
|
+
cat > "$change_file" <<EOF
|
|
266
|
+
{
|
|
267
|
+
"Comment": "p2p-matrix destroy",
|
|
268
|
+
"Changes": [
|
|
269
|
+
{
|
|
270
|
+
"Action": "DELETE",
|
|
271
|
+
"ResourceRecordSet": {
|
|
272
|
+
"Name": "$domain.",
|
|
273
|
+
"Type": "A",
|
|
274
|
+
"TTL": 60,
|
|
275
|
+
"ResourceRecords": [{ "Value": "$public_ip" }]
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
EOF
|
|
281
|
+
local change_file_aws="$change_file"
|
|
282
|
+
if command -v cygpath >/dev/null 2>&1; then
|
|
283
|
+
change_file_aws=$(cygpath -w "$change_file")
|
|
284
|
+
fi
|
|
285
|
+
change_json=$(aws route53 change-resource-record-sets \
|
|
286
|
+
--hosted-zone-id "$zone_id" \
|
|
287
|
+
--change-batch "file://$change_file_aws" \
|
|
288
|
+
--output json 2>/dev/null) \
|
|
289
|
+
|| log " (Route53 A record may already be absent or changed; check DNS manually)"
|
|
290
|
+
rm -f "$change_file"
|
|
291
|
+
if [ -n "${change_json:-}" ]; then
|
|
292
|
+
change_id=$(printf '%s\n' "$change_json" | jq -r '.ChangeInfo.Id // empty' 2>/dev/null)
|
|
293
|
+
[ -n "$change_id" ] && aws route53 wait resource-record-sets-changed --id "$change_id" 2>/dev/null || true
|
|
294
|
+
fi
|
|
295
|
+
verify_route53_a_record_deleted "$zone_id" "$domain" "$public_ip"
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
delete_route53_hosted_zone_if_owned() {
|
|
299
|
+
local zone_id=${ROUTE53_ZONE_ID:-}
|
|
300
|
+
[ -n "$zone_id" ] || return 0
|
|
301
|
+
if [ "${ROUTE53_ZONE_CREATED_BY_DEPLOYER:-}" != "true" ]; then
|
|
302
|
+
log "Route53 hosted zone $zone_id was not created by this deployer run; leaving it in place"
|
|
303
|
+
destroy_evidence_set route53_hosted_zone retained "hosted zone $zone_id was not created by this deployer run"
|
|
304
|
+
return 0
|
|
305
|
+
fi
|
|
306
|
+
log "deleting deployer-created Route53 hosted zone $zone_id ..."
|
|
307
|
+
aws route53 delete-hosted-zone --id "$zone_id" >/dev/null 2>&1 \
|
|
308
|
+
|| log " (hosted zone was not deleted; it may contain records, be already absent, or require manual review)"
|
|
309
|
+
verify_route53_hosted_zone_deleted "$zone_id"
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
normalize_local_path() {
|
|
313
|
+
local path=$1 drive rest
|
|
314
|
+
path=$(printf '%s' "$path" | sed 's#\\#/#g')
|
|
315
|
+
if command -v cygpath >/dev/null 2>&1; then
|
|
316
|
+
cygpath -m "$path" 2>/dev/null && return 0
|
|
317
|
+
fi
|
|
318
|
+
case "$path" in
|
|
319
|
+
/mnt/[A-Za-z]/*)
|
|
320
|
+
drive=${path#/mnt/}
|
|
321
|
+
drive=${drive%%/*}
|
|
322
|
+
rest=${path#/mnt/$drive/}
|
|
323
|
+
printf '%s:/%s\n' "$(printf '%s' "$drive" | tr '[:lower:]' '[:upper:]')" "$rest"
|
|
324
|
+
return 0
|
|
325
|
+
;;
|
|
326
|
+
/cygdrive/[A-Za-z]/*)
|
|
327
|
+
drive=${path#/cygdrive/}
|
|
328
|
+
drive=${drive%%/*}
|
|
329
|
+
rest=${path#/cygdrive/$drive/}
|
|
330
|
+
printf '%s:/%s\n' "$(printf '%s' "$drive" | tr '[:lower:]' '[:upper:]')" "$rest"
|
|
331
|
+
return 0
|
|
332
|
+
;;
|
|
333
|
+
/[A-Za-z]/*)
|
|
334
|
+
drive=${path#/}
|
|
335
|
+
drive=${drive%%/*}
|
|
336
|
+
rest=${path#/$drive/}
|
|
337
|
+
printf '%s:/%s\n' "$(printf '%s' "$drive" | tr '[:lower:]' '[:upper:]')" "$rest"
|
|
338
|
+
return 0
|
|
339
|
+
;;
|
|
340
|
+
esac
|
|
341
|
+
while [ "${#path}" -gt 1 ] && [ "${path%/}" != "$path" ]; do
|
|
342
|
+
case "$path" in [A-Za-z]:/) break ;; esac
|
|
343
|
+
path=${path%/}
|
|
344
|
+
done
|
|
345
|
+
printf '%s\n' "$path"
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
local_dirname() {
|
|
349
|
+
local path
|
|
350
|
+
path=$(normalize_local_path "$1")
|
|
351
|
+
case "$path" in
|
|
352
|
+
*/*) printf '%s\n' "${path%/*}" ;;
|
|
353
|
+
*) printf '.\n' ;;
|
|
354
|
+
esac
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
paths_equal() {
|
|
358
|
+
local left right
|
|
359
|
+
left=$(normalize_local_path "$1")
|
|
360
|
+
right=$(normalize_local_path "$2")
|
|
361
|
+
case "$left:$right" in
|
|
362
|
+
[A-Za-z]:/*:[A-Za-z]:/*)
|
|
363
|
+
[ "$(printf '%s' "$left" | tr '[:upper:]' '[:lower:]')" = "$(printf '%s' "$right" | tr '[:upper:]' '[:lower:]')" ]
|
|
364
|
+
;;
|
|
365
|
+
*)
|
|
366
|
+
[ "$left" = "$right" ]
|
|
367
|
+
;;
|
|
368
|
+
esac
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
current_service_dir() {
|
|
372
|
+
local recorded=$1 asurl=$2 domain=$3 config=${4:-}
|
|
373
|
+
if [ -n "$recorded" ]; then
|
|
374
|
+
printf '%s\n' "$recorded"
|
|
375
|
+
return 0
|
|
376
|
+
fi
|
|
377
|
+
if [ -n "$asurl" ] || [ -n "$domain" ]; then
|
|
378
|
+
direxio_service_dir "${asurl:-$domain}"
|
|
379
|
+
return 0
|
|
380
|
+
fi
|
|
381
|
+
if [ -n "$config" ]; then
|
|
382
|
+
local_dirname "$(local_dirname "$config")"
|
|
383
|
+
fi
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
cc_connect_stop_binary() {
|
|
387
|
+
local binary=$1 runtime_dir=$2 candidate
|
|
388
|
+
if [ -n "$runtime_dir" ]; then
|
|
389
|
+
candidate="$runtime_dir/bin/direxio-connect"
|
|
390
|
+
if [ -x "$candidate" ]; then
|
|
391
|
+
printf '%s\n' "$candidate"
|
|
392
|
+
return 0
|
|
393
|
+
fi
|
|
394
|
+
candidate="$runtime_dir/bin/direxio-connect.exe"
|
|
395
|
+
if [ -x "$candidate" ]; then
|
|
396
|
+
printf '%s\n' "$candidate"
|
|
397
|
+
return 0
|
|
398
|
+
fi
|
|
399
|
+
fi
|
|
400
|
+
if [ -n "$binary" ]; then
|
|
401
|
+
printf '%s\n' "$binary"
|
|
402
|
+
return 0
|
|
403
|
+
fi
|
|
404
|
+
printf 'direxio-connect\n'
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
cc_connect_target_work_dir() {
|
|
408
|
+
local config=$1 runtime_dir=$2 service_dir=$3
|
|
409
|
+
if [ -n "$config" ]; then
|
|
410
|
+
local_dirname "$config"
|
|
411
|
+
return 0
|
|
412
|
+
fi
|
|
413
|
+
if [ -n "$runtime_dir" ]; then
|
|
414
|
+
normalize_local_path "$runtime_dir"
|
|
415
|
+
return 0
|
|
416
|
+
fi
|
|
417
|
+
if [ -n "$service_dir" ]; then
|
|
418
|
+
normalize_local_path "$service_dir/cc-connect"
|
|
419
|
+
fi
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
cc_connect_service_name() {
|
|
423
|
+
local service_id=$1 service_dir=$2 asurl=$3 domain=$4
|
|
424
|
+
if [ -n "$service_id" ]; then
|
|
425
|
+
printf '%s\n' "$service_id"
|
|
426
|
+
return 0
|
|
427
|
+
fi
|
|
428
|
+
if [ -n "$service_dir" ]; then
|
|
429
|
+
basename "$service_dir"
|
|
430
|
+
return 0
|
|
431
|
+
fi
|
|
432
|
+
if [ -n "$asurl" ] || [ -n "$domain" ]; then
|
|
433
|
+
direxio_service_id "${asurl:-$domain}"
|
|
434
|
+
return 0
|
|
435
|
+
fi
|
|
436
|
+
printf 'cc-connect\n'
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
cc_connect_status_work_dir() {
|
|
440
|
+
local binary=$1 service_name=$2 out
|
|
441
|
+
out=$("$binary" daemon status --service-name "$service_name" 2>/dev/null) || return 1
|
|
442
|
+
printf '%s\n' "$out" | sed -nE 's/^[[:space:]]*WorkDir:[[:space:]]*//p' | head -n 1
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
stop_current_cc_connect_daemon() {
|
|
446
|
+
local config=$1 binary=$2 runtime_dir=$3 service_dir=$4 service_name=$5 target_work_dir running_work_dir stop_binary
|
|
447
|
+
target_work_dir=$(cc_connect_target_work_dir "$config" "$runtime_dir" "$service_dir")
|
|
448
|
+
if [ -z "$target_work_dir" ]; then
|
|
449
|
+
log "cc-connect service directory not recorded; skipping local daemon stop"
|
|
450
|
+
return 0
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
stop_binary=$(cc_connect_stop_binary "$binary" "$runtime_dir")
|
|
454
|
+
case "$stop_binary" in
|
|
455
|
+
*/*|[A-Za-z]:/*|[A-Za-z]:\\*) ;;
|
|
456
|
+
*)
|
|
457
|
+
if ! command -v "$stop_binary" >/dev/null 2>&1; then
|
|
458
|
+
log "cc-connect binary not found on PATH; skipping local daemon stop"
|
|
459
|
+
return 0
|
|
460
|
+
fi
|
|
461
|
+
;;
|
|
462
|
+
esac
|
|
463
|
+
|
|
464
|
+
running_work_dir=$(cc_connect_status_work_dir "$stop_binary" "$service_name")
|
|
465
|
+
if [ -z "$running_work_dir" ]; then
|
|
466
|
+
log "cc-connect daemon status has no WorkDir; skipping local daemon stop"
|
|
467
|
+
return 0
|
|
468
|
+
fi
|
|
469
|
+
|
|
470
|
+
if ! paths_equal "$target_work_dir" "$running_work_dir"; then
|
|
471
|
+
log "cc-connect daemon belongs to another service; leaving daemon running"
|
|
472
|
+
return 0
|
|
473
|
+
fi
|
|
474
|
+
|
|
475
|
+
log "stopping cc-connect daemon for current service ..."
|
|
476
|
+
if "$stop_binary" daemon stop --service-name "$service_name" >/dev/null 2>&1; then
|
|
477
|
+
log "cc-connect daemon stopped"
|
|
478
|
+
else
|
|
479
|
+
log "cc-connect daemon stop failed or service was not installed; continuing destroy"
|
|
480
|
+
fi
|
|
481
|
+
log "uninstalling cc-connect daemon for current service ..."
|
|
482
|
+
if "$stop_binary" daemon uninstall --service-name "$service_name" >/dev/null 2>&1; then
|
|
483
|
+
log "cc-connect daemon uninstalled"
|
|
484
|
+
else
|
|
485
|
+
log "cc-connect daemon uninstall failed or service was not installed; continuing destroy"
|
|
486
|
+
fi
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
cleanup_local_service_dir() {
|
|
490
|
+
local service_dir=$1 root=$2 nodes_root src_real nodes_real src_norm nodes_norm name
|
|
491
|
+
|
|
492
|
+
if [ "${P2P_KEEP_WORKDIR:-0}" = "1" ]; then
|
|
493
|
+
log "keeping local service dir because P2P_KEEP_WORKDIR=1: $service_dir"
|
|
494
|
+
return 0
|
|
495
|
+
fi
|
|
496
|
+
|
|
497
|
+
[ -n "$service_dir" ] && [ -d "$service_dir" ] || return 0
|
|
498
|
+
[ -n "$root" ] || return 0
|
|
499
|
+
|
|
500
|
+
nodes_root="$root/nodes"
|
|
501
|
+
[ -d "$nodes_root" ] || {
|
|
502
|
+
log "local service root not found; leaving $service_dir untouched"
|
|
503
|
+
return 0
|
|
504
|
+
}
|
|
505
|
+
src_real=$(cd "$service_dir" 2>/dev/null && pwd -P) || return 0
|
|
506
|
+
nodes_real=$(cd "$nodes_root" 2>/dev/null && pwd -P) || return 0
|
|
507
|
+
src_norm=$(normalize_local_path "$src_real")
|
|
508
|
+
nodes_norm=$(normalize_local_path "$nodes_real")
|
|
509
|
+
case "$src_norm" in
|
|
510
|
+
"$nodes_norm"/*) ;;
|
|
511
|
+
*)
|
|
512
|
+
log "refusing to remove local service dir outside $nodes_norm: $service_dir"
|
|
513
|
+
return 0
|
|
514
|
+
;;
|
|
515
|
+
esac
|
|
516
|
+
|
|
517
|
+
name=$(basename "$src_norm")
|
|
518
|
+
case "$name" in
|
|
519
|
+
""|"."|".."|"nodes"|"cc-connect")
|
|
520
|
+
log "refusing to remove unexpected local service dir: $service_dir"
|
|
521
|
+
return 0
|
|
522
|
+
;;
|
|
523
|
+
esac
|
|
524
|
+
|
|
525
|
+
log "removing local service dir $src_real ..."
|
|
526
|
+
rm -rf -- "$src_real"
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
# 0. Remove DNS record if ops created it through Route53 mode.
|
|
530
|
+
CURRENT_SERVICE_DIR=$(current_service_dir "$AGENT_SERVICE_DIR" "$AS_URL" "$DOMAIN" "$CC_CONNECT_CONFIG")
|
|
531
|
+
CURRENT_SERVICE_NAME=$(cc_connect_service_name "$AGENT_SERVICE_ID" "$CURRENT_SERVICE_DIR" "$AS_URL" "$DOMAIN")
|
|
532
|
+
stop_current_cc_connect_daemon "$CC_CONNECT_CONFIG" "$CC_CONNECT_BINARY" "$CC_CONNECT_RUNTIME_DIR" "$CURRENT_SERVICE_DIR" "$CURRENT_SERVICE_NAME"
|
|
533
|
+
|
|
534
|
+
if [ "${DOMAIN_MODE:-}" = "route53" ]; then
|
|
535
|
+
delete_route53_record "$DOMAIN" "$PUBLIC_IP"
|
|
536
|
+
delete_route53_hosted_zone_if_owned
|
|
537
|
+
fi
|
|
538
|
+
|
|
539
|
+
# 1. Terminate instance.
|
|
540
|
+
if [ -n "${INSTANCE_ID:-}" ]; then
|
|
541
|
+
log "terminating instance $INSTANCE_ID ..."
|
|
542
|
+
aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" >/dev/null 2>&1 || log " (instance may already be gone)"
|
|
543
|
+
aws ec2 wait instance-terminated --instance-ids "$INSTANCE_ID" 2>/dev/null || true
|
|
544
|
+
verify_ec2_instance_terminated "$INSTANCE_ID"
|
|
545
|
+
else
|
|
546
|
+
verify_ec2_instance_terminated ""
|
|
547
|
+
fi
|
|
548
|
+
verify_ebs_root_volume_deleted "${ROOT_VOLUME_ID:-}"
|
|
549
|
+
|
|
550
|
+
# 2. Release Elastic IP.
|
|
551
|
+
if [ -n "${EIP_ID:-}" ]; then
|
|
552
|
+
log "releasing Elastic IP $EIP_ID ..."
|
|
553
|
+
aws ec2 release-address --allocation-id "$EIP_ID" 2>/dev/null || log " (EIP may already be released)"
|
|
554
|
+
verify_elastic_ip_released "$EIP_ID"
|
|
555
|
+
else
|
|
556
|
+
verify_elastic_ip_released ""
|
|
557
|
+
fi
|
|
558
|
+
|
|
559
|
+
# 3. Delete security group after instance/network interfaces detach.
|
|
560
|
+
if [ -n "${SG_ID:-}" ]; then
|
|
561
|
+
log "deleting security group $SG_ID ..."
|
|
562
|
+
for i in 1 2 3 4 5; do
|
|
563
|
+
if aws ec2 delete-security-group --group-id "$SG_ID" 2>/dev/null; then break; fi
|
|
564
|
+
sleep 6
|
|
565
|
+
[ "$i" = 5 ] && log " (security group delete failed; an ENI may still be attached, delete it manually later)"
|
|
566
|
+
done
|
|
567
|
+
verify_security_group_deleted "$SG_ID"
|
|
568
|
+
else
|
|
569
|
+
verify_security_group_deleted ""
|
|
570
|
+
fi
|
|
571
|
+
|
|
572
|
+
# 4. Delete key pair and local private key.
|
|
573
|
+
if [ -n "${KEY_NAME:-}" ]; then
|
|
574
|
+
log "deleting key pair $KEY_NAME ..."
|
|
575
|
+
aws ec2 delete-key-pair --key-name "$KEY_NAME" 2>/dev/null || true
|
|
576
|
+
[ -n "${KEY_FILE:-}" ] && [ -f "$KEY_FILE" ] && rm -f "$KEY_FILE"
|
|
577
|
+
verify_key_pair_deleted "$KEY_NAME"
|
|
578
|
+
else
|
|
579
|
+
verify_key_pair_deleted ""
|
|
580
|
+
fi
|
|
581
|
+
|
|
582
|
+
log "Done. Processed resources recorded in $SRC."
|
|
583
|
+
log "User-managed DNS and domain purchases are outside automatic destroy scope; handle them manually if needed."
|
|
584
|
+
if REPORT_PATH=$(operation_report_write destroy destroy_processed "$SRC" 2>/dev/null); then
|
|
585
|
+
log "operation report written: $REPORT_PATH"
|
|
586
|
+
else
|
|
587
|
+
log "operation report was not written; keep destroy logs for audit"
|
|
588
|
+
fi
|
|
589
|
+
cleanup_local_service_dir "$CURRENT_SERVICE_DIR" "$P2P_ROOT"
|