@mriekki/project-forge 4.20.0 → 4.22.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/assets/components.json +2 -2
- package/assets/generated/ASSEMBLY.json +9 -9
- package/assets/generated/deployment/docs/CLOUDFLARE_INGRESS.md +36 -2
- package/assets/generated/deployment/src/project_forge_deployment/app_cli.py +18 -0
- package/assets/generated/deployment/src/project_forge_deployment/app_runtime.py +120 -8
- package/assets/generated/deployment/src/project_forge_deployment/app_runtime_remote.py +173 -3
- package/assets/generated/deployment/src/project_forge_deployment/cloudflare_ingress.py +594 -1
- package/assets/generated/deployment/src/project_forge_deployment/cloudflare_ingress_remote.py +347 -10
- package/assets/generated/deployment/src/project_forge_deployment/ingress_cli.py +51 -1
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ EXPECTED_SPEC_SHA="${2:-}"
|
|
|
8
8
|
DEPLOY_USER="${3:-}"
|
|
9
9
|
HELD_LOCK_FD="${4:-}"
|
|
10
10
|
APP_ROOT="__APP_ROOT__"
|
|
11
|
+
MATERIAL_ROOT="__MATERIAL_ROOT__"
|
|
11
12
|
APP_SCHEMA_VERSION="__APP_SCHEMA_VERSION__"
|
|
12
13
|
NETWORK_NAME="__NETWORK_NAME__"
|
|
13
14
|
CADDY_CONTAINER="__CADDY_CONTAINER__"
|
|
@@ -26,6 +27,9 @@ D_DATA_TARGET_B64="__D_DATA_TARGET_B64__"
|
|
|
26
27
|
D_SPEC_SHA="__D_SPEC_SHA__"
|
|
27
28
|
D_ENV_KEYS=(__D_ENV_KEYS__)
|
|
28
29
|
D_ENV_VALUES=(__D_ENV_VALUES__)
|
|
30
|
+
D_MATERIAL_NAMES=(__D_MATERIAL_NAMES__)
|
|
31
|
+
D_MATERIAL_TARGETS=(__D_MATERIAL_TARGETS__)
|
|
32
|
+
D_MATERIAL_EXPECTED=(__D_MATERIAL_EXPECTED__)
|
|
29
33
|
|
|
30
34
|
b64d() { printf '%s' "$1" | base64 --decode; }
|
|
31
35
|
meta_value() {
|
|
@@ -53,6 +57,92 @@ network_has_container() {
|
|
|
53
57
|
grep -Fxq "$container"
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
|
|
61
|
+
material_source() {
|
|
62
|
+
printf '%s/%s/%s/%s' "$MATERIAL_ROOT" "$D_APP" "$D_ENV" "$1"
|
|
63
|
+
}
|
|
64
|
+
material_identity() {
|
|
65
|
+
local source="$1" run_as="$2" uid gid dir owner mode hazards
|
|
66
|
+
uid="${run_as%%:*}"
|
|
67
|
+
gid="$uid"
|
|
68
|
+
[[ "$run_as" != *:* ]] || gid="${run_as#*:}"
|
|
69
|
+
command -v setpriv >/dev/null 2>&1 || {
|
|
70
|
+
echo "setpriv is required to verify read-only material access for runAs." >&2; return 1;
|
|
71
|
+
}
|
|
72
|
+
for dir in /srv "$MATERIAL_ROOT" "$MATERIAL_ROOT/$D_APP" "$MATERIAL_ROOT/$D_APP/$D_ENV" "$source"; do
|
|
73
|
+
[[ -d "$dir" && ! -L "$dir" ]] || {
|
|
74
|
+
echo "Read-only material directory missing or linked: $dir" >&2; return 1;
|
|
75
|
+
}
|
|
76
|
+
owner="$(stat -c '%u' "$dir")"
|
|
77
|
+
mode="$(stat -c '%a' "$dir")"
|
|
78
|
+
[[ "$owner" == 0 ]] && (( (8#$mode & 022) == 0 )) || {
|
|
79
|
+
echo "Read-only material directory ownership or mode is unsafe: $dir" >&2; return 1;
|
|
80
|
+
}
|
|
81
|
+
done
|
|
82
|
+
hazards="$(find -P "$source" -mindepth 1 \( -type l -o ! -uid 0 -o -perm /022 -o \( ! -type f -a ! -type d \) \) -print -quit)" || return 1
|
|
83
|
+
[[ -z "$hazards" ]] || {
|
|
84
|
+
echo "Read-only material tree has linked, writable, unowned or special entries." >&2; return 1;
|
|
85
|
+
}
|
|
86
|
+
setpriv --reuid "$uid" --regid "$gid" --clear-groups -- bash -c '
|
|
87
|
+
path="$1"
|
|
88
|
+
[[ -r "$path" && -x "$path" ]] || exit 1
|
|
89
|
+
invalid="$(find -P "$path" \( -type d \( ! -readable -o ! -executable \) -o -type f ! -readable \) -print -quit)" || exit 1
|
|
90
|
+
[[ -z "$invalid" ]]
|
|
91
|
+
' -- "$source" || {
|
|
92
|
+
echo "Read-only material tree is not readable/traversable by runAs." >&2; return 1;
|
|
93
|
+
}
|
|
94
|
+
stat -c '%d:%i' "$source"
|
|
95
|
+
}
|
|
96
|
+
validate_material_entries() {
|
|
97
|
+
[[ "${#D_MATERIAL_NAMES[@]}" -eq "${#D_MATERIAL_TARGETS[@]}" &&
|
|
98
|
+
"${#D_MATERIAL_NAMES[@]}" -eq "${#D_MATERIAL_EXPECTED[@]}" ]] || {
|
|
99
|
+
echo "Read-only material argument count mismatch." >&2; exit 33;
|
|
100
|
+
}
|
|
101
|
+
local index name target part prior data
|
|
102
|
+
local -a segments targets=()
|
|
103
|
+
data="$D_DATA_TARGET"
|
|
104
|
+
for index in "${!D_MATERIAL_NAMES[@]}"; do
|
|
105
|
+
name="$(b64d "${D_MATERIAL_NAMES[$index]}")"
|
|
106
|
+
target="$(b64d "${D_MATERIAL_TARGETS[$index]}")"
|
|
107
|
+
valid_slug "$name" || { echo "Invalid read-only material name." >&2; exit 33; }
|
|
108
|
+
[[ "$target" == /* && "$target" != / && "$target" =~ ^/[A-Za-z0-9._~+:/-]+$ ]] || {
|
|
109
|
+
echo "Invalid read-only material target." >&2; exit 33;
|
|
110
|
+
}
|
|
111
|
+
IFS='/' read -r -a segments <<< "${target#/}"
|
|
112
|
+
for part in "${segments[@]}"; do
|
|
113
|
+
[[ -n "$part" && "$part" != . && "$part" != .. ]] || {
|
|
114
|
+
echo "Read-only material target is not normalized." >&2; exit 33;
|
|
115
|
+
}
|
|
116
|
+
done
|
|
117
|
+
case "$target/" in
|
|
118
|
+
/proc/*|/sys/*|/dev/*|/run/*|/var/run/*)
|
|
119
|
+
echo "Read-only material target overlaps a system path." >&2; exit 33;;
|
|
120
|
+
esac
|
|
121
|
+
for prior in "${targets[@]}" "$data"; do
|
|
122
|
+
[[ -z "$prior" ]] && continue
|
|
123
|
+
if [[ "$target" == "$prior" || "$target" == "$prior/"* || "$prior" == "$target/"* ]]; then
|
|
124
|
+
echo "Read-only material target overlaps another mount." >&2; exit 33
|
|
125
|
+
fi
|
|
126
|
+
done
|
|
127
|
+
targets+=("$target")
|
|
128
|
+
done
|
|
129
|
+
}
|
|
130
|
+
material_probe() {
|
|
131
|
+
local check_expected="$1" index name source identity expected
|
|
132
|
+
for index in "${!D_MATERIAL_NAMES[@]}"; do
|
|
133
|
+
name="$(b64d "${D_MATERIAL_NAMES[$index]}")"
|
|
134
|
+
source="$(material_source "$name")"
|
|
135
|
+
identity="$(material_identity "$source" "$D_RUN_AS")" || return 1
|
|
136
|
+
expected="${D_MATERIAL_EXPECTED[$index]}"
|
|
137
|
+
if [[ "$check_expected" == true && "$identity" != "$expected" ]]; then
|
|
138
|
+
echo "Read-only material directory changed after planning: $name" >&2; return 1
|
|
139
|
+
fi
|
|
140
|
+
if [[ "$check_expected" != true ]]; then
|
|
141
|
+
printf 'MATERIAL\t%s\t%s\n' "$name" "$identity"
|
|
142
|
+
fi
|
|
143
|
+
done
|
|
144
|
+
}
|
|
145
|
+
|
|
56
146
|
probe_http() {
|
|
57
147
|
local container="$1" port="$2" path="$3"
|
|
58
148
|
docker exec "$CADDY_CONTAINER" \
|
|
@@ -75,6 +165,47 @@ wait_ready() {
|
|
|
75
165
|
return 1
|
|
76
166
|
}
|
|
77
167
|
|
|
168
|
+
|
|
169
|
+
material_inventory_state() {
|
|
170
|
+
local file="$1" app="$2" env="$3" container="$4" persistence="$5"
|
|
171
|
+
local number index name target source recorded current raw mounts run_as data_target line
|
|
172
|
+
number="$(meta_value "$file" MATERIAL_COUNT || true)"
|
|
173
|
+
MATERIAL_OK=true
|
|
174
|
+
MATERIAL_ENTRIES=""
|
|
175
|
+
[[ -n "$number" ]] || return 0
|
|
176
|
+
[[ "$number" =~ ^[1-8]$ ]] || { echo "Invalid saved material count." >&2; return 1; }
|
|
177
|
+
D_APP="$app"
|
|
178
|
+
D_ENV="$env"
|
|
179
|
+
run_as="$(meta_value "$file" RUN_AS || true)"
|
|
180
|
+
valid_run_as "$run_as" || { echo "Invalid saved material runAs." >&2; return 1; }
|
|
181
|
+
mounts="$(docker inspect --format '{{range .Mounts}}{{printf "%s@%s@%t@%s\n" .Source .Destination .RW .Type}}{{end}}' "$container" 2>/dev/null)" || {
|
|
182
|
+
MATERIAL_OK=false
|
|
183
|
+
mounts=""
|
|
184
|
+
}
|
|
185
|
+
local expected_count="$number"
|
|
186
|
+
if [[ "$persistence" == directory ]]; then
|
|
187
|
+
data_target="$(b64d "$(meta_value "$file" DATA_TARGET_B64 || true)")"
|
|
188
|
+
line="$APP_ROOT/$app/$env/data@$data_target@true@bind"
|
|
189
|
+
grep -Fxq -- "$line" <<< "$mounts" || MATERIAL_OK=false
|
|
190
|
+
expected_count="$((number + 1))"
|
|
191
|
+
fi
|
|
192
|
+
[[ "$(grep -c '.' <<< "$mounts" || true)" == "$expected_count" ]] || MATERIAL_OK=false
|
|
193
|
+
for ((index=0; index < number; index++)); do
|
|
194
|
+
name="$(b64d "$(meta_value "$file" MATERIAL_NAME_B64_"$index" || true)")"
|
|
195
|
+
target="$(b64d "$(meta_value "$file" MATERIAL_TARGET_B64_"$index" || true)")"
|
|
196
|
+
recorded="$(meta_value "$file" MATERIAL_ID_"$index" || true)"
|
|
197
|
+
valid_slug "$name" && [[ "$target" =~ ^/[A-Za-z0-9._~+:/-]+$ && "$recorded" =~ ^[0-9]+:[0-9]+$ ]] || {
|
|
198
|
+
echo "Invalid saved material mount metadata." >&2; return 1;
|
|
199
|
+
}
|
|
200
|
+
source="$(material_source "$name")"
|
|
201
|
+
current="$(material_identity "$source" "$run_as" 2>/dev/null)" || { MATERIAL_OK=false; current=""; }
|
|
202
|
+
[[ "$current" == "$recorded" ]] || MATERIAL_OK=false
|
|
203
|
+
grep -Fxq -- "$source@$target@false@bind" <<< "$mounts" || MATERIAL_OK=false
|
|
204
|
+
[[ -z "$MATERIAL_ENTRIES" ]] || MATERIAL_ENTRIES+=";"
|
|
205
|
+
MATERIAL_ENTRIES+="$name@$target@$recorded"
|
|
206
|
+
done
|
|
207
|
+
}
|
|
208
|
+
|
|
78
209
|
inventory() {
|
|
79
210
|
local count=0 state current previous release_file
|
|
80
211
|
local owner env app container image spec_sha port health persistence
|
|
@@ -114,9 +245,16 @@ inventory() {
|
|
|
114
245
|
if [[ "$running" == true && "$connected" == true ]]; then
|
|
115
246
|
probe_http "$container" "$port" "$health" && healthy=true || true
|
|
116
247
|
fi
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
248
|
+
material_inventory_state "$release_file" "$app" "$env" "$container" "$persistence" || exit 31
|
|
249
|
+
if [[ -n "$MATERIAL_ENTRIES" ]]; then
|
|
250
|
+
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\t%s\t%s\n' \
|
|
251
|
+
"$owner" "$env" "$app" "$container" "$image" "$spec_sha" "$port" "$health" "$persistence" \
|
|
252
|
+
"$current" "$previous" "$running" "$connected" "$healthy" "$MATERIAL_OK" "$MATERIAL_ENTRIES"
|
|
253
|
+
else
|
|
254
|
+
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' \
|
|
255
|
+
"$owner" "$env" "$app" "$container" "$image" "$spec_sha" "$port" "$health" "$persistence" \
|
|
256
|
+
"$current" "$previous" "$running" "$connected" "$healthy"
|
|
257
|
+
fi
|
|
120
258
|
count=$((count + 1))
|
|
121
259
|
done < <(find "$APP_ROOT" -mindepth 3 -maxdepth 3 -type f -name deployment.env -print 2>/dev/null | sort)
|
|
122
260
|
fi
|
|
@@ -173,6 +311,7 @@ decode_desired() {
|
|
|
173
311
|
exit 33
|
|
174
312
|
}
|
|
175
313
|
valid_run_as "$D_RUN_AS" || { echo "Invalid numeric runAs value." >&2; exit 33; }
|
|
314
|
+
validate_material_entries
|
|
176
315
|
[[ "$D_PERSISTENCE" == "none" || "$D_PERSISTENCE" == "directory" ]] || {
|
|
177
316
|
echo "Invalid persistence mode." >&2
|
|
178
317
|
exit 33
|
|
@@ -216,6 +355,14 @@ write_release_metadata() {
|
|
|
216
355
|
printf 'PERSISTENCE_MODE=%s\n' "$D_PERSISTENCE"
|
|
217
356
|
printf 'DATA_TARGET_B64=%s\n' "$D_DATA_TARGET_B64"
|
|
218
357
|
printf 'SPEC_SHA256=%s\n' "$D_SPEC_SHA"
|
|
358
|
+
if (( ${#D_MATERIAL_NAMES[@]} > 0 )); then
|
|
359
|
+
printf 'MATERIAL_COUNT=%s\n' "${#D_MATERIAL_NAMES[@]}"
|
|
360
|
+
for index in "${!D_MATERIAL_NAMES[@]}"; do
|
|
361
|
+
printf 'MATERIAL_NAME_B64_%s=%s\n' "$index" "${D_MATERIAL_NAMES[$index]}"
|
|
362
|
+
printf 'MATERIAL_TARGET_B64_%s=%s\n' "$index" "${D_MATERIAL_TARGETS[$index]}"
|
|
363
|
+
printf 'MATERIAL_ID_%s=%s\n' "$index" "${D_MATERIAL_EXPECTED[$index]}"
|
|
364
|
+
done
|
|
365
|
+
fi
|
|
219
366
|
printf 'ENV_COUNT=%s\n' "${#D_ENV_KEYS[@]}"
|
|
220
367
|
for index in "${!D_ENV_KEYS[@]}"; do
|
|
221
368
|
key_b64="${D_ENV_KEYS[$index]}"
|
|
@@ -271,6 +418,13 @@ start_desired_container() {
|
|
|
271
418
|
value="$(b64d "${D_ENV_VALUES[$index]}")"
|
|
272
419
|
docker_args+=(--env "$key=$value")
|
|
273
420
|
done
|
|
421
|
+
for index in "${!D_MATERIAL_NAMES[@]}"; do
|
|
422
|
+
local source target name
|
|
423
|
+
name="$(b64d "${D_MATERIAL_NAMES[$index]}")"
|
|
424
|
+
target="$(b64d "${D_MATERIAL_TARGETS[$index]}")"
|
|
425
|
+
source="$(material_source "$name")"
|
|
426
|
+
docker_args+=(--mount "type=bind,src=$source,dst=$target,readonly")
|
|
427
|
+
done
|
|
274
428
|
docker_args+=("$D_IMAGE")
|
|
275
429
|
"${docker_args[@]}" >/dev/null
|
|
276
430
|
}
|
|
@@ -339,6 +493,7 @@ deploy() {
|
|
|
339
493
|
release_file="$release_dir/release.env"
|
|
340
494
|
|
|
341
495
|
acquire_deployment_lock "$lock_file"
|
|
496
|
+
material_probe true || { echo "Read-only material preflight failed." >&2; exit 40; }
|
|
342
497
|
|
|
343
498
|
for path in "$root" "$releases" "$state" "$root/data" "$root/deployment-evidence.jsonl"; do
|
|
344
499
|
[[ ! -L "$path" ]] || { echo "Managed application path must not be a symbolic link: $path" >&2; exit 35; }
|
|
@@ -397,11 +552,22 @@ deploy() {
|
|
|
397
552
|
echo "Release directory collision with different runtime metadata." >&2
|
|
398
553
|
exit 35
|
|
399
554
|
}
|
|
555
|
+
if (( ${#D_MATERIAL_NAMES[@]} > 0 )); then
|
|
556
|
+
[[ "$(release_value "$release_file" MATERIAL_COUNT || true)" == "${#D_MATERIAL_NAMES[@]}" ]] || {
|
|
557
|
+
echo "Existing release material metadata mismatch." >&2; exit 35;
|
|
558
|
+
}
|
|
559
|
+
for index in "${!D_MATERIAL_NAMES[@]}"; do
|
|
560
|
+
[[ "$(release_value "$release_file" MATERIAL_ID_"$index" || true)" == "${D_MATERIAL_EXPECTED[$index]}" ]] || {
|
|
561
|
+
echo "Existing release material directory identity changed." >&2; exit 35;
|
|
562
|
+
}
|
|
563
|
+
done
|
|
564
|
+
fi
|
|
400
565
|
else
|
|
401
566
|
write_release_metadata "$release_file"
|
|
402
567
|
fi
|
|
403
568
|
|
|
404
569
|
docker pull "$D_IMAGE" >/dev/null
|
|
570
|
+
material_probe true || { echo "Read-only material source changed before runtime replacement." >&2; exit 40; }
|
|
405
571
|
|
|
406
572
|
if [[ -n "$current_container" ]]; then
|
|
407
573
|
docker rm -f "$current_container" >/dev/null
|
|
@@ -442,6 +608,10 @@ case "$ACTION" in
|
|
|
442
608
|
deploy)
|
|
443
609
|
deploy
|
|
444
610
|
;;
|
|
611
|
+
material-inspect)
|
|
612
|
+
decode_desired
|
|
613
|
+
material_probe false || exit 40
|
|
614
|
+
;;
|
|
445
615
|
*)
|
|
446
616
|
echo "Unsupported application-runtime action: $ACTION" >&2
|
|
447
617
|
exit 2
|