@mriekki/lean 0.4.0 → 0.4.1
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/deployment/distribution.json +5 -3
- package/assets/generated/ASSEMBLY.json +25 -15
- package/assets/generated/deployment/src/lean_poc_deployment/entrypoint.py +4 -0
- package/assets/generated/deployment/src/lean_poc_deployment/registry_access.py +112 -0
- package/assets/generated/deployment/src/lean_poc_deployment/registry_cli.py +51 -0
- package/assets/generated/deployment/src/lean_poc_deployment/ssh.py +1 -0
- package/package.json +1 -1
package/assets/components.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@mriekki/lean",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"defaultProfile": "greenfield-poc",
|
|
6
6
|
"profiles": {
|
|
7
7
|
"greenfield-poc": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"deployment": {
|
|
25
25
|
"repository": "mriekki/lean-poc-deployment-kit",
|
|
26
|
-
"commit": "
|
|
26
|
+
"commit": "4e52b74e1f23829e37b5f16ac03ada124621ffa0",
|
|
27
27
|
"distribution": "greenfield-poc",
|
|
28
28
|
"descriptor": "assets/deployment/distribution.json",
|
|
29
29
|
"sourceRoot": "assets/generated/deployment",
|
|
@@ -18,9 +18,8 @@
|
|
|
18
18
|
{"source": "src/lean_poc_deployment/errors.py", "destination": "tools/lean-deployment/lean_poc_deployment/errors.py"},
|
|
19
19
|
{"source": "src/lean_poc_deployment/keys.py", "destination": "tools/lean-deployment/lean_poc_deployment/keys.py"},
|
|
20
20
|
{"source": "src/lean_poc_deployment/profiles.py", "destination": "tools/lean-deployment/lean_poc_deployment/profiles.py"},
|
|
21
|
-
{"source": "src/lean_poc_deployment/
|
|
22
|
-
{"source": "src/lean_poc_deployment/
|
|
23
|
-
{"source": "src/lean_poc_deployment/teardown_remote.py", "destination": "tools/lean-deployment/lean_poc_deployment/teardown_remote.py"},
|
|
21
|
+
{"source": "src/lean_poc_deployment/registry_access.py", "destination": "tools/lean-deployment/lean_poc_deployment/registry_access.py"},
|
|
22
|
+
{"source": "src/lean_poc_deployment/registry_cli.py", "destination": "tools/lean-deployment/lean_poc_deployment/registry_cli.py"},
|
|
24
23
|
{"source": "src/lean_poc_deployment/route_registry.py", "destination": "tools/lean-deployment/lean_poc_deployment/route_registry.py"},
|
|
25
24
|
{"source": "src/lean_poc_deployment/route_registry_remote.py", "destination": "tools/lean-deployment/lean_poc_deployment/route_registry_remote.py"},
|
|
26
25
|
{"source": "src/lean_poc_deployment/security.py", "destination": "tools/lean-deployment/lean_poc_deployment/security.py"},
|
|
@@ -29,6 +28,9 @@
|
|
|
29
28
|
{"source": "src/lean_poc_deployment/sqlite_cli.py", "destination": "tools/lean-deployment/lean_poc_deployment/sqlite_cli.py"},
|
|
30
29
|
{"source": "src/lean_poc_deployment/sqlite_import_recovery.py", "destination": "tools/lean-deployment/lean_poc_deployment/sqlite_import_recovery.py"},
|
|
31
30
|
{"source": "src/lean_poc_deployment/ssh.py", "destination": "tools/lean-deployment/lean_poc_deployment/ssh.py"},
|
|
31
|
+
{"source": "src/lean_poc_deployment/teardown.py", "destination": "tools/lean-deployment/lean_poc_deployment/teardown.py"},
|
|
32
|
+
{"source": "src/lean_poc_deployment/teardown_cli.py", "destination": "tools/lean-deployment/lean_poc_deployment/teardown_cli.py"},
|
|
33
|
+
{"source": "src/lean_poc_deployment/teardown_remote.py", "destination": "tools/lean-deployment/lean_poc_deployment/teardown_remote.py"},
|
|
32
34
|
{"source": "src/lean_poc_deployment/trust.py", "destination": "tools/lean-deployment/lean_poc_deployment/trust.py"}
|
|
33
35
|
]
|
|
34
36
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@mriekki/lean",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"components": {
|
|
6
6
|
"core": {
|
|
7
7
|
"repository": "mriekki/lean-agentic-core",
|
|
@@ -243,7 +243,7 @@
|
|
|
243
243
|
},
|
|
244
244
|
"deployment": {
|
|
245
245
|
"repository": "mriekki/lean-poc-deployment-kit",
|
|
246
|
-
"commit": "
|
|
246
|
+
"commit": "4e52b74e1f23829e37b5f16ac03ada124621ffa0",
|
|
247
247
|
"distribution": "greenfield-poc",
|
|
248
248
|
"files": [
|
|
249
249
|
{
|
|
@@ -304,7 +304,7 @@
|
|
|
304
304
|
{
|
|
305
305
|
"source": "src/lean_poc_deployment/entrypoint.py",
|
|
306
306
|
"destination": "tools/lean-deployment/lean_poc_deployment/entrypoint.py",
|
|
307
|
-
"sha256": "
|
|
307
|
+
"sha256": "5161aec6d5a1fc684894a1011b3d577868cedaa7039ea647862a4628067fabd8"
|
|
308
308
|
},
|
|
309
309
|
{
|
|
310
310
|
"source": "src/lean_poc_deployment/errors.py",
|
|
@@ -322,19 +322,14 @@
|
|
|
322
322
|
"sha256": "9d73f86714db5c59a07354348e394647d62a3c31b173c4d3c4c4353872da908a"
|
|
323
323
|
},
|
|
324
324
|
{
|
|
325
|
-
"source": "src/lean_poc_deployment/
|
|
326
|
-
"destination": "tools/lean-deployment/lean_poc_deployment/
|
|
327
|
-
"sha256": "
|
|
325
|
+
"source": "src/lean_poc_deployment/registry_access.py",
|
|
326
|
+
"destination": "tools/lean-deployment/lean_poc_deployment/registry_access.py",
|
|
327
|
+
"sha256": "daab56f3a09a11f61e12c4da62fef44b068f1b71172ba68ecf71837bbc0d1af3"
|
|
328
328
|
},
|
|
329
329
|
{
|
|
330
|
-
"source": "src/lean_poc_deployment/
|
|
331
|
-
"destination": "tools/lean-deployment/lean_poc_deployment/
|
|
332
|
-
"sha256": "
|
|
333
|
-
},
|
|
334
|
-
{
|
|
335
|
-
"source": "src/lean_poc_deployment/teardown_remote.py",
|
|
336
|
-
"destination": "tools/lean-deployment/lean_poc_deployment/teardown_remote.py",
|
|
337
|
-
"sha256": "9745fd9f721eea2da8f5d32688cc6a293e46aa9f9e5c5825c6a08cacb1121fb1"
|
|
330
|
+
"source": "src/lean_poc_deployment/registry_cli.py",
|
|
331
|
+
"destination": "tools/lean-deployment/lean_poc_deployment/registry_cli.py",
|
|
332
|
+
"sha256": "6288d85ae44708362ba09e1b82546617a9660dd9d5601ced081ee44d3fc27bda"
|
|
338
333
|
},
|
|
339
334
|
{
|
|
340
335
|
"source": "src/lean_poc_deployment/route_registry.py",
|
|
@@ -374,7 +369,22 @@
|
|
|
374
369
|
{
|
|
375
370
|
"source": "src/lean_poc_deployment/ssh.py",
|
|
376
371
|
"destination": "tools/lean-deployment/lean_poc_deployment/ssh.py",
|
|
377
|
-
"sha256": "
|
|
372
|
+
"sha256": "916a272ed5978df642e6084a3030c32f2c9dcb4b3b686a194428f9810ec55e9d"
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"source": "src/lean_poc_deployment/teardown.py",
|
|
376
|
+
"destination": "tools/lean-deployment/lean_poc_deployment/teardown.py",
|
|
377
|
+
"sha256": "edcf921aa92988176b756abf4bbc9b0b83ae1d855b6e8d487b67aaad7727cb55"
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
"source": "src/lean_poc_deployment/teardown_cli.py",
|
|
381
|
+
"destination": "tools/lean-deployment/lean_poc_deployment/teardown_cli.py",
|
|
382
|
+
"sha256": "ec1271db5f57135189892e87f2df3ffc5fcb6e763ff1eb9604bc4e50788c3325"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"source": "src/lean_poc_deployment/teardown_remote.py",
|
|
386
|
+
"destination": "tools/lean-deployment/lean_poc_deployment/teardown_remote.py",
|
|
387
|
+
"sha256": "9745fd9f721eea2da8f5d32688cc6a293e46aa9f9e5c5825c6a08cacb1121fb1"
|
|
378
388
|
},
|
|
379
389
|
{
|
|
380
390
|
"source": "src/lean_poc_deployment/trust.py",
|
|
@@ -6,6 +6,7 @@ import sys
|
|
|
6
6
|
|
|
7
7
|
from .app_cli import main as app_main
|
|
8
8
|
from .cli import main as core_main
|
|
9
|
+
from .registry_cli import main as registry_main
|
|
9
10
|
from .sqlite_cli import main as sqlite_main
|
|
10
11
|
from .teardown_cli import main as teardown_main
|
|
11
12
|
|
|
@@ -14,12 +15,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
14
15
|
args = list(sys.argv[1:] if argv is None else argv)
|
|
15
16
|
if args and args[0] == "app":
|
|
16
17
|
return app_main(args[1:])
|
|
18
|
+
if args and args[0] == "registry":
|
|
19
|
+
return registry_main(args[1:])
|
|
17
20
|
if args and args[0] == "sqlite":
|
|
18
21
|
return sqlite_main(args[1:])
|
|
19
22
|
if args and args[0] == "tear-down":
|
|
20
23
|
return teardown_main(args[1:])
|
|
21
24
|
if not args or args == ["--help"] or args == ["-h"]:
|
|
22
25
|
print("Command group: app Generic immutable application runtime inventory/deployment")
|
|
26
|
+
print("Command group: registry Read-only exact-image registry access verification")
|
|
23
27
|
print("Command group: sqlite SQLite persistence import/backup/migration/rollback checks")
|
|
24
28
|
print("Command group: tear-down Scoped destructive POC environment/local cleanup")
|
|
25
29
|
return core_main(args)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Read-only exact-image registry access verification for logical targets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import shlex
|
|
7
|
+
import subprocess
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .errors import DeploymentKitError
|
|
11
|
+
from .profiles import load_profile
|
|
12
|
+
from .ssh import ssh_args, verify_access
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_IMAGE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/:+-]*@sha256:[0-9a-f]{64}$")
|
|
16
|
+
|
|
17
|
+
_REMOTE_SCRIPT = r'''#!/usr/bin/env bash
|
|
18
|
+
set -Eeuo pipefail
|
|
19
|
+
|
|
20
|
+
IMAGE="$1"
|
|
21
|
+
|
|
22
|
+
[[ "$IMAGE" =~ ^[A-Za-z0-9][A-Za-z0-9._/:+-]*@sha256:[0-9a-f]{64}$ ]] || {
|
|
23
|
+
echo "Image must be an immutable sha256 digest reference." >&2
|
|
24
|
+
exit 41
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
command -v docker >/dev/null 2>&1 || {
|
|
28
|
+
echo "Docker CLI is unavailable on the target." >&2
|
|
29
|
+
exit 42
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
config_dir="$HOME/.docker"
|
|
33
|
+
config_file="$config_dir/config.json"
|
|
34
|
+
credential_state="absent"
|
|
35
|
+
|
|
36
|
+
if [[ -e "$config_file" ]]; then
|
|
37
|
+
[[ -d "$config_dir" && ! -L "$config_dir" && -f "$config_file" && ! -L "$config_file" ]] || {
|
|
38
|
+
echo "Docker credential path is not a regular protected directory/file." >&2
|
|
39
|
+
exit 43
|
|
40
|
+
}
|
|
41
|
+
dir_mode="$(stat -c '%a' "$config_dir")"
|
|
42
|
+
file_mode="$(stat -c '%a' "$config_file")"
|
|
43
|
+
current_uid="$(id -u)"
|
|
44
|
+
if (( (8#$dir_mode & 077) != 0 || (8#$file_mode & 077) != 0 )) ||
|
|
45
|
+
[[ "$(stat -c '%u' "$config_dir")" != "$current_uid" || "$(stat -c '%u' "$config_file")" != "$current_uid" ]]; then
|
|
46
|
+
echo "Docker credentials must be owned by the deployment account and deny group/other access." >&2
|
|
47
|
+
exit 43
|
|
48
|
+
fi
|
|
49
|
+
export DOCKER_CONFIG="$config_dir"
|
|
50
|
+
credential_state="protected"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if ! docker manifest inspect "$IMAGE" </dev/null >/dev/null 2>&1; then
|
|
54
|
+
echo "Registry manifest access failed for the exact image." >&2
|
|
55
|
+
exit 44
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
printf 'ACCESS\t%s\n' "$credential_state"
|
|
59
|
+
'''
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def normalize_registry_image(value: str) -> str:
|
|
63
|
+
if not isinstance(value, str) or _IMAGE_RE.fullmatch(value) is None:
|
|
64
|
+
raise DeploymentKitError(
|
|
65
|
+
"image must be an immutable image reference ending in @sha256:<64 lowercase hex>."
|
|
66
|
+
)
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_remote_script() -> str:
|
|
71
|
+
return _REMOTE_SCRIPT
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _parse_access(output: str) -> str:
|
|
75
|
+
lines = [line for line in output.splitlines() if line]
|
|
76
|
+
if len(lines) != 1:
|
|
77
|
+
raise DeploymentKitError("Malformed registry access verification output.")
|
|
78
|
+
parts = lines[0].split("\t")
|
|
79
|
+
if len(parts) != 2 or parts[0] != "ACCESS" or parts[1] not in {"absent", "protected"}:
|
|
80
|
+
raise DeploymentKitError("Malformed registry access verification output.")
|
|
81
|
+
return parts[1]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def verify_registry_access(name: str, image: str) -> dict[str, Any]:
|
|
85
|
+
exact_image = normalize_registry_image(image)
|
|
86
|
+
profile = load_profile(name)
|
|
87
|
+
access = verify_access(profile)
|
|
88
|
+
command = ssh_args(
|
|
89
|
+
profile,
|
|
90
|
+
access["address"],
|
|
91
|
+
command="bash -s -- " + shlex.quote(exact_image),
|
|
92
|
+
)
|
|
93
|
+
completed = subprocess.run(
|
|
94
|
+
command,
|
|
95
|
+
input=build_remote_script().encode("utf-8"),
|
|
96
|
+
check=False,
|
|
97
|
+
capture_output=True,
|
|
98
|
+
)
|
|
99
|
+
stdout = completed.stdout.decode("utf-8", errors="replace")
|
|
100
|
+
stderr = completed.stderr.decode("utf-8", errors="replace")
|
|
101
|
+
if completed.returncode != 0:
|
|
102
|
+
detail = stderr.strip() or stdout.strip() or f"exit {completed.returncode}"
|
|
103
|
+
raise DeploymentKitError(f"Read-only registry access verification failed: {detail}")
|
|
104
|
+
|
|
105
|
+
credential_state = _parse_access(stdout)
|
|
106
|
+
return {
|
|
107
|
+
"targetName": name,
|
|
108
|
+
"image": exact_image,
|
|
109
|
+
"manifestAccess": "verified",
|
|
110
|
+
"credentialConfig": credential_state,
|
|
111
|
+
"mutated": False,
|
|
112
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""CLI for read-only registry access verification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .errors import DeploymentKitError
|
|
11
|
+
from .registry_access import verify_registry_access
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _print(value: Any, *, as_json: bool) -> None:
|
|
15
|
+
if as_json:
|
|
16
|
+
print(json.dumps(value, indent=2, sort_keys=True))
|
|
17
|
+
return
|
|
18
|
+
if isinstance(value, dict):
|
|
19
|
+
for key, item in value.items():
|
|
20
|
+
print(f"{key}: {item}")
|
|
21
|
+
return
|
|
22
|
+
print(value)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
26
|
+
parser = argparse.ArgumentParser(prog="lean-poc-deploy registry")
|
|
27
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
28
|
+
|
|
29
|
+
verify = sub.add_parser(
|
|
30
|
+
"verify",
|
|
31
|
+
help="Verify exact-image registry manifest access without pulling or modifying target state",
|
|
32
|
+
)
|
|
33
|
+
verify.add_argument("name")
|
|
34
|
+
verify.add_argument("--image", required=True)
|
|
35
|
+
verify.add_argument("--json", action="store_true")
|
|
36
|
+
return parser
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main(argv: list[str] | None = None) -> int:
|
|
40
|
+
args = build_parser().parse_args(argv)
|
|
41
|
+
try:
|
|
42
|
+
if args.command == "verify":
|
|
43
|
+
_print(
|
|
44
|
+
verify_registry_access(args.name, args.image),
|
|
45
|
+
as_json=args.json,
|
|
46
|
+
)
|
|
47
|
+
return 0
|
|
48
|
+
except DeploymentKitError as error:
|
|
49
|
+
print(f"ERROR: {error}", file=sys.stderr)
|
|
50
|
+
return 2
|
|
51
|
+
return 2
|
|
@@ -108,6 +108,7 @@ def verify_access(profile: dict[str, Any]) -> dict[str, str]:
|
|
|
108
108
|
text=True,
|
|
109
109
|
encoding="utf-8",
|
|
110
110
|
errors="replace",
|
|
111
|
+
stdin=subprocess.DEVNULL,
|
|
111
112
|
)
|
|
112
113
|
if result.returncode == 0:
|
|
113
114
|
return {"targetName": profile["targetName"], "address": address, "status": "verified"}
|