@mriekki/project-forge 4.16.1 → 4.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/components.json +2 -2
- package/assets/generated/ASSEMBLY.json +7 -7
- package/assets/generated/deployment/src/project_forge_deployment/app_deployment.py +26 -0
- package/assets/generated/deployment/src/project_forge_deployment/app_runtime.py +77 -0
- package/assets/generated/deployment/src/project_forge_deployment/app_runtime_remote.py +39 -5
- package/assets/generated/deployment/src/project_forge_deployment/sqlite_adapter.py +49 -1
- package/assets/generated/deployment/src/project_forge_deployment/sqlite_adapter_remote.py +35 -2
- package/package.json +1 -1
package/assets/components.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.18.0",
|
|
5
5
|
"defaultProfile": "greenfield-project",
|
|
6
6
|
"profiles": {
|
|
7
7
|
"greenfield-project": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"canonicalDescriptor": "distributions/greenfield-workspace.json"
|
|
22
22
|
},
|
|
23
23
|
"deployment": {
|
|
24
|
-
"revision": "
|
|
24
|
+
"revision": "31985f3e24a3832391cfe873fbb3eab27d0fa9d4",
|
|
25
25
|
"distribution": "greenfield-project",
|
|
26
26
|
"descriptor": "assets/deployment/distribution.json",
|
|
27
27
|
"sourceRoot": "assets/generated/deployment",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.18.0",
|
|
5
5
|
"components": {
|
|
6
6
|
"core": {
|
|
7
7
|
"revision": "4c74c1801b82d3aa438447d392024d32ca5697b0",
|
|
@@ -361,7 +361,7 @@
|
|
|
361
361
|
]
|
|
362
362
|
},
|
|
363
363
|
"deployment": {
|
|
364
|
-
"revision": "
|
|
364
|
+
"revision": "31985f3e24a3832391cfe873fbb3eab27d0fa9d4",
|
|
365
365
|
"distribution": "greenfield-project",
|
|
366
366
|
"files": [
|
|
367
367
|
{
|
|
@@ -397,17 +397,17 @@
|
|
|
397
397
|
{
|
|
398
398
|
"source": "src/project_forge_deployment/app_deployment.py",
|
|
399
399
|
"destination": "tools/project-forge-deployment/project_forge_deployment/app_deployment.py",
|
|
400
|
-
"sha256": "
|
|
400
|
+
"sha256": "aeecda05bd2f71276dd49f09fd599cf529ea9fe9b0aae7c1e26ab92bd57e7f22"
|
|
401
401
|
},
|
|
402
402
|
{
|
|
403
403
|
"source": "src/project_forge_deployment/app_runtime.py",
|
|
404
404
|
"destination": "tools/project-forge-deployment/project_forge_deployment/app_runtime.py",
|
|
405
|
-
"sha256": "
|
|
405
|
+
"sha256": "73736aada7c3db0e2a7c74a3e88990316b89e6c34a71b4997cdc4c79c7e31708"
|
|
406
406
|
},
|
|
407
407
|
{
|
|
408
408
|
"source": "src/project_forge_deployment/app_runtime_remote.py",
|
|
409
409
|
"destination": "tools/project-forge-deployment/project_forge_deployment/app_runtime_remote.py",
|
|
410
|
-
"sha256": "
|
|
410
|
+
"sha256": "791cc7592cff93555f7b6facdd31fbe58de5e711732d9dfcb9321500a0d79cdf"
|
|
411
411
|
},
|
|
412
412
|
{
|
|
413
413
|
"source": "src/project_forge_deployment/app_runtime_workstation.py",
|
|
@@ -597,12 +597,12 @@
|
|
|
597
597
|
{
|
|
598
598
|
"source": "src/project_forge_deployment/sqlite_adapter.py",
|
|
599
599
|
"destination": "tools/project-forge-deployment/project_forge_deployment/sqlite_adapter.py",
|
|
600
|
-
"sha256": "
|
|
600
|
+
"sha256": "781219312cb81ffb901dff8b8d04d0cf0511b5d37e86f9ad9024a73cba04d3b9"
|
|
601
601
|
},
|
|
602
602
|
{
|
|
603
603
|
"source": "src/project_forge_deployment/sqlite_adapter_remote.py",
|
|
604
604
|
"destination": "tools/project-forge-deployment/project_forge_deployment/sqlite_adapter_remote.py",
|
|
605
|
-
"sha256": "
|
|
605
|
+
"sha256": "d60239f9b75cf688531319284f0123488f4f0ae812ac9982ce611320d8754793"
|
|
606
606
|
},
|
|
607
607
|
{
|
|
608
608
|
"source": "src/project_forge_deployment/sqlite_cli.py",
|
|
@@ -14,6 +14,7 @@ from .app_runtime import PRODUCTION_APP_DEPLOY_WRITE_PHRASE
|
|
|
14
14
|
from .app_runtime import apply_app_deploy as _apply_runtime_deploy
|
|
15
15
|
from .app_runtime import deploy_write_phrase
|
|
16
16
|
from .app_runtime import plan_app_deploy as _plan_runtime_deploy
|
|
17
|
+
from .app_runtime import render_locked_app_deploy_stage as _render_locked_runtime_deploy_stage
|
|
17
18
|
from .app_runtime_workstation import apply_app_deploy_with_preloaded_image
|
|
18
19
|
from .environment_isolation import environment_isolation_preflight
|
|
19
20
|
from .errors import DeploymentKitError
|
|
@@ -272,6 +273,30 @@ def apply_app_deploy(plan: dict[str, Any]) -> dict[str, Any]:
|
|
|
272
273
|
return _apply_runtime_deploy(_runtime_plan(plan))
|
|
273
274
|
|
|
274
275
|
|
|
276
|
+
def render_locked_app_deploy_stage(
|
|
277
|
+
plan: dict[str, Any], *, inherited_lock_fd: int, authorization: str,
|
|
278
|
+
) -> dict[str, str]:
|
|
279
|
+
"""Render a registry-backed deploy stage after isolation revalidation."""
|
|
280
|
+
|
|
281
|
+
runtime_plan = _runtime_plan(plan)
|
|
282
|
+
review = plan[ISOLATION_REVIEW_KEY]
|
|
283
|
+
if not isinstance(review, dict) or type(review.get("production")) is not bool:
|
|
284
|
+
raise DeploymentKitError("Application deployment plan has malformed Production classification.")
|
|
285
|
+
production = review["production"] or runtime_plan["environment"] == "production"
|
|
286
|
+
expected_authorization = (
|
|
287
|
+
PRODUCTION_APP_DEPLOY_WRITE_PHRASE if production else deploy_write_phrase(runtime_plan)
|
|
288
|
+
)
|
|
289
|
+
if authorization != expected_authorization:
|
|
290
|
+
raise DeploymentKitError("Exact reviewed application deployment authorization phrase is required.")
|
|
291
|
+
revalidate_app_isolation(plan)
|
|
292
|
+
return _render_locked_runtime_deploy_stage(
|
|
293
|
+
runtime_plan,
|
|
294
|
+
inherited_lock_fd=inherited_lock_fd,
|
|
295
|
+
authorization=authorization,
|
|
296
|
+
production=production,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
275
300
|
def apply_app_deploy_from_workstation(plan: dict[str, Any]) -> dict[str, Any]:
|
|
276
301
|
"""Transfer an exact image, then recheck isolation again before runtime mutation."""
|
|
277
302
|
|
|
@@ -291,4 +316,5 @@ __all__ = [
|
|
|
291
316
|
"plan_app_deploy",
|
|
292
317
|
"plan_app_rollout",
|
|
293
318
|
"revalidate_app_isolation",
|
|
319
|
+
"render_locked_app_deploy_stage",
|
|
294
320
|
]
|
|
@@ -6,6 +6,7 @@ import base64
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import json
|
|
8
8
|
import re
|
|
9
|
+
import shlex
|
|
9
10
|
import subprocess
|
|
10
11
|
from typing import Any, Iterable
|
|
11
12
|
|
|
@@ -394,6 +395,82 @@ def deploy_write_phrase(plan: dict[str, Any]) -> str:
|
|
|
394
395
|
)
|
|
395
396
|
|
|
396
397
|
|
|
398
|
+
def render_locked_app_deploy_stage(
|
|
399
|
+
plan: dict[str, Any], *, inherited_lock_fd: int, authorization: str, production: bool = False,
|
|
400
|
+
) -> dict[str, str]:
|
|
401
|
+
"""Render a Kit-validated deployment stage for an existing remote lock scope.
|
|
402
|
+
|
|
403
|
+
The returned Bash stage is not an SSH command or authorization to run it.
|
|
404
|
+
The caller must hold the exact app/environment lock in the same privileged
|
|
405
|
+
remote shell and keep it through its application-specific checks and this
|
|
406
|
+
stage. The remote script verifies the inherited descriptor before deploy.
|
|
407
|
+
"""
|
|
408
|
+
required = {
|
|
409
|
+
"targetName",
|
|
410
|
+
"action",
|
|
411
|
+
"environment",
|
|
412
|
+
"appSlug",
|
|
413
|
+
"ownerRepo",
|
|
414
|
+
"containerName",
|
|
415
|
+
"image",
|
|
416
|
+
"runtimePort",
|
|
417
|
+
"healthPath",
|
|
418
|
+
"persistenceMode",
|
|
419
|
+
"changeType",
|
|
420
|
+
"previousImage",
|
|
421
|
+
"previousRelease",
|
|
422
|
+
"retainsPreviousRelease",
|
|
423
|
+
"publishesHostPort",
|
|
424
|
+
"attachesSharedNetwork",
|
|
425
|
+
"modifiesRoutes",
|
|
426
|
+
"persistentDataDeletion",
|
|
427
|
+
"inventorySnapshot",
|
|
428
|
+
"expectedSpecSha256",
|
|
429
|
+
"spec",
|
|
430
|
+
}
|
|
431
|
+
if not isinstance(plan, dict) or set(plan) != required or plan["action"] != "deploy":
|
|
432
|
+
raise DeploymentKitError("Malformed application deployment plan.")
|
|
433
|
+
if (plan["changeType"] != "update" or plan["retainsPreviousRelease"] is not True
|
|
434
|
+
or not isinstance(plan["expectedSpecSha256"], str)
|
|
435
|
+
or re.fullmatch(r"[0-9a-f]{64}", plan["expectedSpecSha256"]) is None):
|
|
436
|
+
raise DeploymentKitError("Lock handoff is limited to an existing application deployment update.")
|
|
437
|
+
if type(inherited_lock_fd) is not int or not 3 <= inherited_lock_fd <= 256:
|
|
438
|
+
raise DeploymentKitError("Inherited remote lock descriptor must be an integer between 3 and 256.")
|
|
439
|
+
if type(production) is not bool:
|
|
440
|
+
raise DeploymentKitError("Production classification must be a boolean.")
|
|
441
|
+
expected_authorization = (
|
|
442
|
+
PRODUCTION_APP_DEPLOY_WRITE_PHRASE
|
|
443
|
+
if production or plan["environment"] == "production"
|
|
444
|
+
else APP_DEPLOY_WRITE_PHRASE
|
|
445
|
+
)
|
|
446
|
+
if authorization != expected_authorization:
|
|
447
|
+
raise DeploymentKitError("Exact reviewed application deployment authorization phrase is required.")
|
|
448
|
+
|
|
449
|
+
platform = platform_status(plan["targetName"])
|
|
450
|
+
if platform["state"] != "ready":
|
|
451
|
+
raise DeploymentKitError("Shared host platform is no longer ready; application deployment is blocked.")
|
|
452
|
+
current = app_inventory(plan["targetName"])
|
|
453
|
+
if _inventory_snapshot(current) != plan["inventorySnapshot"]:
|
|
454
|
+
raise DeploymentKitError(
|
|
455
|
+
"Application inventory changed after the plan was reviewed; create and authorize a fresh plan."
|
|
456
|
+
)
|
|
457
|
+
profile = load_profile(plan["targetName"])
|
|
458
|
+
evidence = verify_access(profile)
|
|
459
|
+
script = build_remote_script(plan["spec"])
|
|
460
|
+
delimiter = "PROJECT_FORGE_APP_DEPLOY_STAGE_" + hashlib.sha256(script.encode()).hexdigest()[:24]
|
|
461
|
+
if delimiter in script:
|
|
462
|
+
raise DeploymentKitError("Unsafe application deployment script delimiter collision.")
|
|
463
|
+
command = "bash -s -- " + shlex.join([
|
|
464
|
+
"deploy", plan["expectedSpecSha256"], profile["user"], str(inherited_lock_fd),
|
|
465
|
+
])
|
|
466
|
+
return {
|
|
467
|
+
"targetName": plan["targetName"],
|
|
468
|
+
"verifiedAddress": evidence["address"],
|
|
469
|
+
"action": "deploy",
|
|
470
|
+
"script": f"{command} <<'{delimiter}'\n{script.rstrip()}\n{delimiter}\n",
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
397
474
|
def apply_app_deploy(plan: dict[str, Any]) -> dict[str, Any]:
|
|
398
475
|
required = {
|
|
399
476
|
"targetName",
|
|
@@ -6,6 +6,7 @@ set -Eeuo pipefail
|
|
|
6
6
|
ACTION="$1"
|
|
7
7
|
EXPECTED_SPEC_SHA="${2:-}"
|
|
8
8
|
DEPLOY_USER="${3:-}"
|
|
9
|
+
HELD_LOCK_FD="${4:-}"
|
|
9
10
|
APP_ROOT="__APP_ROOT__"
|
|
10
11
|
APP_SCHEMA_VERSION="__APP_SCHEMA_VERSION__"
|
|
11
12
|
NETWORK_NAME="__NETWORK_NAME__"
|
|
@@ -284,6 +285,43 @@ record_evidence() {
|
|
|
284
285
|
chmod 0600 "$root/deployment-evidence.jsonl"
|
|
285
286
|
}
|
|
286
287
|
|
|
288
|
+
acquire_deployment_lock() {
|
|
289
|
+
local lock_file="$1"
|
|
290
|
+
if [[ -z "$HELD_LOCK_FD" ]]; then
|
|
291
|
+
exec 9>>"$lock_file"
|
|
292
|
+
flock -n 9 || {
|
|
293
|
+
echo "Another application deployment holds the lock for $D_APP--$D_ENV." >&2
|
|
294
|
+
exit 75
|
|
295
|
+
}
|
|
296
|
+
return 0
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
[[ "$ACTION" == "deploy" && "$EXPECTED_SPEC_SHA" =~ ^[0-9a-f]{64}$ ]] || {
|
|
300
|
+
echo "An inherited lock is supported only for existing application deployments." >&2
|
|
301
|
+
exit 75
|
|
302
|
+
}
|
|
303
|
+
[[ "$HELD_LOCK_FD" =~ ^[0-9]+$ ]] && (( 10#$HELD_LOCK_FD >= 3 && 10#$HELD_LOCK_FD <= 256 )) || {
|
|
304
|
+
echo "Inherited application lock descriptor is invalid." >&2
|
|
305
|
+
exit 75
|
|
306
|
+
}
|
|
307
|
+
[[ -f "$lock_file" && ! -L "$lock_file" ]] || {
|
|
308
|
+
echo "Inherited application lock path is invalid." >&2
|
|
309
|
+
exit 75
|
|
310
|
+
}
|
|
311
|
+
local inherited_inode expected_inode
|
|
312
|
+
inherited_inode="$(stat -Lc '%d:%i' "/proc/self/fd/$HELD_LOCK_FD" 2>/dev/null || true)"
|
|
313
|
+
expected_inode="$(stat -Lc '%d:%i' "$lock_file" 2>/dev/null || true)"
|
|
314
|
+
[[ -n "$inherited_inode" && "$inherited_inode" == "$expected_inode" ]] || {
|
|
315
|
+
echo "Inherited application lock descriptor does not match this app/environment." >&2
|
|
316
|
+
exit 75
|
|
317
|
+
}
|
|
318
|
+
grep -Eq '^lock:[[:space:]]+[0-9]+:[[:space:]]+FLOCK[[:space:]]+ADVISORY[[:space:]]+WRITE([[:space:]]|$)' \
|
|
319
|
+
"/proc/self/fdinfo/$HELD_LOCK_FD" || {
|
|
320
|
+
echo "Inherited application lock was not held by this descriptor." >&2
|
|
321
|
+
exit 75
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
287
325
|
deploy() {
|
|
288
326
|
platform_ready || { echo "Kit shared platform is not ready." >&2; exit 34; }
|
|
289
327
|
decode_desired
|
|
@@ -300,11 +338,7 @@ deploy() {
|
|
|
300
338
|
release_dir="$releases/$release_id"
|
|
301
339
|
release_file="$release_dir/release.env"
|
|
302
340
|
|
|
303
|
-
|
|
304
|
-
if ! flock -n 9; then
|
|
305
|
-
echo "Another application deployment holds the lock for $D_APP--$D_ENV." >&2
|
|
306
|
-
exit 75
|
|
307
|
-
fi
|
|
341
|
+
acquire_deployment_lock "$lock_file"
|
|
308
342
|
|
|
309
343
|
for path in "$root" "$releases" "$state" "$root/data" "$root/deployment-evidence.jsonl"; do
|
|
310
344
|
[[ ! -L "$path" ]] || { echo "Managed application path must not be a symbolic link: $path" >&2; exit 35; }
|
|
@@ -7,6 +7,7 @@ import hashlib
|
|
|
7
7
|
import json
|
|
8
8
|
import posixpath
|
|
9
9
|
import re
|
|
10
|
+
import shlex
|
|
10
11
|
import subprocess
|
|
11
12
|
from typing import Any, Iterable
|
|
12
13
|
|
|
@@ -332,7 +333,7 @@ def authorization_phrase(plan: dict[str, Any]) -> str:
|
|
|
332
333
|
return prod if production else normal
|
|
333
334
|
|
|
334
335
|
|
|
335
|
-
def
|
|
336
|
+
def _validate_sqlite_plan(plan: dict[str, Any]) -> None:
|
|
336
337
|
required = {
|
|
337
338
|
"targetName", "action", "ownerRepo", "environment", "productionClassified", "appSlug", "image",
|
|
338
339
|
"databaseFile", "runAs", "sourceDatabase", "firstInstall", "stopsManagedRuntime", "createsVerifiedBackup",
|
|
@@ -345,6 +346,10 @@ def apply_sqlite_operation(plan: dict[str, Any]) -> dict[str, Any]:
|
|
|
345
346
|
raise DeploymentKitError("Literal production environment must remain Production-classified.")
|
|
346
347
|
if plan["applicationRecoveryReadyAfterOperation"] is not False:
|
|
347
348
|
raise DeploymentKitError("SQLite operations cannot claim application recovery readiness.")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def apply_sqlite_operation(plan: dict[str, Any]) -> dict[str, Any]:
|
|
352
|
+
_validate_sqlite_plan(plan)
|
|
348
353
|
_require_ready_platform(plan["targetName"])
|
|
349
354
|
current = sqlite_inventory(plan["targetName"])
|
|
350
355
|
if _snapshot(current) != plan["inventorySnapshot"]:
|
|
@@ -373,3 +378,46 @@ def assert_app_spec_matches_persistence(name: str, app_spec: dict[str, Any]) ->
|
|
|
373
378
|
raise DeploymentKitError("Application runAs does not match the prepared SQLite persistence reservation.")
|
|
374
379
|
if app_spec.get("persistence", {}).get("mode") != "directory":
|
|
375
380
|
raise DeploymentKitError("Prepared SQLite persistence requires directory application persistence.")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def render_locked_sqlite_operation_stage(
|
|
384
|
+
plan: dict[str, Any], *, inherited_lock_fd: int, authorization: str,
|
|
385
|
+
) -> dict[str, str]:
|
|
386
|
+
"""Render a Kit-validated SQLite stage for an existing privileged remote lock scope.
|
|
387
|
+
|
|
388
|
+
This returns a Bash *stage*, not an SSH command and not an authorization to
|
|
389
|
+
run it. The caller must have obtained an exclusive non-blocking flock on
|
|
390
|
+
this exact app/environment lock in the SAME remote root shell; the Kit
|
|
391
|
+
verifies that inherited open file description before touching storage.
|
|
392
|
+
|
|
393
|
+
The caller retains the descriptor across its full-backup/current-storage
|
|
394
|
+
comparison, Kit migration, optional Kit rollback-check, and its durable
|
|
395
|
+
application-specific receipt write. Do not execute this stage via a fresh
|
|
396
|
+
SSH/sudo connection: the inherited descriptor cannot cross that boundary.
|
|
397
|
+
"""
|
|
398
|
+
_validate_sqlite_plan(plan)
|
|
399
|
+
if plan["action"] not in {"migrate", "rollback-check"} or plan["firstInstall"]:
|
|
400
|
+
raise DeploymentKitError("Lock handoff is limited to existing-database migration and rollback-check.")
|
|
401
|
+
if type(inherited_lock_fd) is not int or not 3 <= inherited_lock_fd <= 256:
|
|
402
|
+
raise DeploymentKitError("Inherited remote lock descriptor must be an integer between 3 and 256.")
|
|
403
|
+
if authorization != authorization_phrase(plan):
|
|
404
|
+
raise DeploymentKitError("Exact reviewed SQLite operation authorization phrase is required.")
|
|
405
|
+
|
|
406
|
+
_require_ready_platform(plan["targetName"])
|
|
407
|
+
current = sqlite_inventory(plan["targetName"])
|
|
408
|
+
if _snapshot(current) != plan["inventorySnapshot"]:
|
|
409
|
+
raise DeploymentKitError("SQLite state changed after the plan was reviewed; create and authorize a fresh plan.")
|
|
410
|
+
profile = load_profile(plan["targetName"])
|
|
411
|
+
evidence = verify_access(profile)
|
|
412
|
+
script = build_remote_script(plan["spec"])
|
|
413
|
+
delimiter = "PROJECT_FORGE_SQLITE_STAGE_" + hashlib.sha256(script.encode()).hexdigest()[:24]
|
|
414
|
+
if delimiter in script:
|
|
415
|
+
raise DeploymentKitError("Unsafe SQLite script delimiter collision.")
|
|
416
|
+
args = [plan["action"], plan["expectedReservationSha256"] or "NONE", profile["user"], str(inherited_lock_fd)]
|
|
417
|
+
command = "bash -s -- " + " ".join(shlex.quote(arg) for arg in args)
|
|
418
|
+
return {
|
|
419
|
+
"targetName": plan["targetName"],
|
|
420
|
+
"verifiedAddress": evidence["address"],
|
|
421
|
+
"action": plan["action"],
|
|
422
|
+
"script": f"{command} <<'{delimiter}'\n{script.rstrip()}\n{delimiter}\n",
|
|
423
|
+
}
|
|
@@ -6,6 +6,9 @@ set -Eeuo pipefail
|
|
|
6
6
|
ACTION="$1"
|
|
7
7
|
EXPECTED_RESERVATION_SHA="${2:-}"
|
|
8
8
|
DEPLOY_USER="${3:-}"
|
|
9
|
+
# Optional Kit-verified handoff from an already-locked privileged remote session.
|
|
10
|
+
# This is a remote file descriptor, not a workstation/SSH descriptor.
|
|
11
|
+
HELD_LOCK_FD="${4:-}"
|
|
9
12
|
APP_ROOT="__APP_ROOT__"
|
|
10
13
|
SQLITE_SCHEMA_VERSION="__SQLITE_SCHEMA_VERSION__"
|
|
11
14
|
OWNER_B64="__OWNER_B64__"
|
|
@@ -253,12 +256,42 @@ verify_reserved_dirs() {
|
|
|
253
256
|
}
|
|
254
257
|
}
|
|
255
258
|
|
|
259
|
+
acquire_operation_lock() {
|
|
260
|
+
if [[ -z "$HELD_LOCK_FD" ]]; then
|
|
261
|
+
exec 9>>"$LOCK_FILE"
|
|
262
|
+
flock -n 9 || { echo "Another app/persistence operation holds the lock for $APP_SLUG--$ENVIRONMENT." >&2; exit 75; }
|
|
263
|
+
return 0
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
# Never convert an untrusted flag into permission to skip the lock.
|
|
267
|
+
# Inherited lock sharing is limited to existing-reservation migration and
|
|
268
|
+
# compatibility verification; other mutations retain their own lock.
|
|
269
|
+
[[ "$ACTION" == migrate || "$ACTION" == rollback-check ]] && [[ "$FIRST_INSTALL" == false ]] || {
|
|
270
|
+
echo "An inherited lock is not supported for this SQLite action." >&2; exit 75;
|
|
271
|
+
}
|
|
272
|
+
[[ "$HELD_LOCK_FD" =~ ^[0-9]+$ ]] && (( 10#$HELD_LOCK_FD >= 3 && 10#$HELD_LOCK_FD <= 256 )) || {
|
|
273
|
+
echo "Inherited SQLite lock descriptor is invalid." >&2; exit 75;
|
|
274
|
+
}
|
|
275
|
+
[[ -f "$LOCK_FILE" && ! -L "$LOCK_FILE" ]] || {
|
|
276
|
+
echo "Inherited SQLite lock path is invalid." >&2; exit 75;
|
|
277
|
+
}
|
|
278
|
+
local inherited_inode expected_inode
|
|
279
|
+
inherited_inode="$(stat -Lc '%d:%i' "/proc/self/fd/$HELD_LOCK_FD" 2>/dev/null || true)"
|
|
280
|
+
expected_inode="$(stat -Lc '%d:%i' "$LOCK_FILE" 2>/dev/null || true)"
|
|
281
|
+
[[ -n "$inherited_inode" && "$inherited_inode" == "$expected_inode" ]] || {
|
|
282
|
+
echo "Inherited SQLite lock descriptor does not match this app/environment." >&2; exit 75;
|
|
283
|
+
}
|
|
284
|
+
grep -Eq '^lock:[[:space:]]+[0-9]+:[[:space:]]+FLOCK[[:space:]]+ADVISORY[[:space:]]+WRITE([[:space:]]|$)' \
|
|
285
|
+
"/proc/self/fdinfo/$HELD_LOCK_FD" || {
|
|
286
|
+
echo "Inherited SQLite lock was not held by this descriptor." >&2; exit 75;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
256
290
|
operate() {
|
|
257
291
|
decode_spec
|
|
258
292
|
configure_registry_auth
|
|
259
293
|
set_paths
|
|
260
|
-
|
|
261
|
-
flock -n 9 || { echo "Another app/persistence operation holds the lock for $APP_SLUG--$ENVIRONMENT." >&2; exit 75; }
|
|
294
|
+
acquire_operation_lock
|
|
262
295
|
|
|
263
296
|
local db="$DATA/$DATABASE_FILE" stamp backup_name source_real source_parent
|
|
264
297
|
managed_runtime_info "$ROOT"
|