@mriekki/lean 0.1.0 → 0.2.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/README.md +71 -67
- package/assets/components.json +33 -33
- package/assets/core/distribution.json +52 -51
- package/assets/deployment/distribution.json +34 -34
- package/assets/generated/ASSEMBLY.json +38 -33
- package/assets/generated/core/core/skills/core-sync/SKILL.md +78 -120
- package/assets/generated/core/core/skills/deploy/SKILL.md +74 -91
- package/assets/generated/core/core/skills/lean-update/SKILL.md +189 -0
- package/assets/generated/core/core/skills/upstream-fix/SKILL.md +91 -76
- package/assets/generated/deployment/distribution/lean-poc-deploy.py +8 -8
- package/assets/generated/deployment/src/lean_poc_deployment/__init__.py +3 -3
- package/assets/generated/deployment/src/lean_poc_deployment/__main__.py +3 -3
- package/assets/generated/deployment/src/lean_poc_deployment/app_cli.py +140 -140
- package/assets/generated/deployment/src/lean_poc_deployment/app_runtime.py +455 -455
- package/assets/generated/deployment/src/lean_poc_deployment/app_runtime_remote.py +415 -415
- package/assets/generated/deployment/src/lean_poc_deployment/box_platform.py +208 -208
- package/assets/generated/deployment/src/lean_poc_deployment/box_platform_assets.py +100 -100
- package/assets/generated/deployment/src/lean_poc_deployment/box_platform_remote.py +271 -271
- package/assets/generated/deployment/src/lean_poc_deployment/cli.py +437 -437
- package/assets/generated/deployment/src/lean_poc_deployment/doctor.py +121 -121
- package/assets/generated/deployment/src/lean_poc_deployment/entrypoint.py +25 -25
- package/assets/generated/deployment/src/lean_poc_deployment/errors.py +5 -5
- package/assets/generated/deployment/src/lean_poc_deployment/keys.py +171 -171
- package/assets/generated/deployment/src/lean_poc_deployment/profiles.py +204 -204
- package/assets/generated/deployment/src/lean_poc_deployment/route_registry.py +367 -367
- package/assets/generated/deployment/src/lean_poc_deployment/route_registry_remote.py +443 -443
- package/assets/generated/deployment/src/lean_poc_deployment/security.py +290 -290
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter.py +334 -334
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter_remote.py +339 -339
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_cli.py +85 -85
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_import_recovery.py +437 -437
- package/assets/generated/deployment/src/lean_poc_deployment/ssh.py +118 -118
- package/assets/generated/deployment/src/lean_poc_deployment/teardown.py +286 -286
- package/assets/generated/deployment/src/lean_poc_deployment/teardown_cli.py +120 -120
- package/assets/generated/deployment/src/lean_poc_deployment/teardown_remote.py +100 -100
- package/assets/generated/deployment/src/lean_poc_deployment/trust.py +278 -278
- package/bin/lean.mjs +9 -9
- package/package.json +33 -33
- package/src/cli.mjs +131 -131
- package/src/engine.mjs +243 -243
- package/src/package-data.mjs +18 -18
|
@@ -1,416 +1,416 @@
|
|
|
1
|
-
"""Remote Bash template for generic application runtime deployment."""
|
|
2
|
-
|
|
3
|
-
REMOTE_SCRIPT = r'''#!/usr/bin/env bash
|
|
4
|
-
set -Eeuo pipefail
|
|
5
|
-
|
|
6
|
-
ACTION="$1"
|
|
7
|
-
EXPECTED_SPEC_SHA="${2:-}"
|
|
8
|
-
DEPLOY_USER="${3:-}"
|
|
9
|
-
APP_ROOT="__APP_ROOT__"
|
|
10
|
-
APP_SCHEMA_VERSION="__APP_SCHEMA_VERSION__"
|
|
11
|
-
NETWORK_NAME="__NETWORK_NAME__"
|
|
12
|
-
CADDY_CONTAINER="__CADDY_CONTAINER__"
|
|
13
|
-
DESIRED_PRESENT="__DESIRED_PRESENT__"
|
|
14
|
-
|
|
15
|
-
D_OWNER_B64="__D_OWNER_B64__"
|
|
16
|
-
D_ENV_B64="__D_ENV_B64__"
|
|
17
|
-
D_APP_B64="__D_APP_B64__"
|
|
18
|
-
D_CONTAINER_B64="__D_CONTAINER_B64__"
|
|
19
|
-
D_IMAGE_B64="__D_IMAGE_B64__"
|
|
20
|
-
D_PORT="__D_PORT__"
|
|
21
|
-
D_HEALTH_B64="__D_HEALTH_B64__"
|
|
22
|
-
D_RUN_AS_B64="__D_RUN_AS_B64__"
|
|
23
|
-
D_PERSISTENCE="__D_PERSISTENCE__"
|
|
24
|
-
D_DATA_TARGET_B64="__D_DATA_TARGET_B64__"
|
|
25
|
-
D_SPEC_SHA="__D_SPEC_SHA__"
|
|
26
|
-
D_ENV_KEYS=(__D_ENV_KEYS__)
|
|
27
|
-
D_ENV_VALUES=(__D_ENV_VALUES__)
|
|
28
|
-
|
|
29
|
-
b64d() { printf '%s' "$1" | base64 --decode; }
|
|
30
|
-
meta_value() {
|
|
31
|
-
local path="$1" key="$2"
|
|
32
|
-
[[ -f "$path" ]] || return 1
|
|
33
|
-
sed -n "s/^${key}=//p" "$path" | head -n1
|
|
34
|
-
}
|
|
35
|
-
valid_slug() { [[ "$1" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; }
|
|
36
|
-
valid_repo() { [[ "$1" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; }
|
|
37
|
-
valid_container() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$ ]]; }
|
|
38
|
-
valid_image() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._/:+-]*@sha256:[0-9a-f]{64}$ ]]; }
|
|
39
|
-
valid_port() { [[ "$1" =~ ^[0-9]+$ ]] && (( 1 <= 10#$1 && 10#$1 <= 65535 )); }
|
|
40
|
-
valid_run_as() { [[ "$1" =~ ^[0-9]+(:[0-9]+)?$ ]]; }
|
|
41
|
-
valid_env_key() { [[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; }
|
|
42
|
-
|
|
43
|
-
platform_ready() {
|
|
44
|
-
[[ -f /srv/lean-poc-platform/platform.env ]] &&
|
|
45
|
-
docker network inspect "$NETWORK_NAME" >/dev/null 2>&1 &&
|
|
46
|
-
[[ "$(docker inspect --format '{{.State.Status}}' "$CADDY_CONTAINER" 2>/dev/null || true)" == "running" ]]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
network_has_container() {
|
|
50
|
-
local container="$1"
|
|
51
|
-
docker network inspect "$NETWORK_NAME" --format '{{range .Containers}}{{.Name}}{{"\n"}}{{end}}' 2>/dev/null |
|
|
52
|
-
grep -Fxq "$container"
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
probe_http() {
|
|
56
|
-
local container="$1" port="$2" path="$3"
|
|
57
|
-
docker exec "$CADDY_CONTAINER" \
|
|
58
|
-
wget -q -T 5 -O /dev/null "http://${container}:${port}${path}" >/dev/null 2>&1
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
wait_ready() {
|
|
62
|
-
local container="$1" port="$2" path="$3"
|
|
63
|
-
local deadline=$((SECONDS + 120)) state=""
|
|
64
|
-
while (( SECONDS < deadline )); do
|
|
65
|
-
state="$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || true)"
|
|
66
|
-
if [[ "$state" == "running" ]] && network_has_container "$container" && probe_http "$container" "$port" "$path"; then
|
|
67
|
-
return 0
|
|
68
|
-
fi
|
|
69
|
-
if [[ "$state" == "exited" || "$state" == "dead" ]]; then
|
|
70
|
-
return 1
|
|
71
|
-
fi
|
|
72
|
-
sleep 2
|
|
73
|
-
done
|
|
74
|
-
return 1
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
inventory() {
|
|
78
|
-
local count=0 state current previous release_file
|
|
79
|
-
local owner env app container image spec_sha port health persistence
|
|
80
|
-
local running connected healthy
|
|
81
|
-
if [[ -d "$APP_ROOT" ]]; then
|
|
82
|
-
while IFS= read -r state; do
|
|
83
|
-
[[ -n "$state" ]] || continue
|
|
84
|
-
owner="$(meta_value "$state" OWNER_REPO || true)"
|
|
85
|
-
env="$(meta_value "$state" ENVIRONMENT || true)"
|
|
86
|
-
app="$(meta_value "$state" APP_SLUG || true)"
|
|
87
|
-
current="$(meta_value "$state" CURRENT_RELEASE || true)"
|
|
88
|
-
previous="$(meta_value "$state" PREVIOUS_RELEASE || true)"
|
|
89
|
-
spec_sha="$(meta_value "$state" SPEC_SHA256 || true)"
|
|
90
|
-
[[ -n "$owner" && -n "$env" && -n "$app" && -n "$current" && -n "$spec_sha" ]] || {
|
|
91
|
-
echo "Managed application state is malformed: $state" >&2
|
|
92
|
-
exit 31
|
|
93
|
-
}
|
|
94
|
-
release_file="$(dirname "$state")/releases/$current/release.env"
|
|
95
|
-
[[ -f "$release_file" && ! -L "$release_file" ]] || {
|
|
96
|
-
echo "Current application release metadata is missing: $release_file" >&2
|
|
97
|
-
exit 31
|
|
98
|
-
}
|
|
99
|
-
container="$(meta_value "$release_file" CONTAINER_NAME || true)"
|
|
100
|
-
image="$(meta_value "$release_file" IMAGE || true)"
|
|
101
|
-
port="$(meta_value "$release_file" RUNTIME_PORT || true)"
|
|
102
|
-
health="$(b64d "$(meta_value "$release_file" HEALTH_PATH_B64 || true)")"
|
|
103
|
-
persistence="$(meta_value "$release_file" PERSISTENCE_MODE || true)"
|
|
104
|
-
[[ "$(meta_value "$release_file" SPEC_SHA256 || true)" == "$spec_sha" ]] || {
|
|
105
|
-
echo "Application state/release fingerprint mismatch: $state" >&2
|
|
106
|
-
exit 31
|
|
107
|
-
}
|
|
108
|
-
running=false
|
|
109
|
-
connected=false
|
|
110
|
-
healthy=false
|
|
111
|
-
[[ "$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || true)" == "running" ]] && running=true || true
|
|
112
|
-
network_has_container "$container" && connected=true || true
|
|
113
|
-
if [[ "$running" == true && "$connected" == true ]]; then
|
|
114
|
-
probe_http "$container" "$port" "$health" && healthy=true || true
|
|
115
|
-
fi
|
|
116
|
-
printf 'APP\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
117
|
-
"$owner" "$env" "$app" "$container" "$image" "$spec_sha" "$port" "$health" "$persistence" \
|
|
118
|
-
"$current" "$previous" "$running" "$connected" "$healthy"
|
|
119
|
-
count=$((count + 1))
|
|
120
|
-
done < <(find "$APP_ROOT" -mindepth 3 -maxdepth 3 -type f -name deployment.env -print 2>/dev/null | sort)
|
|
121
|
-
fi
|
|
122
|
-
printf 'SUMMARY\tappCount\t%s\n' "$count"
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
configure_registry_auth() {
|
|
126
|
-
[[ -n "$DEPLOY_USER" ]] || { echo "Deployment account is required." >&2; exit 32; }
|
|
127
|
-
[[ "$DEPLOY_USER" =~ ^[A-Za-z_][A-Za-z0-9_.-]{0,31}$ ]] || {
|
|
128
|
-
echo "Deployment account contains unsupported characters." >&2
|
|
129
|
-
exit 32
|
|
130
|
-
}
|
|
131
|
-
local home config dir_mode file_mode deploy_uid
|
|
132
|
-
home="$(getent passwd "$DEPLOY_USER" | cut -d: -f6)"
|
|
133
|
-
[[ -n "$home" && "$home" == /* ]] || { echo "Could not resolve deployment account home." >&2; exit 32; }
|
|
134
|
-
config="$home/.docker"
|
|
135
|
-
if [[ ! -e "$config/config.json" ]]; then
|
|
136
|
-
return 0
|
|
137
|
-
fi
|
|
138
|
-
[[ -d "$config" && ! -L "$config" && -f "$config/config.json" && ! -L "$config/config.json" ]] || {
|
|
139
|
-
echo "Docker credential path is not a regular protected directory/file." >&2
|
|
140
|
-
exit 32
|
|
141
|
-
}
|
|
142
|
-
dir_mode="$(stat -c '%a' "$config")"
|
|
143
|
-
file_mode="$(stat -c '%a' "$config/config.json")"
|
|
144
|
-
deploy_uid="$(id -u "$DEPLOY_USER")"
|
|
145
|
-
if (( (8#$dir_mode & 077) != 0 || (8#$file_mode & 077) != 0 )) ||
|
|
146
|
-
[[ "$(stat -c '%u' "$config")" != "$deploy_uid" || "$(stat -c '%u' "$config/config.json")" != "$deploy_uid" ]]; then
|
|
147
|
-
echo "Docker credentials must be owned by the deployment account and deny group/other access." >&2
|
|
148
|
-
exit 32
|
|
149
|
-
fi
|
|
150
|
-
export DOCKER_CONFIG="$config"
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
decode_desired() {
|
|
154
|
-
[[ "$DESIRED_PRESENT" == true ]] || { echo "Deploy action omitted desired runtime spec." >&2; exit 33; }
|
|
155
|
-
D_OWNER="$(b64d "$D_OWNER_B64")"
|
|
156
|
-
D_ENV="$(b64d "$D_ENV_B64")"
|
|
157
|
-
D_APP="$(b64d "$D_APP_B64")"
|
|
158
|
-
D_CONTAINER="$(b64d "$D_CONTAINER_B64")"
|
|
159
|
-
D_IMAGE="$(b64d "$D_IMAGE_B64")"
|
|
160
|
-
D_HEALTH="$(b64d "$D_HEALTH_B64")"
|
|
161
|
-
D_RUN_AS="$(b64d "$D_RUN_AS_B64")"
|
|
162
|
-
D_DATA_TARGET="$(b64d "$D_DATA_TARGET_B64")"
|
|
163
|
-
|
|
164
|
-
valid_repo "$D_OWNER" || { echo "Invalid owner repository." >&2; exit 33; }
|
|
165
|
-
valid_slug "$D_ENV" || { echo "Invalid environment slug." >&2; exit 33; }
|
|
166
|
-
valid_slug "$D_APP" || { echo "Invalid application slug." >&2; exit 33; }
|
|
167
|
-
valid_container "$D_CONTAINER" || { echo "Invalid container name." >&2; exit 33; }
|
|
168
|
-
valid_image "$D_IMAGE" || { echo "Image is not an immutable sha256 digest reference." >&2; exit 33; }
|
|
169
|
-
valid_port "$D_PORT" || { echo "Invalid runtime port." >&2; exit 33; }
|
|
170
|
-
[[ "$D_HEALTH" == /* && "$D_HEALTH" != *$'\n'* && "$D_HEALTH" != *$'\r'* ]] || {
|
|
171
|
-
echo "Invalid health path." >&2
|
|
172
|
-
exit 33
|
|
173
|
-
}
|
|
174
|
-
valid_run_as "$D_RUN_AS" || { echo "Invalid numeric runAs value." >&2; exit 33; }
|
|
175
|
-
[[ "$D_PERSISTENCE" == "none" || "$D_PERSISTENCE" == "directory" ]] || {
|
|
176
|
-
echo "Invalid persistence mode." >&2
|
|
177
|
-
exit 33
|
|
178
|
-
}
|
|
179
|
-
if [[ "$D_PERSISTENCE" == "directory" ]]; then
|
|
180
|
-
[[ "$D_DATA_TARGET" == /* && "$D_DATA_TARGET" != *"/../"* && "$D_DATA_TARGET" != *$'\n'* ]] || {
|
|
181
|
-
echo "Invalid persistent data container path." >&2
|
|
182
|
-
exit 33
|
|
183
|
-
}
|
|
184
|
-
elif [[ -n "$D_DATA_TARGET" ]]; then
|
|
185
|
-
echo "Non-persistent runtime unexpectedly supplied a data path." >&2
|
|
186
|
-
exit 33
|
|
187
|
-
fi
|
|
188
|
-
[[ "$D_SPEC_SHA" =~ ^[0-9a-f]{64}$ ]] || { echo "Invalid runtime spec fingerprint." >&2; exit 33; }
|
|
189
|
-
[[ "${#D_ENV_KEYS[@]}" -eq "${#D_ENV_VALUES[@]}" ]] || { echo "Environment payload mismatch." >&2; exit 33; }
|
|
190
|
-
|
|
191
|
-
local index key
|
|
192
|
-
for index in "${!D_ENV_KEYS[@]}"; do
|
|
193
|
-
key="$(b64d "${D_ENV_KEYS[$index]}")"
|
|
194
|
-
valid_env_key "$key" || { echo "Invalid environment variable name." >&2; exit 33; }
|
|
195
|
-
done
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
release_value() {
|
|
199
|
-
meta_value "$1" "$2"
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
write_release_metadata() {
|
|
203
|
-
local release_file="$1"
|
|
204
|
-
local tmp="$release_file.tmp.$$" index key_b64 value_b64
|
|
205
|
-
{
|
|
206
|
-
printf 'SCHEMA_VERSION=%s\n' "$APP_SCHEMA_VERSION"
|
|
207
|
-
printf 'OWNER_REPO=%s\n' "$D_OWNER"
|
|
208
|
-
printf 'ENVIRONMENT=%s\n' "$D_ENV"
|
|
209
|
-
printf 'APP_SLUG=%s\n' "$D_APP"
|
|
210
|
-
printf 'CONTAINER_NAME=%s\n' "$D_CONTAINER"
|
|
211
|
-
printf 'IMAGE=%s\n' "$D_IMAGE"
|
|
212
|
-
printf 'RUNTIME_PORT=%s\n' "$D_PORT"
|
|
213
|
-
printf 'HEALTH_PATH_B64=%s\n' "$D_HEALTH_B64"
|
|
214
|
-
printf 'RUN_AS=%s\n' "$D_RUN_AS"
|
|
215
|
-
printf 'PERSISTENCE_MODE=%s\n' "$D_PERSISTENCE"
|
|
216
|
-
printf 'DATA_TARGET_B64=%s\n' "$D_DATA_TARGET_B64"
|
|
217
|
-
printf 'SPEC_SHA256=%s\n' "$D_SPEC_SHA"
|
|
218
|
-
printf 'ENV_COUNT=%s\n' "${#D_ENV_KEYS[@]}"
|
|
219
|
-
for index in "${!D_ENV_KEYS[@]}"; do
|
|
220
|
-
key_b64="${D_ENV_KEYS[$index]}"
|
|
221
|
-
value_b64="${D_ENV_VALUES[$index]}"
|
|
222
|
-
printf 'ENV_KEY_B64_%s=%s\n' "$index" "$key_b64"
|
|
223
|
-
printf 'ENV_VALUE_B64_%s=%s\n' "$index" "$value_b64"
|
|
224
|
-
done
|
|
225
|
-
} > "$tmp"
|
|
226
|
-
chmod 0600 "$tmp"
|
|
227
|
-
mv -f "$tmp" "$release_file"
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
start_desired_container() {
|
|
231
|
-
local root data
|
|
232
|
-
local uid gid index key value
|
|
233
|
-
root="$1"
|
|
234
|
-
data="$root/data"
|
|
235
|
-
uid="${D_RUN_AS%%:*}"
|
|
236
|
-
if [[ "$D_RUN_AS" == *:* ]]; then gid="${D_RUN_AS#*:}"; else gid="$uid"; fi
|
|
237
|
-
|
|
238
|
-
if [[ "$D_PERSISTENCE" == "directory" ]]; then
|
|
239
|
-
if [[ -L "$data" ]]; then
|
|
240
|
-
echo "Persistent data directory must not be a symbolic link." >&2
|
|
241
|
-
return 1
|
|
242
|
-
fi
|
|
243
|
-
if [[ ! -e "$data" ]]; then
|
|
244
|
-
install -d -m 0750 -o "$uid" -g "$gid" "$data"
|
|
245
|
-
elif [[ ! -d "$data" ]]; then
|
|
246
|
-
echo "Persistent data path is not a directory." >&2
|
|
247
|
-
return 1
|
|
248
|
-
elif [[ "$(stat -c '%u' "$data")" != "$uid" || "$(stat -c '%g' "$data")" != "$gid" ]]; then
|
|
249
|
-
echo "Persistent data directory ownership does not match runAs; use the persistence adapter/import path." >&2
|
|
250
|
-
return 1
|
|
251
|
-
fi
|
|
252
|
-
fi
|
|
253
|
-
|
|
254
|
-
local -a docker_args
|
|
255
|
-
docker_args=(
|
|
256
|
-
docker run -d
|
|
257
|
-
--name "$D_CONTAINER"
|
|
258
|
-
--restart unless-stopped
|
|
259
|
-
--network "$NETWORK_NAME"
|
|
260
|
-
--user "$D_RUN_AS"
|
|
261
|
-
--log-driver json-file
|
|
262
|
-
--log-opt max-size=10m
|
|
263
|
-
--log-opt max-file=3
|
|
264
|
-
)
|
|
265
|
-
if [[ "$D_PERSISTENCE" == "directory" ]]; then
|
|
266
|
-
docker_args+=(--mount "type=bind,src=$data,dst=$D_DATA_TARGET")
|
|
267
|
-
fi
|
|
268
|
-
for index in "${!D_ENV_KEYS[@]}"; do
|
|
269
|
-
key="$(b64d "${D_ENV_KEYS[$index]}")"
|
|
270
|
-
value="$(b64d "${D_ENV_VALUES[$index]}")"
|
|
271
|
-
docker_args+=(--env "$key=$value")
|
|
272
|
-
done
|
|
273
|
-
docker_args+=("$D_IMAGE")
|
|
274
|
-
"${docker_args[@]}" >/dev/null
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
record_evidence() {
|
|
278
|
-
local root="$1" outcome="$2" release_id="$3" previous="$4"
|
|
279
|
-
local timestamp
|
|
280
|
-
timestamp="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
|
281
|
-
printf '{"timestamp":"%s","ownerRepo":"%s","environment":"%s","appSlug":"%s","container":"%s","image":"%s","specSha256":"%s","release":"%s","previousRelease":"%s","persistence":"%s","outcome":"%s"}\n' \
|
|
282
|
-
"$timestamp" "$D_OWNER" "$D_ENV" "$D_APP" "$D_CONTAINER" "$D_IMAGE" "$D_SPEC_SHA" \
|
|
283
|
-
"$release_id" "$previous" "$D_PERSISTENCE" "$outcome" >> "$root/deployment-evidence.jsonl"
|
|
284
|
-
chmod 0600 "$root/deployment-evidence.jsonl"
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
deploy() {
|
|
288
|
-
platform_ready || { echo "Kit shared platform is not ready." >&2; exit 34; }
|
|
289
|
-
decode_desired
|
|
290
|
-
configure_registry_auth
|
|
291
|
-
|
|
292
|
-
local root releases state current_release="" previous_release="" current_spec="" current_container="" next_previous=""
|
|
293
|
-
local digest release_id release_dir release_file lock_file tmp
|
|
294
|
-
root="$APP_ROOT/$D_APP/$D_ENV"
|
|
295
|
-
releases="$root/releases"
|
|
296
|
-
state="$root/deployment.env"
|
|
297
|
-
lock_file="/run/lock/lean-poc-app-$D_APP--$D_ENV.lock"
|
|
298
|
-
digest="${D_IMAGE##*@sha256:}"
|
|
299
|
-
release_id="sha256-${digest}-${D_SPEC_SHA:0:16}"
|
|
300
|
-
release_dir="$releases/$release_id"
|
|
301
|
-
release_file="$release_dir/release.env"
|
|
302
|
-
|
|
303
|
-
exec 9>>"$lock_file"
|
|
304
|
-
if ! flock -n 9; then
|
|
305
|
-
echo "Another application deployment holds the lock for $D_APP--$D_ENV." >&2
|
|
306
|
-
exit 75
|
|
307
|
-
fi
|
|
308
|
-
|
|
309
|
-
for path in "$root" "$releases" "$state" "$root/data" "$root/deployment-evidence.jsonl"; do
|
|
310
|
-
[[ ! -L "$path" ]] || { echo "Managed application path must not be a symbolic link: $path" >&2; exit 35; }
|
|
311
|
-
done
|
|
312
|
-
|
|
313
|
-
if [[ -f "$state" ]]; then
|
|
314
|
-
[[ "$(meta_value "$state" OWNER_REPO || true)" == "$D_OWNER" &&
|
|
315
|
-
"$(meta_value "$state" ENVIRONMENT || true)" == "$D_ENV" &&
|
|
316
|
-
"$(meta_value "$state" APP_SLUG || true)" == "$D_APP" ]] || {
|
|
317
|
-
echo "Existing application state has different ownership." >&2
|
|
318
|
-
exit 35
|
|
319
|
-
}
|
|
320
|
-
current_release="$(meta_value "$state" CURRENT_RELEASE || true)"
|
|
321
|
-
previous_release="$(meta_value "$state" PREVIOUS_RELEASE || true)"
|
|
322
|
-
current_spec="$(meta_value "$state" SPEC_SHA256 || true)"
|
|
323
|
-
[[ -n "$current_release" && "$current_spec" =~ ^[0-9a-f]{64}$ ]] || {
|
|
324
|
-
echo "Existing application state is malformed." >&2
|
|
325
|
-
exit 35
|
|
326
|
-
}
|
|
327
|
-
[[ "$EXPECTED_SPEC_SHA" != "NONE" && "$current_spec" == "$EXPECTED_SPEC_SHA" ]] || {
|
|
328
|
-
echo "Application state changed after planning; create a fresh plan." >&2
|
|
329
|
-
exit 36
|
|
330
|
-
}
|
|
331
|
-
local current_file="$releases/$current_release/release.env"
|
|
332
|
-
[[ -f "$current_file" && ! -L "$current_file" ]] || {
|
|
333
|
-
echo "Current release metadata is missing." >&2
|
|
334
|
-
exit 35
|
|
335
|
-
}
|
|
336
|
-
current_container="$(release_value "$current_file" CONTAINER_NAME || true)"
|
|
337
|
-
valid_container "$current_container" || { echo "Current release container metadata is malformed." >&2; exit 35; }
|
|
338
|
-
if [[ "$current_spec" == "$D_SPEC_SHA" ]]; then
|
|
339
|
-
next_previous="$previous_release"
|
|
340
|
-
else
|
|
341
|
-
next_previous="$current_release"
|
|
342
|
-
fi
|
|
343
|
-
else
|
|
344
|
-
[[ "$EXPECTED_SPEC_SHA" == "NONE" ]] || {
|
|
345
|
-
echo "Application state disappeared after planning; create a fresh plan." >&2
|
|
346
|
-
exit 36
|
|
347
|
-
}
|
|
348
|
-
fi
|
|
349
|
-
|
|
350
|
-
if docker inspect "$D_CONTAINER" >/dev/null 2>&1 && [[ "$D_CONTAINER" != "$current_container" ]]; then
|
|
351
|
-
echo "Desired container name is already occupied outside this managed runtime." >&2
|
|
352
|
-
exit 37
|
|
353
|
-
fi
|
|
354
|
-
|
|
355
|
-
install -d -m 0750 "$APP_ROOT" "$APP_ROOT/$D_APP" "$root" "$releases"
|
|
356
|
-
if [[ -e "$release_dir" && ! -d "$release_dir" ]]; then
|
|
357
|
-
echo "Release path exists and is not a directory." >&2
|
|
358
|
-
exit 35
|
|
359
|
-
fi
|
|
360
|
-
install -d -m 0750 "$release_dir"
|
|
361
|
-
if [[ -f "$release_file" ]]; then
|
|
362
|
-
[[ "$(release_value "$release_file" SPEC_SHA256 || true)" == "$D_SPEC_SHA" ]] || {
|
|
363
|
-
echo "Release directory collision with different runtime metadata." >&2
|
|
364
|
-
exit 35
|
|
365
|
-
}
|
|
366
|
-
else
|
|
367
|
-
write_release_metadata "$release_file"
|
|
368
|
-
fi
|
|
369
|
-
|
|
370
|
-
docker pull "$D_IMAGE" >/dev/null
|
|
371
|
-
|
|
372
|
-
if [[ -n "$current_container" ]]; then
|
|
373
|
-
docker rm -f "$current_container" >/dev/null
|
|
374
|
-
fi
|
|
375
|
-
|
|
376
|
-
if ! start_desired_container "$root"; then
|
|
377
|
-
docker rm -f "$D_CONTAINER" >/dev/null 2>&1 || true
|
|
378
|
-
record_evidence "$root" "failed-start" "$release_id" "$current_release"
|
|
379
|
-
echo "Application container failed to start; previous release metadata was retained but runtime rollback was not attempted." >&2
|
|
380
|
-
exit 38
|
|
381
|
-
fi
|
|
382
|
-
if ! wait_ready "$D_CONTAINER" "$D_PORT" "$D_HEALTH"; then
|
|
383
|
-
docker rm -f "$D_CONTAINER" >/dev/null 2>&1 || true
|
|
384
|
-
record_evidence "$root" "failed-readiness" "$release_id" "$current_release"
|
|
385
|
-
echo "Application failed readiness; previous release metadata was retained but runtime rollback was not attempted." >&2
|
|
386
|
-
exit 39
|
|
387
|
-
fi
|
|
388
|
-
|
|
389
|
-
tmp="$state.tmp.$$"
|
|
390
|
-
{
|
|
391
|
-
printf 'SCHEMA_VERSION=%s\n' "$APP_SCHEMA_VERSION"
|
|
392
|
-
printf 'OWNER_REPO=%s\n' "$D_OWNER"
|
|
393
|
-
printf 'ENVIRONMENT=%s\n' "$D_ENV"
|
|
394
|
-
printf 'APP_SLUG=%s\n' "$D_APP"
|
|
395
|
-
printf 'CURRENT_RELEASE=%s\n' "$release_id"
|
|
396
|
-
printf 'PREVIOUS_RELEASE=%s\n' "$next_previous"
|
|
397
|
-
printf 'SPEC_SHA256=%s\n' "$D_SPEC_SHA"
|
|
398
|
-
} > "$tmp"
|
|
399
|
-
chmod 0600 "$tmp"
|
|
400
|
-
mv -f "$tmp" "$state"
|
|
401
|
-
record_evidence "$root" "deployed" "$release_id" "$next_previous"
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
case "$ACTION" in
|
|
405
|
-
inventory)
|
|
406
|
-
inventory
|
|
407
|
-
;;
|
|
408
|
-
deploy)
|
|
409
|
-
deploy
|
|
410
|
-
;;
|
|
411
|
-
*)
|
|
412
|
-
echo "Unsupported application-runtime action: $ACTION" >&2
|
|
413
|
-
exit 2
|
|
414
|
-
;;
|
|
415
|
-
esac
|
|
1
|
+
"""Remote Bash template for generic application runtime deployment."""
|
|
2
|
+
|
|
3
|
+
REMOTE_SCRIPT = r'''#!/usr/bin/env bash
|
|
4
|
+
set -Eeuo pipefail
|
|
5
|
+
|
|
6
|
+
ACTION="$1"
|
|
7
|
+
EXPECTED_SPEC_SHA="${2:-}"
|
|
8
|
+
DEPLOY_USER="${3:-}"
|
|
9
|
+
APP_ROOT="__APP_ROOT__"
|
|
10
|
+
APP_SCHEMA_VERSION="__APP_SCHEMA_VERSION__"
|
|
11
|
+
NETWORK_NAME="__NETWORK_NAME__"
|
|
12
|
+
CADDY_CONTAINER="__CADDY_CONTAINER__"
|
|
13
|
+
DESIRED_PRESENT="__DESIRED_PRESENT__"
|
|
14
|
+
|
|
15
|
+
D_OWNER_B64="__D_OWNER_B64__"
|
|
16
|
+
D_ENV_B64="__D_ENV_B64__"
|
|
17
|
+
D_APP_B64="__D_APP_B64__"
|
|
18
|
+
D_CONTAINER_B64="__D_CONTAINER_B64__"
|
|
19
|
+
D_IMAGE_B64="__D_IMAGE_B64__"
|
|
20
|
+
D_PORT="__D_PORT__"
|
|
21
|
+
D_HEALTH_B64="__D_HEALTH_B64__"
|
|
22
|
+
D_RUN_AS_B64="__D_RUN_AS_B64__"
|
|
23
|
+
D_PERSISTENCE="__D_PERSISTENCE__"
|
|
24
|
+
D_DATA_TARGET_B64="__D_DATA_TARGET_B64__"
|
|
25
|
+
D_SPEC_SHA="__D_SPEC_SHA__"
|
|
26
|
+
D_ENV_KEYS=(__D_ENV_KEYS__)
|
|
27
|
+
D_ENV_VALUES=(__D_ENV_VALUES__)
|
|
28
|
+
|
|
29
|
+
b64d() { printf '%s' "$1" | base64 --decode; }
|
|
30
|
+
meta_value() {
|
|
31
|
+
local path="$1" key="$2"
|
|
32
|
+
[[ -f "$path" ]] || return 1
|
|
33
|
+
sed -n "s/^${key}=//p" "$path" | head -n1
|
|
34
|
+
}
|
|
35
|
+
valid_slug() { [[ "$1" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; }
|
|
36
|
+
valid_repo() { [[ "$1" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; }
|
|
37
|
+
valid_container() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$ ]]; }
|
|
38
|
+
valid_image() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._/:+-]*@sha256:[0-9a-f]{64}$ ]]; }
|
|
39
|
+
valid_port() { [[ "$1" =~ ^[0-9]+$ ]] && (( 1 <= 10#$1 && 10#$1 <= 65535 )); }
|
|
40
|
+
valid_run_as() { [[ "$1" =~ ^[0-9]+(:[0-9]+)?$ ]]; }
|
|
41
|
+
valid_env_key() { [[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; }
|
|
42
|
+
|
|
43
|
+
platform_ready() {
|
|
44
|
+
[[ -f /srv/lean-poc-platform/platform.env ]] &&
|
|
45
|
+
docker network inspect "$NETWORK_NAME" >/dev/null 2>&1 &&
|
|
46
|
+
[[ "$(docker inspect --format '{{.State.Status}}' "$CADDY_CONTAINER" 2>/dev/null || true)" == "running" ]]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
network_has_container() {
|
|
50
|
+
local container="$1"
|
|
51
|
+
docker network inspect "$NETWORK_NAME" --format '{{range .Containers}}{{.Name}}{{"\n"}}{{end}}' 2>/dev/null |
|
|
52
|
+
grep -Fxq "$container"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
probe_http() {
|
|
56
|
+
local container="$1" port="$2" path="$3"
|
|
57
|
+
docker exec "$CADDY_CONTAINER" \
|
|
58
|
+
wget -q -T 5 -O /dev/null "http://${container}:${port}${path}" >/dev/null 2>&1
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
wait_ready() {
|
|
62
|
+
local container="$1" port="$2" path="$3"
|
|
63
|
+
local deadline=$((SECONDS + 120)) state=""
|
|
64
|
+
while (( SECONDS < deadline )); do
|
|
65
|
+
state="$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || true)"
|
|
66
|
+
if [[ "$state" == "running" ]] && network_has_container "$container" && probe_http "$container" "$port" "$path"; then
|
|
67
|
+
return 0
|
|
68
|
+
fi
|
|
69
|
+
if [[ "$state" == "exited" || "$state" == "dead" ]]; then
|
|
70
|
+
return 1
|
|
71
|
+
fi
|
|
72
|
+
sleep 2
|
|
73
|
+
done
|
|
74
|
+
return 1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
inventory() {
|
|
78
|
+
local count=0 state current previous release_file
|
|
79
|
+
local owner env app container image spec_sha port health persistence
|
|
80
|
+
local running connected healthy
|
|
81
|
+
if [[ -d "$APP_ROOT" ]]; then
|
|
82
|
+
while IFS= read -r state; do
|
|
83
|
+
[[ -n "$state" ]] || continue
|
|
84
|
+
owner="$(meta_value "$state" OWNER_REPO || true)"
|
|
85
|
+
env="$(meta_value "$state" ENVIRONMENT || true)"
|
|
86
|
+
app="$(meta_value "$state" APP_SLUG || true)"
|
|
87
|
+
current="$(meta_value "$state" CURRENT_RELEASE || true)"
|
|
88
|
+
previous="$(meta_value "$state" PREVIOUS_RELEASE || true)"
|
|
89
|
+
spec_sha="$(meta_value "$state" SPEC_SHA256 || true)"
|
|
90
|
+
[[ -n "$owner" && -n "$env" && -n "$app" && -n "$current" && -n "$spec_sha" ]] || {
|
|
91
|
+
echo "Managed application state is malformed: $state" >&2
|
|
92
|
+
exit 31
|
|
93
|
+
}
|
|
94
|
+
release_file="$(dirname "$state")/releases/$current/release.env"
|
|
95
|
+
[[ -f "$release_file" && ! -L "$release_file" ]] || {
|
|
96
|
+
echo "Current application release metadata is missing: $release_file" >&2
|
|
97
|
+
exit 31
|
|
98
|
+
}
|
|
99
|
+
container="$(meta_value "$release_file" CONTAINER_NAME || true)"
|
|
100
|
+
image="$(meta_value "$release_file" IMAGE || true)"
|
|
101
|
+
port="$(meta_value "$release_file" RUNTIME_PORT || true)"
|
|
102
|
+
health="$(b64d "$(meta_value "$release_file" HEALTH_PATH_B64 || true)")"
|
|
103
|
+
persistence="$(meta_value "$release_file" PERSISTENCE_MODE || true)"
|
|
104
|
+
[[ "$(meta_value "$release_file" SPEC_SHA256 || true)" == "$spec_sha" ]] || {
|
|
105
|
+
echo "Application state/release fingerprint mismatch: $state" >&2
|
|
106
|
+
exit 31
|
|
107
|
+
}
|
|
108
|
+
running=false
|
|
109
|
+
connected=false
|
|
110
|
+
healthy=false
|
|
111
|
+
[[ "$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || true)" == "running" ]] && running=true || true
|
|
112
|
+
network_has_container "$container" && connected=true || true
|
|
113
|
+
if [[ "$running" == true && "$connected" == true ]]; then
|
|
114
|
+
probe_http "$container" "$port" "$health" && healthy=true || true
|
|
115
|
+
fi
|
|
116
|
+
printf 'APP\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
117
|
+
"$owner" "$env" "$app" "$container" "$image" "$spec_sha" "$port" "$health" "$persistence" \
|
|
118
|
+
"$current" "$previous" "$running" "$connected" "$healthy"
|
|
119
|
+
count=$((count + 1))
|
|
120
|
+
done < <(find "$APP_ROOT" -mindepth 3 -maxdepth 3 -type f -name deployment.env -print 2>/dev/null | sort)
|
|
121
|
+
fi
|
|
122
|
+
printf 'SUMMARY\tappCount\t%s\n' "$count"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
configure_registry_auth() {
|
|
126
|
+
[[ -n "$DEPLOY_USER" ]] || { echo "Deployment account is required." >&2; exit 32; }
|
|
127
|
+
[[ "$DEPLOY_USER" =~ ^[A-Za-z_][A-Za-z0-9_.-]{0,31}$ ]] || {
|
|
128
|
+
echo "Deployment account contains unsupported characters." >&2
|
|
129
|
+
exit 32
|
|
130
|
+
}
|
|
131
|
+
local home config dir_mode file_mode deploy_uid
|
|
132
|
+
home="$(getent passwd "$DEPLOY_USER" | cut -d: -f6)"
|
|
133
|
+
[[ -n "$home" && "$home" == /* ]] || { echo "Could not resolve deployment account home." >&2; exit 32; }
|
|
134
|
+
config="$home/.docker"
|
|
135
|
+
if [[ ! -e "$config/config.json" ]]; then
|
|
136
|
+
return 0
|
|
137
|
+
fi
|
|
138
|
+
[[ -d "$config" && ! -L "$config" && -f "$config/config.json" && ! -L "$config/config.json" ]] || {
|
|
139
|
+
echo "Docker credential path is not a regular protected directory/file." >&2
|
|
140
|
+
exit 32
|
|
141
|
+
}
|
|
142
|
+
dir_mode="$(stat -c '%a' "$config")"
|
|
143
|
+
file_mode="$(stat -c '%a' "$config/config.json")"
|
|
144
|
+
deploy_uid="$(id -u "$DEPLOY_USER")"
|
|
145
|
+
if (( (8#$dir_mode & 077) != 0 || (8#$file_mode & 077) != 0 )) ||
|
|
146
|
+
[[ "$(stat -c '%u' "$config")" != "$deploy_uid" || "$(stat -c '%u' "$config/config.json")" != "$deploy_uid" ]]; then
|
|
147
|
+
echo "Docker credentials must be owned by the deployment account and deny group/other access." >&2
|
|
148
|
+
exit 32
|
|
149
|
+
fi
|
|
150
|
+
export DOCKER_CONFIG="$config"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
decode_desired() {
|
|
154
|
+
[[ "$DESIRED_PRESENT" == true ]] || { echo "Deploy action omitted desired runtime spec." >&2; exit 33; }
|
|
155
|
+
D_OWNER="$(b64d "$D_OWNER_B64")"
|
|
156
|
+
D_ENV="$(b64d "$D_ENV_B64")"
|
|
157
|
+
D_APP="$(b64d "$D_APP_B64")"
|
|
158
|
+
D_CONTAINER="$(b64d "$D_CONTAINER_B64")"
|
|
159
|
+
D_IMAGE="$(b64d "$D_IMAGE_B64")"
|
|
160
|
+
D_HEALTH="$(b64d "$D_HEALTH_B64")"
|
|
161
|
+
D_RUN_AS="$(b64d "$D_RUN_AS_B64")"
|
|
162
|
+
D_DATA_TARGET="$(b64d "$D_DATA_TARGET_B64")"
|
|
163
|
+
|
|
164
|
+
valid_repo "$D_OWNER" || { echo "Invalid owner repository." >&2; exit 33; }
|
|
165
|
+
valid_slug "$D_ENV" || { echo "Invalid environment slug." >&2; exit 33; }
|
|
166
|
+
valid_slug "$D_APP" || { echo "Invalid application slug." >&2; exit 33; }
|
|
167
|
+
valid_container "$D_CONTAINER" || { echo "Invalid container name." >&2; exit 33; }
|
|
168
|
+
valid_image "$D_IMAGE" || { echo "Image is not an immutable sha256 digest reference." >&2; exit 33; }
|
|
169
|
+
valid_port "$D_PORT" || { echo "Invalid runtime port." >&2; exit 33; }
|
|
170
|
+
[[ "$D_HEALTH" == /* && "$D_HEALTH" != *$'\n'* && "$D_HEALTH" != *$'\r'* ]] || {
|
|
171
|
+
echo "Invalid health path." >&2
|
|
172
|
+
exit 33
|
|
173
|
+
}
|
|
174
|
+
valid_run_as "$D_RUN_AS" || { echo "Invalid numeric runAs value." >&2; exit 33; }
|
|
175
|
+
[[ "$D_PERSISTENCE" == "none" || "$D_PERSISTENCE" == "directory" ]] || {
|
|
176
|
+
echo "Invalid persistence mode." >&2
|
|
177
|
+
exit 33
|
|
178
|
+
}
|
|
179
|
+
if [[ "$D_PERSISTENCE" == "directory" ]]; then
|
|
180
|
+
[[ "$D_DATA_TARGET" == /* && "$D_DATA_TARGET" != *"/../"* && "$D_DATA_TARGET" != *$'\n'* ]] || {
|
|
181
|
+
echo "Invalid persistent data container path." >&2
|
|
182
|
+
exit 33
|
|
183
|
+
}
|
|
184
|
+
elif [[ -n "$D_DATA_TARGET" ]]; then
|
|
185
|
+
echo "Non-persistent runtime unexpectedly supplied a data path." >&2
|
|
186
|
+
exit 33
|
|
187
|
+
fi
|
|
188
|
+
[[ "$D_SPEC_SHA" =~ ^[0-9a-f]{64}$ ]] || { echo "Invalid runtime spec fingerprint." >&2; exit 33; }
|
|
189
|
+
[[ "${#D_ENV_KEYS[@]}" -eq "${#D_ENV_VALUES[@]}" ]] || { echo "Environment payload mismatch." >&2; exit 33; }
|
|
190
|
+
|
|
191
|
+
local index key
|
|
192
|
+
for index in "${!D_ENV_KEYS[@]}"; do
|
|
193
|
+
key="$(b64d "${D_ENV_KEYS[$index]}")"
|
|
194
|
+
valid_env_key "$key" || { echo "Invalid environment variable name." >&2; exit 33; }
|
|
195
|
+
done
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
release_value() {
|
|
199
|
+
meta_value "$1" "$2"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
write_release_metadata() {
|
|
203
|
+
local release_file="$1"
|
|
204
|
+
local tmp="$release_file.tmp.$$" index key_b64 value_b64
|
|
205
|
+
{
|
|
206
|
+
printf 'SCHEMA_VERSION=%s\n' "$APP_SCHEMA_VERSION"
|
|
207
|
+
printf 'OWNER_REPO=%s\n' "$D_OWNER"
|
|
208
|
+
printf 'ENVIRONMENT=%s\n' "$D_ENV"
|
|
209
|
+
printf 'APP_SLUG=%s\n' "$D_APP"
|
|
210
|
+
printf 'CONTAINER_NAME=%s\n' "$D_CONTAINER"
|
|
211
|
+
printf 'IMAGE=%s\n' "$D_IMAGE"
|
|
212
|
+
printf 'RUNTIME_PORT=%s\n' "$D_PORT"
|
|
213
|
+
printf 'HEALTH_PATH_B64=%s\n' "$D_HEALTH_B64"
|
|
214
|
+
printf 'RUN_AS=%s\n' "$D_RUN_AS"
|
|
215
|
+
printf 'PERSISTENCE_MODE=%s\n' "$D_PERSISTENCE"
|
|
216
|
+
printf 'DATA_TARGET_B64=%s\n' "$D_DATA_TARGET_B64"
|
|
217
|
+
printf 'SPEC_SHA256=%s\n' "$D_SPEC_SHA"
|
|
218
|
+
printf 'ENV_COUNT=%s\n' "${#D_ENV_KEYS[@]}"
|
|
219
|
+
for index in "${!D_ENV_KEYS[@]}"; do
|
|
220
|
+
key_b64="${D_ENV_KEYS[$index]}"
|
|
221
|
+
value_b64="${D_ENV_VALUES[$index]}"
|
|
222
|
+
printf 'ENV_KEY_B64_%s=%s\n' "$index" "$key_b64"
|
|
223
|
+
printf 'ENV_VALUE_B64_%s=%s\n' "$index" "$value_b64"
|
|
224
|
+
done
|
|
225
|
+
} > "$tmp"
|
|
226
|
+
chmod 0600 "$tmp"
|
|
227
|
+
mv -f "$tmp" "$release_file"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
start_desired_container() {
|
|
231
|
+
local root data
|
|
232
|
+
local uid gid index key value
|
|
233
|
+
root="$1"
|
|
234
|
+
data="$root/data"
|
|
235
|
+
uid="${D_RUN_AS%%:*}"
|
|
236
|
+
if [[ "$D_RUN_AS" == *:* ]]; then gid="${D_RUN_AS#*:}"; else gid="$uid"; fi
|
|
237
|
+
|
|
238
|
+
if [[ "$D_PERSISTENCE" == "directory" ]]; then
|
|
239
|
+
if [[ -L "$data" ]]; then
|
|
240
|
+
echo "Persistent data directory must not be a symbolic link." >&2
|
|
241
|
+
return 1
|
|
242
|
+
fi
|
|
243
|
+
if [[ ! -e "$data" ]]; then
|
|
244
|
+
install -d -m 0750 -o "$uid" -g "$gid" "$data"
|
|
245
|
+
elif [[ ! -d "$data" ]]; then
|
|
246
|
+
echo "Persistent data path is not a directory." >&2
|
|
247
|
+
return 1
|
|
248
|
+
elif [[ "$(stat -c '%u' "$data")" != "$uid" || "$(stat -c '%g' "$data")" != "$gid" ]]; then
|
|
249
|
+
echo "Persistent data directory ownership does not match runAs; use the persistence adapter/import path." >&2
|
|
250
|
+
return 1
|
|
251
|
+
fi
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
local -a docker_args
|
|
255
|
+
docker_args=(
|
|
256
|
+
docker run -d
|
|
257
|
+
--name "$D_CONTAINER"
|
|
258
|
+
--restart unless-stopped
|
|
259
|
+
--network "$NETWORK_NAME"
|
|
260
|
+
--user "$D_RUN_AS"
|
|
261
|
+
--log-driver json-file
|
|
262
|
+
--log-opt max-size=10m
|
|
263
|
+
--log-opt max-file=3
|
|
264
|
+
)
|
|
265
|
+
if [[ "$D_PERSISTENCE" == "directory" ]]; then
|
|
266
|
+
docker_args+=(--mount "type=bind,src=$data,dst=$D_DATA_TARGET")
|
|
267
|
+
fi
|
|
268
|
+
for index in "${!D_ENV_KEYS[@]}"; do
|
|
269
|
+
key="$(b64d "${D_ENV_KEYS[$index]}")"
|
|
270
|
+
value="$(b64d "${D_ENV_VALUES[$index]}")"
|
|
271
|
+
docker_args+=(--env "$key=$value")
|
|
272
|
+
done
|
|
273
|
+
docker_args+=("$D_IMAGE")
|
|
274
|
+
"${docker_args[@]}" >/dev/null
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
record_evidence() {
|
|
278
|
+
local root="$1" outcome="$2" release_id="$3" previous="$4"
|
|
279
|
+
local timestamp
|
|
280
|
+
timestamp="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
|
281
|
+
printf '{"timestamp":"%s","ownerRepo":"%s","environment":"%s","appSlug":"%s","container":"%s","image":"%s","specSha256":"%s","release":"%s","previousRelease":"%s","persistence":"%s","outcome":"%s"}\n' \
|
|
282
|
+
"$timestamp" "$D_OWNER" "$D_ENV" "$D_APP" "$D_CONTAINER" "$D_IMAGE" "$D_SPEC_SHA" \
|
|
283
|
+
"$release_id" "$previous" "$D_PERSISTENCE" "$outcome" >> "$root/deployment-evidence.jsonl"
|
|
284
|
+
chmod 0600 "$root/deployment-evidence.jsonl"
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
deploy() {
|
|
288
|
+
platform_ready || { echo "Kit shared platform is not ready." >&2; exit 34; }
|
|
289
|
+
decode_desired
|
|
290
|
+
configure_registry_auth
|
|
291
|
+
|
|
292
|
+
local root releases state current_release="" previous_release="" current_spec="" current_container="" next_previous=""
|
|
293
|
+
local digest release_id release_dir release_file lock_file tmp
|
|
294
|
+
root="$APP_ROOT/$D_APP/$D_ENV"
|
|
295
|
+
releases="$root/releases"
|
|
296
|
+
state="$root/deployment.env"
|
|
297
|
+
lock_file="/run/lock/lean-poc-app-$D_APP--$D_ENV.lock"
|
|
298
|
+
digest="${D_IMAGE##*@sha256:}"
|
|
299
|
+
release_id="sha256-${digest}-${D_SPEC_SHA:0:16}"
|
|
300
|
+
release_dir="$releases/$release_id"
|
|
301
|
+
release_file="$release_dir/release.env"
|
|
302
|
+
|
|
303
|
+
exec 9>>"$lock_file"
|
|
304
|
+
if ! flock -n 9; then
|
|
305
|
+
echo "Another application deployment holds the lock for $D_APP--$D_ENV." >&2
|
|
306
|
+
exit 75
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
for path in "$root" "$releases" "$state" "$root/data" "$root/deployment-evidence.jsonl"; do
|
|
310
|
+
[[ ! -L "$path" ]] || { echo "Managed application path must not be a symbolic link: $path" >&2; exit 35; }
|
|
311
|
+
done
|
|
312
|
+
|
|
313
|
+
if [[ -f "$state" ]]; then
|
|
314
|
+
[[ "$(meta_value "$state" OWNER_REPO || true)" == "$D_OWNER" &&
|
|
315
|
+
"$(meta_value "$state" ENVIRONMENT || true)" == "$D_ENV" &&
|
|
316
|
+
"$(meta_value "$state" APP_SLUG || true)" == "$D_APP" ]] || {
|
|
317
|
+
echo "Existing application state has different ownership." >&2
|
|
318
|
+
exit 35
|
|
319
|
+
}
|
|
320
|
+
current_release="$(meta_value "$state" CURRENT_RELEASE || true)"
|
|
321
|
+
previous_release="$(meta_value "$state" PREVIOUS_RELEASE || true)"
|
|
322
|
+
current_spec="$(meta_value "$state" SPEC_SHA256 || true)"
|
|
323
|
+
[[ -n "$current_release" && "$current_spec" =~ ^[0-9a-f]{64}$ ]] || {
|
|
324
|
+
echo "Existing application state is malformed." >&2
|
|
325
|
+
exit 35
|
|
326
|
+
}
|
|
327
|
+
[[ "$EXPECTED_SPEC_SHA" != "NONE" && "$current_spec" == "$EXPECTED_SPEC_SHA" ]] || {
|
|
328
|
+
echo "Application state changed after planning; create a fresh plan." >&2
|
|
329
|
+
exit 36
|
|
330
|
+
}
|
|
331
|
+
local current_file="$releases/$current_release/release.env"
|
|
332
|
+
[[ -f "$current_file" && ! -L "$current_file" ]] || {
|
|
333
|
+
echo "Current release metadata is missing." >&2
|
|
334
|
+
exit 35
|
|
335
|
+
}
|
|
336
|
+
current_container="$(release_value "$current_file" CONTAINER_NAME || true)"
|
|
337
|
+
valid_container "$current_container" || { echo "Current release container metadata is malformed." >&2; exit 35; }
|
|
338
|
+
if [[ "$current_spec" == "$D_SPEC_SHA" ]]; then
|
|
339
|
+
next_previous="$previous_release"
|
|
340
|
+
else
|
|
341
|
+
next_previous="$current_release"
|
|
342
|
+
fi
|
|
343
|
+
else
|
|
344
|
+
[[ "$EXPECTED_SPEC_SHA" == "NONE" ]] || {
|
|
345
|
+
echo "Application state disappeared after planning; create a fresh plan." >&2
|
|
346
|
+
exit 36
|
|
347
|
+
}
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
if docker inspect "$D_CONTAINER" >/dev/null 2>&1 && [[ "$D_CONTAINER" != "$current_container" ]]; then
|
|
351
|
+
echo "Desired container name is already occupied outside this managed runtime." >&2
|
|
352
|
+
exit 37
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
install -d -m 0750 "$APP_ROOT" "$APP_ROOT/$D_APP" "$root" "$releases"
|
|
356
|
+
if [[ -e "$release_dir" && ! -d "$release_dir" ]]; then
|
|
357
|
+
echo "Release path exists and is not a directory." >&2
|
|
358
|
+
exit 35
|
|
359
|
+
fi
|
|
360
|
+
install -d -m 0750 "$release_dir"
|
|
361
|
+
if [[ -f "$release_file" ]]; then
|
|
362
|
+
[[ "$(release_value "$release_file" SPEC_SHA256 || true)" == "$D_SPEC_SHA" ]] || {
|
|
363
|
+
echo "Release directory collision with different runtime metadata." >&2
|
|
364
|
+
exit 35
|
|
365
|
+
}
|
|
366
|
+
else
|
|
367
|
+
write_release_metadata "$release_file"
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
docker pull "$D_IMAGE" >/dev/null
|
|
371
|
+
|
|
372
|
+
if [[ -n "$current_container" ]]; then
|
|
373
|
+
docker rm -f "$current_container" >/dev/null
|
|
374
|
+
fi
|
|
375
|
+
|
|
376
|
+
if ! start_desired_container "$root"; then
|
|
377
|
+
docker rm -f "$D_CONTAINER" >/dev/null 2>&1 || true
|
|
378
|
+
record_evidence "$root" "failed-start" "$release_id" "$current_release"
|
|
379
|
+
echo "Application container failed to start; previous release metadata was retained but runtime rollback was not attempted." >&2
|
|
380
|
+
exit 38
|
|
381
|
+
fi
|
|
382
|
+
if ! wait_ready "$D_CONTAINER" "$D_PORT" "$D_HEALTH"; then
|
|
383
|
+
docker rm -f "$D_CONTAINER" >/dev/null 2>&1 || true
|
|
384
|
+
record_evidence "$root" "failed-readiness" "$release_id" "$current_release"
|
|
385
|
+
echo "Application failed readiness; previous release metadata was retained but runtime rollback was not attempted." >&2
|
|
386
|
+
exit 39
|
|
387
|
+
fi
|
|
388
|
+
|
|
389
|
+
tmp="$state.tmp.$$"
|
|
390
|
+
{
|
|
391
|
+
printf 'SCHEMA_VERSION=%s\n' "$APP_SCHEMA_VERSION"
|
|
392
|
+
printf 'OWNER_REPO=%s\n' "$D_OWNER"
|
|
393
|
+
printf 'ENVIRONMENT=%s\n' "$D_ENV"
|
|
394
|
+
printf 'APP_SLUG=%s\n' "$D_APP"
|
|
395
|
+
printf 'CURRENT_RELEASE=%s\n' "$release_id"
|
|
396
|
+
printf 'PREVIOUS_RELEASE=%s\n' "$next_previous"
|
|
397
|
+
printf 'SPEC_SHA256=%s\n' "$D_SPEC_SHA"
|
|
398
|
+
} > "$tmp"
|
|
399
|
+
chmod 0600 "$tmp"
|
|
400
|
+
mv -f "$tmp" "$state"
|
|
401
|
+
record_evidence "$root" "deployed" "$release_id" "$next_previous"
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
case "$ACTION" in
|
|
405
|
+
inventory)
|
|
406
|
+
inventory
|
|
407
|
+
;;
|
|
408
|
+
deploy)
|
|
409
|
+
deploy
|
|
410
|
+
;;
|
|
411
|
+
*)
|
|
412
|
+
echo "Unsupported application-runtime action: $ACTION" >&2
|
|
413
|
+
exit 2
|
|
414
|
+
;;
|
|
415
|
+
esac
|
|
416
416
|
'''
|