arbiter-server 0.9.1.dev1__py3-none-any.whl
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.
- arbiter_server/__init__.py +1 -0
- arbiter_server/__main__.py +5 -0
- arbiter_server/app.py +44 -0
- arbiter_server/artifacts.py +344 -0
- arbiter_server/cli_errors.py +31 -0
- arbiter_server/config.py +231 -0
- arbiter_server/deploy/docker/arbiter-docker +4477 -0
- arbiter_server/deploy/docker/compose.yaml +101 -0
- arbiter_server/file_protection/__init__.py +20 -0
- arbiter_server/file_protection/posix.py +70 -0
- arbiter_server/file_protection/windows.py +379 -0
- arbiter_server/main.py +2843 -0
- arbiter_server/plugins/__init__.py +36 -0
- arbiter_server/py.typed +1 -0
- arbiter_server/services.py +706 -0
- arbiter_server/storage.py +60 -0
- arbiter_server/version.py +135 -0
- arbiter_server-0.9.1.dev1.dist-info/METADATA +26 -0
- arbiter_server-0.9.1.dev1.dist-info/RECORD +22 -0
- arbiter_server-0.9.1.dev1.dist-info/WHEEL +5 -0
- arbiter_server-0.9.1.dev1.dist-info/entry_points.txt +2 -0
- arbiter_server-0.9.1.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,4477 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
deploy_dir="${ARBITER_DOCKER_DIR:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)}"
|
|
5
|
+
systemd_unit_dir="${ARBITER_SYSTEMD_DIR:-/etc/systemd/system}"
|
|
6
|
+
compose_file="$deploy_dir/compose.yaml"
|
|
7
|
+
compose_env_file="$deploy_dir/docker.env"
|
|
8
|
+
requirements_file="$deploy_dir/requirements.txt"
|
|
9
|
+
compose_override_file="$deploy_dir/compose.override.yaml"
|
|
10
|
+
|
|
11
|
+
usage() {
|
|
12
|
+
cat <<'EOF'
|
|
13
|
+
Usage: arbiter-docker COMMAND
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
prepare Shortcut for bundle prepare
|
|
17
|
+
bundle Manage the root requirements and prepared wheelhouse
|
|
18
|
+
sync-env Run arbiter-server env bootstrap for the configured config
|
|
19
|
+
edit-config Edit the main config file with $ARBITER_EDITOR, $EDITOR, or vi
|
|
20
|
+
edit-requirements
|
|
21
|
+
Edit Python requirements installed inside the container
|
|
22
|
+
edit-env Edit the configured env file with $ARBITER_EDITOR, $EDITOR, or vi
|
|
23
|
+
edit-docker Edit docker.env with $ARBITER_EDITOR, $EDITOR, or vi
|
|
24
|
+
up Start or update the Docker Compose service
|
|
25
|
+
restart Recreate the Docker Compose service
|
|
26
|
+
test Call version_info through the computed MCP URL
|
|
27
|
+
down Stop and remove the Docker Compose service
|
|
28
|
+
ps Show Docker Compose service status
|
|
29
|
+
logs Follow Docker Compose logs
|
|
30
|
+
info Show deployment paths and Docker Compose version
|
|
31
|
+
doctor Check generated files, Docker Compose, and optional agent access
|
|
32
|
+
install Promote this prepared deployment directory to a Linux host
|
|
33
|
+
|
|
34
|
+
Doctor options:
|
|
35
|
+
--preinstall Check this directory before sudo install; skip Docker daemon checks
|
|
36
|
+
--agent-user USER Check that USER cannot read/write deployment state
|
|
37
|
+
--agent-uid UID Check that UID cannot read/write deployment state
|
|
38
|
+
|
|
39
|
+
Install options:
|
|
40
|
+
--to DIR Install target directory, default /opt/arbiter
|
|
41
|
+
--user USER Dedicated service config owner, default arbiter
|
|
42
|
+
--group GROUP Dedicated service config group, default USER
|
|
43
|
+
--service NAME systemd service name, default arbiter
|
|
44
|
+
--no-start Install and enable the service without starting it
|
|
45
|
+
--replace-config Replace installed config from this staging dir
|
|
46
|
+
--replace-env With --replace-config, also replace the installed env
|
|
47
|
+
--dry-run Print the install plan without changing the host
|
|
48
|
+
|
|
49
|
+
Environment:
|
|
50
|
+
ARBITER_DOCKER_DIR Deployment directory, default script directory
|
|
51
|
+
ARBITER_CLIENT_COMMAND
|
|
52
|
+
Arbiter client command for test, default arbiter,
|
|
53
|
+
then ../skill/bin/arbiter for checkout deployments
|
|
54
|
+
ARBITER_EDITOR Editor for edit-config/edit-env
|
|
55
|
+
EOF
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
require_file() {
|
|
59
|
+
local path="$1"
|
|
60
|
+
[[ -f "$path" ]] || {
|
|
61
|
+
printf 'error: missing file: %s\n' "$path" >&2
|
|
62
|
+
exit 1
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
require_dir() {
|
|
67
|
+
local path="$1"
|
|
68
|
+
[[ -d "$path" ]] || {
|
|
69
|
+
printf 'error: missing directory: %s\n' "$path" >&2
|
|
70
|
+
exit 1
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
validate_requirements_file() {
|
|
75
|
+
local path="$1"
|
|
76
|
+
|
|
77
|
+
awk '
|
|
78
|
+
{
|
|
79
|
+
line = $0
|
|
80
|
+
sub(/^[[:space:]]+/, "", line)
|
|
81
|
+
sub(/[[:space:]]+$/, "", line)
|
|
82
|
+
if (line == "" || line ~ /^#/) {
|
|
83
|
+
next
|
|
84
|
+
}
|
|
85
|
+
sub(/[[:space:]]+#.*$/, "", line)
|
|
86
|
+
sub(/[[:space:]]+$/, "", line)
|
|
87
|
+
if (line ~ /^\//) {
|
|
88
|
+
next
|
|
89
|
+
}
|
|
90
|
+
if (line ~ /^[A-Za-z0-9][A-Za-z0-9_.-]*(\[[A-Za-z0-9_.-]+(,[A-Za-z0-9_.-]+)*\])?==[^<>=!~[:space:]#]+$/) {
|
|
91
|
+
name = line
|
|
92
|
+
sub(/==.*/, "", name)
|
|
93
|
+
sub(/\[.*/, "", name)
|
|
94
|
+
version = line
|
|
95
|
+
sub(/^[^=]*==/, "", version)
|
|
96
|
+
if ((name in pins) && pins[name] != version) {
|
|
97
|
+
printf "%s:%d: conflicting package pins for %s: %s and %s\n", FILENAME, FNR, name, pins[name], version
|
|
98
|
+
invalid = 1
|
|
99
|
+
}
|
|
100
|
+
pins[name] = version
|
|
101
|
+
if (name == "arbiter-suite") {
|
|
102
|
+
meta_all = 1
|
|
103
|
+
}
|
|
104
|
+
if (name == "arbiter-server" || name == "arbiter-smtp" || name == "arbiter-imap") {
|
|
105
|
+
all_component = 1
|
|
106
|
+
}
|
|
107
|
+
next
|
|
108
|
+
}
|
|
109
|
+
printf "%s:%d: requirement must be an exact package pin (name==version) or an absolute container path: %s\n", FILENAME, FNR, $0
|
|
110
|
+
invalid = 1
|
|
111
|
+
}
|
|
112
|
+
END {
|
|
113
|
+
if (meta_all && all_component) {
|
|
114
|
+
printf "%s: arbiter-suite meta package cannot be combined directly with arbiter-server, arbiter-smtp, or arbiter-imap pins; generate expanded real package pins with arbiter-server deploy docker docker.requirement=...\n", FILENAME
|
|
115
|
+
invalid = 1
|
|
116
|
+
}
|
|
117
|
+
exit invalid
|
|
118
|
+
}
|
|
119
|
+
' "$path"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
requirements_has_absolute_paths() {
|
|
123
|
+
local path="$1"
|
|
124
|
+
|
|
125
|
+
awk '
|
|
126
|
+
{
|
|
127
|
+
line = $0
|
|
128
|
+
sub(/^[[:space:]]+/, "", line)
|
|
129
|
+
sub(/[[:space:]]+$/, "", line)
|
|
130
|
+
if (line == "" || line ~ /^#/) {
|
|
131
|
+
next
|
|
132
|
+
}
|
|
133
|
+
sub(/[[:space:]]+#.*$/, "", line)
|
|
134
|
+
sub(/[[:space:]]+$/, "", line)
|
|
135
|
+
if (line ~ /^\//) {
|
|
136
|
+
found = 1
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
END {
|
|
140
|
+
exit found ? 0 : 1
|
|
141
|
+
}
|
|
142
|
+
' "$path"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
requirements_has_source_paths() {
|
|
146
|
+
local path="$1"
|
|
147
|
+
|
|
148
|
+
awk '
|
|
149
|
+
{
|
|
150
|
+
line = $0
|
|
151
|
+
sub(/^[[:space:]]+/, "", line)
|
|
152
|
+
sub(/[[:space:]]+$/, "", line)
|
|
153
|
+
if (line == "" || line ~ /^#/) {
|
|
154
|
+
next
|
|
155
|
+
}
|
|
156
|
+
sub(/[[:space:]]+#.*$/, "", line)
|
|
157
|
+
sub(/[[:space:]]+$/, "", line)
|
|
158
|
+
if (line ~ /^\/source\/arbiter(\/|$)/) {
|
|
159
|
+
found = 1
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
END {
|
|
163
|
+
exit found ? 0 : 1
|
|
164
|
+
}
|
|
165
|
+
' "$path"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
reject_source_requirements_for_wheelhouse_command() {
|
|
169
|
+
local command_name="$1"
|
|
170
|
+
|
|
171
|
+
if requirements_has_source_paths "$requirements_file"; then
|
|
172
|
+
printf 'error: bundle %s does not support local checkout requirements: %s\n' "$command_name" "$requirements_file" >&2
|
|
173
|
+
printf ' /source/arbiter/... requirements are installed by Docker Compose at container startup\n' >&2
|
|
174
|
+
printf ' run %s restart to recreate staging with the local source mount\n' "$deploy_dir/arbiter-docker" >&2
|
|
175
|
+
printf ' run %s install to promote local checkout requirements to a wheel-backed install\n' "$deploy_dir/arbiter-docker" >&2
|
|
176
|
+
return 1
|
|
177
|
+
fi
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
edit_requirements() {
|
|
181
|
+
local edited_requirements
|
|
182
|
+
|
|
183
|
+
require_file "$requirements_file"
|
|
184
|
+
edited_requirements="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-requirements.XXXXXX")"
|
|
185
|
+
cp "$requirements_file" "$edited_requirements"
|
|
186
|
+
|
|
187
|
+
if ! run_editor "$edited_requirements"; then
|
|
188
|
+
rm -f "$edited_requirements"
|
|
189
|
+
return 1
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
if ! validate_requirements_file "$edited_requirements"; then
|
|
193
|
+
printf 'error: requirements unchanged: %s\n' "$requirements_file" >&2
|
|
194
|
+
rm -f "$edited_requirements"
|
|
195
|
+
return 1
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
cp "$edited_requirements" "$requirements_file"
|
|
199
|
+
rm -f "$edited_requirements"
|
|
200
|
+
printf 'updated requirements file: %s\n' "$requirements_file"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
bundle_list_roots() {
|
|
204
|
+
require_file "$requirements_file"
|
|
205
|
+
validate_requirements_file "$requirements_file"
|
|
206
|
+
awk '
|
|
207
|
+
{
|
|
208
|
+
line = $0
|
|
209
|
+
sub(/^[[:space:]]+/, "", line)
|
|
210
|
+
sub(/[[:space:]]+#.*$/, "", line)
|
|
211
|
+
sub(/[[:space:]]+$/, "", line)
|
|
212
|
+
if (line == "") {
|
|
213
|
+
next
|
|
214
|
+
}
|
|
215
|
+
printf "root\t%s\n", line
|
|
216
|
+
}
|
|
217
|
+
' "$requirements_file"
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
bundle_list_all() {
|
|
221
|
+
local active_wheels_dir
|
|
222
|
+
local root_requirements
|
|
223
|
+
local resolved_requirements
|
|
224
|
+
local wheel
|
|
225
|
+
local filename
|
|
226
|
+
local base
|
|
227
|
+
local distribution
|
|
228
|
+
local version
|
|
229
|
+
local normalized_name
|
|
230
|
+
local requirement
|
|
231
|
+
local label
|
|
232
|
+
|
|
233
|
+
require_file "$requirements_file"
|
|
234
|
+
validate_requirements_file "$requirements_file"
|
|
235
|
+
active_wheels_dir="$(wheels_dir_path)"
|
|
236
|
+
require_dir "$active_wheels_dir"
|
|
237
|
+
|
|
238
|
+
root_requirements="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-roots.XXXXXX")"
|
|
239
|
+
resolved_requirements="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-resolved.XXXXXX")"
|
|
240
|
+
|
|
241
|
+
awk '
|
|
242
|
+
function normalize(name) {
|
|
243
|
+
name = tolower(name)
|
|
244
|
+
gsub(/[-_.]+/, "-", name)
|
|
245
|
+
return name
|
|
246
|
+
}
|
|
247
|
+
function wheel_requirement(path, base, parts) {
|
|
248
|
+
base = path
|
|
249
|
+
sub(/^.*\//, "", base)
|
|
250
|
+
sub(/\.whl$/, "", base)
|
|
251
|
+
split(base, parts, "-")
|
|
252
|
+
if (parts[1] != "" && parts[2] != "") {
|
|
253
|
+
print normalize(parts[1]) "==" parts[2]
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
{
|
|
257
|
+
line = $0
|
|
258
|
+
sub(/^[[:space:]]+/, "", line)
|
|
259
|
+
sub(/[[:space:]]+#.*$/, "", line)
|
|
260
|
+
sub(/[[:space:]]+$/, "", line)
|
|
261
|
+
if (line == "") {
|
|
262
|
+
next
|
|
263
|
+
}
|
|
264
|
+
if (line ~ /^\/wheels\/.*\.whl$/) {
|
|
265
|
+
wheel_requirement(line)
|
|
266
|
+
next
|
|
267
|
+
}
|
|
268
|
+
name = line
|
|
269
|
+
sub(/==.*/, "", name)
|
|
270
|
+
sub(/\[.*/, "", name)
|
|
271
|
+
version = line
|
|
272
|
+
sub(/^[^=]*==/, "", version)
|
|
273
|
+
print normalize(name) "==" version
|
|
274
|
+
}
|
|
275
|
+
' "$requirements_file" | sort -u >"$root_requirements"
|
|
276
|
+
|
|
277
|
+
shopt -s nullglob
|
|
278
|
+
for wheel in "$active_wheels_dir"/*.whl; do
|
|
279
|
+
filename="${wheel##*/}"
|
|
280
|
+
base="${filename%.whl}"
|
|
281
|
+
distribution="${base%%-*}"
|
|
282
|
+
base="${base#*-}"
|
|
283
|
+
version="${base%%-*}"
|
|
284
|
+
if [[ -z "$distribution" || -z "$version" ]]; then
|
|
285
|
+
continue
|
|
286
|
+
fi
|
|
287
|
+
normalized_name="$(printf '%s\n' "$distribution" | tr '[:upper:]' '[:lower:]' | sed 's/[-_.][-_\.]*/-/g')"
|
|
288
|
+
requirement="$normalized_name==$version"
|
|
289
|
+
if grep -Fxq "$requirement" "$root_requirements"; then
|
|
290
|
+
label="root"
|
|
291
|
+
else
|
|
292
|
+
label="transitive"
|
|
293
|
+
fi
|
|
294
|
+
printf '%s\t%s\n' "$label" "$requirement"
|
|
295
|
+
done | sort -u >"$resolved_requirements"
|
|
296
|
+
shopt -u nullglob
|
|
297
|
+
|
|
298
|
+
if [[ ! -s "$resolved_requirements" ]]; then
|
|
299
|
+
printf 'error: wheelhouse is empty: %s\n' "$active_wheels_dir" >&2
|
|
300
|
+
printf ' run %s bundle prepare to build the dependency wheelhouse\n' "$deploy_dir/arbiter-docker" >&2
|
|
301
|
+
rm -f "$root_requirements" "$resolved_requirements"
|
|
302
|
+
return 1
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
cat "$resolved_requirements"
|
|
306
|
+
rm -f "$root_requirements" "$resolved_requirements"
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
bundle_list_plugins() {
|
|
310
|
+
printf 'imap\n'
|
|
311
|
+
printf 'smtp\n'
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
bundle_plugin_package() {
|
|
315
|
+
case "$1" in
|
|
316
|
+
imap)
|
|
317
|
+
printf 'arbiter-imap\n'
|
|
318
|
+
;;
|
|
319
|
+
smtp)
|
|
320
|
+
printf 'arbiter-smtp\n'
|
|
321
|
+
;;
|
|
322
|
+
*)
|
|
323
|
+
return 1
|
|
324
|
+
;;
|
|
325
|
+
esac
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
bundle_meta_plugins() {
|
|
329
|
+
case "$1" in
|
|
330
|
+
arbiter-suite)
|
|
331
|
+
bundle_list_plugins
|
|
332
|
+
;;
|
|
333
|
+
*)
|
|
334
|
+
return 1
|
|
335
|
+
;;
|
|
336
|
+
esac
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
bundle_item_plugins() {
|
|
340
|
+
if bundle_plugin_package "$1" >/dev/null; then
|
|
341
|
+
printf '%s\n' "$1"
|
|
342
|
+
return
|
|
343
|
+
fi
|
|
344
|
+
bundle_meta_plugins "$1"
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
bundle_supported_items_message() {
|
|
348
|
+
printf 'supported plugins: imap, smtp; supported meta packages: arbiter-suite\n'
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
normalize_distribution_name() {
|
|
352
|
+
printf '%s\n' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[-_.][-_\.]*/-/g'
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
bundle_requirement_version() {
|
|
356
|
+
local package="$1"
|
|
357
|
+
|
|
358
|
+
awk -v package="$package" '
|
|
359
|
+
function normalize(name) {
|
|
360
|
+
name = tolower(name)
|
|
361
|
+
gsub(/[-_.]+/, "-", name)
|
|
362
|
+
return name
|
|
363
|
+
}
|
|
364
|
+
BEGIN {
|
|
365
|
+
wanted = normalize(package)
|
|
366
|
+
}
|
|
367
|
+
{
|
|
368
|
+
line = $0
|
|
369
|
+
sub(/^[[:space:]]+/, "", line)
|
|
370
|
+
sub(/[[:space:]]+#.*$/, "", line)
|
|
371
|
+
sub(/[[:space:]]+$/, "", line)
|
|
372
|
+
if (line == "" || line ~ /^\//) {
|
|
373
|
+
next
|
|
374
|
+
}
|
|
375
|
+
name = line
|
|
376
|
+
sub(/==.*/, "", name)
|
|
377
|
+
sub(/\[.*/, "", name)
|
|
378
|
+
version = line
|
|
379
|
+
sub(/^[^=]*==/, "", version)
|
|
380
|
+
if (normalize(name) == wanted) {
|
|
381
|
+
print version
|
|
382
|
+
found = 1
|
|
383
|
+
exit
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
END {
|
|
387
|
+
exit found ? 0 : 1
|
|
388
|
+
}
|
|
389
|
+
' "$requirements_file"
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
bundle_write_without_packages() {
|
|
393
|
+
local output_file="$1"
|
|
394
|
+
shift
|
|
395
|
+
local packages="$*"
|
|
396
|
+
|
|
397
|
+
awk -v packages="$packages" '
|
|
398
|
+
function normalize(name) {
|
|
399
|
+
name = tolower(name)
|
|
400
|
+
gsub(/[-_.]+/, "-", name)
|
|
401
|
+
return name
|
|
402
|
+
}
|
|
403
|
+
BEGIN {
|
|
404
|
+
split(packages, package_list, " ")
|
|
405
|
+
for (i in package_list) {
|
|
406
|
+
skip[normalize(package_list[i])] = 1
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
{
|
|
410
|
+
line = $0
|
|
411
|
+
sub(/^[[:space:]]+/, "", line)
|
|
412
|
+
sub(/[[:space:]]+#.*$/, "", line)
|
|
413
|
+
sub(/[[:space:]]+$/, "", line)
|
|
414
|
+
if (line != "" && line !~ /^\//) {
|
|
415
|
+
name = line
|
|
416
|
+
sub(/==.*/, "", name)
|
|
417
|
+
sub(/\[.*/, "", name)
|
|
418
|
+
if (normalize(name) in skip) {
|
|
419
|
+
next
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
print $0
|
|
423
|
+
}
|
|
424
|
+
' "$requirements_file" >"$output_file"
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
bundle_update_requirements_file() {
|
|
428
|
+
local updated_file="$1"
|
|
429
|
+
|
|
430
|
+
if ! validate_requirements_file "$updated_file"; then
|
|
431
|
+
printf 'error: generated requirements are invalid; requirements unchanged: %s\n' "$requirements_file" >&2
|
|
432
|
+
rm -f "$updated_file"
|
|
433
|
+
return 1
|
|
434
|
+
fi
|
|
435
|
+
mv "$updated_file" "$requirements_file"
|
|
436
|
+
printf 'updated requirements file: %s\n' "$requirements_file"
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
bundle_add_plugin() {
|
|
440
|
+
local item="${1:-}"
|
|
441
|
+
local plugin
|
|
442
|
+
local package
|
|
443
|
+
local version
|
|
444
|
+
local tmp_file
|
|
445
|
+
local last_char
|
|
446
|
+
local plugins_text
|
|
447
|
+
local selected_count=0
|
|
448
|
+
local added_count=0
|
|
449
|
+
local -a plugins=()
|
|
450
|
+
|
|
451
|
+
if [[ -z "$item" || $# -ne 1 ]]; then
|
|
452
|
+
printf 'error: bundle add requires exactly one plugin or meta package name\n' >&2
|
|
453
|
+
{ printf ' '; bundle_supported_items_message; } >&2
|
|
454
|
+
return 2
|
|
455
|
+
fi
|
|
456
|
+
if ! plugins_text="$(bundle_item_plugins "$item")"; then
|
|
457
|
+
printf 'error: unsupported bundle item: %s\n' "$item" >&2
|
|
458
|
+
{ printf ' '; bundle_supported_items_message; } >&2
|
|
459
|
+
return 2
|
|
460
|
+
fi
|
|
461
|
+
while IFS= read -r plugin; do
|
|
462
|
+
[[ -n "$plugin" ]] || continue
|
|
463
|
+
plugins+=("$plugin")
|
|
464
|
+
done <<<"$plugins_text"
|
|
465
|
+
|
|
466
|
+
require_file "$requirements_file"
|
|
467
|
+
validate_requirements_file "$requirements_file"
|
|
468
|
+
if bundle_requirement_version arbiter-suite >/dev/null; then
|
|
469
|
+
printf 'bundle item already selected by arbiter-suite: %s\n' "$item"
|
|
470
|
+
return 0
|
|
471
|
+
fi
|
|
472
|
+
if ! version="$(bundle_requirement_version arbiter-server)"; then
|
|
473
|
+
printf 'error: cannot add bundle item without an arbiter-server package pin: %s\n' "$item" >&2
|
|
474
|
+
printf ' add arbiter-server==VERSION to requirements.txt first\n' >&2
|
|
475
|
+
return 1
|
|
476
|
+
fi
|
|
477
|
+
|
|
478
|
+
tmp_file="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-requirements.XXXXXX")"
|
|
479
|
+
cp "$requirements_file" "$tmp_file"
|
|
480
|
+
last_char=""
|
|
481
|
+
if [[ -s "$tmp_file" ]]; then
|
|
482
|
+
last_char="$(tail -c 1 "$tmp_file")"
|
|
483
|
+
fi
|
|
484
|
+
if [[ -n "$last_char" ]]; then
|
|
485
|
+
printf '\n' >>"$tmp_file"
|
|
486
|
+
fi
|
|
487
|
+
for plugin in "${plugins[@]}"; do
|
|
488
|
+
package="$(bundle_plugin_package "$plugin")"
|
|
489
|
+
if bundle_requirement_version "$package" >/dev/null; then
|
|
490
|
+
selected_count=$((selected_count + 1))
|
|
491
|
+
continue
|
|
492
|
+
fi
|
|
493
|
+
printf '%s==%s\n' "$package" "$version" >>"$tmp_file"
|
|
494
|
+
added_count=$((added_count + 1))
|
|
495
|
+
done
|
|
496
|
+
if [[ "$added_count" -eq 0 ]]; then
|
|
497
|
+
rm -f "$tmp_file"
|
|
498
|
+
if [[ "$selected_count" -eq "${#plugins[@]}" ]]; then
|
|
499
|
+
printf 'bundle item already selected: %s\n' "$item"
|
|
500
|
+
fi
|
|
501
|
+
return 0
|
|
502
|
+
fi
|
|
503
|
+
bundle_update_requirements_file "$tmp_file"
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
bundle_remove_plugin() {
|
|
507
|
+
local item="${1:-}"
|
|
508
|
+
local plugin
|
|
509
|
+
local package
|
|
510
|
+
local suite_version
|
|
511
|
+
local tmp_file
|
|
512
|
+
local other_requirements_file
|
|
513
|
+
local supported_plugin
|
|
514
|
+
local supported_package
|
|
515
|
+
local plugins_text
|
|
516
|
+
local remove_plugins=""
|
|
517
|
+
local selected_count=0
|
|
518
|
+
local -a plugins=()
|
|
519
|
+
local -a packages=()
|
|
520
|
+
|
|
521
|
+
if [[ -z "$item" || $# -ne 1 ]]; then
|
|
522
|
+
printf 'error: bundle remove requires exactly one plugin or meta package name\n' >&2
|
|
523
|
+
{ printf ' '; bundle_supported_items_message; } >&2
|
|
524
|
+
return 2
|
|
525
|
+
fi
|
|
526
|
+
if ! plugins_text="$(bundle_item_plugins "$item")"; then
|
|
527
|
+
printf 'error: unsupported bundle item: %s\n' "$item" >&2
|
|
528
|
+
{ printf ' '; bundle_supported_items_message; } >&2
|
|
529
|
+
return 2
|
|
530
|
+
fi
|
|
531
|
+
while IFS= read -r plugin; do
|
|
532
|
+
[[ -n "$plugin" ]] || continue
|
|
533
|
+
plugins+=("$plugin")
|
|
534
|
+
done <<<"$plugins_text"
|
|
535
|
+
for plugin in "${plugins[@]}"; do
|
|
536
|
+
remove_plugins+=" $plugin "
|
|
537
|
+
packages+=("$(bundle_plugin_package "$plugin")")
|
|
538
|
+
done
|
|
539
|
+
|
|
540
|
+
require_file "$requirements_file"
|
|
541
|
+
validate_requirements_file "$requirements_file"
|
|
542
|
+
tmp_file="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-requirements.XXXXXX")"
|
|
543
|
+
|
|
544
|
+
if suite_version="$(bundle_requirement_version arbiter-suite)"; then
|
|
545
|
+
other_requirements_file="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-requirements.XXXXXX")"
|
|
546
|
+
bundle_write_without_packages \
|
|
547
|
+
"$other_requirements_file" \
|
|
548
|
+
arbiter-suite \
|
|
549
|
+
arbiter-server \
|
|
550
|
+
arbiter-imap \
|
|
551
|
+
arbiter-smtp
|
|
552
|
+
{
|
|
553
|
+
printf 'arbiter-server==%s\n' "$suite_version"
|
|
554
|
+
while IFS= read -r supported_plugin; do
|
|
555
|
+
[[ "$remove_plugins" != *" $supported_plugin "* ]] || continue
|
|
556
|
+
supported_package="$(bundle_plugin_package "$supported_plugin")"
|
|
557
|
+
printf '%s==%s\n' "$supported_package" "$suite_version"
|
|
558
|
+
done < <(bundle_list_plugins)
|
|
559
|
+
awk '
|
|
560
|
+
{
|
|
561
|
+
line = $0
|
|
562
|
+
sub(/^[[:space:]]+/, "", line)
|
|
563
|
+
sub(/[[:space:]]+$/, "", line)
|
|
564
|
+
if (line != "") {
|
|
565
|
+
print $0
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
' "$other_requirements_file"
|
|
569
|
+
} >"$tmp_file"
|
|
570
|
+
rm -f "$other_requirements_file"
|
|
571
|
+
bundle_update_requirements_file "$tmp_file"
|
|
572
|
+
return
|
|
573
|
+
fi
|
|
574
|
+
|
|
575
|
+
for package in "${packages[@]}"; do
|
|
576
|
+
if bundle_requirement_version "$package" >/dev/null; then
|
|
577
|
+
selected_count=$((selected_count + 1))
|
|
578
|
+
fi
|
|
579
|
+
done
|
|
580
|
+
if [[ "$selected_count" -eq 0 ]]; then
|
|
581
|
+
rm -f "$tmp_file"
|
|
582
|
+
printf 'bundle item not selected: %s\n' "$item"
|
|
583
|
+
return 0
|
|
584
|
+
fi
|
|
585
|
+
|
|
586
|
+
bundle_write_without_packages "$tmp_file" "${packages[@]}"
|
|
587
|
+
bundle_update_requirements_file "$tmp_file"
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
wheel_name_version() {
|
|
591
|
+
local wheel_path="$1"
|
|
592
|
+
local filename
|
|
593
|
+
local base
|
|
594
|
+
local distribution
|
|
595
|
+
local version
|
|
596
|
+
|
|
597
|
+
filename="${wheel_path##*/}"
|
|
598
|
+
base="${filename%.whl}"
|
|
599
|
+
distribution="${base%%-*}"
|
|
600
|
+
base="${base#*-}"
|
|
601
|
+
version="${base%%-*}"
|
|
602
|
+
if [[ -n "$distribution" && -n "$version" ]]; then
|
|
603
|
+
printf '%s\t%s\n' "$(normalize_distribution_name "$distribution")" "$version"
|
|
604
|
+
fi
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
bundle_write_root_versions() {
|
|
608
|
+
local output_file="$1"
|
|
609
|
+
local line
|
|
610
|
+
local name
|
|
611
|
+
local base_name
|
|
612
|
+
local version
|
|
613
|
+
local wheel_info
|
|
614
|
+
|
|
615
|
+
: >"$output_file"
|
|
616
|
+
require_file "$requirements_file"
|
|
617
|
+
validate_requirements_file "$requirements_file"
|
|
618
|
+
while IFS= read -r line; do
|
|
619
|
+
[[ -n "$line" ]] || continue
|
|
620
|
+
if [[ "$line" == /wheels/*.whl ]]; then
|
|
621
|
+
wheel_info="$(wheel_name_version "$line")"
|
|
622
|
+
if [[ -n "$wheel_info" ]]; then
|
|
623
|
+
printf 'root\t%s\n' "$wheel_info" >>"$output_file"
|
|
624
|
+
fi
|
|
625
|
+
continue
|
|
626
|
+
fi
|
|
627
|
+
name="${line%%==*}"
|
|
628
|
+
base_name="${name%%[*}"
|
|
629
|
+
version="${line#*==}"
|
|
630
|
+
printf 'root\t%s\t%s\n' "$(normalize_distribution_name "$base_name")" "$version" >>"$output_file"
|
|
631
|
+
done < <(
|
|
632
|
+
awk '
|
|
633
|
+
{
|
|
634
|
+
line = $0
|
|
635
|
+
sub(/^[[:space:]]+/, "", line)
|
|
636
|
+
sub(/[[:space:]]+#.*$/, "", line)
|
|
637
|
+
sub(/[[:space:]]+$/, "", line)
|
|
638
|
+
if (line != "") {
|
|
639
|
+
print line
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
' "$requirements_file"
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
bundle_root_summary() {
|
|
647
|
+
local root_versions
|
|
648
|
+
|
|
649
|
+
root_versions="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-roots.XXXXXX")"
|
|
650
|
+
bundle_write_root_versions "$root_versions"
|
|
651
|
+
awk -F '\t' '$1 == "root" { print $2 "==" $3 }' "$root_versions" | sort | paste -sd ' ' -
|
|
652
|
+
rm -f "$root_versions"
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
print_bundle_prepare_start() {
|
|
656
|
+
printf 'preparing bundle: %s\n' "$(bundle_root_summary)"
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
bundle_write_wheelhouse_versions() {
|
|
660
|
+
local output_file="$1"
|
|
661
|
+
local active_wheels_dir
|
|
662
|
+
local root_versions
|
|
663
|
+
local wheel
|
|
664
|
+
local wheel_info
|
|
665
|
+
local normalized_name
|
|
666
|
+
local version
|
|
667
|
+
local label
|
|
668
|
+
|
|
669
|
+
: >"$output_file"
|
|
670
|
+
active_wheels_dir="$(wheels_dir_path)"
|
|
671
|
+
[[ -d "$active_wheels_dir" ]] || return 0
|
|
672
|
+
root_versions="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-roots.XXXXXX")"
|
|
673
|
+
bundle_write_root_versions "$root_versions"
|
|
674
|
+
|
|
675
|
+
shopt -s nullglob
|
|
676
|
+
for wheel in "$active_wheels_dir"/*.whl; do
|
|
677
|
+
wheel_info="$(wheel_name_version "$wheel")"
|
|
678
|
+
[[ -n "$wheel_info" ]] || continue
|
|
679
|
+
normalized_name="${wheel_info%%$'\t'*}"
|
|
680
|
+
version="${wheel_info#*$'\t'}"
|
|
681
|
+
if grep -Fxq "root $normalized_name $version" "$root_versions"; then
|
|
682
|
+
label="root"
|
|
683
|
+
else
|
|
684
|
+
label="transitive"
|
|
685
|
+
fi
|
|
686
|
+
printf '%s\t%s\t%s\n' "$label" "$normalized_name" "$version" >>"$output_file"
|
|
687
|
+
done
|
|
688
|
+
shopt -u nullglob
|
|
689
|
+
sort -u "$output_file" -o "$output_file"
|
|
690
|
+
rm -f "$root_versions"
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
bundle_write_upgrade_snapshot() {
|
|
694
|
+
local output_file="$1"
|
|
695
|
+
local root_versions
|
|
696
|
+
local wheel_versions
|
|
697
|
+
|
|
698
|
+
root_versions="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-upgrade-roots.XXXXXX")"
|
|
699
|
+
wheel_versions="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-upgrade-wheels.XXXXXX")"
|
|
700
|
+
bundle_write_root_versions "$root_versions"
|
|
701
|
+
bundle_write_wheelhouse_versions "$wheel_versions"
|
|
702
|
+
{
|
|
703
|
+
cat "$root_versions"
|
|
704
|
+
awk -F '\t' '$1 == "transitive" { print }' "$wheel_versions"
|
|
705
|
+
} | sort -u >"$output_file"
|
|
706
|
+
rm -f "$root_versions" "$wheel_versions"
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
bundle_print_upgrade_section() {
|
|
710
|
+
local title="$1"
|
|
711
|
+
local label="$2"
|
|
712
|
+
local before_file="$3"
|
|
713
|
+
local after_file="$4"
|
|
714
|
+
|
|
715
|
+
printf '%s:\n' "$title"
|
|
716
|
+
awk -F '\t' -v label="$label" '
|
|
717
|
+
FILENAME == ARGV[1] && $1 == label {
|
|
718
|
+
before[$2] = $3
|
|
719
|
+
names[$2] = 1
|
|
720
|
+
next
|
|
721
|
+
}
|
|
722
|
+
FILENAME == ARGV[2] && $1 == label {
|
|
723
|
+
after[$2] = $3
|
|
724
|
+
names[$2] = 1
|
|
725
|
+
next
|
|
726
|
+
}
|
|
727
|
+
END {
|
|
728
|
+
for (name in names) {
|
|
729
|
+
old = before[name]
|
|
730
|
+
new = after[name]
|
|
731
|
+
if (old == new) {
|
|
732
|
+
continue
|
|
733
|
+
}
|
|
734
|
+
if (old == "") {
|
|
735
|
+
old = "not present"
|
|
736
|
+
}
|
|
737
|
+
if (new == "") {
|
|
738
|
+
new = "removed"
|
|
739
|
+
}
|
|
740
|
+
printf " %s %s -> %s\n", name, old, new
|
|
741
|
+
changed = 1
|
|
742
|
+
}
|
|
743
|
+
if (!changed) {
|
|
744
|
+
print " no changes"
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
' "$before_file" "$after_file" | sort
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
bundle_print_upgrade_summary() {
|
|
751
|
+
local before_file="$1"
|
|
752
|
+
local after_file="$2"
|
|
753
|
+
|
|
754
|
+
printf 'bundle upgrade complete: %s\n' "$deploy_dir"
|
|
755
|
+
bundle_print_upgrade_section root root "$before_file" "$after_file"
|
|
756
|
+
bundle_print_upgrade_section transitive transitive "$before_file" "$after_file"
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
release_line_upper_bound() {
|
|
760
|
+
local release_line="$1"
|
|
761
|
+
local prefix
|
|
762
|
+
local last
|
|
763
|
+
|
|
764
|
+
prefix="${release_line%.*}"
|
|
765
|
+
last="${release_line##*.}"
|
|
766
|
+
if [[ "$prefix" == "$release_line" ]]; then
|
|
767
|
+
printf '%s\n' "$((10#$last + 1))"
|
|
768
|
+
else
|
|
769
|
+
printf '%s.%s\n' "$prefix" "$((10#$last + 1))"
|
|
770
|
+
fi
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
repo_root_from_start() {
|
|
774
|
+
local start="$1"
|
|
775
|
+
|
|
776
|
+
[[ -n "$start" ]] || return 1
|
|
777
|
+
if [[ ! -d "$start" ]]; then
|
|
778
|
+
start="$(dirname -- "$start")"
|
|
779
|
+
fi
|
|
780
|
+
start="$(cd -- "$start" 2>/dev/null && pwd -P)" || return 1
|
|
781
|
+
|
|
782
|
+
while [[ "$start" != "/" ]]; do
|
|
783
|
+
if [[ -f "$start/server/pyproject.toml" && -f "$start/plugins/imap/pyproject.toml" && -f "$start/plugins/smtp/pyproject.toml" ]]; then
|
|
784
|
+
printf '%s\n' "$start"
|
|
785
|
+
return 0
|
|
786
|
+
fi
|
|
787
|
+
start="$(dirname -- "$start")"
|
|
788
|
+
done
|
|
789
|
+
return 1
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
find_repo_root() {
|
|
793
|
+
repo_root_from_start "$PWD" || repo_root_from_start "$deploy_dir"
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
repo_source_dir_for_package() {
|
|
797
|
+
case "$(normalize_distribution_name "$1")" in
|
|
798
|
+
arbiter-server)
|
|
799
|
+
printf 'server\n'
|
|
800
|
+
;;
|
|
801
|
+
arbiter-imap)
|
|
802
|
+
printf 'plugins/imap\n'
|
|
803
|
+
;;
|
|
804
|
+
arbiter-smtp)
|
|
805
|
+
printf 'plugins/smtp\n'
|
|
806
|
+
;;
|
|
807
|
+
arbiter-suite)
|
|
808
|
+
printf 'meta/arbiter-suite\n'
|
|
809
|
+
;;
|
|
810
|
+
esac
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
bundle_add_repo_wheel_sources_for_package() {
|
|
814
|
+
local source_list="$1"
|
|
815
|
+
local repo_root="$2"
|
|
816
|
+
local package_name="$3"
|
|
817
|
+
local normalized_name
|
|
818
|
+
local source_dir
|
|
819
|
+
|
|
820
|
+
normalized_name="$(normalize_distribution_name "$package_name")"
|
|
821
|
+
if [[ "$normalized_name" == "arbiter-suite" ]]; then
|
|
822
|
+
for package_name in arbiter-server arbiter-imap arbiter-smtp arbiter-suite; do
|
|
823
|
+
source_dir="$(repo_source_dir_for_package "$package_name")"
|
|
824
|
+
[[ -n "$source_dir" && -d "$repo_root/$source_dir" ]] || continue
|
|
825
|
+
printf '%s\n' "$repo_root/$source_dir" >>"$source_list"
|
|
826
|
+
done
|
|
827
|
+
return
|
|
828
|
+
fi
|
|
829
|
+
|
|
830
|
+
source_dir="$(repo_source_dir_for_package "$normalized_name")"
|
|
831
|
+
[[ -n "$source_dir" && -d "$repo_root/$source_dir" ]] || return
|
|
832
|
+
printf '%s\n' "$repo_root/$source_dir" >>"$source_list"
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
bundle_refresh_repo_wheels() {
|
|
836
|
+
local tmp_dir="$1"
|
|
837
|
+
local repo_root
|
|
838
|
+
local active_wheels_dir
|
|
839
|
+
local python_bin="${ARBITER_PYTHON:-}"
|
|
840
|
+
local build_dir
|
|
841
|
+
local output_file
|
|
842
|
+
local source_list
|
|
843
|
+
local line
|
|
844
|
+
local name
|
|
845
|
+
local wheel
|
|
846
|
+
local built_count=0
|
|
847
|
+
|
|
848
|
+
repo_root="$(find_repo_root)" || return 0
|
|
849
|
+
active_wheels_dir="$(wheels_dir_path)"
|
|
850
|
+
build_dir="$tmp_dir/repo-wheels"
|
|
851
|
+
output_file="$tmp_dir/repo-wheel-build.out"
|
|
852
|
+
source_list="$tmp_dir/repo-wheel-sources"
|
|
853
|
+
|
|
854
|
+
if [[ -z "$python_bin" ]]; then
|
|
855
|
+
if [[ -x "$repo_root/.venv/bin/python" ]]; then
|
|
856
|
+
python_bin="$repo_root/.venv/bin/python"
|
|
857
|
+
elif command -v python >/dev/null; then
|
|
858
|
+
python_bin="python"
|
|
859
|
+
elif command -v python3 >/dev/null; then
|
|
860
|
+
python_bin="python3"
|
|
861
|
+
else
|
|
862
|
+
printf 'error: cannot build local repo wheels: python command not found\n' >&2
|
|
863
|
+
printf ' set ARBITER_PYTHON or use bundle upgrade --pypi-only\n' >&2
|
|
864
|
+
return 1
|
|
865
|
+
fi
|
|
866
|
+
fi
|
|
867
|
+
|
|
868
|
+
mkdir -p "$active_wheels_dir" "$build_dir"
|
|
869
|
+
: >"$source_list"
|
|
870
|
+
|
|
871
|
+
while IFS= read -r line; do
|
|
872
|
+
[[ -n "$line" ]] || continue
|
|
873
|
+
[[ "$line" != /* ]] || continue
|
|
874
|
+
name="${line%%==*}"
|
|
875
|
+
name="${name%%[*}"
|
|
876
|
+
bundle_add_repo_wheel_sources_for_package "$source_list" "$repo_root" "$name"
|
|
877
|
+
done < <(
|
|
878
|
+
awk '
|
|
879
|
+
{
|
|
880
|
+
line = $0
|
|
881
|
+
sub(/^[[:space:]]+/, "", line)
|
|
882
|
+
sub(/[[:space:]]+#.*$/, "", line)
|
|
883
|
+
sub(/[[:space:]]+$/, "", line)
|
|
884
|
+
if (line != "") {
|
|
885
|
+
print line
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
' "$requirements_file"
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
if [[ ! -s "$source_list" ]]; then
|
|
892
|
+
return 0
|
|
893
|
+
fi
|
|
894
|
+
sort -u "$source_list" -o "$source_list"
|
|
895
|
+
|
|
896
|
+
while IFS= read -r source_dir; do
|
|
897
|
+
if ! "$python_bin" -m pip --disable-pip-version-check wheel --no-deps --no-build-isolation --wheel-dir "$build_dir" "$source_dir" >"$output_file" 2>&1; then
|
|
898
|
+
cat "$output_file" >&2
|
|
899
|
+
printf 'error: failed to build local repo wheel: %s\n' "$source_dir" >&2
|
|
900
|
+
printf ' use --pypi-only to resolve only from the package index\n' >&2
|
|
901
|
+
return 1
|
|
902
|
+
fi
|
|
903
|
+
built_count=$((built_count + 1))
|
|
904
|
+
done <"$source_list"
|
|
905
|
+
|
|
906
|
+
if [[ "$built_count" -eq 0 ]]; then
|
|
907
|
+
return 0
|
|
908
|
+
fi
|
|
909
|
+
|
|
910
|
+
shopt -s nullglob
|
|
911
|
+
for wheel in "$build_dir"/*.whl; do
|
|
912
|
+
cp "$wheel" "$active_wheels_dir/${wheel##*/}"
|
|
913
|
+
done
|
|
914
|
+
shopt -u nullglob
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
bundle_upgrade_build_input() {
|
|
918
|
+
local target="$1"
|
|
919
|
+
local input_file="$2"
|
|
920
|
+
local roots_file="$3"
|
|
921
|
+
local mode="all"
|
|
922
|
+
local target_name=""
|
|
923
|
+
local target_base=""
|
|
924
|
+
local target_normalized=""
|
|
925
|
+
local release_upper=""
|
|
926
|
+
local found_target=0
|
|
927
|
+
local root_count=0
|
|
928
|
+
local line
|
|
929
|
+
local name
|
|
930
|
+
local base_name
|
|
931
|
+
local version
|
|
932
|
+
local normalized_name
|
|
933
|
+
local display_name
|
|
934
|
+
local spec
|
|
935
|
+
local has_wheel_root=0
|
|
936
|
+
|
|
937
|
+
require_file "$requirements_file"
|
|
938
|
+
validate_requirements_file "$requirements_file"
|
|
939
|
+
|
|
940
|
+
if [[ -n "$target" ]]; then
|
|
941
|
+
if [[ "$target" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
|
|
942
|
+
mode="release"
|
|
943
|
+
release_upper="$(release_line_upper_bound "$target")"
|
|
944
|
+
elif [[ "$target" == *"=="* ]]; then
|
|
945
|
+
mode="exact"
|
|
946
|
+
target_name="${target%%==*}"
|
|
947
|
+
target_base="${target_name%%[*}"
|
|
948
|
+
target_normalized="$(normalize_distribution_name "$target_base")"
|
|
949
|
+
else
|
|
950
|
+
mode="package"
|
|
951
|
+
target_normalized="$(normalize_distribution_name "$target")"
|
|
952
|
+
fi
|
|
953
|
+
fi
|
|
954
|
+
|
|
955
|
+
while IFS= read -r line; do
|
|
956
|
+
[[ -n "$line" ]] || continue
|
|
957
|
+
if [[ "$line" == /wheels/*.whl ]]; then
|
|
958
|
+
has_wheel_root=1
|
|
959
|
+
printf '%s\n' "$line" >>"$input_file"
|
|
960
|
+
root_count=$((root_count + 1))
|
|
961
|
+
continue
|
|
962
|
+
fi
|
|
963
|
+
|
|
964
|
+
name="${line%%==*}"
|
|
965
|
+
base_name="${name%%[*}"
|
|
966
|
+
version="${line#*==}"
|
|
967
|
+
normalized_name="$(normalize_distribution_name "$base_name")"
|
|
968
|
+
display_name="$name"
|
|
969
|
+
spec="$line"
|
|
970
|
+
|
|
971
|
+
case "$mode" in
|
|
972
|
+
all)
|
|
973
|
+
spec="$name>=$version"
|
|
974
|
+
;;
|
|
975
|
+
release)
|
|
976
|
+
spec="$name>=$version,>=$target,<$release_upper"
|
|
977
|
+
;;
|
|
978
|
+
package)
|
|
979
|
+
if [[ "$normalized_name" == "$target_normalized" ]]; then
|
|
980
|
+
spec="$name>=$version"
|
|
981
|
+
found_target=1
|
|
982
|
+
fi
|
|
983
|
+
;;
|
|
984
|
+
exact)
|
|
985
|
+
if [[ "$normalized_name" == "$target_normalized" ]]; then
|
|
986
|
+
spec="$target"
|
|
987
|
+
display_name="$target_name"
|
|
988
|
+
found_target=1
|
|
989
|
+
fi
|
|
990
|
+
;;
|
|
991
|
+
esac
|
|
992
|
+
|
|
993
|
+
printf '%s\n' "$spec" >>"$input_file"
|
|
994
|
+
printf '%s\t%s\n' "$normalized_name" "$display_name" >>"$roots_file"
|
|
995
|
+
root_count=$((root_count + 1))
|
|
996
|
+
done < <(
|
|
997
|
+
awk '
|
|
998
|
+
{
|
|
999
|
+
line = $0
|
|
1000
|
+
sub(/^[[:space:]]+/, "", line)
|
|
1001
|
+
sub(/[[:space:]]+#.*$/, "", line)
|
|
1002
|
+
sub(/[[:space:]]+$/, "", line)
|
|
1003
|
+
if (line != "") {
|
|
1004
|
+
print line
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
' "$requirements_file"
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
if [[ "$root_count" -eq 0 ]]; then
|
|
1011
|
+
printf 'error: requirements file has no root requirements: %s\n' "$requirements_file" >&2
|
|
1012
|
+
return 1
|
|
1013
|
+
fi
|
|
1014
|
+
if [[ "$has_wheel_root" -eq 1 ]]; then
|
|
1015
|
+
if [[ "$mode" != all ]]; then
|
|
1016
|
+
printf 'error: targeted bundle upgrade requires package root requirements, but this bundle has wheel roots\n' >&2
|
|
1017
|
+
printf ' use package pins for index-managed upgrades, or run bundle upgrade with no arguments to refresh the wheelhouse\n' >&2
|
|
1018
|
+
return 1
|
|
1019
|
+
fi
|
|
1020
|
+
if [[ ! -s "$roots_file" ]]; then
|
|
1021
|
+
return 3
|
|
1022
|
+
fi
|
|
1023
|
+
printf 'error: cannot mix wheel roots and package roots for bundle upgrade\n' >&2
|
|
1024
|
+
printf ' use either package pins for index-managed upgrades or wheel paths for local artifact bundles\n' >&2
|
|
1025
|
+
return 1
|
|
1026
|
+
fi
|
|
1027
|
+
if [[ "$mode" == package || "$mode" == exact ]] && [[ "$found_target" -eq 0 ]]; then
|
|
1028
|
+
printf 'error: package is not a root requirement: %s\n' "$target" >&2
|
|
1029
|
+
printf ' use bundle add first, or run bundle list to see root requirements\n' >&2
|
|
1030
|
+
return 1
|
|
1031
|
+
fi
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
bundle_pypi_prepare_build_input() {
|
|
1035
|
+
local input_file="$1"
|
|
1036
|
+
local roots_file="$2"
|
|
1037
|
+
local root_count=0
|
|
1038
|
+
local line
|
|
1039
|
+
local name
|
|
1040
|
+
local base_name
|
|
1041
|
+
|
|
1042
|
+
require_file "$requirements_file"
|
|
1043
|
+
validate_requirements_file "$requirements_file"
|
|
1044
|
+
if requirements_has_absolute_paths "$requirements_file"; then
|
|
1045
|
+
printf 'error: prepare --pypi-only requires package pins, but requirements.txt contains absolute paths\n' >&2
|
|
1046
|
+
printf ' replace /wheels/*.whl or source paths with package==version pins\n' >&2
|
|
1047
|
+
return 1
|
|
1048
|
+
fi
|
|
1049
|
+
|
|
1050
|
+
while IFS= read -r line; do
|
|
1051
|
+
[[ -n "$line" ]] || continue
|
|
1052
|
+
name="${line%%==*}"
|
|
1053
|
+
base_name="${name%%[*}"
|
|
1054
|
+
printf '%s\n' "$name" >>"$input_file"
|
|
1055
|
+
printf '%s\t%s\n' "$(normalize_distribution_name "$base_name")" "$name" >>"$roots_file"
|
|
1056
|
+
root_count=$((root_count + 1))
|
|
1057
|
+
done < <(
|
|
1058
|
+
awk '
|
|
1059
|
+
{
|
|
1060
|
+
line = $0
|
|
1061
|
+
sub(/^[[:space:]]+/, "", line)
|
|
1062
|
+
sub(/[[:space:]]+#.*$/, "", line)
|
|
1063
|
+
sub(/[[:space:]]+$/, "", line)
|
|
1064
|
+
if (line != "") {
|
|
1065
|
+
print line
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
' "$requirements_file"
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
if [[ "$root_count" -eq 0 ]]; then
|
|
1072
|
+
printf 'error: requirements file has no root requirements: %s\n' "$requirements_file" >&2
|
|
1073
|
+
return 1
|
|
1074
|
+
fi
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
bundle_upgrade_resolve() {
|
|
1078
|
+
local input_file="$1"
|
|
1079
|
+
local roots_file="$2"
|
|
1080
|
+
local output_file="$3"
|
|
1081
|
+
local pypi_only="$4"
|
|
1082
|
+
local allow_pre="${5:-0}"
|
|
1083
|
+
local action_label="${6:-bundle upgrade}"
|
|
1084
|
+
local tmp_dir
|
|
1085
|
+
local image
|
|
1086
|
+
local docker_user
|
|
1087
|
+
local active_wheels_dir
|
|
1088
|
+
local parse_script
|
|
1089
|
+
local -a resolve_command
|
|
1090
|
+
local -a parse_command
|
|
1091
|
+
local -a wheel_mount_args=()
|
|
1092
|
+
local -a find_links_args=()
|
|
1093
|
+
local -a pre_args=()
|
|
1094
|
+
|
|
1095
|
+
tmp_dir="$(dirname "$input_file")"
|
|
1096
|
+
image="$(compose_env_value ARBITER_IMAGE python:3.11-slim)"
|
|
1097
|
+
docker_user="$(id -u):$(id -g)"
|
|
1098
|
+
active_wheels_dir="$(wheels_dir_path)"
|
|
1099
|
+
if [[ "$pypi_only" -eq 0 && -d "$active_wheels_dir" ]]; then
|
|
1100
|
+
wheel_mount_args=(-v "$active_wheels_dir:/wheels:ro")
|
|
1101
|
+
find_links_args=(--find-links /wheels)
|
|
1102
|
+
fi
|
|
1103
|
+
if [[ "$allow_pre" -eq 1 ]]; then
|
|
1104
|
+
pre_args=(--pre)
|
|
1105
|
+
fi
|
|
1106
|
+
require_docker_access
|
|
1107
|
+
|
|
1108
|
+
resolve_command=(
|
|
1109
|
+
docker run --rm
|
|
1110
|
+
--user "$docker_user"
|
|
1111
|
+
-v "$tmp_dir:/work"
|
|
1112
|
+
)
|
|
1113
|
+
if [[ "${#wheel_mount_args[@]}" -gt 0 ]]; then
|
|
1114
|
+
resolve_command+=("${wheel_mount_args[@]}")
|
|
1115
|
+
fi
|
|
1116
|
+
resolve_command+=(
|
|
1117
|
+
"$image"
|
|
1118
|
+
python -m pip --disable-pip-version-check install
|
|
1119
|
+
--dry-run --ignore-installed
|
|
1120
|
+
)
|
|
1121
|
+
if [[ "${#pre_args[@]}" -gt 0 ]]; then
|
|
1122
|
+
resolve_command+=("${pre_args[@]}")
|
|
1123
|
+
fi
|
|
1124
|
+
if [[ "${#find_links_args[@]}" -gt 0 ]]; then
|
|
1125
|
+
resolve_command+=("${find_links_args[@]}")
|
|
1126
|
+
fi
|
|
1127
|
+
resolve_command+=(
|
|
1128
|
+
--report /work/report.json
|
|
1129
|
+
-r /work/requirements.in
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
if ! "${resolve_command[@]}" >"$tmp_dir/pip-resolve.out" 2>&1; then
|
|
1133
|
+
cat "$tmp_dir/pip-resolve.out" >&2
|
|
1134
|
+
printf 'error: failed to resolve %s\n' "$action_label" >&2
|
|
1135
|
+
return 1
|
|
1136
|
+
fi
|
|
1137
|
+
|
|
1138
|
+
parse_script='import json
|
|
1139
|
+
import re
|
|
1140
|
+
import sys
|
|
1141
|
+
|
|
1142
|
+
def normalize(name):
|
|
1143
|
+
return re.sub(r"[-_.]+", "-", name).lower()
|
|
1144
|
+
|
|
1145
|
+
with open("/work/roots.tsv", encoding="utf-8") as handle:
|
|
1146
|
+
roots = [line.rstrip("\n").split("\t", 1) for line in handle if line.strip()]
|
|
1147
|
+
with open("/work/report.json", encoding="utf-8") as handle:
|
|
1148
|
+
report = json.load(handle)
|
|
1149
|
+
|
|
1150
|
+
versions = {}
|
|
1151
|
+
for entry in report.get("install", []):
|
|
1152
|
+
metadata = entry.get("metadata", {})
|
|
1153
|
+
name = metadata.get("name")
|
|
1154
|
+
version = metadata.get("version")
|
|
1155
|
+
if name and version:
|
|
1156
|
+
versions[normalize(name)] = version
|
|
1157
|
+
|
|
1158
|
+
missing = [display for normalized, display in roots if normalized not in versions]
|
|
1159
|
+
if missing:
|
|
1160
|
+
print("missing resolved root package versions: " + ", ".join(missing), file=sys.stderr)
|
|
1161
|
+
sys.exit(1)
|
|
1162
|
+
|
|
1163
|
+
with open("/work/requirements.out", "w", encoding="utf-8") as handle:
|
|
1164
|
+
for normalized, display in roots:
|
|
1165
|
+
handle.write(f"{display}=={versions[normalized]}\n")
|
|
1166
|
+
'
|
|
1167
|
+
parse_command=(
|
|
1168
|
+
docker run --rm
|
|
1169
|
+
--user "$docker_user"
|
|
1170
|
+
-v "$tmp_dir:/work"
|
|
1171
|
+
"$image"
|
|
1172
|
+
python -c "$parse_script"
|
|
1173
|
+
)
|
|
1174
|
+
"${parse_command[@]}"
|
|
1175
|
+
cp "$tmp_dir/requirements.out" "$output_file"
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
prepare_pypi_only_requirements() {
|
|
1179
|
+
local target_requirements_file="$1"
|
|
1180
|
+
local tmp_dir
|
|
1181
|
+
local input_file
|
|
1182
|
+
local roots_file
|
|
1183
|
+
local output_file
|
|
1184
|
+
|
|
1185
|
+
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/arbiter-pypi-prepare.XXXXXX")"
|
|
1186
|
+
input_file="$tmp_dir/requirements.in"
|
|
1187
|
+
roots_file="$tmp_dir/roots.tsv"
|
|
1188
|
+
output_file="$tmp_dir/requirements.resolved"
|
|
1189
|
+
|
|
1190
|
+
if ! bundle_pypi_prepare_build_input "$input_file" "$roots_file"; then
|
|
1191
|
+
rm -rf "$tmp_dir"
|
|
1192
|
+
return 1
|
|
1193
|
+
fi
|
|
1194
|
+
if ! bundle_upgrade_resolve "$input_file" "$roots_file" "$output_file" 1 1 "package-index preparation"; then
|
|
1195
|
+
rm -rf "$tmp_dir"
|
|
1196
|
+
return 1
|
|
1197
|
+
fi
|
|
1198
|
+
if ! validate_requirements_file "$output_file"; then
|
|
1199
|
+
printf 'error: resolved requirements are invalid; requirements unchanged: %s\n' "$requirements_file" >&2
|
|
1200
|
+
rm -rf "$tmp_dir"
|
|
1201
|
+
return 1
|
|
1202
|
+
fi
|
|
1203
|
+
|
|
1204
|
+
cp "$output_file" "$target_requirements_file"
|
|
1205
|
+
rm -rf "$tmp_dir"
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
prepare_pypi_only() {
|
|
1209
|
+
local target_wheels_dir="$1"
|
|
1210
|
+
local docker_user="$2"
|
|
1211
|
+
local image="$3"
|
|
1212
|
+
local tmp_dir
|
|
1213
|
+
local staged_requirements_file
|
|
1214
|
+
local staged_wheels_dir
|
|
1215
|
+
|
|
1216
|
+
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/arbiter-pypi-prepare-transaction.XXXXXX")"
|
|
1217
|
+
staged_requirements_file="$tmp_dir/requirements.txt"
|
|
1218
|
+
staged_wheels_dir="$tmp_dir/wheels"
|
|
1219
|
+
mkdir -p "$staged_wheels_dir"
|
|
1220
|
+
|
|
1221
|
+
if ! prepare_pypi_only_requirements "$staged_requirements_file"; then
|
|
1222
|
+
rm -rf "$tmp_dir"
|
|
1223
|
+
return 1
|
|
1224
|
+
fi
|
|
1225
|
+
if ! prepare_dependency_wheelhouse "$staged_requirements_file" "$staged_wheels_dir" "$docker_user" "$image" 1 1; then
|
|
1226
|
+
rm -rf "$tmp_dir"
|
|
1227
|
+
return 1
|
|
1228
|
+
fi
|
|
1229
|
+
if ! validate_dependency_wheelhouse "$staged_requirements_file" "$staged_wheels_dir" "$docker_user" "$image" 1; then
|
|
1230
|
+
rm -rf "$tmp_dir"
|
|
1231
|
+
return 1
|
|
1232
|
+
fi
|
|
1233
|
+
|
|
1234
|
+
replace_dependency_wheelhouse "$staged_wheels_dir" "$target_wheels_dir"
|
|
1235
|
+
if ! cmp -s "$requirements_file" "$staged_requirements_file"; then
|
|
1236
|
+
cp "$staged_requirements_file" "$requirements_file"
|
|
1237
|
+
printf 'updated requirements file from package index: %s\n' "$requirements_file"
|
|
1238
|
+
fi
|
|
1239
|
+
printf 'bundle prepare complete: %s\n' "$target_wheels_dir"
|
|
1240
|
+
rm -rf "$tmp_dir"
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
bundle_upgrade() {
|
|
1244
|
+
local target=""
|
|
1245
|
+
local pypi_only=0
|
|
1246
|
+
local tmp_dir
|
|
1247
|
+
local input_file
|
|
1248
|
+
local roots_file
|
|
1249
|
+
local output_file
|
|
1250
|
+
local before_file
|
|
1251
|
+
local after_file
|
|
1252
|
+
local build_status
|
|
1253
|
+
|
|
1254
|
+
while (($#)); do
|
|
1255
|
+
case "$1" in
|
|
1256
|
+
--pypi-only)
|
|
1257
|
+
pypi_only=1
|
|
1258
|
+
;;
|
|
1259
|
+
--)
|
|
1260
|
+
shift
|
|
1261
|
+
while (($#)); do
|
|
1262
|
+
if [[ -n "$target" ]]; then
|
|
1263
|
+
printf 'error: bundle upgrade accepts at most one package or release-line argument\n' >&2
|
|
1264
|
+
return 2
|
|
1265
|
+
fi
|
|
1266
|
+
target="$1"
|
|
1267
|
+
shift
|
|
1268
|
+
done
|
|
1269
|
+
break
|
|
1270
|
+
;;
|
|
1271
|
+
-*)
|
|
1272
|
+
printf 'error: unknown bundle upgrade option: %s\n' "$1" >&2
|
|
1273
|
+
return 2
|
|
1274
|
+
;;
|
|
1275
|
+
*)
|
|
1276
|
+
if [[ -n "$target" ]]; then
|
|
1277
|
+
printf 'error: bundle upgrade accepts at most one package or release-line argument\n' >&2
|
|
1278
|
+
return 2
|
|
1279
|
+
fi
|
|
1280
|
+
target="$1"
|
|
1281
|
+
;;
|
|
1282
|
+
esac
|
|
1283
|
+
shift
|
|
1284
|
+
done
|
|
1285
|
+
|
|
1286
|
+
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/arbiter-bundle-upgrade.XXXXXX")"
|
|
1287
|
+
input_file="$tmp_dir/requirements.in"
|
|
1288
|
+
roots_file="$tmp_dir/roots.tsv"
|
|
1289
|
+
output_file="$tmp_dir/requirements.resolved"
|
|
1290
|
+
before_file="$tmp_dir/before.tsv"
|
|
1291
|
+
after_file="$tmp_dir/after.tsv"
|
|
1292
|
+
bundle_write_upgrade_snapshot "$before_file"
|
|
1293
|
+
|
|
1294
|
+
if [[ "$pypi_only" -eq 0 ]]; then
|
|
1295
|
+
if ! bundle_refresh_repo_wheels "$tmp_dir"; then
|
|
1296
|
+
rm -rf "$tmp_dir"
|
|
1297
|
+
return 1
|
|
1298
|
+
fi
|
|
1299
|
+
fi
|
|
1300
|
+
|
|
1301
|
+
if bundle_upgrade_build_input "$target" "$input_file" "$roots_file"; then
|
|
1302
|
+
build_status=0
|
|
1303
|
+
else
|
|
1304
|
+
build_status="$?"
|
|
1305
|
+
if [[ "$build_status" -eq 3 ]]; then
|
|
1306
|
+
if ! prepare_quiet; then
|
|
1307
|
+
rm -rf "$tmp_dir"
|
|
1308
|
+
return 1
|
|
1309
|
+
fi
|
|
1310
|
+
bundle_write_upgrade_snapshot "$after_file"
|
|
1311
|
+
bundle_print_upgrade_summary "$before_file" "$after_file"
|
|
1312
|
+
rm -rf "$tmp_dir"
|
|
1313
|
+
return
|
|
1314
|
+
fi
|
|
1315
|
+
rm -rf "$tmp_dir"
|
|
1316
|
+
return 1
|
|
1317
|
+
fi
|
|
1318
|
+
if ! bundle_upgrade_resolve "$input_file" "$roots_file" "$output_file" "$pypi_only"; then
|
|
1319
|
+
rm -rf "$tmp_dir"
|
|
1320
|
+
return 1
|
|
1321
|
+
fi
|
|
1322
|
+
if ! validate_requirements_file "$output_file"; then
|
|
1323
|
+
printf 'error: resolved requirements are invalid; requirements unchanged: %s\n' "$requirements_file" >&2
|
|
1324
|
+
rm -rf "$tmp_dir"
|
|
1325
|
+
return 1
|
|
1326
|
+
fi
|
|
1327
|
+
|
|
1328
|
+
if cmp -s "$requirements_file" "$output_file"; then
|
|
1329
|
+
:
|
|
1330
|
+
else
|
|
1331
|
+
cp "$output_file" "$requirements_file"
|
|
1332
|
+
fi
|
|
1333
|
+
if ! prepare_quiet; then
|
|
1334
|
+
rm -rf "$tmp_dir"
|
|
1335
|
+
return 1
|
|
1336
|
+
fi
|
|
1337
|
+
bundle_write_upgrade_snapshot "$after_file"
|
|
1338
|
+
bundle_print_upgrade_summary "$before_file" "$after_file"
|
|
1339
|
+
rm -rf "$tmp_dir"
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
env_file_value() {
|
|
1343
|
+
local env_path="$1"
|
|
1344
|
+
local key="$2"
|
|
1345
|
+
[[ -f "$env_path" ]] || return 1
|
|
1346
|
+
|
|
1347
|
+
awk -v key="$key" '
|
|
1348
|
+
$0 ~ "^[[:space:]]*#" { next }
|
|
1349
|
+
index($0, key "=") == 1 {
|
|
1350
|
+
print substr($0, length(key) + 2)
|
|
1351
|
+
found = 1
|
|
1352
|
+
exit
|
|
1353
|
+
}
|
|
1354
|
+
END {
|
|
1355
|
+
if (!found) {
|
|
1356
|
+
exit 1
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
' "$env_path"
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
set_env_file_value() {
|
|
1363
|
+
local env_path="$1"
|
|
1364
|
+
local key="$2"
|
|
1365
|
+
local value="$3"
|
|
1366
|
+
local tmp_path
|
|
1367
|
+
|
|
1368
|
+
require_file "$env_path"
|
|
1369
|
+
tmp_path="$(mktemp "${TMPDIR:-/tmp}/arbiter-env.XXXXXX")"
|
|
1370
|
+
awk -v key="$key" -v value="$value" '
|
|
1371
|
+
index($0, key "=") == 1 {
|
|
1372
|
+
print key "=" value
|
|
1373
|
+
found = 1
|
|
1374
|
+
next
|
|
1375
|
+
}
|
|
1376
|
+
{ print }
|
|
1377
|
+
END {
|
|
1378
|
+
if (!found) {
|
|
1379
|
+
print key "=" value
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
' "$env_path" >"$tmp_path"
|
|
1383
|
+
mv "$tmp_path" "$env_path"
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
compose_env_value() {
|
|
1387
|
+
local key="$1"
|
|
1388
|
+
local default="$2"
|
|
1389
|
+
|
|
1390
|
+
env_file_value "$compose_env_file" "$key" || printf '%s\n' "$default"
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
set_compose_env_value() {
|
|
1394
|
+
local key="$1"
|
|
1395
|
+
local value="$2"
|
|
1396
|
+
|
|
1397
|
+
set_env_file_value "$compose_env_file" "$key" "$value"
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
container_name_value() {
|
|
1401
|
+
compose_env_value ARBITER_CONTAINER_NAME arbiter-staging
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
compose_project_name() {
|
|
1405
|
+
local name="${COMPOSE_PROJECT_NAME:-}"
|
|
1406
|
+
|
|
1407
|
+
if [[ -z "$name" ]]; then
|
|
1408
|
+
name="$(basename -- "$deploy_dir")"
|
|
1409
|
+
fi
|
|
1410
|
+
printf '%s\n' "$name" | tr '[:upper:]' '[:lower:]'
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
deploy_path_from_compose_value() {
|
|
1414
|
+
local value="$1"
|
|
1415
|
+
|
|
1416
|
+
if [[ "$value" = /* ]]; then
|
|
1417
|
+
printf '%s\n' "$value"
|
|
1418
|
+
else
|
|
1419
|
+
printf '%s/%s\n' "$deploy_dir" "${value#./}"
|
|
1420
|
+
fi
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
config_dir_path() {
|
|
1424
|
+
deploy_path_from_compose_value "$(compose_env_value ARBITER_CONFIG_DIR ./conf)"
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
wheels_dir_path() {
|
|
1428
|
+
deploy_path_from_compose_value "$(compose_env_value ARBITER_WHEELS_DIR ./wheels)"
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
plugin_data_dir_path() {
|
|
1432
|
+
deploy_path_from_compose_value "$(compose_env_value ARBITER_PLUGIN_DATA_DIR ./data/plugins)"
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
config_name_value() {
|
|
1436
|
+
compose_env_value ARBITER_CONFIG_NAME arbiter-server
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
app_env_path() {
|
|
1440
|
+
deploy_path_from_compose_value "$(compose_env_value ARBITER_APP_ENV_FILE ./conf/.env)"
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
server_host_url_value() {
|
|
1444
|
+
local host
|
|
1445
|
+
|
|
1446
|
+
host="$(compose_env_value ARBITER_HOST_BIND 127.0.0.1)"
|
|
1447
|
+
case "$host" in
|
|
1448
|
+
0.0.0.0)
|
|
1449
|
+
printf '127.0.0.1\n'
|
|
1450
|
+
;;
|
|
1451
|
+
*:*)
|
|
1452
|
+
printf '[%s]\n' "$host"
|
|
1453
|
+
;;
|
|
1454
|
+
*)
|
|
1455
|
+
printf '%s\n' "$host"
|
|
1456
|
+
;;
|
|
1457
|
+
esac
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
mcp_url_value() {
|
|
1461
|
+
printf 'http://%s:%s/mcp\n' \
|
|
1462
|
+
"$(server_host_url_value)" \
|
|
1463
|
+
"$(compose_env_value ARBITER_HOST_PORT 18025)"
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
print_mcp_url_value() {
|
|
1467
|
+
local url="$1"
|
|
1468
|
+
|
|
1469
|
+
printf ' '
|
|
1470
|
+
doctor_prefix 32 "✔"
|
|
1471
|
+
printf ' MCP URL: '
|
|
1472
|
+
doctor_prefix 94 "$url"
|
|
1473
|
+
printf '\n'
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
print_mcp_url() {
|
|
1477
|
+
print_mcp_url_value "$(mcp_url_value)"
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
print_staging_port_note() {
|
|
1481
|
+
local host_port
|
|
1482
|
+
|
|
1483
|
+
deployment_is_staged || return 0
|
|
1484
|
+
host_port="$(compose_env_value ARBITER_HOST_PORT 18025)"
|
|
1485
|
+
[[ "$host_port" != 8025 ]] || return 0
|
|
1486
|
+
printf ' '
|
|
1487
|
+
doctor_prefix 32 "✔"
|
|
1488
|
+
printf ' Staging MCP port: 8025 -> %s to prevent collision\n' "$host_port"
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
resolve_arbiter_client_command() {
|
|
1492
|
+
local checkout_client
|
|
1493
|
+
|
|
1494
|
+
if [[ -n "${ARBITER_CLIENT_COMMAND:-}" ]]; then
|
|
1495
|
+
command -v "$ARBITER_CLIENT_COMMAND"
|
|
1496
|
+
return
|
|
1497
|
+
fi
|
|
1498
|
+
if command -v arbiter >/dev/null; then
|
|
1499
|
+
command -v arbiter
|
|
1500
|
+
return
|
|
1501
|
+
fi
|
|
1502
|
+
checkout_client="$deploy_dir/../skill/bin/arbiter"
|
|
1503
|
+
if [[ -x "$checkout_client" ]]; then
|
|
1504
|
+
printf '%s\n' "$checkout_client"
|
|
1505
|
+
return
|
|
1506
|
+
fi
|
|
1507
|
+
return 1
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
test_server_url() {
|
|
1511
|
+
local url="$1"
|
|
1512
|
+
local client_command
|
|
1513
|
+
local output_file
|
|
1514
|
+
local timeout
|
|
1515
|
+
local start_time
|
|
1516
|
+
|
|
1517
|
+
if ! client_command="$(resolve_arbiter_client_command)"; then
|
|
1518
|
+
printf 'error: Arbiter client command not found\n' >&2
|
|
1519
|
+
printf ' activate the environment that provides arbiter, set ARBITER_CLIENT_COMMAND,\n' >&2
|
|
1520
|
+
printf ' or run from a checkout with ../skill/bin/arbiter available\n' >&2
|
|
1521
|
+
return 1
|
|
1522
|
+
fi
|
|
1523
|
+
|
|
1524
|
+
output_file="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-test.XXXXXX")"
|
|
1525
|
+
timeout="${ARBITER_TEST_TIMEOUT:-60}"
|
|
1526
|
+
start_time="$SECONDS"
|
|
1527
|
+
while true; do
|
|
1528
|
+
if "$client_command" mcp call version_info "arbiter.mcp_url=$url" >"$output_file" 2>&1; then
|
|
1529
|
+
rm -f "$output_file"
|
|
1530
|
+
printf ' '
|
|
1531
|
+
doctor_prefix 32 "✔"
|
|
1532
|
+
printf ' MCP test: '
|
|
1533
|
+
doctor_prefix 94 "$url"
|
|
1534
|
+
printf '\n'
|
|
1535
|
+
return 0
|
|
1536
|
+
fi
|
|
1537
|
+
if ((SECONDS - start_time >= timeout)); then
|
|
1538
|
+
break
|
|
1539
|
+
fi
|
|
1540
|
+
sleep 1
|
|
1541
|
+
done
|
|
1542
|
+
rm -f "$output_file"
|
|
1543
|
+
printf ' '
|
|
1544
|
+
doctor_prefix 31 "✘"
|
|
1545
|
+
printf ' MCP test: '
|
|
1546
|
+
doctor_prefix 94 "$url"
|
|
1547
|
+
printf '\n'
|
|
1548
|
+
return 1
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
test_server() {
|
|
1552
|
+
test_server_url "$(mcp_url_value)"
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
config_main_file() {
|
|
1556
|
+
printf '%s/%s.yaml\n' "$(config_dir_path)" "$(config_name_value)"
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
sync_env() {
|
|
1560
|
+
local active_config_dir
|
|
1561
|
+
local active_config_name
|
|
1562
|
+
|
|
1563
|
+
active_config_dir="$(config_dir_path)"
|
|
1564
|
+
active_config_name="$(config_name_value)"
|
|
1565
|
+
require_dir "$active_config_dir"
|
|
1566
|
+
require_file "$(config_main_file)"
|
|
1567
|
+
arbiter-server --config-dir "$active_config_dir" --config-name "$active_config_name" env bootstrap
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
run_editor() {
|
|
1571
|
+
local path="$1"
|
|
1572
|
+
local -a editor=()
|
|
1573
|
+
|
|
1574
|
+
if [[ -n "${ARBITER_EDITOR:-}" ]]; then
|
|
1575
|
+
read -r -a editor <<<"$ARBITER_EDITOR"
|
|
1576
|
+
elif [[ -n "${EDITOR:-}" ]]; then
|
|
1577
|
+
read -r -a editor <<<"$EDITOR"
|
|
1578
|
+
else
|
|
1579
|
+
editor=(vi)
|
|
1580
|
+
fi
|
|
1581
|
+
|
|
1582
|
+
"${editor[@]}" "$path"
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
inspect_existing_container() {
|
|
1586
|
+
local container_name="$1"
|
|
1587
|
+
local output_file="$2"
|
|
1588
|
+
|
|
1589
|
+
docker inspect "$container_name" --format \
|
|
1590
|
+
'name={{.Name}}
|
|
1591
|
+
project={{index .Config.Labels "com.docker.compose.project"}}
|
|
1592
|
+
service={{index .Config.Labels "com.docker.compose.service"}}
|
|
1593
|
+
config_files={{index .Config.Labels "com.docker.compose.project.config_files"}}
|
|
1594
|
+
working_dir={{index .Config.Labels "com.docker.compose.project.working_dir"}}
|
|
1595
|
+
oneoff={{index .Config.Labels "com.docker.compose.oneoff"}}
|
|
1596
|
+
image={{.Config.Image}}
|
|
1597
|
+
created={{.Created}}
|
|
1598
|
+
status={{.State.Status}}
|
|
1599
|
+
restart={{.HostConfig.RestartPolicy.Name}}' >"$output_file" 2>/dev/null
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
container_info_value() {
|
|
1603
|
+
local info_file="$1"
|
|
1604
|
+
local key="$2"
|
|
1605
|
+
|
|
1606
|
+
awk -F= -v key="$key" '$1 == key { sub(/^[^=]*=/, ""); print; exit }' "$info_file"
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
print_container_owner_details() {
|
|
1610
|
+
local info_file="$1"
|
|
1611
|
+
local prefix="$2"
|
|
1612
|
+
local project
|
|
1613
|
+
local service
|
|
1614
|
+
local config_files
|
|
1615
|
+
local working_dir
|
|
1616
|
+
local image
|
|
1617
|
+
local status
|
|
1618
|
+
local restart
|
|
1619
|
+
|
|
1620
|
+
project="$(container_info_value "$info_file" project)"
|
|
1621
|
+
service="$(container_info_value "$info_file" service)"
|
|
1622
|
+
config_files="$(container_info_value "$info_file" config_files)"
|
|
1623
|
+
working_dir="$(container_info_value "$info_file" working_dir)"
|
|
1624
|
+
image="$(container_info_value "$info_file" image)"
|
|
1625
|
+
status="$(container_info_value "$info_file" status)"
|
|
1626
|
+
restart="$(container_info_value "$info_file" restart)"
|
|
1627
|
+
|
|
1628
|
+
if [[ -n "$project" ]]; then
|
|
1629
|
+
printf '%sowner compose project: %s\n' "$prefix" "$project"
|
|
1630
|
+
[[ -n "$service" ]] && printf '%sowner compose service: %s\n' "$prefix" "$service"
|
|
1631
|
+
[[ -n "$working_dir" ]] && printf '%sowner deployment dir: %s\n' "$prefix" "$working_dir"
|
|
1632
|
+
[[ -n "$config_files" ]] && printf '%sowner compose file: %s\n' "$prefix" "$config_files"
|
|
1633
|
+
else
|
|
1634
|
+
printf '%sowner: not labeled as a Docker Compose container\n' "$prefix"
|
|
1635
|
+
fi
|
|
1636
|
+
[[ -n "$image" ]] && printf '%simage: %s\n' "$prefix" "$image"
|
|
1637
|
+
[[ -n "$status" ]] && printf '%sstatus: %s\n' "$prefix" "$status"
|
|
1638
|
+
[[ -n "$restart" ]] && printf '%srestart policy: %s\n' "$prefix" "$restart"
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
check_container_name_owner() {
|
|
1642
|
+
local mode="$1"
|
|
1643
|
+
local container_name
|
|
1644
|
+
local expected_project
|
|
1645
|
+
local info_file
|
|
1646
|
+
local actual_project
|
|
1647
|
+
local actual_config_files
|
|
1648
|
+
local actual_working_dir
|
|
1649
|
+
|
|
1650
|
+
container_name="$(container_name_value)"
|
|
1651
|
+
expected_project="$(compose_project_name)"
|
|
1652
|
+
info_file="$(mktemp "${TMPDIR:-/tmp}/arbiter-container-owner.XXXXXX")"
|
|
1653
|
+
|
|
1654
|
+
if ! inspect_existing_container "$container_name" "$info_file"; then
|
|
1655
|
+
rm -f "$info_file"
|
|
1656
|
+
return 0
|
|
1657
|
+
fi
|
|
1658
|
+
|
|
1659
|
+
actual_project="$(container_info_value "$info_file" project)"
|
|
1660
|
+
actual_config_files="$(container_info_value "$info_file" config_files)"
|
|
1661
|
+
actual_working_dir="$(container_info_value "$info_file" working_dir)"
|
|
1662
|
+
|
|
1663
|
+
if [[ "$actual_project" == "$expected_project" && "$actual_working_dir" == "$deploy_dir" ]]; then
|
|
1664
|
+
rm -f "$info_file"
|
|
1665
|
+
return 0
|
|
1666
|
+
fi
|
|
1667
|
+
if [[ "$actual_project" == "$expected_project" && "$actual_config_files" == "$compose_file" ]]; then
|
|
1668
|
+
rm -f "$info_file"
|
|
1669
|
+
return 0
|
|
1670
|
+
fi
|
|
1671
|
+
|
|
1672
|
+
case "$mode" in
|
|
1673
|
+
error)
|
|
1674
|
+
printf 'error: container name is already owned by another deployment: %s\n' "$container_name" >&2
|
|
1675
|
+
printf ' this deployment dir: %s\n' "$deploy_dir" >&2
|
|
1676
|
+
printf ' this compose project: %s\n' "$expected_project" >&2
|
|
1677
|
+
print_container_owner_details "$info_file" ' ' >&2
|
|
1678
|
+
printf ' note: docker ps shows only running containers; use docker ps -a --filter name=^/%s$ to see stopped containers\n' "$container_name" >&2
|
|
1679
|
+
printf ' stop it with the owning deployment helper, or set ARBITER_CONTAINER_NAME in %s\n' "$compose_env_file" >&2
|
|
1680
|
+
;;
|
|
1681
|
+
warn)
|
|
1682
|
+
printf 'container name in use by another deployment: %s\n' "$container_name"
|
|
1683
|
+
printf ' this deployment dir: %s\n' "$deploy_dir"
|
|
1684
|
+
printf ' this compose project: %s\n' "$expected_project"
|
|
1685
|
+
print_container_owner_details "$info_file" ' '
|
|
1686
|
+
;;
|
|
1687
|
+
esac
|
|
1688
|
+
|
|
1689
|
+
rm -f "$info_file"
|
|
1690
|
+
return 1
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
deployment_is_staged() {
|
|
1694
|
+
grep -q 'arbiter\.deployment_scope=staged' "$compose_file"
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
cidr_range() {
|
|
1698
|
+
local cidr="$1"
|
|
1699
|
+
local ip
|
|
1700
|
+
local prefix
|
|
1701
|
+
local a
|
|
1702
|
+
local b
|
|
1703
|
+
local c
|
|
1704
|
+
local d
|
|
1705
|
+
local ip_int
|
|
1706
|
+
local host_count
|
|
1707
|
+
local mask
|
|
1708
|
+
local start
|
|
1709
|
+
local end
|
|
1710
|
+
|
|
1711
|
+
[[ "$cidr" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/([0-9]+)$ ]] || return 1
|
|
1712
|
+
ip="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}.${BASH_REMATCH[4]}"
|
|
1713
|
+
prefix="${BASH_REMATCH[5]}"
|
|
1714
|
+
IFS=. read -r a b c d <<<"$ip"
|
|
1715
|
+
((a >= 0 && a <= 255 && b >= 0 && b <= 255 && c >= 0 && c <= 255 && d >= 0 && d <= 255)) || return 1
|
|
1716
|
+
((prefix >= 0 && prefix <= 32)) || return 1
|
|
1717
|
+
|
|
1718
|
+
ip_int=$(((a << 24) + (b << 16) + (c << 8) + d))
|
|
1719
|
+
host_count=$((1 << (32 - prefix)))
|
|
1720
|
+
mask=$((0xFFFFFFFF ^ (host_count - 1)))
|
|
1721
|
+
start=$((ip_int & mask))
|
|
1722
|
+
end=$((start + host_count - 1))
|
|
1723
|
+
printf '%s %s\n' "$start" "$end"
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
cidr_overlaps() {
|
|
1727
|
+
local left="$1"
|
|
1728
|
+
local right="$2"
|
|
1729
|
+
local left_range
|
|
1730
|
+
local right_range
|
|
1731
|
+
local left_start
|
|
1732
|
+
local left_end
|
|
1733
|
+
local right_start
|
|
1734
|
+
local right_end
|
|
1735
|
+
|
|
1736
|
+
left_range="$(cidr_range "$left")" || return 1
|
|
1737
|
+
right_range="$(cidr_range "$right")" || return 1
|
|
1738
|
+
read -r left_start left_end <<<"$left_range"
|
|
1739
|
+
read -r right_start right_end <<<"$right_range"
|
|
1740
|
+
((left_start <= right_end && right_start <= left_end))
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
docker_network_subnet_lines() {
|
|
1744
|
+
local network_ids
|
|
1745
|
+
|
|
1746
|
+
network_ids="$(docker network ls -q)" || return 1
|
|
1747
|
+
[[ -n "$network_ids" ]] || return 0
|
|
1748
|
+
docker network inspect $network_ids --format '{{.Name}} {{range .IPAM.Config}}{{.Subnet}} {{end}}' 2>/dev/null
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
docker_subnet_conflict() {
|
|
1752
|
+
local candidate_subnet="$1"
|
|
1753
|
+
local configured_network="$2"
|
|
1754
|
+
local network_line
|
|
1755
|
+
local network_name
|
|
1756
|
+
local network_subnets
|
|
1757
|
+
local network_subnet
|
|
1758
|
+
|
|
1759
|
+
while IFS= read -r network_line; do
|
|
1760
|
+
[[ -n "$network_line" ]] || continue
|
|
1761
|
+
network_name="${network_line%% *}"
|
|
1762
|
+
network_subnets="${network_line#* }"
|
|
1763
|
+
[[ "$network_subnets" != "$network_line" ]] || continue
|
|
1764
|
+
[[ "$network_name" != "$configured_network" ]] || continue
|
|
1765
|
+
for network_subnet in $network_subnets; do
|
|
1766
|
+
[[ "$network_subnet" == */* ]] || continue
|
|
1767
|
+
if cidr_overlaps "$candidate_subnet" "$network_subnet"; then
|
|
1768
|
+
return 0
|
|
1769
|
+
fi
|
|
1770
|
+
done
|
|
1771
|
+
done < <(docker_network_subnet_lines)
|
|
1772
|
+
|
|
1773
|
+
return 1
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
staging_subnet_candidates() {
|
|
1777
|
+
local third_octet
|
|
1778
|
+
|
|
1779
|
+
printf '172.31.251.0/24\n'
|
|
1780
|
+
for third_octet in {200..239}; do
|
|
1781
|
+
printf '10.213.%s.0/24\n' "$third_octet"
|
|
1782
|
+
done
|
|
1783
|
+
for third_octet in {200..239}; do
|
|
1784
|
+
printf '192.168.%s.0/24\n' "$third_octet"
|
|
1785
|
+
done
|
|
1786
|
+
for third_octet in {240..249}; do
|
|
1787
|
+
printf '172.31.%s.0/24\n' "$third_octet"
|
|
1788
|
+
done
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
ensure_staging_subnet_available() {
|
|
1792
|
+
local configured_network
|
|
1793
|
+
local configured_subnet
|
|
1794
|
+
local candidate_subnet
|
|
1795
|
+
|
|
1796
|
+
deployment_is_staged || return 0
|
|
1797
|
+
configured_network="$(compose_env_value ARBITER_DOCKER_NETWORK_NAME arbiter-staging)"
|
|
1798
|
+
configured_subnet="$(compose_env_value ARBITER_DOCKER_SUBNET 172.31.251.0/24)"
|
|
1799
|
+
[[ -n "$configured_subnet" ]] || return 0
|
|
1800
|
+
|
|
1801
|
+
if ! docker_subnet_conflict "$configured_subnet" "$configured_network"; then
|
|
1802
|
+
return 0
|
|
1803
|
+
fi
|
|
1804
|
+
|
|
1805
|
+
while IFS= read -r candidate_subnet; do
|
|
1806
|
+
[[ -n "$candidate_subnet" ]] || continue
|
|
1807
|
+
if ! docker_subnet_conflict "$candidate_subnet" "$configured_network"; then
|
|
1808
|
+
set_compose_env_value ARBITER_DOCKER_SUBNET "$candidate_subnet"
|
|
1809
|
+
printf 'updated staging Docker subnet: %s -> %s\n' "$configured_subnet" "$candidate_subnet"
|
|
1810
|
+
return 0
|
|
1811
|
+
fi
|
|
1812
|
+
done < <(staging_subnet_candidates)
|
|
1813
|
+
|
|
1814
|
+
printf 'error: could not find an unused staging Docker subnet\n' >&2
|
|
1815
|
+
return 1
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
compose() {
|
|
1819
|
+
local active_config_dir
|
|
1820
|
+
|
|
1821
|
+
require_file "$compose_file"
|
|
1822
|
+
active_config_dir="$(config_dir_path)"
|
|
1823
|
+
require_dir "$active_config_dir"
|
|
1824
|
+
require_file "$(config_main_file)"
|
|
1825
|
+
require_file "$(app_env_path)"
|
|
1826
|
+
require_file "$compose_env_file"
|
|
1827
|
+
require_file "$requirements_file"
|
|
1828
|
+
validate_requirements_file "$requirements_file"
|
|
1829
|
+
if [[ "${1:-}" == up ]]; then
|
|
1830
|
+
ensure_plugin_data_dir_ready
|
|
1831
|
+
fi
|
|
1832
|
+
require_docker_access
|
|
1833
|
+
if [[ "${1:-}" == up ]]; then
|
|
1834
|
+
ensure_staging_subnet_available
|
|
1835
|
+
check_container_name_owner error
|
|
1836
|
+
fi
|
|
1837
|
+
|
|
1838
|
+
cd "$deploy_dir"
|
|
1839
|
+
if [[ -f "$compose_override_file" ]]; then
|
|
1840
|
+
docker compose --env-file "$compose_env_file" -f "$compose_file" -f "$compose_override_file" "$@"
|
|
1841
|
+
else
|
|
1842
|
+
docker compose --env-file "$compose_env_file" -f "$compose_file" "$@"
|
|
1843
|
+
fi
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
docker_access_error_matches_permission() {
|
|
1847
|
+
grep -Eiq 'permission denied|permission.*docker|docker.*permission' "$1"
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
print_docker_access_error() {
|
|
1851
|
+
local output_file="$1"
|
|
1852
|
+
local user_name
|
|
1853
|
+
local groups_line
|
|
1854
|
+
|
|
1855
|
+
user_name="$(id -un 2>/dev/null || printf '%s' "${USER:-unknown}")"
|
|
1856
|
+
groups_line="$(id -nG 2>/dev/null || true)"
|
|
1857
|
+
|
|
1858
|
+
printf 'error: Docker daemon is not accessible by user %s\n' "$user_name" >&2
|
|
1859
|
+
if [[ -n "$groups_line" ]]; then
|
|
1860
|
+
printf ' current groups: %s\n' "$groups_line" >&2
|
|
1861
|
+
fi
|
|
1862
|
+
printf ' run as a user with Docker access, for example:\n' >&2
|
|
1863
|
+
printf ' sudo usermod -aG docker %s\n' "$user_name" >&2
|
|
1864
|
+
printf ' then log out and back in so group membership is refreshed\n' >&2
|
|
1865
|
+
printf ' alternatively run this command with sudo or use the installed systemd service\n' >&2
|
|
1866
|
+
if [[ -s "$output_file" ]]; then
|
|
1867
|
+
printf ' docker said: %s\n' "$(tr '\n' ' ' <"$output_file" | sed 's/[[:space:]]*$//')" >&2
|
|
1868
|
+
fi
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
require_docker_access() {
|
|
1872
|
+
local output_file
|
|
1873
|
+
|
|
1874
|
+
if ! command -v docker >/dev/null; then
|
|
1875
|
+
printf 'error: docker command not found\n' >&2
|
|
1876
|
+
return 1
|
|
1877
|
+
fi
|
|
1878
|
+
|
|
1879
|
+
output_file="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-access.XXXXXX")"
|
|
1880
|
+
if docker info >/dev/null 2>"$output_file"; then
|
|
1881
|
+
rm -f "$output_file"
|
|
1882
|
+
return 0
|
|
1883
|
+
fi
|
|
1884
|
+
|
|
1885
|
+
if docker_access_error_matches_permission "$output_file"; then
|
|
1886
|
+
print_docker_access_error "$output_file"
|
|
1887
|
+
else
|
|
1888
|
+
cat "$output_file" >&2
|
|
1889
|
+
printf 'error: Docker daemon is not available\n' >&2
|
|
1890
|
+
fi
|
|
1891
|
+
rm -f "$output_file"
|
|
1892
|
+
return 1
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
doctor_check_docker_access() {
|
|
1896
|
+
local output_file
|
|
1897
|
+
|
|
1898
|
+
if ! command -v docker >/dev/null; then
|
|
1899
|
+
doctor_fail "docker command not found"
|
|
1900
|
+
return
|
|
1901
|
+
fi
|
|
1902
|
+
|
|
1903
|
+
output_file="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-access.XXXXXX")"
|
|
1904
|
+
if docker info >/dev/null 2>"$output_file"; then
|
|
1905
|
+
doctor_ok "Docker daemon is accessible"
|
|
1906
|
+
elif docker_access_error_matches_permission "$output_file"; then
|
|
1907
|
+
doctor_fail "Docker daemon is not accessible by this user"
|
|
1908
|
+
doctor_detail "run as a user with Docker access, or add this user to the docker group and log out/back in"
|
|
1909
|
+
else
|
|
1910
|
+
doctor_fail "Docker daemon is not available"
|
|
1911
|
+
if [[ -s "$output_file" ]]; then
|
|
1912
|
+
doctor_detail "$(tr '\n' ' ' <"$output_file" | sed 's/[[:space:]]*$//')"
|
|
1913
|
+
fi
|
|
1914
|
+
fi
|
|
1915
|
+
rm -f "$output_file"
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
doctor_check_container_name_owner() {
|
|
1919
|
+
local container_name
|
|
1920
|
+
|
|
1921
|
+
if ! command -v docker >/dev/null || ! docker info >/dev/null 2>&1; then
|
|
1922
|
+
return
|
|
1923
|
+
fi
|
|
1924
|
+
|
|
1925
|
+
container_name="$(container_name_value)"
|
|
1926
|
+
if check_container_name_owner quiet; then
|
|
1927
|
+
doctor_ok "container name is not owned by another deployment: $container_name"
|
|
1928
|
+
else
|
|
1929
|
+
doctor_warn "container name is in use by another deployment: $container_name"
|
|
1930
|
+
check_container_name_owner warn || true
|
|
1931
|
+
fi
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
manifest_compose_hash() {
|
|
1935
|
+
local manifest_file="$deploy_dir/.arbiter-deploy.json"
|
|
1936
|
+
|
|
1937
|
+
[[ -f "$manifest_file" ]] || return 1
|
|
1938
|
+
awk '
|
|
1939
|
+
/"compose.yaml"/ { in_compose = 1 }
|
|
1940
|
+
in_compose && /"sha256"/ {
|
|
1941
|
+
line = $0
|
|
1942
|
+
sub(/^.*"sha256"[[:space:]]*:[[:space:]]*"/, "", line)
|
|
1943
|
+
sub(/".*$/, "", line)
|
|
1944
|
+
print line
|
|
1945
|
+
found = 1
|
|
1946
|
+
exit
|
|
1947
|
+
}
|
|
1948
|
+
END { exit found ? 0 : 1 }
|
|
1949
|
+
' "$manifest_file"
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
file_sha256() {
|
|
1953
|
+
local path="$1"
|
|
1954
|
+
|
|
1955
|
+
if command -v sha256sum >/dev/null; then
|
|
1956
|
+
sha256sum "$path" | awk '{print $1}'
|
|
1957
|
+
else
|
|
1958
|
+
shasum -a 256 "$path" | awk '{print $1}'
|
|
1959
|
+
fi
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
compose_file_matches_manifest() {
|
|
1963
|
+
local expected_hash
|
|
1964
|
+
local current_hash
|
|
1965
|
+
|
|
1966
|
+
require_file "$compose_file"
|
|
1967
|
+
expected_hash="$(manifest_compose_hash)" || return 1
|
|
1968
|
+
[[ -n "$expected_hash" ]] || return 1
|
|
1969
|
+
if ! command -v sha256sum >/dev/null && ! command -v shasum >/dev/null; then
|
|
1970
|
+
return 1
|
|
1971
|
+
fi
|
|
1972
|
+
current_hash="$(file_sha256 "$compose_file")"
|
|
1973
|
+
[[ "$current_hash" == "$expected_hash" ]]
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
compose_manifest_mismatch_reason() {
|
|
1977
|
+
local manifest_file="$deploy_dir/.arbiter-deploy.json"
|
|
1978
|
+
local expected_hash
|
|
1979
|
+
local current_hash
|
|
1980
|
+
|
|
1981
|
+
if [[ ! -f "$manifest_file" ]]; then
|
|
1982
|
+
printf 'deployment manifest is missing'
|
|
1983
|
+
return
|
|
1984
|
+
fi
|
|
1985
|
+
expected_hash="$(manifest_compose_hash || true)"
|
|
1986
|
+
if [[ -z "$expected_hash" ]]; then
|
|
1987
|
+
printf 'compose.yaml is not recorded in the deployment manifest'
|
|
1988
|
+
return
|
|
1989
|
+
fi
|
|
1990
|
+
if ! command -v sha256sum >/dev/null && ! command -v shasum >/dev/null; then
|
|
1991
|
+
printf 'sha256sum or shasum is not available'
|
|
1992
|
+
return
|
|
1993
|
+
fi
|
|
1994
|
+
current_hash="$(file_sha256 "$compose_file")"
|
|
1995
|
+
if [[ "$current_hash" != "$expected_hash" ]]; then
|
|
1996
|
+
printf 'compose.yaml has local edits'
|
|
1997
|
+
return
|
|
1998
|
+
fi
|
|
1999
|
+
printf 'compose.yaml manifest ownership could not be verified'
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
args_include() {
|
|
2003
|
+
local expected="$1"
|
|
2004
|
+
shift
|
|
2005
|
+
local arg
|
|
2006
|
+
|
|
2007
|
+
for arg in "$@"; do
|
|
2008
|
+
[[ "$arg" == "$expected" ]] && return 0
|
|
2009
|
+
done
|
|
2010
|
+
return 1
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
args_array_include() {
|
|
2014
|
+
local expected="$1"
|
|
2015
|
+
shift
|
|
2016
|
+
|
|
2017
|
+
if [[ "$#" -eq 0 ]]; then
|
|
2018
|
+
return 1
|
|
2019
|
+
fi
|
|
2020
|
+
args_include "$expected" "$@"
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
compose_down() {
|
|
2024
|
+
local -a args=("$@")
|
|
2025
|
+
local has_remove_orphans=0
|
|
2026
|
+
local reason
|
|
2027
|
+
|
|
2028
|
+
if [[ "${#args[@]}" -gt 0 ]] && args_array_include --remove-orphans "${args[@]}"; then
|
|
2029
|
+
has_remove_orphans=1
|
|
2030
|
+
fi
|
|
2031
|
+
|
|
2032
|
+
if compose_file_matches_manifest && [[ "$has_remove_orphans" -eq 0 ]]; then
|
|
2033
|
+
if [[ "${#args[@]}" -gt 0 ]]; then
|
|
2034
|
+
args=(--remove-orphans "${args[@]}")
|
|
2035
|
+
else
|
|
2036
|
+
args=(--remove-orphans)
|
|
2037
|
+
fi
|
|
2038
|
+
elif [[ "$has_remove_orphans" -eq 0 ]]; then
|
|
2039
|
+
reason="$(compose_manifest_mismatch_reason)"
|
|
2040
|
+
printf 'not removing orphan containers automatically: %s\n' "$reason" >&2
|
|
2041
|
+
printf 'pass --remove-orphans to down if you want to remove stale services\n' >&2
|
|
2042
|
+
fi
|
|
2043
|
+
if [[ "${#args[@]}" -gt 0 ]]; then
|
|
2044
|
+
compose down "${args[@]}"
|
|
2045
|
+
else
|
|
2046
|
+
compose down
|
|
2047
|
+
fi
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
info() {
|
|
2051
|
+
printf 'deploy dir: %s\n' "$deploy_dir"
|
|
2052
|
+
printf 'compose file: %s\n' "$compose_file"
|
|
2053
|
+
if [[ -f "$compose_override_file" ]]; then
|
|
2054
|
+
printf 'compose override file: %s\n' "$compose_override_file"
|
|
2055
|
+
fi
|
|
2056
|
+
printf 'compose project: %s\n' "$(compose_project_name)"
|
|
2057
|
+
printf 'container name: %s\n' "$(container_name_value)"
|
|
2058
|
+
printf 'config dir: %s\n' "$(config_dir_path)"
|
|
2059
|
+
printf 'config name: %s\n' "$(config_name_value)"
|
|
2060
|
+
printf 'app env file: %s\n' "$(app_env_path)"
|
|
2061
|
+
printf 'docker env file: %s\n' "$compose_env_file"
|
|
2062
|
+
printf 'requirements file: %s\n' "$requirements_file"
|
|
2063
|
+
printf 'wheels dir: %s\n' "$(wheels_dir_path)"
|
|
2064
|
+
printf 'plugin data dir: %s\n' "$(plugin_data_dir_path)"
|
|
2065
|
+
print_mcp_url
|
|
2066
|
+
if command -v docker >/dev/null && docker info >/dev/null 2>&1; then
|
|
2067
|
+
check_container_name_owner warn || true
|
|
2068
|
+
fi
|
|
2069
|
+
docker compose version || true
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
doctor_status=0
|
|
2073
|
+
doctor_agent_uid=""
|
|
2074
|
+
doctor_agent_user=""
|
|
2075
|
+
doctor_agent_groups=""
|
|
2076
|
+
doctor_agent_group_names=""
|
|
2077
|
+
doctor_preinstall=0
|
|
2078
|
+
doctor_quiet=0
|
|
2079
|
+
|
|
2080
|
+
color_enabled() {
|
|
2081
|
+
[[ "${ARBITER_COLOR:-}" == always ]] && return 0
|
|
2082
|
+
[[ "${ARBITER_COLOR:-}" == never ]] && return 1
|
|
2083
|
+
[[ -t 1 ]]
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
doctor_prefix() {
|
|
2087
|
+
local color="$1"
|
|
2088
|
+
local label="$2"
|
|
2089
|
+
|
|
2090
|
+
if color_enabled; then
|
|
2091
|
+
printf '\033[%sm%s\033[0m' "$color" "$label"
|
|
2092
|
+
else
|
|
2093
|
+
printf '%s' "$label"
|
|
2094
|
+
fi
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
doctor_line() {
|
|
2098
|
+
local color="$1"
|
|
2099
|
+
local label="$2"
|
|
2100
|
+
local message="$3"
|
|
2101
|
+
|
|
2102
|
+
doctor_prefix "$color" "$label"
|
|
2103
|
+
printf ': %s\n' "$message"
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
doctor_ok() {
|
|
2107
|
+
[[ "${doctor_quiet:-0}" -eq 1 ]] && return
|
|
2108
|
+
doctor_line 32 ok "$1"
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
doctor_warn() {
|
|
2112
|
+
doctor_line 33 warn "$1"
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
doctor_detail() {
|
|
2116
|
+
printf ' %s\n' "$1"
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
doctor_fail() {
|
|
2120
|
+
doctor_line 31 fail "$1"
|
|
2121
|
+
doctor_status=1
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
doctor_check_file() {
|
|
2125
|
+
local path="$1"
|
|
2126
|
+
local description="$2"
|
|
2127
|
+
|
|
2128
|
+
if [[ -f "$path" ]]; then
|
|
2129
|
+
doctor_ok "$description exists: $path"
|
|
2130
|
+
else
|
|
2131
|
+
doctor_fail "$description is missing: $path"
|
|
2132
|
+
fi
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
path_is_under_deploy_dir() {
|
|
2136
|
+
local path="$1"
|
|
2137
|
+
local real_deploy_dir
|
|
2138
|
+
local real_path
|
|
2139
|
+
|
|
2140
|
+
real_deploy_dir="$(normalize_existing_path "$deploy_dir")"
|
|
2141
|
+
real_path="$(normalize_existing_path "$path")"
|
|
2142
|
+
[[ "$real_path" == "$real_deploy_dir" || "$real_path" == "$real_deploy_dir/"* ]]
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
normalize_existing_path() {
|
|
2146
|
+
local path="$1"
|
|
2147
|
+
|
|
2148
|
+
if command -v readlink >/dev/null && readlink -f "$path" >/dev/null 2>&1; then
|
|
2149
|
+
readlink -f "$path"
|
|
2150
|
+
elif command -v realpath >/dev/null; then
|
|
2151
|
+
realpath "$path"
|
|
2152
|
+
else
|
|
2153
|
+
python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$path"
|
|
2154
|
+
fi
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
normalize_path_allow_missing() {
|
|
2158
|
+
local path="$1"
|
|
2159
|
+
|
|
2160
|
+
if command -v realpath >/dev/null && realpath -m / >/dev/null 2>&1; then
|
|
2161
|
+
realpath -m -- "$path"
|
|
2162
|
+
elif command -v readlink >/dev/null && readlink -m / >/dev/null 2>&1; then
|
|
2163
|
+
readlink -m -- "$path"
|
|
2164
|
+
else
|
|
2165
|
+
python3 -c 'import os, sys; print(os.path.abspath(sys.argv[1]))' "$path"
|
|
2166
|
+
fi
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
path_is_under_deploy_dir_allow_missing() {
|
|
2170
|
+
local path="$1"
|
|
2171
|
+
local real_deploy_dir
|
|
2172
|
+
local real_path
|
|
2173
|
+
|
|
2174
|
+
real_deploy_dir="$(normalize_path_allow_missing "$deploy_dir")"
|
|
2175
|
+
real_path="$(normalize_path_allow_missing "$path")"
|
|
2176
|
+
[[ "$real_path" == "$real_deploy_dir" || "$real_path" == "$real_deploy_dir/"* ]]
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
doctor_check_path_under_deploy_dir() {
|
|
2180
|
+
local path="$1"
|
|
2181
|
+
local description="$2"
|
|
2182
|
+
|
|
2183
|
+
[[ -e "$path" ]] || return
|
|
2184
|
+
if path_is_under_deploy_dir "$path"; then
|
|
2185
|
+
doctor_ok "$description is inside deployment directory"
|
|
2186
|
+
else
|
|
2187
|
+
doctor_fail "$description is outside deployment directory: $path"
|
|
2188
|
+
fi
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
doctor_check_install_deploy_path() {
|
|
2192
|
+
local value="$1"
|
|
2193
|
+
local description="$2"
|
|
2194
|
+
local env_name="$3"
|
|
2195
|
+
local path
|
|
2196
|
+
local real_deploy_dir
|
|
2197
|
+
local real_path
|
|
2198
|
+
|
|
2199
|
+
if [[ "$value" = /* ]]; then
|
|
2200
|
+
doctor_fail "$description uses an absolute host path for install: $env_name=$value"
|
|
2201
|
+
printf ' edit docker.env to use a path relative to the deployment directory\n'
|
|
2202
|
+
return
|
|
2203
|
+
fi
|
|
2204
|
+
|
|
2205
|
+
path="$(deploy_path_from_compose_value "$value")"
|
|
2206
|
+
real_deploy_dir="$(normalize_path_allow_missing "$deploy_dir")"
|
|
2207
|
+
real_path="$(normalize_path_allow_missing "$path")"
|
|
2208
|
+
if [[ "$real_path" != "$real_deploy_dir" && "$real_path" != "$real_deploy_dir/"* ]]; then
|
|
2209
|
+
doctor_fail "$description is outside deployment directory: $path"
|
|
2210
|
+
printf ' edit docker.env to use a path inside the deployment directory\n'
|
|
2211
|
+
return
|
|
2212
|
+
fi
|
|
2213
|
+
if [[ "$real_path" == "$real_deploy_dir" ]]; then
|
|
2214
|
+
doctor_fail "$description resolves to the deployment directory root: $env_name=$value"
|
|
2215
|
+
printf ' edit docker.env to use a dedicated path inside the deployment directory\n'
|
|
2216
|
+
return
|
|
2217
|
+
fi
|
|
2218
|
+
doctor_ok "$description is inside deployment directory"
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
doctor_check_dir() {
|
|
2222
|
+
local path="$1"
|
|
2223
|
+
local description="$2"
|
|
2224
|
+
|
|
2225
|
+
if [[ -d "$path" ]]; then
|
|
2226
|
+
doctor_ok "$description exists: $path"
|
|
2227
|
+
else
|
|
2228
|
+
doctor_fail "$description is missing: $path"
|
|
2229
|
+
fi
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
container_user_write_digit() {
|
|
2233
|
+
local path="$1"
|
|
2234
|
+
local container_user
|
|
2235
|
+
local container_uid
|
|
2236
|
+
local container_gid
|
|
2237
|
+
local owner_uid
|
|
2238
|
+
local owner_gid
|
|
2239
|
+
local mode
|
|
2240
|
+
local perms
|
|
2241
|
+
|
|
2242
|
+
[[ -d "$path" ]] || return 1
|
|
2243
|
+
container_user="$(env_file_value "$compose_env_file" ARBITER_CONTAINER_USER || true)"
|
|
2244
|
+
[[ "$container_user" =~ ^[0-9]+:[0-9]+$ ]] || return 1
|
|
2245
|
+
container_uid="${container_user%%:*}"
|
|
2246
|
+
container_gid="${container_user#*:}"
|
|
2247
|
+
|
|
2248
|
+
read -r owner_uid owner_gid mode < <(stat_owner_group_mode "$path")
|
|
2249
|
+
perms="${mode: -3}"
|
|
2250
|
+
if [[ "$container_uid" == "$owner_uid" ]]; then
|
|
2251
|
+
printf '%s\n' "${perms:0:1}"
|
|
2252
|
+
elif [[ "$container_gid" == "$owner_gid" ]]; then
|
|
2253
|
+
printf '%s\n' "${perms:1:1}"
|
|
2254
|
+
else
|
|
2255
|
+
printf '%s\n' "${perms:2:1}"
|
|
2256
|
+
fi
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
container_user_can_write_dir() {
|
|
2260
|
+
local path="$1"
|
|
2261
|
+
local digit
|
|
2262
|
+
|
|
2263
|
+
digit="$(container_user_write_digit "$path")" || return 1
|
|
2264
|
+
(( (10#$digit & 2) != 0 && (10#$digit & 1) != 0 ))
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
ensure_plugin_data_dir_ready() {
|
|
2268
|
+
local path
|
|
2269
|
+
local container_user
|
|
2270
|
+
local write_check
|
|
2271
|
+
|
|
2272
|
+
path="$(plugin_data_dir_path)"
|
|
2273
|
+
container_user="$(env_file_value "$compose_env_file" ARBITER_CONTAINER_USER || true)"
|
|
2274
|
+
|
|
2275
|
+
if ! mkdir -p "$path"; then
|
|
2276
|
+
printf 'error: could not create plugin data directory: %s\n' "$path" >&2
|
|
2277
|
+
return 1
|
|
2278
|
+
fi
|
|
2279
|
+
if ! chmod 700 "$path"; then
|
|
2280
|
+
printf 'error: could not secure plugin data directory: %s\n' "$path" >&2
|
|
2281
|
+
printf ' chown it to the deployment helper user or run this command as root\n' >&2
|
|
2282
|
+
return 1
|
|
2283
|
+
fi
|
|
2284
|
+
|
|
2285
|
+
write_check="$path/.arbiter-write-check.$$"
|
|
2286
|
+
if ! : >"$write_check" 2>/dev/null; then
|
|
2287
|
+
printf 'error: plugin data directory is not writable: %s\n' "$path" >&2
|
|
2288
|
+
printf ' chown or chmod it so the deployment helper can prepare plugin state\n' >&2
|
|
2289
|
+
return 1
|
|
2290
|
+
fi
|
|
2291
|
+
rm -f "$write_check"
|
|
2292
|
+
|
|
2293
|
+
if container_user_can_write_dir "$path"; then
|
|
2294
|
+
return 0
|
|
2295
|
+
fi
|
|
2296
|
+
|
|
2297
|
+
if [[ "$(id -u)" == "0" && "$container_user" =~ ^[0-9]+:[0-9]+$ ]]; then
|
|
2298
|
+
chown "$container_user" "$path"
|
|
2299
|
+
chmod 700 "$path"
|
|
2300
|
+
fi
|
|
2301
|
+
|
|
2302
|
+
if ! container_user_can_write_dir "$path"; then
|
|
2303
|
+
printf 'error: plugin data directory is not writable by container user: %s\n' "$container_user" >&2
|
|
2304
|
+
printf ' plugin data dir: %s\n' "$path" >&2
|
|
2305
|
+
printf ' chown or chmod it so the container user can create plugin state\n' >&2
|
|
2306
|
+
return 1
|
|
2307
|
+
fi
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
doctor_check_plugin_data_permissions() {
|
|
2311
|
+
local path="$1"
|
|
2312
|
+
local mode
|
|
2313
|
+
local perms
|
|
2314
|
+
|
|
2315
|
+
[[ -d "$path" ]] || return
|
|
2316
|
+
mode="$(stat_mode "$path")"
|
|
2317
|
+
perms="${mode: -3}"
|
|
2318
|
+
if (( (10#${perms:1:1} & 7) != 0 || (10#${perms:2:1} & 7) != 0 )); then
|
|
2319
|
+
doctor_fail "plugin data directory is accessible outside its owner: $path"
|
|
2320
|
+
doctor_detail "run $deploy_dir/arbiter-docker up or chmod 700 $path"
|
|
2321
|
+
else
|
|
2322
|
+
doctor_ok "plugin data directory is only accessible by its owner"
|
|
2323
|
+
fi
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
doctor_check_plugin_data_compose_wiring() {
|
|
2327
|
+
if grep -Eq '^[[:space:]]*-[[:space:]]*\$\{ARBITER_PLUGIN_DATA_DIR:-\./data/plugins\}:/data/plugins[[:space:]]*$' "$compose_file"; then
|
|
2328
|
+
doctor_ok "compose mounts plugin data directory at /data/plugins"
|
|
2329
|
+
else
|
|
2330
|
+
doctor_fail "compose file does not mount plugin data directory at /data/plugins"
|
|
2331
|
+
doctor_detail "run $deploy_dir/arbiter-docker update to refresh generated Docker files"
|
|
2332
|
+
fi
|
|
2333
|
+
|
|
2334
|
+
if grep -Fq '"arbiter.storage.plugin_data_dir=/data/plugins"' "$compose_file"; then
|
|
2335
|
+
doctor_ok "server config points plugin data storage at /data/plugins"
|
|
2336
|
+
else
|
|
2337
|
+
doctor_fail "compose file does not configure arbiter.storage.plugin_data_dir=/data/plugins"
|
|
2338
|
+
doctor_detail "run $deploy_dir/arbiter-docker update to refresh generated Docker files"
|
|
2339
|
+
fi
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
doctor_check_container_user_can_write_dir() {
|
|
2343
|
+
local path="$1"
|
|
2344
|
+
local description="$2"
|
|
2345
|
+
local container_user
|
|
2346
|
+
|
|
2347
|
+
[[ -d "$path" ]] || return
|
|
2348
|
+
container_user="$(env_file_value "$compose_env_file" ARBITER_CONTAINER_USER || true)"
|
|
2349
|
+
[[ "$container_user" =~ ^[0-9]+:[0-9]+$ ]] || return
|
|
2350
|
+
|
|
2351
|
+
if container_user_can_write_dir "$path"; then
|
|
2352
|
+
doctor_ok "$description is writable by container user: $container_user"
|
|
2353
|
+
else
|
|
2354
|
+
doctor_fail "$description is not writable by container user: $container_user"
|
|
2355
|
+
doctor_detail "chown or chmod $path so $container_user can create plugin state"
|
|
2356
|
+
fi
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
doctor_check_env_file() {
|
|
2360
|
+
local path="$1"
|
|
2361
|
+
local description="$2"
|
|
2362
|
+
|
|
2363
|
+
if [[ ! -f "$path" ]]; then
|
|
2364
|
+
return
|
|
2365
|
+
fi
|
|
2366
|
+
|
|
2367
|
+
if awk '
|
|
2368
|
+
/^[[:space:]]*($|#)/ { next }
|
|
2369
|
+
/^[A-Za-z_][A-Za-z0-9_]*=/ { next }
|
|
2370
|
+
{
|
|
2371
|
+
printf "%s:%d: invalid env assignment: %s\n", FILENAME, FNR, $0
|
|
2372
|
+
invalid = 1
|
|
2373
|
+
}
|
|
2374
|
+
END { exit invalid }
|
|
2375
|
+
' "$path"; then
|
|
2376
|
+
doctor_ok "$description has valid KEY=VALUE lines"
|
|
2377
|
+
else
|
|
2378
|
+
doctor_fail "$description contains invalid env lines"
|
|
2379
|
+
fi
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
doctor_file_other_perm_digit() {
|
|
2383
|
+
local path="$1"
|
|
2384
|
+
local mode
|
|
2385
|
+
local perms
|
|
2386
|
+
|
|
2387
|
+
mode="$(stat_mode "$path")"
|
|
2388
|
+
perms="${mode: -3}"
|
|
2389
|
+
printf '%s\n' "${perms:2:1}"
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
doctor_file_group_perm_digit() {
|
|
2393
|
+
local path="$1"
|
|
2394
|
+
local mode
|
|
2395
|
+
local perms
|
|
2396
|
+
|
|
2397
|
+
mode="$(stat_mode "$path")"
|
|
2398
|
+
perms="${mode: -3}"
|
|
2399
|
+
printf '%s\n' "${perms:1:1}"
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
stat_mode() {
|
|
2403
|
+
local path="$1"
|
|
2404
|
+
|
|
2405
|
+
if stat -c '%a' "$path" >/dev/null 2>&1; then
|
|
2406
|
+
stat -c '%a' "$path"
|
|
2407
|
+
else
|
|
2408
|
+
stat -f '%Lp' "$path"
|
|
2409
|
+
fi
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
stat_owner_group_mode() {
|
|
2413
|
+
local path="$1"
|
|
2414
|
+
|
|
2415
|
+
if stat -c '%u %g %a' "$path" >/dev/null 2>&1; then
|
|
2416
|
+
stat -c '%u %g %a' "$path"
|
|
2417
|
+
else
|
|
2418
|
+
stat -f '%u %g %Lp' "$path"
|
|
2419
|
+
fi
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
doctor_check_config_file_permissions() {
|
|
2423
|
+
local config_dir="$1"
|
|
2424
|
+
local app_env="$2"
|
|
2425
|
+
local app_env_real
|
|
2426
|
+
local file
|
|
2427
|
+
local file_real
|
|
2428
|
+
local other_digit
|
|
2429
|
+
local checked=0
|
|
2430
|
+
local failed=0
|
|
2431
|
+
|
|
2432
|
+
[[ -d "$config_dir" ]] || return
|
|
2433
|
+
app_env_real="$(normalize_path_allow_missing "$app_env")"
|
|
2434
|
+
|
|
2435
|
+
while IFS= read -r -d '' file; do
|
|
2436
|
+
file_real="$(normalize_path_allow_missing "$file")"
|
|
2437
|
+
[[ "$file_real" == "$app_env_real" ]] && continue
|
|
2438
|
+
checked=1
|
|
2439
|
+
other_digit="$(doctor_file_other_perm_digit "$file")"
|
|
2440
|
+
if (( (10#$other_digit & 6) != 0 )); then
|
|
2441
|
+
doctor_fail "config file is world-readable or world-writable: $file"
|
|
2442
|
+
failed=1
|
|
2443
|
+
fi
|
|
2444
|
+
done < <(find "$config_dir" -type f -print0)
|
|
2445
|
+
|
|
2446
|
+
[[ "$checked" -eq 1 ]] || return
|
|
2447
|
+
if [[ "$failed" -eq 0 ]]; then
|
|
2448
|
+
doctor_ok "config files are not world-readable or world-writable"
|
|
2449
|
+
fi
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
doctor_check_app_env_permissions() {
|
|
2453
|
+
local path="$1"
|
|
2454
|
+
local group_digit
|
|
2455
|
+
local other_digit
|
|
2456
|
+
|
|
2457
|
+
[[ -f "$path" ]] || return
|
|
2458
|
+
group_digit="$(doctor_file_group_perm_digit "$path")"
|
|
2459
|
+
other_digit="$(doctor_file_other_perm_digit "$path")"
|
|
2460
|
+
if (( (10#$group_digit & 6) != 0 || (10#$other_digit & 6) != 0 )); then
|
|
2461
|
+
doctor_fail "app env file is readable or writable outside its owner: $path"
|
|
2462
|
+
else
|
|
2463
|
+
doctor_ok "app env file is only readable and writable by its owner"
|
|
2464
|
+
fi
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
doctor_check_container_user_config() {
|
|
2468
|
+
local container_user
|
|
2469
|
+
|
|
2470
|
+
if ! grep -Eq '^[[:space:]]*user:[[:space:]]*\$\{ARBITER_CONTAINER_USER:-' "$compose_file"; then
|
|
2471
|
+
doctor_fail "compose service does not use ARBITER_CONTAINER_USER"
|
|
2472
|
+
return
|
|
2473
|
+
fi
|
|
2474
|
+
if ! container_user="$(env_file_value "$compose_env_file" ARBITER_CONTAINER_USER)"; then
|
|
2475
|
+
doctor_fail "docker env is missing ARBITER_CONTAINER_USER"
|
|
2476
|
+
return
|
|
2477
|
+
fi
|
|
2478
|
+
if [[ ! "$container_user" =~ ^[0-9]+:[0-9]+$ ]]; then
|
|
2479
|
+
doctor_fail "ARBITER_CONTAINER_USER must be numeric uid:gid, got: $container_user"
|
|
2480
|
+
return
|
|
2481
|
+
fi
|
|
2482
|
+
if [[ "$container_user" =~ ^0: || "$container_user" =~ :0$ ]]; then
|
|
2483
|
+
doctor_fail "ARBITER_CONTAINER_USER must not be root: $container_user"
|
|
2484
|
+
return
|
|
2485
|
+
fi
|
|
2486
|
+
doctor_ok "container user is non-root: $container_user"
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
doctor_check_requirements_file() {
|
|
2490
|
+
local path="$1"
|
|
2491
|
+
|
|
2492
|
+
if [[ ! -f "$path" ]]; then
|
|
2493
|
+
return
|
|
2494
|
+
fi
|
|
2495
|
+
|
|
2496
|
+
if validate_requirements_file "$path"; then
|
|
2497
|
+
doctor_ok "requirements file entries are syntactically valid"
|
|
2498
|
+
else
|
|
2499
|
+
doctor_fail "requirements file contains unpinned package requirements"
|
|
2500
|
+
fi
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
doctor_group_contains() {
|
|
2504
|
+
local gid="$1"
|
|
2505
|
+
local group
|
|
2506
|
+
|
|
2507
|
+
for group in $doctor_agent_groups; do
|
|
2508
|
+
[[ "$group" == "$gid" ]] && return 0
|
|
2509
|
+
done
|
|
2510
|
+
return 1
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
doctor_agent_perm_digit() {
|
|
2514
|
+
local path="$1"
|
|
2515
|
+
local owner_uid
|
|
2516
|
+
local owner_gid
|
|
2517
|
+
local mode
|
|
2518
|
+
local perms
|
|
2519
|
+
|
|
2520
|
+
read -r owner_uid owner_gid mode < <(stat_owner_group_mode "$path")
|
|
2521
|
+
perms="${mode: -3}"
|
|
2522
|
+
|
|
2523
|
+
if [[ "$doctor_agent_uid" == "$owner_uid" ]]; then
|
|
2524
|
+
printf '%s\n' "${perms:0:1}"
|
|
2525
|
+
elif doctor_group_contains "$owner_gid"; then
|
|
2526
|
+
printf '%s\n' "${perms:1:1}"
|
|
2527
|
+
else
|
|
2528
|
+
printf '%s\n' "${perms:2:1}"
|
|
2529
|
+
fi
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
doctor_agent_has_perm() {
|
|
2533
|
+
local path="$1"
|
|
2534
|
+
local bit="$2"
|
|
2535
|
+
local digit
|
|
2536
|
+
|
|
2537
|
+
[[ -e "$path" ]] || return 1
|
|
2538
|
+
digit="$(doctor_agent_perm_digit "$path")"
|
|
2539
|
+
(( (10#$digit & bit) != 0 ))
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
doctor_check_agent_cannot_read() {
|
|
2543
|
+
local path="$1"
|
|
2544
|
+
local description="$2"
|
|
2545
|
+
|
|
2546
|
+
[[ -e "$path" ]] || return
|
|
2547
|
+
if doctor_agent_has_perm "$path" 4; then
|
|
2548
|
+
doctor_fail "$description is readable by agent identity: $path"
|
|
2549
|
+
else
|
|
2550
|
+
doctor_ok "$description is not readable by agent identity"
|
|
2551
|
+
fi
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
doctor_check_agent_cannot_write_file() {
|
|
2555
|
+
local path="$1"
|
|
2556
|
+
local description="$2"
|
|
2557
|
+
|
|
2558
|
+
[[ -e "$path" ]] || return
|
|
2559
|
+
if doctor_agent_has_perm "$path" 2; then
|
|
2560
|
+
doctor_fail "$description is writable by agent identity: $path"
|
|
2561
|
+
else
|
|
2562
|
+
doctor_ok "$description is not writable by agent identity"
|
|
2563
|
+
fi
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
doctor_check_agent_cannot_write_dir() {
|
|
2567
|
+
local path="$1"
|
|
2568
|
+
local description="$2"
|
|
2569
|
+
|
|
2570
|
+
[[ -d "$path" ]] || return
|
|
2571
|
+
if doctor_agent_has_perm "$path" 2 && doctor_agent_has_perm "$path" 1; then
|
|
2572
|
+
doctor_fail "$description is writable by agent identity: $path"
|
|
2573
|
+
else
|
|
2574
|
+
doctor_ok "$description is not writable by agent identity"
|
|
2575
|
+
fi
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
doctor_dir_has_sticky_bit() {
|
|
2579
|
+
local path="$1"
|
|
2580
|
+
local mode
|
|
2581
|
+
local special
|
|
2582
|
+
|
|
2583
|
+
mode="$(stat_mode "$path")"
|
|
2584
|
+
special=$((10#$mode / 1000 % 10))
|
|
2585
|
+
(( (special & 1) != 0 ))
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
doctor_check_agent_cannot_replace_deploy_dir() {
|
|
2589
|
+
local deploy_parent
|
|
2590
|
+
|
|
2591
|
+
deploy_parent="$(dirname "$deploy_dir")"
|
|
2592
|
+
[[ -d "$deploy_parent" ]] || return
|
|
2593
|
+
|
|
2594
|
+
if ! doctor_agent_has_perm "$deploy_parent" 2 || ! doctor_agent_has_perm "$deploy_parent" 1; then
|
|
2595
|
+
doctor_ok "deployment parent directory does not allow agent replacement"
|
|
2596
|
+
return
|
|
2597
|
+
fi
|
|
2598
|
+
|
|
2599
|
+
if doctor_dir_has_sticky_bit "$deploy_parent"; then
|
|
2600
|
+
doctor_warn "deployment parent directory is writable but sticky: $deploy_parent"
|
|
2601
|
+
return
|
|
2602
|
+
fi
|
|
2603
|
+
|
|
2604
|
+
doctor_fail "deployment parent directory allows agent replacement: $deploy_parent"
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
doctor_check_docker_socket() {
|
|
2608
|
+
local socket="/var/run/docker.sock"
|
|
2609
|
+
|
|
2610
|
+
if [[ ! -e "$socket" ]]; then
|
|
2611
|
+
doctor_warn "Docker socket not found at $socket"
|
|
2612
|
+
return
|
|
2613
|
+
fi
|
|
2614
|
+
|
|
2615
|
+
if doctor_agent_has_perm "$socket" 2; then
|
|
2616
|
+
doctor_fail "Docker socket is writable by agent identity: $socket"
|
|
2617
|
+
else
|
|
2618
|
+
doctor_ok "Docker socket is not writable by agent identity"
|
|
2619
|
+
fi
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
doctor_check_preinstall_ready() {
|
|
2623
|
+
local active_config_dir
|
|
2624
|
+
local active_app_env
|
|
2625
|
+
local active_requirements_file
|
|
2626
|
+
local active_wheels_dir
|
|
2627
|
+
local active_plugin_data_dir
|
|
2628
|
+
local wheel_requirement
|
|
2629
|
+
local wheel_path
|
|
2630
|
+
|
|
2631
|
+
active_config_dir="$(config_dir_path)"
|
|
2632
|
+
active_app_env="$(app_env_path)"
|
|
2633
|
+
active_requirements_file="$(deploy_path_from_compose_value "$(compose_env_value ARBITER_REQUIREMENTS_FILE ./requirements.txt)")"
|
|
2634
|
+
active_wheels_dir="$(wheels_dir_path)"
|
|
2635
|
+
active_plugin_data_dir="$(plugin_data_dir_path)"
|
|
2636
|
+
|
|
2637
|
+
doctor_check_install_deploy_path "$(compose_env_value ARBITER_CONFIG_DIR ./conf)" "config directory" "ARBITER_CONFIG_DIR"
|
|
2638
|
+
doctor_check_install_deploy_path "$(compose_env_value ARBITER_APP_ENV_FILE ./conf/.env)" "app env file" "ARBITER_APP_ENV_FILE"
|
|
2639
|
+
doctor_check_install_deploy_path "$(compose_env_value ARBITER_REQUIREMENTS_FILE ./requirements.txt)" "runtime requirements file" "ARBITER_REQUIREMENTS_FILE"
|
|
2640
|
+
doctor_check_install_deploy_path "$(compose_env_value ARBITER_WHEELS_DIR ./wheels)" "wheels directory" "ARBITER_WHEELS_DIR"
|
|
2641
|
+
doctor_check_install_deploy_path "$(compose_env_value ARBITER_PLUGIN_DATA_DIR ./data/plugins)" "plugin data directory" "ARBITER_PLUGIN_DATA_DIR"
|
|
2642
|
+
doctor_check_dir "$active_plugin_data_dir" "plugin data directory"
|
|
2643
|
+
doctor_check_plugin_data_permissions "$active_plugin_data_dir"
|
|
2644
|
+
doctor_check_plugin_data_compose_wiring
|
|
2645
|
+
doctor_check_container_user_can_write_dir "$active_plugin_data_dir" "plugin data directory"
|
|
2646
|
+
doctor_check_file "$active_requirements_file" "runtime requirements file"
|
|
2647
|
+
|
|
2648
|
+
if [[ -f "$active_requirements_file" ]] && grep -Eq '^[[:space:]]*/source/arbiter(/|$)' "$active_requirements_file"; then
|
|
2649
|
+
doctor_warn "preinstall found local checkout requirements: $active_requirements_file"
|
|
2650
|
+
printf ' install will build local wheels and write wheel-backed requirements only in the install target\n'
|
|
2651
|
+
printf ' run %s install to install wheel-backed artifacts without changing staging requirements\n' "$deploy_dir/arbiter-docker"
|
|
2652
|
+
fi
|
|
2653
|
+
|
|
2654
|
+
while IFS= read -r wheel_requirement; do
|
|
2655
|
+
[[ -n "$wheel_requirement" ]] || continue
|
|
2656
|
+
wheel_path="$active_wheels_dir/${wheel_requirement#/wheels/}"
|
|
2657
|
+
if [[ -f "$wheel_path" ]]; then
|
|
2658
|
+
doctor_ok "wheel requirement exists: $wheel_path"
|
|
2659
|
+
else
|
|
2660
|
+
doctor_fail "wheel requirement is missing from deployment wheelhouse: $wheel_path"
|
|
2661
|
+
printf ' run %s bundle prepare to rebuild the deployment wheelhouse\n' "$deploy_dir/arbiter-docker"
|
|
2662
|
+
fi
|
|
2663
|
+
done < <(
|
|
2664
|
+
awk '
|
|
2665
|
+
{
|
|
2666
|
+
line = $0
|
|
2667
|
+
sub(/^[[:space:]]+/, "", line)
|
|
2668
|
+
sub(/[[:space:]]+#.*$/, "", line)
|
|
2669
|
+
sub(/[[:space:]]+$/, "", line)
|
|
2670
|
+
if (line ~ /^\/wheels\/.*\.whl$/) {
|
|
2671
|
+
print line
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
' "$active_requirements_file"
|
|
2675
|
+
)
|
|
2676
|
+
|
|
2677
|
+
if [[ -f "$compose_override_file" ]] && grep -q '/source/arbiter' "$compose_override_file"; then
|
|
2678
|
+
doctor_warn "preinstall found local checkout compose override: $compose_override_file"
|
|
2679
|
+
printf ' install will omit the local checkout override only in the install target\n'
|
|
2680
|
+
printf ' run %s install to install without changing the staging local checkout override\n' "$deploy_dir/arbiter-docker"
|
|
2681
|
+
fi
|
|
2682
|
+
|
|
2683
|
+
if [[ "$doctor_status" -eq 0 ]]; then
|
|
2684
|
+
doctor_ok "preinstall checks passed"
|
|
2685
|
+
fi
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
doctor_check_docker_network_subnet() {
|
|
2689
|
+
local configured_network
|
|
2690
|
+
local configured_subnet
|
|
2691
|
+
local network_line
|
|
2692
|
+
local network_name
|
|
2693
|
+
local network_subnets
|
|
2694
|
+
local network_subnet
|
|
2695
|
+
|
|
2696
|
+
configured_network="$(compose_env_value ARBITER_DOCKER_NETWORK_NAME arbiter-staging)"
|
|
2697
|
+
configured_subnet="$(compose_env_value ARBITER_DOCKER_SUBNET 172.31.251.0/24)"
|
|
2698
|
+
[[ -n "$configured_subnet" ]] || return
|
|
2699
|
+
|
|
2700
|
+
if ! docker network ls >/dev/null 2>&1; then
|
|
2701
|
+
return
|
|
2702
|
+
fi
|
|
2703
|
+
|
|
2704
|
+
while read -r network_line; do
|
|
2705
|
+
[[ -n "$network_line" ]] || continue
|
|
2706
|
+
network_name="${network_line%% *}"
|
|
2707
|
+
network_subnets="${network_line#* }"
|
|
2708
|
+
[[ "$network_subnets" != "$network_line" ]] || continue
|
|
2709
|
+
[[ "$network_name" != "$configured_network" ]] || continue
|
|
2710
|
+
for network_subnet in $network_subnets; do
|
|
2711
|
+
[[ "$network_subnet" == */* ]] || continue
|
|
2712
|
+
if cidr_overlaps "$configured_subnet" "$network_subnet"; then
|
|
2713
|
+
doctor_fail "Docker subnet $configured_subnet overlaps network $network_name ($network_subnet)"
|
|
2714
|
+
return
|
|
2715
|
+
fi
|
|
2716
|
+
done
|
|
2717
|
+
done < <(docker_network_subnet_lines)
|
|
2718
|
+
|
|
2719
|
+
doctor_ok "Docker subnet is not already used by another network"
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
doctor_resolve_agent() {
|
|
2723
|
+
if [[ -n "$doctor_agent_user" ]]; then
|
|
2724
|
+
if ! doctor_agent_uid="$(id -u "$doctor_agent_user" 2>/dev/null)"; then
|
|
2725
|
+
doctor_fail "agent user does not exist: $doctor_agent_user"
|
|
2726
|
+
return 1
|
|
2727
|
+
fi
|
|
2728
|
+
doctor_agent_groups="$(id -G "$doctor_agent_user")"
|
|
2729
|
+
doctor_agent_group_names="$(id -nG "$doctor_agent_user")"
|
|
2730
|
+
return 0
|
|
2731
|
+
fi
|
|
2732
|
+
|
|
2733
|
+
if [[ -n "$doctor_agent_uid" ]]; then
|
|
2734
|
+
doctor_agent_user="$(getent passwd "$doctor_agent_uid" | cut -d: -f1 || true)"
|
|
2735
|
+
if [[ -z "$doctor_agent_user" ]]; then
|
|
2736
|
+
doctor_fail "agent uid does not resolve to a local user: $doctor_agent_uid"
|
|
2737
|
+
return 1
|
|
2738
|
+
fi
|
|
2739
|
+
doctor_agent_groups="$(id -G "$doctor_agent_user")"
|
|
2740
|
+
doctor_agent_group_names="$(id -nG "$doctor_agent_user")"
|
|
2741
|
+
return 0
|
|
2742
|
+
fi
|
|
2743
|
+
|
|
2744
|
+
return 1
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
doctor_check_agent_access() {
|
|
2748
|
+
local active_config_dir
|
|
2749
|
+
local active_config_file
|
|
2750
|
+
local active_app_env
|
|
2751
|
+
local active_plugin_data_dir
|
|
2752
|
+
|
|
2753
|
+
if ! doctor_resolve_agent; then
|
|
2754
|
+
doctor_warn "skipping agent permission checks; pass --agent-user USER or --agent-uid UID"
|
|
2755
|
+
return
|
|
2756
|
+
fi
|
|
2757
|
+
|
|
2758
|
+
doctor_ok "checking agent identity: ${doctor_agent_user:-uid $doctor_agent_uid} (uid $doctor_agent_uid)"
|
|
2759
|
+
|
|
2760
|
+
if [[ " $doctor_agent_group_names " == *" docker "* ]]; then
|
|
2761
|
+
doctor_fail "agent identity is in the docker group"
|
|
2762
|
+
else
|
|
2763
|
+
doctor_ok "agent identity is not in the docker group"
|
|
2764
|
+
fi
|
|
2765
|
+
|
|
2766
|
+
active_config_dir="$(config_dir_path)"
|
|
2767
|
+
active_config_file="$(config_main_file)"
|
|
2768
|
+
active_app_env="$(app_env_path)"
|
|
2769
|
+
doctor_check_agent_cannot_replace_deploy_dir
|
|
2770
|
+
doctor_check_agent_cannot_write_dir "$deploy_dir" "deployment directory"
|
|
2771
|
+
doctor_check_agent_cannot_write_file "$compose_file" "compose file"
|
|
2772
|
+
doctor_check_agent_cannot_write_dir "$active_config_dir" "config directory"
|
|
2773
|
+
doctor_check_agent_cannot_write_file "$active_config_file" "main config file"
|
|
2774
|
+
doctor_check_agent_cannot_write_file "$compose_env_file" "docker env file"
|
|
2775
|
+
doctor_check_agent_cannot_write_file "$active_app_env" "app env file"
|
|
2776
|
+
doctor_check_agent_cannot_write_file "$requirements_file" "requirements file"
|
|
2777
|
+
doctor_check_agent_cannot_write_file "$deploy_dir/arbiter-docker" "helper script"
|
|
2778
|
+
doctor_check_agent_cannot_read "$active_app_env" "app env file"
|
|
2779
|
+
doctor_check_docker_socket
|
|
2780
|
+
doctor_warn "permission checks do not inspect ACLs, sudo rules, or other direct service paths"
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
doctor() {
|
|
2784
|
+
local active_config_dir
|
|
2785
|
+
local active_config_file
|
|
2786
|
+
local active_app_env
|
|
2787
|
+
local active_plugin_data_dir
|
|
2788
|
+
|
|
2789
|
+
doctor_status=0
|
|
2790
|
+
doctor_agent_uid=""
|
|
2791
|
+
doctor_agent_user=""
|
|
2792
|
+
doctor_agent_groups=""
|
|
2793
|
+
doctor_agent_group_names=""
|
|
2794
|
+
doctor_preinstall=0
|
|
2795
|
+
doctor_quiet=0
|
|
2796
|
+
|
|
2797
|
+
while (($#)); do
|
|
2798
|
+
case "$1" in
|
|
2799
|
+
--preinstall)
|
|
2800
|
+
doctor_preinstall=1
|
|
2801
|
+
shift
|
|
2802
|
+
;;
|
|
2803
|
+
--quiet)
|
|
2804
|
+
doctor_quiet=1
|
|
2805
|
+
shift
|
|
2806
|
+
;;
|
|
2807
|
+
--agent-user)
|
|
2808
|
+
[[ $# -ge 2 ]] || {
|
|
2809
|
+
printf 'error: --agent-user requires a value\n' >&2
|
|
2810
|
+
exit 2
|
|
2811
|
+
}
|
|
2812
|
+
doctor_agent_user="$2"
|
|
2813
|
+
shift 2
|
|
2814
|
+
;;
|
|
2815
|
+
--agent-uid)
|
|
2816
|
+
[[ $# -ge 2 ]] || {
|
|
2817
|
+
printf 'error: --agent-uid requires a value\n' >&2
|
|
2818
|
+
exit 2
|
|
2819
|
+
}
|
|
2820
|
+
doctor_agent_uid="$2"
|
|
2821
|
+
shift 2
|
|
2822
|
+
;;
|
|
2823
|
+
-h | --help)
|
|
2824
|
+
usage
|
|
2825
|
+
exit 0
|
|
2826
|
+
;;
|
|
2827
|
+
*)
|
|
2828
|
+
printf 'error: unknown doctor option: %s\n\n' "$1" >&2
|
|
2829
|
+
usage >&2
|
|
2830
|
+
exit 2
|
|
2831
|
+
;;
|
|
2832
|
+
esac
|
|
2833
|
+
done
|
|
2834
|
+
|
|
2835
|
+
if [[ -n "$doctor_agent_user" && -n "$doctor_agent_uid" ]]; then
|
|
2836
|
+
printf 'error: use either --agent-user or --agent-uid, not both\n' >&2
|
|
2837
|
+
exit 2
|
|
2838
|
+
fi
|
|
2839
|
+
|
|
2840
|
+
active_config_dir="$(config_dir_path)"
|
|
2841
|
+
active_config_file="$(config_main_file)"
|
|
2842
|
+
active_app_env="$(app_env_path)"
|
|
2843
|
+
active_plugin_data_dir="$(plugin_data_dir_path)"
|
|
2844
|
+
doctor_check_dir "$deploy_dir" "deployment directory"
|
|
2845
|
+
doctor_check_file "$compose_file" "compose file"
|
|
2846
|
+
doctor_check_dir "$active_config_dir" "config directory"
|
|
2847
|
+
doctor_check_dir "$active_plugin_data_dir" "plugin data directory"
|
|
2848
|
+
doctor_check_file "$active_config_file" "main config file"
|
|
2849
|
+
doctor_check_file "$active_app_env" "app env file"
|
|
2850
|
+
doctor_check_file "$compose_env_file" "docker env file"
|
|
2851
|
+
doctor_check_file "$requirements_file" "requirements file"
|
|
2852
|
+
doctor_check_file "$deploy_dir/arbiter-docker" "helper script"
|
|
2853
|
+
doctor_check_env_file "$active_app_env" "app env file"
|
|
2854
|
+
doctor_check_env_file "$compose_env_file" "docker env file"
|
|
2855
|
+
doctor_check_plugin_data_permissions "$active_plugin_data_dir"
|
|
2856
|
+
doctor_check_config_file_permissions "$active_config_dir" "$active_app_env"
|
|
2857
|
+
doctor_check_app_env_permissions "$active_app_env"
|
|
2858
|
+
doctor_check_container_user_config
|
|
2859
|
+
doctor_check_plugin_data_compose_wiring
|
|
2860
|
+
doctor_check_container_user_can_write_dir "$active_plugin_data_dir" "plugin data directory"
|
|
2861
|
+
doctor_check_requirements_file "$requirements_file"
|
|
2862
|
+
|
|
2863
|
+
if [[ "$doctor_preinstall" -eq 1 ]]; then
|
|
2864
|
+
doctor_check_preinstall_ready
|
|
2865
|
+
return "$doctor_status"
|
|
2866
|
+
fi
|
|
2867
|
+
|
|
2868
|
+
doctor_check_docker_access
|
|
2869
|
+
doctor_check_container_name_owner
|
|
2870
|
+
if docker compose version >/dev/null 2>&1; then
|
|
2871
|
+
doctor_ok "$(docker compose version)"
|
|
2872
|
+
else
|
|
2873
|
+
doctor_fail "Docker Compose is not available"
|
|
2874
|
+
fi
|
|
2875
|
+
doctor_check_docker_network_subnet
|
|
2876
|
+
|
|
2877
|
+
doctor_check_agent_access
|
|
2878
|
+
return "$doctor_status"
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
shell_quote() {
|
|
2882
|
+
printf '%q' "$1"
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
print_command() {
|
|
2886
|
+
local arg
|
|
2887
|
+
|
|
2888
|
+
printf 'would run:'
|
|
2889
|
+
for arg in "$@"; do
|
|
2890
|
+
printf ' '
|
|
2891
|
+
shell_quote "$arg"
|
|
2892
|
+
done
|
|
2893
|
+
printf '\n'
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
run_install_command() {
|
|
2897
|
+
if [[ "${install_dry_run:-0}" -eq 1 ]]; then
|
|
2898
|
+
print_command "$@"
|
|
2899
|
+
else
|
|
2900
|
+
"$@"
|
|
2901
|
+
fi
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
install_report_success() {
|
|
2905
|
+
local unit_file="$systemd_unit_dir/${install_service}.service"
|
|
2906
|
+
|
|
2907
|
+
if [[ "$install_dry_run" -ne 0 ]]; then
|
|
2908
|
+
return 0
|
|
2909
|
+
fi
|
|
2910
|
+
|
|
2911
|
+
printf 'success: installed Arbiter Docker deployment\n'
|
|
2912
|
+
printf 'installed to: %s\n' "$install_target_dir"
|
|
2913
|
+
printf 'systemd unit: %s\n' "$unit_file"
|
|
2914
|
+
print_mcp_url_value "$(install_mcp_url_value)"
|
|
2915
|
+
if [[ "$install_start" -eq 1 ]]; then
|
|
2916
|
+
printf 'service: %s.service enabled and restarted\n' "$install_service"
|
|
2917
|
+
else
|
|
2918
|
+
printf 'service: %s.service enabled but not started\n' "$install_service"
|
|
2919
|
+
fi
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
install_require_root() {
|
|
2923
|
+
if [[ "$(id -u)" != 0 ]]; then
|
|
2924
|
+
printf 'error: install requires root; rerun with sudo\n' >&2
|
|
2925
|
+
exit 1
|
|
2926
|
+
fi
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
install_relative_path() {
|
|
2930
|
+
local path="$1"
|
|
2931
|
+
local real_deploy_dir
|
|
2932
|
+
local real_path
|
|
2933
|
+
|
|
2934
|
+
real_deploy_dir="$(normalize_existing_path "$deploy_dir")"
|
|
2935
|
+
real_path="$(normalize_existing_path "$path")"
|
|
2936
|
+
printf '%s\n' "${real_path#"$real_deploy_dir"/}"
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
install_target_path_from_compose_value() {
|
|
2940
|
+
local value="$1"
|
|
2941
|
+
local path
|
|
2942
|
+
local real_install_target_dir
|
|
2943
|
+
local real_path
|
|
2944
|
+
|
|
2945
|
+
if [[ "$value" = /* ]]; then
|
|
2946
|
+
printf 'error: install requires relative docker.env host paths, got: %s\n' "$value" >&2
|
|
2947
|
+
return 1
|
|
2948
|
+
fi
|
|
2949
|
+
path="$install_target_dir/${value#./}"
|
|
2950
|
+
real_install_target_dir="$(normalize_path_allow_missing "$install_target_dir")"
|
|
2951
|
+
real_path="$(normalize_path_allow_missing "$path")"
|
|
2952
|
+
if [[ "$real_path" != "$real_install_target_dir" && "$real_path" != "$real_install_target_dir/"* ]]; then
|
|
2953
|
+
printf 'error: install requires docker.env host paths inside --to, got: %s\n' "$value" >&2
|
|
2954
|
+
return 1
|
|
2955
|
+
fi
|
|
2956
|
+
if [[ "$real_path" == "$real_install_target_dir" ]]; then
|
|
2957
|
+
printf 'error: install requires docker.env host paths below --to, got deployment root: %s\n' "$value" >&2
|
|
2958
|
+
return 1
|
|
2959
|
+
fi
|
|
2960
|
+
printf '%s\n' "$real_path"
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
install_target_requirements_file_path() {
|
|
2964
|
+
install_target_path_from_compose_value \
|
|
2965
|
+
"$(compose_env_value ARBITER_REQUIREMENTS_FILE ./requirements.txt)"
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
install_target_wheels_dir_path() {
|
|
2969
|
+
install_target_path_from_compose_value \
|
|
2970
|
+
"$(compose_env_value ARBITER_WHEELS_DIR ./wheels)"
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
install_local_wheel_python() {
|
|
2974
|
+
local repo_root="$1"
|
|
2975
|
+
local python_bin="${ARBITER_PYTHON:-}"
|
|
2976
|
+
|
|
2977
|
+
if [[ -n "$python_bin" ]]; then
|
|
2978
|
+
printf '%s\n' "$python_bin"
|
|
2979
|
+
return 0
|
|
2980
|
+
fi
|
|
2981
|
+
if [[ -x "$repo_root/.venv/bin/python" ]]; then
|
|
2982
|
+
printf '%s\n' "$repo_root/.venv/bin/python"
|
|
2983
|
+
return 0
|
|
2984
|
+
fi
|
|
2985
|
+
if command -v python >/dev/null; then
|
|
2986
|
+
printf 'python\n'
|
|
2987
|
+
return 0
|
|
2988
|
+
fi
|
|
2989
|
+
if command -v python3 >/dev/null; then
|
|
2990
|
+
printf 'python3\n'
|
|
2991
|
+
return 0
|
|
2992
|
+
fi
|
|
2993
|
+
return 1
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
install_chown_to_invoking_user() {
|
|
2997
|
+
[[ -n "${SUDO_UID:-}" && -n "${SUDO_GID:-}" ]] || return 0
|
|
2998
|
+
[[ "$#" -gt 0 ]] || return 0
|
|
2999
|
+
chown "$SUDO_UID:$SUDO_GID" "$@"
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
install_chown_wheelhouse_to_invoking_user() {
|
|
3003
|
+
local target_wheels_dir="$1"
|
|
3004
|
+
local wheel
|
|
3005
|
+
local -a paths=()
|
|
3006
|
+
|
|
3007
|
+
[[ -n "${SUDO_UID:-}" && -n "${SUDO_GID:-}" ]] || return 0
|
|
3008
|
+
[[ -d "$target_wheels_dir" ]] || return 0
|
|
3009
|
+
paths+=("$target_wheels_dir")
|
|
3010
|
+
shopt -s nullglob
|
|
3011
|
+
for wheel in "$target_wheels_dir"/*.whl; do
|
|
3012
|
+
paths+=("$wheel")
|
|
3013
|
+
done
|
|
3014
|
+
shopt -u nullglob
|
|
3015
|
+
install_chown_to_invoking_user "${paths[@]}"
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
install_local_checkout_requirements_file=""
|
|
3019
|
+
install_local_checkout_has_source_override=0
|
|
3020
|
+
|
|
3021
|
+
install_build_source_wheel() {
|
|
3022
|
+
local python_bin="$1"
|
|
3023
|
+
local source_dir="$2"
|
|
3024
|
+
local target_wheels_dir="$3"
|
|
3025
|
+
local build_dir
|
|
3026
|
+
local output_file
|
|
3027
|
+
local built_wheels
|
|
3028
|
+
local wheel
|
|
3029
|
+
|
|
3030
|
+
build_dir="$(mktemp -d "${TMPDIR:-/tmp}/arbiter-install-wheel.XXXXXX")"
|
|
3031
|
+
output_file="$build_dir/pip-wheel.out"
|
|
3032
|
+
if ! "$python_bin" -m pip --disable-pip-version-check wheel --no-deps --no-build-isolation --wheel-dir "$build_dir" "$source_dir" >"$output_file" 2>&1; then
|
|
3033
|
+
cat "$output_file" >&2
|
|
3034
|
+
printf 'error: failed to build install wheel from local source: %s\n' "$source_dir" >&2
|
|
3035
|
+
rm -rf "$build_dir"
|
|
3036
|
+
return 1
|
|
3037
|
+
fi
|
|
3038
|
+
|
|
3039
|
+
built_wheels=("$build_dir"/*.whl)
|
|
3040
|
+
if [[ "${#built_wheels[@]}" -ne 1 || ! -f "${built_wheels[0]}" ]]; then
|
|
3041
|
+
printf 'error: expected one wheel from local source, got %s: %s\n' "${#built_wheels[@]}" "$source_dir" >&2
|
|
3042
|
+
rm -rf "$build_dir"
|
|
3043
|
+
return 1
|
|
3044
|
+
fi
|
|
3045
|
+
|
|
3046
|
+
wheel="${built_wheels[0]}"
|
|
3047
|
+
mkdir -p "$target_wheels_dir"
|
|
3048
|
+
cp "$wheel" "$target_wheels_dir/${wheel##*/}"
|
|
3049
|
+
install_chown_to_invoking_user "$target_wheels_dir/${wheel##*/}"
|
|
3050
|
+
printf '%s\n' "${wheel##*/}"
|
|
3051
|
+
rm -rf "$build_dir"
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
install_prepare_local_checkout_wheels() {
|
|
3055
|
+
local active_requirements_file
|
|
3056
|
+
local active_wheels_dir
|
|
3057
|
+
local repo_root
|
|
3058
|
+
local python_bin
|
|
3059
|
+
local tmp_requirements=""
|
|
3060
|
+
local raw_line
|
|
3061
|
+
local line
|
|
3062
|
+
local source_suffix
|
|
3063
|
+
local source_dir
|
|
3064
|
+
local wheel_name
|
|
3065
|
+
local promoted=0
|
|
3066
|
+
local has_source_requirements=0
|
|
3067
|
+
local has_source_override=0
|
|
3068
|
+
local image
|
|
3069
|
+
local docker_user
|
|
3070
|
+
|
|
3071
|
+
install_local_checkout_requirements_file=""
|
|
3072
|
+
install_local_checkout_has_source_override=0
|
|
3073
|
+
active_requirements_file="$(deploy_path_from_compose_value "$(compose_env_value ARBITER_REQUIREMENTS_FILE ./requirements.txt)")"
|
|
3074
|
+
active_wheels_dir="$(wheels_dir_path)"
|
|
3075
|
+
|
|
3076
|
+
if [[ -f "$active_requirements_file" ]] && requirements_has_source_paths "$active_requirements_file"; then
|
|
3077
|
+
has_source_requirements=1
|
|
3078
|
+
fi
|
|
3079
|
+
if [[ -f "$compose_override_file" ]] && grep -q '/source/arbiter' "$compose_override_file"; then
|
|
3080
|
+
has_source_override=1
|
|
3081
|
+
install_local_checkout_has_source_override=1
|
|
3082
|
+
fi
|
|
3083
|
+
if [[ "$has_source_requirements" -eq 0 && "$has_source_override" -eq 0 ]]; then
|
|
3084
|
+
return 0
|
|
3085
|
+
fi
|
|
3086
|
+
|
|
3087
|
+
if [[ "${install_dry_run:-0}" -eq 1 ]]; then
|
|
3088
|
+
printf 'would prepare wheel-backed install artifacts from local checkout without changing staging\n'
|
|
3089
|
+
return 0
|
|
3090
|
+
fi
|
|
3091
|
+
|
|
3092
|
+
require_file "$active_requirements_file"
|
|
3093
|
+
validate_requirements_file "$active_requirements_file"
|
|
3094
|
+
|
|
3095
|
+
if [[ "$has_source_requirements" -eq 1 ]]; then
|
|
3096
|
+
if ! repo_root="$(find_repo_root)"; then
|
|
3097
|
+
printf 'error: cannot promote local checkout deployment: Arbiter repo root not found\n' >&2
|
|
3098
|
+
return 1
|
|
3099
|
+
fi
|
|
3100
|
+
if ! python_bin="$(install_local_wheel_python "$repo_root")"; then
|
|
3101
|
+
printf 'error: cannot promote local checkout deployment: python command not found\n' >&2
|
|
3102
|
+
printf ' set ARBITER_PYTHON to the Python interpreter for building local wheels\n' >&2
|
|
3103
|
+
return 1
|
|
3104
|
+
fi
|
|
3105
|
+
|
|
3106
|
+
tmp_requirements="$(mktemp "${TMPDIR:-/tmp}/arbiter-install-requirements.XXXXXX")"
|
|
3107
|
+
while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do
|
|
3108
|
+
line="$raw_line"
|
|
3109
|
+
line="${line#"${line%%[![:space:]]*}"}"
|
|
3110
|
+
line="${line%"${line##*[![:space:]]}"}"
|
|
3111
|
+
line="${line%%[[:space:]]#*}"
|
|
3112
|
+
line="${line%"${line##*[![:space:]]}"}"
|
|
3113
|
+
if [[ -z "$line" || "$line" == \#* ]]; then
|
|
3114
|
+
printf '%s\n' "$raw_line" >>"$tmp_requirements"
|
|
3115
|
+
continue
|
|
3116
|
+
fi
|
|
3117
|
+
if [[ "$line" == /source/arbiter || "$line" == /source/arbiter/* ]]; then
|
|
3118
|
+
source_suffix="${line#/source/arbiter}"
|
|
3119
|
+
source_suffix="${source_suffix#/}"
|
|
3120
|
+
source_dir="$repo_root"
|
|
3121
|
+
if [[ -n "$source_suffix" ]]; then
|
|
3122
|
+
source_dir="$repo_root/$source_suffix"
|
|
3123
|
+
fi
|
|
3124
|
+
if [[ ! -d "$source_dir" || ! -f "$source_dir/pyproject.toml" ]]; then
|
|
3125
|
+
printf 'error: cannot map local checkout requirement to package source: %s\n' "$line" >&2
|
|
3126
|
+
printf ' expected pyproject.toml at %s\n' "$source_dir" >&2
|
|
3127
|
+
rm -f "$tmp_requirements"
|
|
3128
|
+
return 1
|
|
3129
|
+
fi
|
|
3130
|
+
wheel_name="$(install_build_source_wheel "$python_bin" "$source_dir" "$active_wheels_dir")" || {
|
|
3131
|
+
rm -f "$tmp_requirements"
|
|
3132
|
+
return 1
|
|
3133
|
+
}
|
|
3134
|
+
printf '/wheels/%s\n' "$wheel_name" >>"$tmp_requirements"
|
|
3135
|
+
promoted=1
|
|
3136
|
+
continue
|
|
3137
|
+
fi
|
|
3138
|
+
printf '%s\n' "$raw_line" >>"$tmp_requirements"
|
|
3139
|
+
done <"$active_requirements_file"
|
|
3140
|
+
fi
|
|
3141
|
+
|
|
3142
|
+
if [[ "$promoted" -eq 1 ]]; then
|
|
3143
|
+
if ! validate_requirements_file "$tmp_requirements"; then
|
|
3144
|
+
printf 'error: generated install requirements are invalid; staging requirements unchanged: %s\n' "$active_requirements_file" >&2
|
|
3145
|
+
rm -f "$tmp_requirements"
|
|
3146
|
+
return 1
|
|
3147
|
+
fi
|
|
3148
|
+
image="$(compose_env_value ARBITER_IMAGE python:3.11-slim)"
|
|
3149
|
+
docker_user="$(id -u):$(id -g)"
|
|
3150
|
+
if ! prepare_dependency_wheelhouse "$tmp_requirements" "$active_wheels_dir" "$docker_user" "$image" 1 0; then
|
|
3151
|
+
rm -f "$tmp_requirements"
|
|
3152
|
+
return 1
|
|
3153
|
+
fi
|
|
3154
|
+
if ! validate_dependency_wheelhouse "$tmp_requirements" "$active_wheels_dir" "$docker_user" "$image" 1; then
|
|
3155
|
+
rm -f "$tmp_requirements"
|
|
3156
|
+
return 1
|
|
3157
|
+
fi
|
|
3158
|
+
install_chown_wheelhouse_to_invoking_user "$active_wheels_dir"
|
|
3159
|
+
install_chown_to_invoking_user "$tmp_requirements"
|
|
3160
|
+
install_local_checkout_requirements_file="$tmp_requirements"
|
|
3161
|
+
printf 'prepared wheel-backed install requirements from local checkout: %s\n' "$install_local_checkout_requirements_file"
|
|
3162
|
+
else
|
|
3163
|
+
if [[ -n "$tmp_requirements" ]]; then
|
|
3164
|
+
rm -f "$tmp_requirements"
|
|
3165
|
+
fi
|
|
3166
|
+
fi
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
install_apply_local_checkout_install_artifacts() {
|
|
3170
|
+
local target_requirements_file
|
|
3171
|
+
local target_compose_override_file
|
|
3172
|
+
|
|
3173
|
+
if [[ "${install_dry_run:-0}" -eq 1 ]]; then
|
|
3174
|
+
return 0
|
|
3175
|
+
fi
|
|
3176
|
+
|
|
3177
|
+
if [[ -n "$install_local_checkout_requirements_file" ]]; then
|
|
3178
|
+
target_requirements_file="$(install_target_requirements_file_path)"
|
|
3179
|
+
install_copy_file_protected "$install_local_checkout_requirements_file" "$target_requirements_file" 0640
|
|
3180
|
+
rm -f "$install_local_checkout_requirements_file"
|
|
3181
|
+
install_local_checkout_requirements_file=""
|
|
3182
|
+
printf 'installed wheel-backed requirements without changing staging: %s\n' "$target_requirements_file"
|
|
3183
|
+
fi
|
|
3184
|
+
|
|
3185
|
+
target_compose_override_file="$install_target_dir/compose.override.yaml"
|
|
3186
|
+
if [[ "$install_local_checkout_has_source_override" -eq 1 && -f "$target_compose_override_file" ]] && grep -q '/source/arbiter' "$target_compose_override_file"; then
|
|
3187
|
+
rm -f "$target_compose_override_file"
|
|
3188
|
+
printf 'omitted local checkout compose override from install target: %s\n' "$target_compose_override_file"
|
|
3189
|
+
fi
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
install_target_app_env_path() {
|
|
3193
|
+
install_target_path_from_compose_value \
|
|
3194
|
+
"$(install_env_value ARBITER_APP_ENV_FILE ./conf/.env)"
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
install_env_value() {
|
|
3198
|
+
local key="$1"
|
|
3199
|
+
local default="$2"
|
|
3200
|
+
|
|
3201
|
+
if [[ "${install_dry_run:-0}" -eq 1 ]]; then
|
|
3202
|
+
case "$key" in
|
|
3203
|
+
ARBITER_HOST_BIND)
|
|
3204
|
+
printf '127.0.0.1\n'
|
|
3205
|
+
return
|
|
3206
|
+
;;
|
|
3207
|
+
ARBITER_HOST_PORT)
|
|
3208
|
+
printf '8025\n'
|
|
3209
|
+
return
|
|
3210
|
+
;;
|
|
3211
|
+
esac
|
|
3212
|
+
fi
|
|
3213
|
+
env_file_value "$install_target_dir/docker.env" "$key" || printf '%s\n' "$default"
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
install_server_host_url_value() {
|
|
3217
|
+
local host
|
|
3218
|
+
|
|
3219
|
+
host="$(install_env_value ARBITER_HOST_BIND 127.0.0.1)"
|
|
3220
|
+
case "$host" in
|
|
3221
|
+
0.0.0.0)
|
|
3222
|
+
printf '127.0.0.1\n'
|
|
3223
|
+
;;
|
|
3224
|
+
*:*)
|
|
3225
|
+
printf '[%s]\n' "$host"
|
|
3226
|
+
;;
|
|
3227
|
+
*)
|
|
3228
|
+
printf '%s\n' "$host"
|
|
3229
|
+
;;
|
|
3230
|
+
esac
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
install_mcp_url_value() {
|
|
3234
|
+
printf 'http://%s:%s/mcp\n' \
|
|
3235
|
+
"$(install_server_host_url_value)" \
|
|
3236
|
+
"$(install_env_value ARBITER_HOST_PORT 8025)"
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
install_ensure_identity() {
|
|
3240
|
+
if [[ "${install_dry_run:-0}" -eq 1 ]]; then
|
|
3241
|
+
printf 'would create system group if missing: %s\n' "$install_group"
|
|
3242
|
+
printf 'would create system user if missing: %s\n' "$install_user"
|
|
3243
|
+
return
|
|
3244
|
+
fi
|
|
3245
|
+
|
|
3246
|
+
if ! getent group "$install_group" >/dev/null; then
|
|
3247
|
+
groupadd --system "$install_group"
|
|
3248
|
+
fi
|
|
3249
|
+
|
|
3250
|
+
if ! id -u "$install_user" >/dev/null 2>&1; then
|
|
3251
|
+
useradd \
|
|
3252
|
+
--system \
|
|
3253
|
+
--gid "$install_group" \
|
|
3254
|
+
--home-dir "$install_target_dir" \
|
|
3255
|
+
--shell /usr/sbin/nologin \
|
|
3256
|
+
--comment "Arbiter service user" \
|
|
3257
|
+
"$install_user"
|
|
3258
|
+
fi
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
install_reject_symlinks() {
|
|
3262
|
+
local path="$1"
|
|
3263
|
+
local symlink
|
|
3264
|
+
|
|
3265
|
+
symlink="$(find "$path" -type l -print -quit)"
|
|
3266
|
+
if [[ -n "$symlink" ]]; then
|
|
3267
|
+
printf 'error: install does not support symlinks in deployment tree: %s\n' "$symlink" >&2
|
|
3268
|
+
return 1
|
|
3269
|
+
fi
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
install_copy_file_protected() {
|
|
3273
|
+
local source_file="$1"
|
|
3274
|
+
local target_file="$2"
|
|
3275
|
+
local mode="$3"
|
|
3276
|
+
local target_parent
|
|
3277
|
+
local tmp_file
|
|
3278
|
+
|
|
3279
|
+
target_parent="$(dirname -- "$target_file")"
|
|
3280
|
+
mkdir -p "$target_parent"
|
|
3281
|
+
chmod 0750 "$target_parent"
|
|
3282
|
+
chown "$install_user:$install_group" "$target_parent"
|
|
3283
|
+
tmp_file="$(mktemp "$target_parent/.arbiter-install.XXXXXX")"
|
|
3284
|
+
if ! cat "$source_file" >"$tmp_file"; then
|
|
3285
|
+
rm -f "$tmp_file"
|
|
3286
|
+
return 1
|
|
3287
|
+
fi
|
|
3288
|
+
chown "$install_user:$install_group" "$tmp_file"
|
|
3289
|
+
chmod "$mode" "$tmp_file"
|
|
3290
|
+
mv -f "$tmp_file" "$target_file"
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
install_file_mode_for_relative_path() {
|
|
3294
|
+
local relative_path="$1"
|
|
3295
|
+
local app_env_relative_path="${2:-}"
|
|
3296
|
+
|
|
3297
|
+
if [[ -n "$app_env_relative_path" && "$relative_path" == "$app_env_relative_path" ]]; then
|
|
3298
|
+
printf '0600\n'
|
|
3299
|
+
return
|
|
3300
|
+
fi
|
|
3301
|
+
if [[ "$relative_path" == "arbiter-docker" ]]; then
|
|
3302
|
+
printf '0750\n'
|
|
3303
|
+
return
|
|
3304
|
+
fi
|
|
3305
|
+
printf '0640\n'
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
install_copy_tree_protected() {
|
|
3309
|
+
local source_root="$1"
|
|
3310
|
+
local target_root="$2"
|
|
3311
|
+
local app_env_relative_path="${3:-}"
|
|
3312
|
+
local source_path
|
|
3313
|
+
local relative_path
|
|
3314
|
+
local target_path
|
|
3315
|
+
local mode
|
|
3316
|
+
|
|
3317
|
+
install_reject_symlinks "$source_root"
|
|
3318
|
+
mkdir -p "$target_root"
|
|
3319
|
+
chmod 0750 "$target_root"
|
|
3320
|
+
chown "$install_user:$install_group" "$target_root"
|
|
3321
|
+
|
|
3322
|
+
while IFS= read -r -d '' source_path; do
|
|
3323
|
+
[[ "$source_path" == "$source_root" ]] && continue
|
|
3324
|
+
relative_path="${source_path#"$source_root"/}"
|
|
3325
|
+
mkdir -p "$target_root/$relative_path"
|
|
3326
|
+
chmod 0750 "$target_root/$relative_path"
|
|
3327
|
+
chown "$install_user:$install_group" "$target_root/$relative_path"
|
|
3328
|
+
done < <(find "$source_root" -type d -print0)
|
|
3329
|
+
|
|
3330
|
+
while IFS= read -r -d '' source_path; do
|
|
3331
|
+
relative_path="${source_path#"$source_root"/}"
|
|
3332
|
+
target_path="$target_root/$relative_path"
|
|
3333
|
+
mode="$(install_file_mode_for_relative_path "$relative_path" "$app_env_relative_path")"
|
|
3334
|
+
install_copy_file_protected "$source_path" "$target_path" "$mode"
|
|
3335
|
+
done < <(find "$source_root" -type f -print0)
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
install_backup_existing_path() {
|
|
3339
|
+
local source_path="$1"
|
|
3340
|
+
local backup_prefix="$2"
|
|
3341
|
+
local app_env_relative_path="${3:-}"
|
|
3342
|
+
local timestamp
|
|
3343
|
+
local backup_parent
|
|
3344
|
+
local backup_path
|
|
3345
|
+
local attempt=0
|
|
3346
|
+
|
|
3347
|
+
[[ -e "$source_path" ]] || return 0
|
|
3348
|
+
|
|
3349
|
+
timestamp="$(date -u '+%Y%m%dT%H%M%SZ')"
|
|
3350
|
+
backup_parent="$install_target_dir/backup"
|
|
3351
|
+
mkdir -p "$backup_parent"
|
|
3352
|
+
chmod 0750 "$backup_parent"
|
|
3353
|
+
chown "$install_user:$install_group" "$backup_parent"
|
|
3354
|
+
|
|
3355
|
+
backup_path="$backup_parent/${backup_prefix}-${timestamp}"
|
|
3356
|
+
while [[ -e "$backup_path" ]]; do
|
|
3357
|
+
attempt=$((attempt + 1))
|
|
3358
|
+
backup_path="$backup_parent/${backup_prefix}-${timestamp}-${attempt}"
|
|
3359
|
+
done
|
|
3360
|
+
|
|
3361
|
+
mv "$source_path" "$backup_path"
|
|
3362
|
+
chown -R "$install_user:$install_group" "$backup_path"
|
|
3363
|
+
find "$backup_path" -type d -exec chmod 0750 {} +
|
|
3364
|
+
find "$backup_path" -type f -exec chmod 0640 {} +
|
|
3365
|
+
if [[ -n "$app_env_relative_path" && -f "$backup_path/$app_env_relative_path" ]]; then
|
|
3366
|
+
chmod 0600 "$backup_path/$app_env_relative_path"
|
|
3367
|
+
elif [[ -f "$backup_path/.env" ]]; then
|
|
3368
|
+
chmod 0600 "$backup_path/.env"
|
|
3369
|
+
fi
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
install_copy_deployment() {
|
|
3373
|
+
local source_dir
|
|
3374
|
+
local target_dir
|
|
3375
|
+
local target_docker_env_file
|
|
3376
|
+
local preserve_config_dir_value="./conf"
|
|
3377
|
+
local preserve_app_env_value="./conf/.env"
|
|
3378
|
+
local preserve_config_name_value="arbiter-server"
|
|
3379
|
+
local preserve_plugin_data_dir_value="./data/plugins"
|
|
3380
|
+
local preserve_plugin_data_dir_env=0
|
|
3381
|
+
local preserve_config_dir
|
|
3382
|
+
local preserve_app_env
|
|
3383
|
+
local preserve_plugin_data_dir
|
|
3384
|
+
local staging_config_dir
|
|
3385
|
+
local staging_app_env
|
|
3386
|
+
local preserve_tmp_dir=""
|
|
3387
|
+
local preserve_config=0
|
|
3388
|
+
local preserve_env=0
|
|
3389
|
+
local replace_config_dir=0
|
|
3390
|
+
local target_is_source=0
|
|
3391
|
+
local preserve_config_dir_real
|
|
3392
|
+
local preserve_app_env_real
|
|
3393
|
+
local preserve_app_env_config_rel=""
|
|
3394
|
+
local staging_app_env_rel=""
|
|
3395
|
+
|
|
3396
|
+
source_dir="$(normalize_existing_path "$deploy_dir")"
|
|
3397
|
+
target_dir="$install_target_dir"
|
|
3398
|
+
target_docker_env_file="$target_dir/docker.env"
|
|
3399
|
+
|
|
3400
|
+
if [[ "${install_dry_run:-0}" -eq 1 ]]; then
|
|
3401
|
+
printf 'would copy deployment: %s -> %s\n' "$source_dir" "$target_dir"
|
|
3402
|
+
if [[ "$install_replace_config" -eq 0 ]]; then
|
|
3403
|
+
printf 'would preserve installed config and env if present: %s\n' "$target_dir"
|
|
3404
|
+
elif [[ "$install_replace_env" -eq 0 ]]; then
|
|
3405
|
+
printf 'would replace installed config from staging and preserve installed env if present\n'
|
|
3406
|
+
else
|
|
3407
|
+
printf 'would replace installed config and env from staging\n'
|
|
3408
|
+
fi
|
|
3409
|
+
return
|
|
3410
|
+
fi
|
|
3411
|
+
|
|
3412
|
+
if [[ "$(normalize_path_allow_missing "$target_dir")" == "$source_dir" ]]; then
|
|
3413
|
+
target_is_source=1
|
|
3414
|
+
fi
|
|
3415
|
+
|
|
3416
|
+
if [[ "$target_is_source" -eq 0 ]]; then
|
|
3417
|
+
install_reject_symlinks "$source_dir"
|
|
3418
|
+
fi
|
|
3419
|
+
|
|
3420
|
+
if [[ -e "$target_dir" && -f "$target_docker_env_file" ]]; then
|
|
3421
|
+
preserve_plugin_data_dir_value="$(env_file_value "$target_docker_env_file" ARBITER_PLUGIN_DATA_DIR || printf './data/plugins\n')"
|
|
3422
|
+
preserve_plugin_data_dir="$(install_target_path_from_compose_value "$preserve_plugin_data_dir_value")"
|
|
3423
|
+
if [[ -e "$preserve_plugin_data_dir" || -L "$preserve_plugin_data_dir" ]]; then
|
|
3424
|
+
install_reject_symlinks "$preserve_plugin_data_dir"
|
|
3425
|
+
fi
|
|
3426
|
+
preserve_plugin_data_dir_env=1
|
|
3427
|
+
fi
|
|
3428
|
+
|
|
3429
|
+
if [[ -e "$target_dir" ]]; then
|
|
3430
|
+
if [[ -f "$target_docker_env_file" ]]; then
|
|
3431
|
+
preserve_config_dir_value="$(env_file_value "$target_docker_env_file" ARBITER_CONFIG_DIR || printf './conf\n')"
|
|
3432
|
+
preserve_app_env_value="$(env_file_value "$target_docker_env_file" ARBITER_APP_ENV_FILE || printf './conf/.env\n')"
|
|
3433
|
+
preserve_config_name_value="$(env_file_value "$target_docker_env_file" ARBITER_CONFIG_NAME || printf 'arbiter-server\n')"
|
|
3434
|
+
fi
|
|
3435
|
+
preserve_config_dir="$(install_target_path_from_compose_value "$preserve_config_dir_value")"
|
|
3436
|
+
preserve_app_env="$(install_target_path_from_compose_value "$preserve_app_env_value")"
|
|
3437
|
+
if [[ -e "$preserve_config_dir" || -L "$preserve_config_dir" ]]; then
|
|
3438
|
+
install_reject_symlinks "$preserve_config_dir"
|
|
3439
|
+
fi
|
|
3440
|
+
if [[ -e "$preserve_app_env" || -L "$preserve_app_env" ]]; then
|
|
3441
|
+
install_reject_symlinks "$preserve_app_env"
|
|
3442
|
+
fi
|
|
3443
|
+
if [[ "$install_replace_config" -eq 0 && ( -e "$preserve_config_dir" || -e "$preserve_app_env" ) ]]; then
|
|
3444
|
+
preserve_config=1
|
|
3445
|
+
preserve_tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/arbiter-install-config.XXXXXX")"
|
|
3446
|
+
if [[ -e "$preserve_app_env" ]]; then
|
|
3447
|
+
preserve_config_dir_real="$(normalize_path_allow_missing "$preserve_config_dir")"
|
|
3448
|
+
preserve_app_env_real="$(normalize_path_allow_missing "$preserve_app_env")"
|
|
3449
|
+
case "$preserve_app_env_real" in
|
|
3450
|
+
"$preserve_config_dir_real"/*)
|
|
3451
|
+
preserve_app_env_config_rel="${preserve_app_env_real#"$preserve_config_dir_real"/}"
|
|
3452
|
+
;;
|
|
3453
|
+
*)
|
|
3454
|
+
cp -a "$preserve_app_env" "$preserve_tmp_dir/app-env"
|
|
3455
|
+
;;
|
|
3456
|
+
esac
|
|
3457
|
+
fi
|
|
3458
|
+
if [[ -e "$preserve_config_dir" ]]; then
|
|
3459
|
+
cp -a "$preserve_config_dir" "$preserve_tmp_dir/config-dir"
|
|
3460
|
+
install_backup_existing_path \
|
|
3461
|
+
"$preserve_config_dir" \
|
|
3462
|
+
conf \
|
|
3463
|
+
"$preserve_app_env_config_rel"
|
|
3464
|
+
fi
|
|
3465
|
+
elif [[ "$install_replace_config" -eq 1 ]]; then
|
|
3466
|
+
if [[ "$install_replace_env" -eq 0 && -e "$preserve_app_env" ]]; then
|
|
3467
|
+
preserve_env=1
|
|
3468
|
+
preserve_tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/arbiter-install-config.XXXXXX")"
|
|
3469
|
+
cp -a "$preserve_app_env" "$preserve_tmp_dir/app-env"
|
|
3470
|
+
fi
|
|
3471
|
+
if [[ "$target_is_source" -eq 0 && -e "$preserve_config_dir" ]]; then
|
|
3472
|
+
replace_config_dir=1
|
|
3473
|
+
fi
|
|
3474
|
+
fi
|
|
3475
|
+
fi
|
|
3476
|
+
|
|
3477
|
+
if [[ "$replace_config_dir" -eq 1 ]]; then
|
|
3478
|
+
rm -rf "$preserve_config_dir"
|
|
3479
|
+
fi
|
|
3480
|
+
|
|
3481
|
+
mkdir -p "$target_dir"
|
|
3482
|
+
if [[ "$target_is_source" -eq 0 ]]; then
|
|
3483
|
+
staging_app_env_rel="$(install_relative_path "$(app_env_path)")"
|
|
3484
|
+
install_copy_tree_protected "$source_dir" "$target_dir" "$staging_app_env_rel"
|
|
3485
|
+
fi
|
|
3486
|
+
|
|
3487
|
+
if [[ "$preserve_config" -eq 1 ]]; then
|
|
3488
|
+
staging_config_dir="$(install_target_path_from_compose_value "$(compose_env_value ARBITER_CONFIG_DIR ./conf)")"
|
|
3489
|
+
staging_app_env="$(install_target_path_from_compose_value "$(compose_env_value ARBITER_APP_ENV_FILE ./conf/.env)")"
|
|
3490
|
+
|
|
3491
|
+
rm -rf "$staging_config_dir"
|
|
3492
|
+
case "$(normalize_path_allow_missing "$staging_app_env")" in
|
|
3493
|
+
"$(normalize_path_allow_missing "$staging_config_dir")"/*)
|
|
3494
|
+
;;
|
|
3495
|
+
*)
|
|
3496
|
+
rm -f "$staging_app_env"
|
|
3497
|
+
;;
|
|
3498
|
+
esac
|
|
3499
|
+
|
|
3500
|
+
if [[ -e "$preserve_tmp_dir/config-dir" ]]; then
|
|
3501
|
+
mkdir -p "$(dirname -- "$preserve_config_dir")"
|
|
3502
|
+
rm -rf "$preserve_config_dir"
|
|
3503
|
+
install_copy_tree_protected \
|
|
3504
|
+
"$preserve_tmp_dir/config-dir" \
|
|
3505
|
+
"$preserve_config_dir" \
|
|
3506
|
+
"$preserve_app_env_config_rel"
|
|
3507
|
+
fi
|
|
3508
|
+
if [[ -e "$preserve_tmp_dir/app-env" ]]; then
|
|
3509
|
+
mkdir -p "$(dirname -- "$preserve_app_env")"
|
|
3510
|
+
install_copy_file_protected "$preserve_tmp_dir/app-env" "$preserve_app_env" 0600
|
|
3511
|
+
fi
|
|
3512
|
+
set_env_file_value "$target_docker_env_file" ARBITER_CONFIG_DIR "$preserve_config_dir_value"
|
|
3513
|
+
set_env_file_value "$target_docker_env_file" ARBITER_APP_ENV_FILE "$preserve_app_env_value"
|
|
3514
|
+
set_env_file_value "$target_docker_env_file" ARBITER_CONFIG_NAME "$preserve_config_name_value"
|
|
3515
|
+
rm -rf "$preserve_tmp_dir"
|
|
3516
|
+
elif [[ "$preserve_env" -eq 1 ]]; then
|
|
3517
|
+
mkdir -p "$(dirname -- "$preserve_app_env")"
|
|
3518
|
+
install_copy_file_protected "$preserve_tmp_dir/app-env" "$preserve_app_env" 0600
|
|
3519
|
+
set_env_file_value "$target_docker_env_file" ARBITER_APP_ENV_FILE "$preserve_app_env_value"
|
|
3520
|
+
rm -rf "$preserve_tmp_dir"
|
|
3521
|
+
fi
|
|
3522
|
+
if [[ "$preserve_plugin_data_dir_env" -eq 1 ]]; then
|
|
3523
|
+
set_env_file_value "$target_docker_env_file" ARBITER_PLUGIN_DATA_DIR "$preserve_plugin_data_dir_value"
|
|
3524
|
+
fi
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
install_mark_deployment_installed() {
|
|
3528
|
+
local target_compose_file="$install_target_dir/compose.yaml"
|
|
3529
|
+
local target_docker_env_file="$install_target_dir/docker.env"
|
|
3530
|
+
local tmp_path
|
|
3531
|
+
local install_uid
|
|
3532
|
+
local install_gid
|
|
3533
|
+
|
|
3534
|
+
if [[ "${install_dry_run:-0}" -eq 1 ]]; then
|
|
3535
|
+
printf 'would set deployment scope in compose.yaml: %s\n' "arbiter.deployment_scope=installed"
|
|
3536
|
+
printf 'would set installed Docker identity and container user in docker.env\n'
|
|
3537
|
+
return
|
|
3538
|
+
fi
|
|
3539
|
+
|
|
3540
|
+
require_file "$target_compose_file"
|
|
3541
|
+
tmp_path="$(mktemp "${TMPDIR:-/tmp}/arbiter-compose.XXXXXX")"
|
|
3542
|
+
sed \
|
|
3543
|
+
-e 's/arbiter\.deployment_scope=staged/arbiter.deployment_scope=installed/g' \
|
|
3544
|
+
-e 's/ARBITER_CONTAINER_NAME:-arbiter-staging/ARBITER_CONTAINER_NAME:-arbiter/g' \
|
|
3545
|
+
-e 's/ARBITER_HOST_PORT:-18025/ARBITER_HOST_PORT:-8025/g' \
|
|
3546
|
+
-e 's/ARBITER_DOCKER_NETWORK_NAME:-arbiter-staging/ARBITER_DOCKER_NETWORK_NAME:-arbiter/g' \
|
|
3547
|
+
-e 's/ARBITER_DOCKER_BRIDGE_NAME:-arbiter-stg0/ARBITER_DOCKER_BRIDGE_NAME:-arbiter0/g' \
|
|
3548
|
+
-e 's#ARBITER_DOCKER_SUBNET:-172\.31\.251\.0/24#ARBITER_DOCKER_SUBNET:-172.31.250.0/24#g' \
|
|
3549
|
+
"$target_compose_file" >"$tmp_path"
|
|
3550
|
+
mv "$tmp_path" "$target_compose_file"
|
|
3551
|
+
|
|
3552
|
+
require_file "$target_docker_env_file"
|
|
3553
|
+
tmp_path="$(mktemp "${TMPDIR:-/tmp}/arbiter-docker-env.XXXXXX")"
|
|
3554
|
+
sed \
|
|
3555
|
+
-e 's/^ARBITER_CONTAINER_NAME=.*/ARBITER_CONTAINER_NAME=arbiter/' \
|
|
3556
|
+
-e 's/^ARBITER_HOST_BIND=.*/ARBITER_HOST_BIND=127.0.0.1/' \
|
|
3557
|
+
-e 's/^ARBITER_HOST_PORT=.*/ARBITER_HOST_PORT=8025/' \
|
|
3558
|
+
-e 's/^ARBITER_DOCKER_NETWORK_NAME=.*/ARBITER_DOCKER_NETWORK_NAME=arbiter/' \
|
|
3559
|
+
-e 's/^ARBITER_DOCKER_BRIDGE_NAME=.*/ARBITER_DOCKER_BRIDGE_NAME=arbiter0/' \
|
|
3560
|
+
-e 's#^ARBITER_DOCKER_SUBNET=.*#ARBITER_DOCKER_SUBNET=172.31.250.0/24#' \
|
|
3561
|
+
"$target_docker_env_file" >"$tmp_path"
|
|
3562
|
+
mv "$tmp_path" "$target_docker_env_file"
|
|
3563
|
+
|
|
3564
|
+
install_uid="$(id -u "$install_user")"
|
|
3565
|
+
install_gid="$(getent group "$install_group" | cut -d: -f3)"
|
|
3566
|
+
if [[ -z "$install_gid" ]]; then
|
|
3567
|
+
install_gid="$(id -g "$install_user")"
|
|
3568
|
+
fi
|
|
3569
|
+
set_env_file_value "$target_docker_env_file" ARBITER_CONTAINER_USER "$install_uid:$install_gid"
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
install_metadata_quote() {
|
|
3573
|
+
printf '%q' "$1"
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
install_write_target_metadata() {
|
|
3577
|
+
local metadata_file="$install_target_dir/.arbiter-install.env"
|
|
3578
|
+
local installed_at
|
|
3579
|
+
local source_dir
|
|
3580
|
+
|
|
3581
|
+
if [[ "${install_dry_run:-0}" -eq 1 ]]; then
|
|
3582
|
+
printf 'would write install metadata: %s\n' "$metadata_file"
|
|
3583
|
+
return
|
|
3584
|
+
fi
|
|
3585
|
+
|
|
3586
|
+
installed_at="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
|
3587
|
+
source_dir="$(normalize_existing_path "$deploy_dir")"
|
|
3588
|
+
rm -f "$install_target_dir/.arbiter-installations"
|
|
3589
|
+
cat >"$metadata_file" <<EOF
|
|
3590
|
+
ARBITER_INSTALL_PATH=$(install_metadata_quote "$install_target_dir")
|
|
3591
|
+
ARBITER_INSTALL_SERVICE=$(install_metadata_quote "$install_service")
|
|
3592
|
+
ARBITER_INSTALL_SYSTEMD_UNIT=$(install_metadata_quote "$systemd_unit_dir/${install_service}.service")
|
|
3593
|
+
ARBITER_INSTALL_USER=$(install_metadata_quote "$install_user")
|
|
3594
|
+
ARBITER_INSTALL_GROUP=$(install_metadata_quote "$install_group")
|
|
3595
|
+
ARBITER_INSTALL_MCP_URL=$(install_metadata_quote "$(install_mcp_url_value)")
|
|
3596
|
+
ARBITER_INSTALLED_AT=$(install_metadata_quote "$installed_at")
|
|
3597
|
+
ARBITER_INSTALLED_FROM=$(install_metadata_quote "$source_dir")
|
|
3598
|
+
EOF
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
install_apply_permissions() {
|
|
3602
|
+
local app_env_target
|
|
3603
|
+
local plugin_data_target
|
|
3604
|
+
|
|
3605
|
+
if [[ "${install_dry_run:-0}" -eq 1 ]]; then
|
|
3606
|
+
printf 'would chown/chmod deployment for %s:%s: %s\n' \
|
|
3607
|
+
"$install_user" "$install_group" "$install_target_dir"
|
|
3608
|
+
return
|
|
3609
|
+
fi
|
|
3610
|
+
|
|
3611
|
+
chown -R "$install_user:$install_group" "$install_target_dir"
|
|
3612
|
+
find "$install_target_dir" -type d -exec chmod 0750 {} +
|
|
3613
|
+
find "$install_target_dir" -type f -exec chmod 0640 {} +
|
|
3614
|
+
chmod 0750 "$install_target_dir/arbiter-docker"
|
|
3615
|
+
if [[ -d "$install_target_dir/backup" ]]; then
|
|
3616
|
+
find "$install_target_dir/backup" -type f -exec chmod 0600 {} +
|
|
3617
|
+
fi
|
|
3618
|
+
plugin_data_target="$(install_target_path_from_compose_value "$(compose_env_value ARBITER_PLUGIN_DATA_DIR ./data/plugins)")"
|
|
3619
|
+
if [[ -d "$plugin_data_target" ]]; then
|
|
3620
|
+
find "$plugin_data_target" -type d -exec chmod 0700 {} +
|
|
3621
|
+
find "$plugin_data_target" -type f -exec chmod 0600 {} +
|
|
3622
|
+
fi
|
|
3623
|
+
|
|
3624
|
+
app_env_target="$(install_target_app_env_path)"
|
|
3625
|
+
if [[ -f "$app_env_target" ]]; then
|
|
3626
|
+
chmod 0600 "$app_env_target"
|
|
3627
|
+
fi
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
install_record_location() {
|
|
3631
|
+
local registry_file="$deploy_dir/.arbiter-installations"
|
|
3632
|
+
|
|
3633
|
+
if [[ "${install_dry_run:-0}" -eq 1 ]]; then
|
|
3634
|
+
printf 'would record install location: %s in %s\n' \
|
|
3635
|
+
"$install_target_dir" "$registry_file"
|
|
3636
|
+
return
|
|
3637
|
+
fi
|
|
3638
|
+
|
|
3639
|
+
touch "$registry_file"
|
|
3640
|
+
if ! grep -Fx -- "$install_target_dir" "$registry_file" >/dev/null; then
|
|
3641
|
+
printf '%s\n' "$install_target_dir" >>"$registry_file"
|
|
3642
|
+
fi
|
|
3643
|
+
chmod 0644 "$registry_file"
|
|
3644
|
+
if [[ -n "${SUDO_UID:-}" && -n "${SUDO_GID:-}" ]]; then
|
|
3645
|
+
chown "$SUDO_UID:$SUDO_GID" "$registry_file"
|
|
3646
|
+
fi
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
install_test_server() {
|
|
3650
|
+
local url
|
|
3651
|
+
local client_command
|
|
3652
|
+
|
|
3653
|
+
url="$(install_mcp_url_value)"
|
|
3654
|
+
if [[ "${install_dry_run:-0}" -eq 1 ]]; then
|
|
3655
|
+
printf 'would test MCP URL: %s\n' "$url"
|
|
3656
|
+
return 0
|
|
3657
|
+
fi
|
|
3658
|
+
|
|
3659
|
+
if ! client_command="$(resolve_arbiter_client_command)"; then
|
|
3660
|
+
doctor_warn "skipping MCP test; Arbiter client command not found"
|
|
3661
|
+
return 0
|
|
3662
|
+
fi
|
|
3663
|
+
test_server_url "$url"
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
copy_existing_wheelhouse() {
|
|
3667
|
+
local source_wheels_dir="$1"
|
|
3668
|
+
local target_wheels_dir="$2"
|
|
3669
|
+
local wheel
|
|
3670
|
+
|
|
3671
|
+
[[ -d "$source_wheels_dir" ]] || return 0
|
|
3672
|
+
mkdir -p "$target_wheels_dir"
|
|
3673
|
+
shopt -s nullglob
|
|
3674
|
+
for wheel in "$source_wheels_dir"/*.whl; do
|
|
3675
|
+
cp -p "$wheel" "$target_wheels_dir/"
|
|
3676
|
+
done
|
|
3677
|
+
shopt -u nullglob
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
replace_dependency_wheelhouse() {
|
|
3681
|
+
local source_wheels_dir="$1"
|
|
3682
|
+
local target_wheels_dir="$2"
|
|
3683
|
+
|
|
3684
|
+
mkdir -p "$target_wheels_dir"
|
|
3685
|
+
find "$target_wheels_dir" -maxdepth 1 -type f -name '*.whl' -delete
|
|
3686
|
+
find "$source_wheels_dir" -maxdepth 1 -type f -name '*.whl' -exec cp -p {} "$target_wheels_dir/" \;
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
prune_dependency_wheelhouse() {
|
|
3690
|
+
local target_requirements_file
|
|
3691
|
+
local target_wheels_dir
|
|
3692
|
+
local docker_user
|
|
3693
|
+
local image
|
|
3694
|
+
local quiet
|
|
3695
|
+
local tmp_dir
|
|
3696
|
+
local output_file=""
|
|
3697
|
+
local prune_script
|
|
3698
|
+
local -a report_command
|
|
3699
|
+
local -a prune_command
|
|
3700
|
+
|
|
3701
|
+
target_requirements_file="$1"
|
|
3702
|
+
target_wheels_dir="$2"
|
|
3703
|
+
docker_user="$3"
|
|
3704
|
+
image="$4"
|
|
3705
|
+
quiet="${5:-0}"
|
|
3706
|
+
|
|
3707
|
+
require_file "$target_requirements_file"
|
|
3708
|
+
require_dir "$target_wheels_dir"
|
|
3709
|
+
require_docker_access
|
|
3710
|
+
|
|
3711
|
+
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/arbiter-wheelhouse-prune.XXXXXX")"
|
|
3712
|
+
report_command=(
|
|
3713
|
+
docker run --rm
|
|
3714
|
+
--user "$docker_user"
|
|
3715
|
+
-v "$target_requirements_file:/requirements.txt:ro"
|
|
3716
|
+
-v "$target_wheels_dir:/wheels:ro"
|
|
3717
|
+
-v "$tmp_dir:/work"
|
|
3718
|
+
"$image"
|
|
3719
|
+
python -m pip --disable-pip-version-check install
|
|
3720
|
+
--dry-run --ignore-installed
|
|
3721
|
+
--no-index --find-links /wheels
|
|
3722
|
+
--report /work/report.json
|
|
3723
|
+
-r /requirements.txt
|
|
3724
|
+
)
|
|
3725
|
+
|
|
3726
|
+
prune_script='import json
|
|
3727
|
+
import re
|
|
3728
|
+
from pathlib import Path
|
|
3729
|
+
|
|
3730
|
+
def normalize(name):
|
|
3731
|
+
return re.sub(r"[-_.]+", "-", name).lower()
|
|
3732
|
+
|
|
3733
|
+
with open("/work/report.json", encoding="utf-8") as handle:
|
|
3734
|
+
report = json.load(handle)
|
|
3735
|
+
|
|
3736
|
+
referenced = set()
|
|
3737
|
+
for entry in report.get("install", []):
|
|
3738
|
+
metadata = entry.get("metadata", {})
|
|
3739
|
+
name = metadata.get("name")
|
|
3740
|
+
version = metadata.get("version")
|
|
3741
|
+
if name and version:
|
|
3742
|
+
referenced.add((normalize(name), str(version)))
|
|
3743
|
+
|
|
3744
|
+
for wheel in Path("/wheels").glob("*.whl"):
|
|
3745
|
+
parts = wheel.name[:-4].split("-", 2)
|
|
3746
|
+
if len(parts) < 2 or (normalize(parts[0]), parts[1]) not in referenced:
|
|
3747
|
+
wheel.unlink()
|
|
3748
|
+
'
|
|
3749
|
+
prune_command=(
|
|
3750
|
+
docker run --rm
|
|
3751
|
+
--user "$docker_user"
|
|
3752
|
+
-v "$tmp_dir:/work:ro"
|
|
3753
|
+
-v "$target_wheels_dir:/wheels"
|
|
3754
|
+
"$image"
|
|
3755
|
+
python -c "$prune_script"
|
|
3756
|
+
)
|
|
3757
|
+
|
|
3758
|
+
if [[ "$quiet" -eq 1 ]]; then
|
|
3759
|
+
output_file="$(mktemp "${TMPDIR:-/tmp}/arbiter-wheelhouse-prune.XXXXXX")"
|
|
3760
|
+
fi
|
|
3761
|
+
|
|
3762
|
+
if [[ "$quiet" -eq 1 ]]; then
|
|
3763
|
+
if ! "${report_command[@]}" >"$output_file" 2>&1; then
|
|
3764
|
+
cat "$output_file" >&2
|
|
3765
|
+
rm -f "$output_file"
|
|
3766
|
+
rm -rf "$tmp_dir"
|
|
3767
|
+
printf 'error: failed to resolve prepared wheelhouse contents: %s\n' "$target_wheels_dir" >&2
|
|
3768
|
+
return 1
|
|
3769
|
+
fi
|
|
3770
|
+
if ! "${prune_command[@]}" >"$output_file" 2>&1; then
|
|
3771
|
+
cat "$output_file" >&2
|
|
3772
|
+
rm -f "$output_file"
|
|
3773
|
+
rm -rf "$tmp_dir"
|
|
3774
|
+
printf 'error: failed to prune dependency wheelhouse: %s\n' "$target_wheels_dir" >&2
|
|
3775
|
+
return 1
|
|
3776
|
+
fi
|
|
3777
|
+
else
|
|
3778
|
+
if ! "${report_command[@]}"; then
|
|
3779
|
+
rm -rf "$tmp_dir"
|
|
3780
|
+
printf 'error: failed to resolve prepared wheelhouse contents: %s\n' "$target_wheels_dir" >&2
|
|
3781
|
+
return 1
|
|
3782
|
+
fi
|
|
3783
|
+
if ! "${prune_command[@]}"; then
|
|
3784
|
+
rm -rf "$tmp_dir"
|
|
3785
|
+
printf 'error: failed to prune dependency wheelhouse: %s\n' "$target_wheels_dir" >&2
|
|
3786
|
+
return 1
|
|
3787
|
+
fi
|
|
3788
|
+
fi
|
|
3789
|
+
|
|
3790
|
+
if [[ -n "$output_file" ]]; then
|
|
3791
|
+
rm -f "$output_file"
|
|
3792
|
+
fi
|
|
3793
|
+
rm -rf "$tmp_dir"
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
prepare_dependency_wheelhouse() {
|
|
3797
|
+
local target_requirements_file
|
|
3798
|
+
local target_wheels_dir
|
|
3799
|
+
local image
|
|
3800
|
+
local docker_user
|
|
3801
|
+
local quiet
|
|
3802
|
+
local pypi_only
|
|
3803
|
+
local tmp_wheels_dir
|
|
3804
|
+
local output_file=""
|
|
3805
|
+
local -a docker_command
|
|
3806
|
+
local -a wheel_input_mount_args=()
|
|
3807
|
+
local -a find_links_args=()
|
|
3808
|
+
|
|
3809
|
+
target_requirements_file="$1"
|
|
3810
|
+
target_wheels_dir="$2"
|
|
3811
|
+
docker_user="$3"
|
|
3812
|
+
image="$4"
|
|
3813
|
+
quiet="${5:-0}"
|
|
3814
|
+
pypi_only="${6:-0}"
|
|
3815
|
+
|
|
3816
|
+
if [[ "$pypi_only" -eq 0 ]]; then
|
|
3817
|
+
wheel_input_mount_args=(-v "$target_wheels_dir:/wheels:ro")
|
|
3818
|
+
find_links_args=(--find-links /wheels)
|
|
3819
|
+
fi
|
|
3820
|
+
|
|
3821
|
+
if [[ "${install_dry_run:-0}" -eq 1 ]]; then
|
|
3822
|
+
docker_command=(
|
|
3823
|
+
docker run --rm
|
|
3824
|
+
--user "$docker_user"
|
|
3825
|
+
-v "$target_requirements_file:/requirements.txt:ro"
|
|
3826
|
+
)
|
|
3827
|
+
if [[ "${#wheel_input_mount_args[@]}" -gt 0 ]]; then
|
|
3828
|
+
docker_command+=("${wheel_input_mount_args[@]}")
|
|
3829
|
+
fi
|
|
3830
|
+
docker_command+=(
|
|
3831
|
+
-v "$target_wheels_dir:/wheelhouse"
|
|
3832
|
+
"$image"
|
|
3833
|
+
python -m pip --disable-pip-version-check wheel
|
|
3834
|
+
--no-cache-dir
|
|
3835
|
+
)
|
|
3836
|
+
if [[ "${#find_links_args[@]}" -gt 0 ]]; then
|
|
3837
|
+
docker_command+=("${find_links_args[@]}")
|
|
3838
|
+
fi
|
|
3839
|
+
docker_command+=(
|
|
3840
|
+
--wheel-dir /wheelhouse
|
|
3841
|
+
-r /requirements.txt
|
|
3842
|
+
)
|
|
3843
|
+
printf 'would prepare dependency wheelhouse: %s\n' "$target_wheels_dir"
|
|
3844
|
+
print_command "${docker_command[@]}"
|
|
3845
|
+
return 0
|
|
3846
|
+
fi
|
|
3847
|
+
|
|
3848
|
+
require_file "$target_requirements_file"
|
|
3849
|
+
require_docker_access
|
|
3850
|
+
|
|
3851
|
+
mkdir -p "$target_wheels_dir"
|
|
3852
|
+
tmp_wheels_dir="$(mktemp -d "${TMPDIR:-/tmp}/arbiter-wheelhouse.XXXXXX")"
|
|
3853
|
+
if [[ "$pypi_only" -eq 0 ]]; then
|
|
3854
|
+
copy_existing_wheelhouse "$target_wheels_dir" "$tmp_wheels_dir"
|
|
3855
|
+
fi
|
|
3856
|
+
docker_command=(
|
|
3857
|
+
docker run --rm
|
|
3858
|
+
--user "$docker_user"
|
|
3859
|
+
-v "$target_requirements_file:/requirements.txt:ro"
|
|
3860
|
+
)
|
|
3861
|
+
if [[ "${#wheel_input_mount_args[@]}" -gt 0 ]]; then
|
|
3862
|
+
docker_command+=("${wheel_input_mount_args[@]}")
|
|
3863
|
+
fi
|
|
3864
|
+
docker_command+=(
|
|
3865
|
+
-v "$tmp_wheels_dir:/wheelhouse"
|
|
3866
|
+
"$image"
|
|
3867
|
+
python -m pip --disable-pip-version-check wheel
|
|
3868
|
+
--no-cache-dir
|
|
3869
|
+
)
|
|
3870
|
+
if [[ "${#find_links_args[@]}" -gt 0 ]]; then
|
|
3871
|
+
docker_command+=("${find_links_args[@]}")
|
|
3872
|
+
fi
|
|
3873
|
+
docker_command+=(
|
|
3874
|
+
--wheel-dir /wheelhouse
|
|
3875
|
+
-r /requirements.txt
|
|
3876
|
+
)
|
|
3877
|
+
|
|
3878
|
+
if [[ "$quiet" -eq 1 ]]; then
|
|
3879
|
+
output_file="$(mktemp "${TMPDIR:-/tmp}/arbiter-wheelhouse-build.XXXXXX")"
|
|
3880
|
+
else
|
|
3881
|
+
printf 'preparing dependency wheelhouse with %s: %s\n' "$image" "$target_wheels_dir"
|
|
3882
|
+
fi
|
|
3883
|
+
|
|
3884
|
+
if [[ "$quiet" -eq 1 ]]; then
|
|
3885
|
+
if ! "${docker_command[@]}" >"$output_file" 2>&1; then
|
|
3886
|
+
cat "$output_file" >&2
|
|
3887
|
+
rm -f "$output_file"
|
|
3888
|
+
printf 'error: failed to prepare dependency wheelhouse: %s\n' "$target_wheels_dir" >&2
|
|
3889
|
+
printf ' check network access during install prep, package pins, and Docker image compatibility\n' >&2
|
|
3890
|
+
rm -rf "$tmp_wheels_dir"
|
|
3891
|
+
return 1
|
|
3892
|
+
fi
|
|
3893
|
+
elif ! "${docker_command[@]}"; then
|
|
3894
|
+
printf 'error: failed to prepare dependency wheelhouse: %s\n' "$target_wheels_dir" >&2
|
|
3895
|
+
printf ' check network access during install prep, package pins, and Docker image compatibility\n' >&2
|
|
3896
|
+
rm -rf "$tmp_wheels_dir"
|
|
3897
|
+
return 1
|
|
3898
|
+
fi
|
|
3899
|
+
if ! prune_dependency_wheelhouse "$target_requirements_file" "$tmp_wheels_dir" "$docker_user" "$image" "$quiet"; then
|
|
3900
|
+
rm -rf "$tmp_wheels_dir"
|
|
3901
|
+
return 1
|
|
3902
|
+
fi
|
|
3903
|
+
replace_dependency_wheelhouse "$tmp_wheels_dir" "$target_wheels_dir"
|
|
3904
|
+
rm -rf "$tmp_wheels_dir"
|
|
3905
|
+
if [[ -n "$output_file" ]]; then
|
|
3906
|
+
rm -f "$output_file"
|
|
3907
|
+
else
|
|
3908
|
+
printf 'prepared dependency wheelhouse: %s\n' "$target_wheels_dir"
|
|
3909
|
+
fi
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
validate_dependency_wheelhouse() {
|
|
3913
|
+
local target_requirements_file
|
|
3914
|
+
local target_wheels_dir
|
|
3915
|
+
local image
|
|
3916
|
+
local docker_user
|
|
3917
|
+
local quiet
|
|
3918
|
+
local output_file=""
|
|
3919
|
+
local -a docker_command
|
|
3920
|
+
|
|
3921
|
+
target_requirements_file="$1"
|
|
3922
|
+
target_wheels_dir="$2"
|
|
3923
|
+
docker_user="$3"
|
|
3924
|
+
image="$4"
|
|
3925
|
+
quiet="${5:-0}"
|
|
3926
|
+
|
|
3927
|
+
if [[ "${install_dry_run:-0}" -eq 1 ]]; then
|
|
3928
|
+
docker_command=(
|
|
3929
|
+
docker run --rm
|
|
3930
|
+
--user "$docker_user"
|
|
3931
|
+
-v "$target_requirements_file:/requirements.txt:ro"
|
|
3932
|
+
-v "$target_wheels_dir:/wheels:ro"
|
|
3933
|
+
"$image"
|
|
3934
|
+
python -m pip --disable-pip-version-check install --no-cache-dir
|
|
3935
|
+
--target /tmp/arbiter-wheelhouse-check
|
|
3936
|
+
--no-index --find-links /wheels
|
|
3937
|
+
-r /requirements.txt
|
|
3938
|
+
)
|
|
3939
|
+
printf 'would validate dependency wheelhouse: %s\n' "$target_wheels_dir"
|
|
3940
|
+
print_command "${docker_command[@]}"
|
|
3941
|
+
return 0
|
|
3942
|
+
fi
|
|
3943
|
+
|
|
3944
|
+
require_file "$target_requirements_file"
|
|
3945
|
+
require_dir "$target_wheels_dir"
|
|
3946
|
+
require_docker_access
|
|
3947
|
+
|
|
3948
|
+
docker_command=(
|
|
3949
|
+
docker run --rm
|
|
3950
|
+
--user "$docker_user"
|
|
3951
|
+
-v "$target_requirements_file:/requirements.txt:ro"
|
|
3952
|
+
-v "$target_wheels_dir:/wheels:ro"
|
|
3953
|
+
"$image"
|
|
3954
|
+
python -m pip --disable-pip-version-check install --no-cache-dir
|
|
3955
|
+
--target /tmp/arbiter-wheelhouse-check
|
|
3956
|
+
--no-index --find-links /wheels
|
|
3957
|
+
-r /requirements.txt
|
|
3958
|
+
)
|
|
3959
|
+
|
|
3960
|
+
if [[ "$quiet" -eq 1 ]]; then
|
|
3961
|
+
output_file="$(mktemp "${TMPDIR:-/tmp}/arbiter-wheelhouse-check.XXXXXX")"
|
|
3962
|
+
else
|
|
3963
|
+
printf 'validating dependency wheelhouse with %s: %s\n' "$image" "$target_wheels_dir"
|
|
3964
|
+
fi
|
|
3965
|
+
|
|
3966
|
+
if [[ "$quiet" -eq 1 ]]; then
|
|
3967
|
+
if ! "${docker_command[@]}" >"$output_file" 2>&1; then
|
|
3968
|
+
cat "$output_file" >&2
|
|
3969
|
+
rm -f "$output_file"
|
|
3970
|
+
printf 'error: dependency wheelhouse validation failed: %s\n' "$target_wheels_dir" >&2
|
|
3971
|
+
printf ' check requirements.txt pins, Docker image compatibility, and network access during install prep\n' >&2
|
|
3972
|
+
return 1
|
|
3973
|
+
fi
|
|
3974
|
+
elif ! "${docker_command[@]}"; then
|
|
3975
|
+
if [[ -n "$output_file" ]]; then
|
|
3976
|
+
cat "$output_file" >&2
|
|
3977
|
+
rm -f "$output_file"
|
|
3978
|
+
fi
|
|
3979
|
+
printf 'error: dependency wheelhouse validation failed: %s\n' "$target_wheels_dir" >&2
|
|
3980
|
+
printf ' check requirements.txt pins, Docker image compatibility, and network access during install prep\n' >&2
|
|
3981
|
+
return 1
|
|
3982
|
+
fi
|
|
3983
|
+
if [[ -n "$output_file" ]]; then
|
|
3984
|
+
rm -f "$output_file"
|
|
3985
|
+
else
|
|
3986
|
+
printf 'validated dependency wheelhouse: %s\n' "$target_wheels_dir"
|
|
3987
|
+
fi
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
prepare() {
|
|
3991
|
+
local target_wheels_dir
|
|
3992
|
+
local image
|
|
3993
|
+
local docker_user
|
|
3994
|
+
local pypi_only=0
|
|
3995
|
+
local tmp_dir
|
|
3996
|
+
|
|
3997
|
+
while (($#)); do
|
|
3998
|
+
case "$1" in
|
|
3999
|
+
--pypi-only)
|
|
4000
|
+
pypi_only=1
|
|
4001
|
+
;;
|
|
4002
|
+
*)
|
|
4003
|
+
printf 'error: unknown prepare option: %s\n' "$1" >&2
|
|
4004
|
+
return 2
|
|
4005
|
+
;;
|
|
4006
|
+
esac
|
|
4007
|
+
shift
|
|
4008
|
+
done
|
|
4009
|
+
|
|
4010
|
+
require_file "$requirements_file"
|
|
4011
|
+
validate_requirements_file "$requirements_file"
|
|
4012
|
+
target_wheels_dir="$(wheels_dir_path)"
|
|
4013
|
+
image="$(compose_env_value ARBITER_IMAGE python:3.11-slim)"
|
|
4014
|
+
docker_user="$(id -u):$(id -g)"
|
|
4015
|
+
if [[ "$pypi_only" -eq 1 ]]; then
|
|
4016
|
+
print_bundle_prepare_start
|
|
4017
|
+
if ! prepare_pypi_only "$target_wheels_dir" "$docker_user" "$image"; then
|
|
4018
|
+
return 1
|
|
4019
|
+
fi
|
|
4020
|
+
return
|
|
4021
|
+
fi
|
|
4022
|
+
if ! reject_source_requirements_for_wheelhouse_command prepare; then
|
|
4023
|
+
return 1
|
|
4024
|
+
fi
|
|
4025
|
+
print_bundle_prepare_start
|
|
4026
|
+
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/arbiter-bundle-prepare.XXXXXX")"
|
|
4027
|
+
if ! bundle_refresh_repo_wheels "$tmp_dir"; then
|
|
4028
|
+
rm -rf "$tmp_dir"
|
|
4029
|
+
return 1
|
|
4030
|
+
fi
|
|
4031
|
+
rm -rf "$tmp_dir"
|
|
4032
|
+
prepare_dependency_wheelhouse "$requirements_file" "$target_wheels_dir" "$docker_user" "$image" 1 "$pypi_only"
|
|
4033
|
+
validate_dependency_wheelhouse "$requirements_file" "$target_wheels_dir" "$docker_user" "$image" 1
|
|
4034
|
+
printf 'bundle prepare complete: %s\n' "$target_wheels_dir"
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
prepare_quiet() {
|
|
4038
|
+
local target_wheels_dir
|
|
4039
|
+
local image
|
|
4040
|
+
local docker_user
|
|
4041
|
+
|
|
4042
|
+
require_file "$requirements_file"
|
|
4043
|
+
validate_requirements_file "$requirements_file"
|
|
4044
|
+
if ! reject_source_requirements_for_wheelhouse_command prepare; then
|
|
4045
|
+
return 1
|
|
4046
|
+
fi
|
|
4047
|
+
target_wheels_dir="$(wheels_dir_path)"
|
|
4048
|
+
image="$(compose_env_value ARBITER_IMAGE python:3.11-slim)"
|
|
4049
|
+
docker_user="$(id -u):$(id -g)"
|
|
4050
|
+
prepare_dependency_wheelhouse "$requirements_file" "$target_wheels_dir" "$docker_user" "$image" 1 0
|
|
4051
|
+
validate_dependency_wheelhouse "$requirements_file" "$target_wheels_dir" "$docker_user" "$image" 1
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
check() {
|
|
4055
|
+
local target_wheels_dir
|
|
4056
|
+
local image
|
|
4057
|
+
local docker_user
|
|
4058
|
+
|
|
4059
|
+
if (($#)); then
|
|
4060
|
+
printf 'error: check does not accept options yet; run bundle check with no arguments\n' >&2
|
|
4061
|
+
return 2
|
|
4062
|
+
fi
|
|
4063
|
+
require_file "$requirements_file"
|
|
4064
|
+
validate_requirements_file "$requirements_file"
|
|
4065
|
+
if ! reject_source_requirements_for_wheelhouse_command check; then
|
|
4066
|
+
return 1
|
|
4067
|
+
fi
|
|
4068
|
+
target_wheels_dir="$(wheels_dir_path)"
|
|
4069
|
+
image="$(compose_env_value ARBITER_IMAGE python:3.11-slim)"
|
|
4070
|
+
docker_user="$(id -u):$(id -g)"
|
|
4071
|
+
validate_dependency_wheelhouse "$requirements_file" "$target_wheels_dir" "$docker_user" "$image" 1
|
|
4072
|
+
printf 'bundle check passed: %s\n' "$deploy_dir"
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
bundle() {
|
|
4076
|
+
local subcommand="${1:-}"
|
|
4077
|
+
if (($#)); then
|
|
4078
|
+
shift
|
|
4079
|
+
fi
|
|
4080
|
+
|
|
4081
|
+
case "$subcommand" in
|
|
4082
|
+
prepare)
|
|
4083
|
+
prepare "$@"
|
|
4084
|
+
;;
|
|
4085
|
+
check)
|
|
4086
|
+
check "$@"
|
|
4087
|
+
;;
|
|
4088
|
+
list)
|
|
4089
|
+
case "${1:-}" in
|
|
4090
|
+
"")
|
|
4091
|
+
bundle_list_roots
|
|
4092
|
+
;;
|
|
4093
|
+
all)
|
|
4094
|
+
bundle_list_all
|
|
4095
|
+
;;
|
|
4096
|
+
*)
|
|
4097
|
+
printf 'error: unknown bundle list argument: %s\n' "$1" >&2
|
|
4098
|
+
return 2
|
|
4099
|
+
;;
|
|
4100
|
+
esac
|
|
4101
|
+
;;
|
|
4102
|
+
list-plugins)
|
|
4103
|
+
if (($#)); then
|
|
4104
|
+
printf 'error: list-plugins does not accept arguments\n' >&2
|
|
4105
|
+
return 2
|
|
4106
|
+
fi
|
|
4107
|
+
bundle_list_plugins
|
|
4108
|
+
;;
|
|
4109
|
+
upgrade)
|
|
4110
|
+
bundle_upgrade "$@"
|
|
4111
|
+
;;
|
|
4112
|
+
add)
|
|
4113
|
+
bundle_add_plugin "$@"
|
|
4114
|
+
;;
|
|
4115
|
+
remove)
|
|
4116
|
+
bundle_remove_plugin "$@"
|
|
4117
|
+
;;
|
|
4118
|
+
"" | -h | --help | help)
|
|
4119
|
+
cat <<'EOF'
|
|
4120
|
+
Usage: arbiter-docker bundle COMMAND
|
|
4121
|
+
|
|
4122
|
+
Commands:
|
|
4123
|
+
prepare Build and validate the dependency wheelhouse
|
|
4124
|
+
check Validate the prepared wheelhouse without downloading packages
|
|
4125
|
+
list Show root requirements
|
|
4126
|
+
list all Show root and transitive wheelhouse packages
|
|
4127
|
+
list-plugins Show service plugins supported by add/remove
|
|
4128
|
+
add ITEM Add a service plugin, or all plugins in a meta package
|
|
4129
|
+
remove ITEM Remove a service plugin, or all plugins in a meta package
|
|
4130
|
+
upgrade Upgrade package root requirements and rebuild the wheelhouse
|
|
4131
|
+
|
|
4132
|
+
Upgrade options:
|
|
4133
|
+
--pypi-only Resolve from the package index only; skip local repo wheels
|
|
4134
|
+
|
|
4135
|
+
Prepare options:
|
|
4136
|
+
--pypi-only Resolve selected packages from the index instead of the checked-out repo, rewrite requirements.txt after success, and build without the existing wheelhouse
|
|
4137
|
+
EOF
|
|
4138
|
+
;;
|
|
4139
|
+
*)
|
|
4140
|
+
printf 'error: unknown bundle command: %s\n' "$subcommand" >&2
|
|
4141
|
+
return 2
|
|
4142
|
+
;;
|
|
4143
|
+
esac
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
install_validate_wheelhouse() {
|
|
4147
|
+
local target_requirements_file
|
|
4148
|
+
local target_wheels_dir
|
|
4149
|
+
local image
|
|
4150
|
+
local uid
|
|
4151
|
+
local gid
|
|
4152
|
+
|
|
4153
|
+
target_requirements_file="$(install_target_requirements_file_path)"
|
|
4154
|
+
target_wheels_dir="$(install_target_wheels_dir_path)"
|
|
4155
|
+
image="$(compose_env_value ARBITER_IMAGE python:3.11-slim)"
|
|
4156
|
+
|
|
4157
|
+
if [[ "$install_dry_run" -eq 1 ]]; then
|
|
4158
|
+
validate_dependency_wheelhouse "$target_requirements_file" "$target_wheels_dir" "$install_user:$install_group" "$image"
|
|
4159
|
+
return
|
|
4160
|
+
fi
|
|
4161
|
+
|
|
4162
|
+
uid="$(id -u "$install_user")"
|
|
4163
|
+
gid="$(id -g "$install_user")"
|
|
4164
|
+
if ! validate_dependency_wheelhouse "$target_requirements_file" "$target_wheels_dir" "$uid:$gid" "$image" 1; then
|
|
4165
|
+
printf ' install aborted before writing/restarting the systemd service\n' >&2
|
|
4166
|
+
printf ' run %s prepare, then rerun: sudo %s install\n' "$deploy_dir/arbiter-docker" "$deploy_dir/arbiter-docker" >&2
|
|
4167
|
+
return 1
|
|
4168
|
+
fi
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
install_write_systemd_unit() {
|
|
4172
|
+
local unit_file
|
|
4173
|
+
local docker_bin
|
|
4174
|
+
local compose_files
|
|
4175
|
+
local docker_unit_lines
|
|
4176
|
+
|
|
4177
|
+
unit_file="$systemd_unit_dir/${install_service}.service"
|
|
4178
|
+
|
|
4179
|
+
if [[ "$install_dry_run" -eq 1 ]]; then
|
|
4180
|
+
printf 'would write systemd unit: %s\n' "$unit_file"
|
|
4181
|
+
return
|
|
4182
|
+
fi
|
|
4183
|
+
|
|
4184
|
+
docker_bin="$(command -v docker || true)"
|
|
4185
|
+
if [[ -z "$docker_bin" ]]; then
|
|
4186
|
+
printf 'error: docker command not found\n' >&2
|
|
4187
|
+
exit 1
|
|
4188
|
+
fi
|
|
4189
|
+
if ! command -v systemctl >/dev/null; then
|
|
4190
|
+
printf 'error: systemctl command not found\n' >&2
|
|
4191
|
+
exit 1
|
|
4192
|
+
fi
|
|
4193
|
+
compose_files="-f $install_target_dir/compose.yaml"
|
|
4194
|
+
if [[ -f "$install_target_dir/compose.override.yaml" ]]; then
|
|
4195
|
+
compose_files="$compose_files -f $install_target_dir/compose.override.yaml"
|
|
4196
|
+
fi
|
|
4197
|
+
mkdir -p "$systemd_unit_dir"
|
|
4198
|
+
docker_unit_lines=""
|
|
4199
|
+
if systemctl cat docker.service >/dev/null 2>&1; then
|
|
4200
|
+
docker_unit_lines="Requires=docker.service
|
|
4201
|
+
After=docker.service"
|
|
4202
|
+
else
|
|
4203
|
+
doctor_warn "docker.service not found; generated unit will rely on Docker socket availability"
|
|
4204
|
+
fi
|
|
4205
|
+
|
|
4206
|
+
cat >"$unit_file" <<EOF
|
|
4207
|
+
[Unit]
|
|
4208
|
+
Description=Arbiter Docker service
|
|
4209
|
+
$docker_unit_lines
|
|
4210
|
+
|
|
4211
|
+
[Service]
|
|
4212
|
+
Type=simple
|
|
4213
|
+
WorkingDirectory=$install_target_dir
|
|
4214
|
+
ExecStart=$docker_bin compose --env-file $install_target_dir/docker.env $compose_files up
|
|
4215
|
+
ExecStop=$docker_bin compose --env-file $install_target_dir/docker.env $compose_files down
|
|
4216
|
+
Restart=on-failure
|
|
4217
|
+
|
|
4218
|
+
[Install]
|
|
4219
|
+
WantedBy=multi-user.target
|
|
4220
|
+
EOF
|
|
4221
|
+
chmod 0644 "$unit_file"
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
install_stop_service_for_recreate() {
|
|
4225
|
+
if [[ "$install_dry_run" -eq 1 ]]; then
|
|
4226
|
+
print_command systemctl stop "${install_service}.service"
|
|
4227
|
+
return
|
|
4228
|
+
fi
|
|
4229
|
+
systemctl stop "${install_service}.service" >/dev/null 2>&1 || true
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
install_reset_failed_service_for_recreate() {
|
|
4233
|
+
if [[ "$install_dry_run" -eq 1 ]]; then
|
|
4234
|
+
print_command systemctl reset-failed "${install_service}.service"
|
|
4235
|
+
return
|
|
4236
|
+
fi
|
|
4237
|
+
systemctl reset-failed "${install_service}.service" >/dev/null 2>&1 || true
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
install_compose_down_for_recreate() {
|
|
4241
|
+
local docker_bin
|
|
4242
|
+
|
|
4243
|
+
if [[ "$install_dry_run" -eq 1 ]]; then
|
|
4244
|
+
print_command docker compose \
|
|
4245
|
+
--env-file "$install_target_dir/docker.env" \
|
|
4246
|
+
-f "$install_target_dir/compose.yaml" \
|
|
4247
|
+
down --remove-orphans
|
|
4248
|
+
return
|
|
4249
|
+
fi
|
|
4250
|
+
|
|
4251
|
+
docker_bin="$(command -v docker || true)"
|
|
4252
|
+
if [[ -z "$docker_bin" ]]; then
|
|
4253
|
+
printf 'error: docker command not found\n' >&2
|
|
4254
|
+
exit 1
|
|
4255
|
+
fi
|
|
4256
|
+
if [[ -f "$install_target_dir/compose.override.yaml" ]]; then
|
|
4257
|
+
run_install_command "$docker_bin" compose \
|
|
4258
|
+
--env-file "$install_target_dir/docker.env" \
|
|
4259
|
+
-f "$install_target_dir/compose.yaml" \
|
|
4260
|
+
-f "$install_target_dir/compose.override.yaml" \
|
|
4261
|
+
down --remove-orphans
|
|
4262
|
+
else
|
|
4263
|
+
run_install_command "$docker_bin" compose \
|
|
4264
|
+
--env-file "$install_target_dir/docker.env" \
|
|
4265
|
+
-f "$install_target_dir/compose.yaml" \
|
|
4266
|
+
down --remove-orphans
|
|
4267
|
+
fi
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
install_deployment() {
|
|
4271
|
+
install_target_dir="/opt/arbiter"
|
|
4272
|
+
install_user="arbiter"
|
|
4273
|
+
install_group=""
|
|
4274
|
+
install_service="arbiter"
|
|
4275
|
+
install_start=1
|
|
4276
|
+
install_dry_run=0
|
|
4277
|
+
install_replace_config=0
|
|
4278
|
+
install_replace_env=0
|
|
4279
|
+
|
|
4280
|
+
while (($#)); do
|
|
4281
|
+
case "$1" in
|
|
4282
|
+
--to)
|
|
4283
|
+
[[ $# -ge 2 ]] || {
|
|
4284
|
+
printf 'error: --to requires a value\n' >&2
|
|
4285
|
+
exit 2
|
|
4286
|
+
}
|
|
4287
|
+
install_target_dir="$2"
|
|
4288
|
+
shift 2
|
|
4289
|
+
;;
|
|
4290
|
+
--user)
|
|
4291
|
+
[[ $# -ge 2 ]] || {
|
|
4292
|
+
printf 'error: --user requires a value\n' >&2
|
|
4293
|
+
exit 2
|
|
4294
|
+
}
|
|
4295
|
+
install_user="$2"
|
|
4296
|
+
shift 2
|
|
4297
|
+
;;
|
|
4298
|
+
--group)
|
|
4299
|
+
[[ $# -ge 2 ]] || {
|
|
4300
|
+
printf 'error: --group requires a value\n' >&2
|
|
4301
|
+
exit 2
|
|
4302
|
+
}
|
|
4303
|
+
install_group="$2"
|
|
4304
|
+
shift 2
|
|
4305
|
+
;;
|
|
4306
|
+
--service)
|
|
4307
|
+
[[ $# -ge 2 ]] || {
|
|
4308
|
+
printf 'error: --service requires a value\n' >&2
|
|
4309
|
+
exit 2
|
|
4310
|
+
}
|
|
4311
|
+
install_service="$2"
|
|
4312
|
+
shift 2
|
|
4313
|
+
;;
|
|
4314
|
+
--no-start)
|
|
4315
|
+
install_start=0
|
|
4316
|
+
shift
|
|
4317
|
+
;;
|
|
4318
|
+
--start)
|
|
4319
|
+
install_start=1
|
|
4320
|
+
shift
|
|
4321
|
+
;;
|
|
4322
|
+
--replace-config)
|
|
4323
|
+
install_replace_config=1
|
|
4324
|
+
shift
|
|
4325
|
+
;;
|
|
4326
|
+
--replace-env)
|
|
4327
|
+
install_replace_env=1
|
|
4328
|
+
shift
|
|
4329
|
+
;;
|
|
4330
|
+
--dry-run)
|
|
4331
|
+
install_dry_run=1
|
|
4332
|
+
shift
|
|
4333
|
+
;;
|
|
4334
|
+
-h | --help)
|
|
4335
|
+
usage
|
|
4336
|
+
exit 0
|
|
4337
|
+
;;
|
|
4338
|
+
*)
|
|
4339
|
+
printf 'error: unknown install option: %s\n\n' "$1" >&2
|
|
4340
|
+
usage >&2
|
|
4341
|
+
exit 2
|
|
4342
|
+
;;
|
|
4343
|
+
esac
|
|
4344
|
+
done
|
|
4345
|
+
|
|
4346
|
+
if [[ "$install_target_dir" != /* ]]; then
|
|
4347
|
+
printf 'error: --to must be an absolute path: %s\n' "$install_target_dir" >&2
|
|
4348
|
+
exit 2
|
|
4349
|
+
fi
|
|
4350
|
+
if [[ "$install_replace_env" -eq 1 && "$install_replace_config" -eq 0 ]]; then
|
|
4351
|
+
printf 'error: --replace-env requires --replace-config\n' >&2
|
|
4352
|
+
exit 2
|
|
4353
|
+
fi
|
|
4354
|
+
if [[ "$install_target_dir" =~ [[:space:]] ]]; then
|
|
4355
|
+
printf 'error: --to must not contain whitespace: %s\n' "$install_target_dir" >&2
|
|
4356
|
+
exit 2
|
|
4357
|
+
fi
|
|
4358
|
+
if [[ ! "$install_service" =~ ^[A-Za-z0-9_.@-]+$ ]]; then
|
|
4359
|
+
printf 'error: --service contains unsupported characters: %s\n' "$install_service" >&2
|
|
4360
|
+
exit 2
|
|
4361
|
+
fi
|
|
4362
|
+
if [[ ! "$install_user" =~ ^[A-Za-z_][A-Za-z0-9_.-]*[$]?$ ]]; then
|
|
4363
|
+
printf 'error: --user contains unsupported characters: %s\n' "$install_user" >&2
|
|
4364
|
+
exit 2
|
|
4365
|
+
fi
|
|
4366
|
+
if [[ -z "$install_group" ]]; then
|
|
4367
|
+
install_group="$install_user"
|
|
4368
|
+
fi
|
|
4369
|
+
if [[ ! "$install_group" =~ ^[A-Za-z_][A-Za-z0-9_.-]*[$]?$ ]]; then
|
|
4370
|
+
printf 'error: --group contains unsupported characters: %s\n' "$install_group" >&2
|
|
4371
|
+
exit 2
|
|
4372
|
+
fi
|
|
4373
|
+
|
|
4374
|
+
printf 'installing Arbiter to %s as %s:%s (service: %s.service)\n' \
|
|
4375
|
+
"$install_target_dir" "$install_user" "$install_group" "$install_service"
|
|
4376
|
+
|
|
4377
|
+
install_prepare_local_checkout_wheels
|
|
4378
|
+
doctor --preinstall --quiet
|
|
4379
|
+
|
|
4380
|
+
if [[ "$install_dry_run" -ne 1 ]]; then
|
|
4381
|
+
install_require_root
|
|
4382
|
+
fi
|
|
4383
|
+
|
|
4384
|
+
install_ensure_identity
|
|
4385
|
+
install_copy_deployment
|
|
4386
|
+
install_apply_local_checkout_install_artifacts
|
|
4387
|
+
install_mark_deployment_installed
|
|
4388
|
+
install_write_target_metadata
|
|
4389
|
+
install_apply_permissions
|
|
4390
|
+
install_validate_wheelhouse
|
|
4391
|
+
install_apply_permissions
|
|
4392
|
+
install_write_systemd_unit
|
|
4393
|
+
install_record_location
|
|
4394
|
+
run_install_command systemctl daemon-reload
|
|
4395
|
+
run_install_command systemctl enable "${install_service}.service"
|
|
4396
|
+
if [[ "$install_start" -eq 1 ]]; then
|
|
4397
|
+
install_stop_service_for_recreate
|
|
4398
|
+
install_reset_failed_service_for_recreate
|
|
4399
|
+
install_compose_down_for_recreate
|
|
4400
|
+
run_install_command systemctl restart "${install_service}.service"
|
|
4401
|
+
fi
|
|
4402
|
+
if [[ "$install_start" -eq 1 ]]; then
|
|
4403
|
+
install_test_server
|
|
4404
|
+
fi
|
|
4405
|
+
install_report_success
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
command="${1:-}"
|
|
4409
|
+
if (($#)); then
|
|
4410
|
+
shift
|
|
4411
|
+
fi
|
|
4412
|
+
|
|
4413
|
+
case "$command" in
|
|
4414
|
+
prepare)
|
|
4415
|
+
prepare "$@"
|
|
4416
|
+
;;
|
|
4417
|
+
bundle)
|
|
4418
|
+
bundle "$@"
|
|
4419
|
+
;;
|
|
4420
|
+
sync-env)
|
|
4421
|
+
sync_env
|
|
4422
|
+
;;
|
|
4423
|
+
edit-config)
|
|
4424
|
+
require_file "$(config_main_file)"
|
|
4425
|
+
run_editor "$(config_main_file)"
|
|
4426
|
+
;;
|
|
4427
|
+
edit-requirements)
|
|
4428
|
+
edit_requirements
|
|
4429
|
+
;;
|
|
4430
|
+
edit-env)
|
|
4431
|
+
require_file "$(app_env_path)"
|
|
4432
|
+
run_editor "$(app_env_path)"
|
|
4433
|
+
;;
|
|
4434
|
+
edit-docker)
|
|
4435
|
+
require_file "$compose_env_file"
|
|
4436
|
+
run_editor "$compose_env_file"
|
|
4437
|
+
;;
|
|
4438
|
+
up)
|
|
4439
|
+
compose up -d
|
|
4440
|
+
print_staging_port_note
|
|
4441
|
+
print_mcp_url
|
|
4442
|
+
;;
|
|
4443
|
+
restart)
|
|
4444
|
+
compose up -d --force-recreate
|
|
4445
|
+
print_staging_port_note
|
|
4446
|
+
print_mcp_url
|
|
4447
|
+
;;
|
|
4448
|
+
test)
|
|
4449
|
+
test_server
|
|
4450
|
+
;;
|
|
4451
|
+
down)
|
|
4452
|
+
compose_down "$@"
|
|
4453
|
+
;;
|
|
4454
|
+
ps)
|
|
4455
|
+
compose ps
|
|
4456
|
+
;;
|
|
4457
|
+
logs)
|
|
4458
|
+
compose logs --timestamps -f
|
|
4459
|
+
;;
|
|
4460
|
+
info)
|
|
4461
|
+
info
|
|
4462
|
+
;;
|
|
4463
|
+
doctor)
|
|
4464
|
+
doctor "$@"
|
|
4465
|
+
;;
|
|
4466
|
+
install)
|
|
4467
|
+
install_deployment "$@"
|
|
4468
|
+
;;
|
|
4469
|
+
"" | -h | --help | help)
|
|
4470
|
+
usage
|
|
4471
|
+
;;
|
|
4472
|
+
*)
|
|
4473
|
+
printf 'error: unknown command: %s\n\n' "$command" >&2
|
|
4474
|
+
usage >&2
|
|
4475
|
+
exit 2
|
|
4476
|
+
;;
|
|
4477
|
+
esac
|