@mriekki/lean 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -67
- package/assets/components.json +33 -33
- package/assets/core/distribution.json +52 -51
- package/assets/deployment/distribution.json +34 -34
- package/assets/generated/ASSEMBLY.json +38 -33
- package/assets/generated/core/core/skills/core-sync/SKILL.md +78 -120
- package/assets/generated/core/core/skills/deploy/SKILL.md +74 -91
- package/assets/generated/core/core/skills/lean-update/SKILL.md +189 -0
- package/assets/generated/core/core/skills/upstream-fix/SKILL.md +91 -76
- package/assets/generated/deployment/distribution/lean-poc-deploy.py +8 -8
- package/assets/generated/deployment/src/lean_poc_deployment/__init__.py +3 -3
- package/assets/generated/deployment/src/lean_poc_deployment/__main__.py +3 -3
- package/assets/generated/deployment/src/lean_poc_deployment/app_cli.py +140 -140
- package/assets/generated/deployment/src/lean_poc_deployment/app_runtime.py +455 -455
- package/assets/generated/deployment/src/lean_poc_deployment/app_runtime_remote.py +415 -415
- package/assets/generated/deployment/src/lean_poc_deployment/box_platform.py +208 -208
- package/assets/generated/deployment/src/lean_poc_deployment/box_platform_assets.py +100 -100
- package/assets/generated/deployment/src/lean_poc_deployment/box_platform_remote.py +271 -271
- package/assets/generated/deployment/src/lean_poc_deployment/cli.py +437 -437
- package/assets/generated/deployment/src/lean_poc_deployment/doctor.py +121 -121
- package/assets/generated/deployment/src/lean_poc_deployment/entrypoint.py +25 -25
- package/assets/generated/deployment/src/lean_poc_deployment/errors.py +5 -5
- package/assets/generated/deployment/src/lean_poc_deployment/keys.py +171 -171
- package/assets/generated/deployment/src/lean_poc_deployment/profiles.py +204 -204
- package/assets/generated/deployment/src/lean_poc_deployment/route_registry.py +367 -367
- package/assets/generated/deployment/src/lean_poc_deployment/route_registry_remote.py +443 -443
- package/assets/generated/deployment/src/lean_poc_deployment/security.py +290 -290
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter.py +334 -334
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter_remote.py +339 -339
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_cli.py +85 -85
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_import_recovery.py +437 -437
- package/assets/generated/deployment/src/lean_poc_deployment/ssh.py +118 -118
- package/assets/generated/deployment/src/lean_poc_deployment/teardown.py +286 -286
- package/assets/generated/deployment/src/lean_poc_deployment/teardown_cli.py +120 -120
- package/assets/generated/deployment/src/lean_poc_deployment/teardown_remote.py +100 -100
- package/assets/generated/deployment/src/lean_poc_deployment/trust.py +278 -278
- package/bin/lean.mjs +9 -9
- package/package.json +33 -33
- package/src/cli.mjs +131 -131
- package/src/engine.mjs +243 -243
- package/src/package-data.mjs +18 -18
|
@@ -1,208 +1,208 @@
|
|
|
1
|
-
"""Shared Ubuntu box platform ownership and lifecycle operations."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import base64
|
|
6
|
-
import hashlib
|
|
7
|
-
import json
|
|
8
|
-
import shlex
|
|
9
|
-
import subprocess
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
from .box_platform_assets import (
|
|
13
|
-
AVAHI_SERVICE,
|
|
14
|
-
CADDYFILE,
|
|
15
|
-
CADDY_CONTAINER,
|
|
16
|
-
CADDY_IMAGE,
|
|
17
|
-
COMPOSE_YAML,
|
|
18
|
-
HOST_PLATFORM_WRITE_PHRASE,
|
|
19
|
-
NETWORK_NAME,
|
|
20
|
-
PLATFORM_DISTRIBUTION,
|
|
21
|
-
PLATFORM_ROOT,
|
|
22
|
-
PLATFORM_SCHEMA_VERSION,
|
|
23
|
-
PLATFORM_VERSION,
|
|
24
|
-
PUBLISH_ALIAS_SH,
|
|
25
|
-
)
|
|
26
|
-
from .box_platform_remote import REMOTE_SCRIPT
|
|
27
|
-
from .errors import DeploymentKitError
|
|
28
|
-
from .profiles import load_profile
|
|
29
|
-
from .ssh import ssh_args, verify_access
|
|
30
|
-
|
|
31
|
-
_BOOL_KEYS = {
|
|
32
|
-
"markerPresent",
|
|
33
|
-
"prerequisitesReady",
|
|
34
|
-
"networkPresent",
|
|
35
|
-
"caddyRunning",
|
|
36
|
-
"avahiRunning",
|
|
37
|
-
"port80InUse",
|
|
38
|
-
}
|
|
39
|
-
_EXPECTED_KEYS = {
|
|
40
|
-
"state",
|
|
41
|
-
"markerPresent",
|
|
42
|
-
"markerSchemaVersion",
|
|
43
|
-
"markerDistribution",
|
|
44
|
-
"markerPlatformVersion",
|
|
45
|
-
"expectedPlatformVersion",
|
|
46
|
-
"prerequisitesReady",
|
|
47
|
-
"networkPresent",
|
|
48
|
-
"caddyRunning",
|
|
49
|
-
"avahiRunning",
|
|
50
|
-
"port80InUse",
|
|
51
|
-
"platformRoot",
|
|
52
|
-
"networkName",
|
|
53
|
-
"caddyContainer",
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def build_remote_script() -> str:
|
|
58
|
-
replacements = {
|
|
59
|
-
"__PLATFORM_ROOT__": PLATFORM_ROOT,
|
|
60
|
-
"__NETWORK_NAME__": NETWORK_NAME,
|
|
61
|
-
"__CADDY_CONTAINER__": CADDY_CONTAINER,
|
|
62
|
-
"__CADDY_IMAGE__": CADDY_IMAGE,
|
|
63
|
-
"__SCHEMA_VERSION__": str(PLATFORM_SCHEMA_VERSION),
|
|
64
|
-
"__PLATFORM_VERSION__": PLATFORM_VERSION,
|
|
65
|
-
"__DISTRIBUTION__": PLATFORM_DISTRIBUTION,
|
|
66
|
-
"__COMPOSE_B64__": base64.b64encode(COMPOSE_YAML.encode()).decode("ascii"),
|
|
67
|
-
"__CADDYFILE_B64__": base64.b64encode(CADDYFILE.encode()).decode("ascii"),
|
|
68
|
-
"__PUBLISH_ALIAS_B64__": base64.b64encode(PUBLISH_ALIAS_SH.encode()).decode("ascii"),
|
|
69
|
-
"__AVAHI_SERVICE_B64__": base64.b64encode(AVAHI_SERVICE.encode()).decode("ascii"),
|
|
70
|
-
}
|
|
71
|
-
script = REMOTE_SCRIPT
|
|
72
|
-
for placeholder, value in replacements.items():
|
|
73
|
-
script = script.replace(placeholder, value)
|
|
74
|
-
if "__" in script:
|
|
75
|
-
raise DeploymentKitError("Internal host-platform script contains an unresolved placeholder.")
|
|
76
|
-
return script
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _parse_status(output: str) -> dict[str, Any]:
|
|
80
|
-
result: dict[str, Any] = {}
|
|
81
|
-
for raw in output.splitlines():
|
|
82
|
-
if not raw.strip():
|
|
83
|
-
continue
|
|
84
|
-
if "=" not in raw:
|
|
85
|
-
raise DeploymentKitError(f"Malformed host-platform status line: {raw!r}")
|
|
86
|
-
key, value = raw.split("=", 1)
|
|
87
|
-
if key not in _EXPECTED_KEYS or key in result:
|
|
88
|
-
raise DeploymentKitError(f"Unexpected or duplicate host-platform status key: {key!r}")
|
|
89
|
-
if key in _BOOL_KEYS:
|
|
90
|
-
if value not in {"true", "false"}:
|
|
91
|
-
raise DeploymentKitError(f"Invalid boolean host-platform status value for {key}.")
|
|
92
|
-
result[key] = value == "true"
|
|
93
|
-
else:
|
|
94
|
-
result[key] = value
|
|
95
|
-
missing = _EXPECTED_KEYS - result.keys()
|
|
96
|
-
if missing:
|
|
97
|
-
raise DeploymentKitError(
|
|
98
|
-
"Host-platform status omitted required keys: " + ", ".join(sorted(missing))
|
|
99
|
-
)
|
|
100
|
-
return result
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _run_remote(profile: dict[str, Any], address: str, action: str, *, sudo: bool) -> str:
|
|
104
|
-
if action not in {"status", "install", "upgrade"}:
|
|
105
|
-
raise DeploymentKitError(f"Unsupported host-platform action: {action}")
|
|
106
|
-
remote_command = "bash -s -- " + shlex.quote(action)
|
|
107
|
-
if sudo:
|
|
108
|
-
remote_command = "sudo -n " + remote_command
|
|
109
|
-
command = ssh_args(profile, address, command=remote_command)
|
|
110
|
-
completed = subprocess.run(
|
|
111
|
-
command,
|
|
112
|
-
input=build_remote_script().encode("utf-8"),
|
|
113
|
-
check=False,
|
|
114
|
-
capture_output=True,
|
|
115
|
-
)
|
|
116
|
-
stdout = completed.stdout.decode("utf-8", errors="replace")
|
|
117
|
-
stderr = completed.stderr.decode("utf-8", errors="replace")
|
|
118
|
-
if completed.returncode != 0:
|
|
119
|
-
detail = stderr.strip() or stdout.strip() or f"exit {completed.returncode}"
|
|
120
|
-
raise DeploymentKitError(f"Remote host-platform {action} failed: {detail}")
|
|
121
|
-
return stdout
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def platform_status(name: str) -> dict[str, Any]:
|
|
125
|
-
profile = load_profile(name)
|
|
126
|
-
evidence = verify_access(profile)
|
|
127
|
-
status = _parse_status(_run_remote(profile, evidence["address"], "status", sudo=True))
|
|
128
|
-
status["targetName"] = name
|
|
129
|
-
status["verifiedAddress"] = evidence["address"]
|
|
130
|
-
return status
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def platform_doctor(name: str) -> dict[str, Any]:
|
|
134
|
-
status = platform_status(name)
|
|
135
|
-
next_actions = {
|
|
136
|
-
"installable": "platform-install",
|
|
137
|
-
"upgrade-required": "platform-upgrade",
|
|
138
|
-
"drifted": "inspect-platform-drift",
|
|
139
|
-
"blocked": "prepare-host-prerequisites",
|
|
140
|
-
"blocked-port-80": "identify-existing-port-80-owner",
|
|
141
|
-
"incompatible": "manual-platform-recovery",
|
|
142
|
-
"ready": "none",
|
|
143
|
-
}
|
|
144
|
-
status["nextAction"] = next_actions.get(status["state"], "manual-review")
|
|
145
|
-
status["mutated"] = False
|
|
146
|
-
return status
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def _snapshot(status: dict[str, Any]) -> str:
|
|
150
|
-
selected = {key: status[key] for key in sorted(_EXPECTED_KEYS)}
|
|
151
|
-
return hashlib.sha256(json.dumps(selected, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def plan_platform_change(name: str, action: str) -> dict[str, Any]:
|
|
155
|
-
if action not in {"install", "upgrade"}:
|
|
156
|
-
raise DeploymentKitError("Platform change action must be install or upgrade.")
|
|
157
|
-
status = platform_status(name)
|
|
158
|
-
expected_state = "installable" if action == "install" else "upgrade-required"
|
|
159
|
-
if status["state"] != expected_state:
|
|
160
|
-
raise DeploymentKitError(
|
|
161
|
-
f"Platform {action} requires state '{expected_state}', but target '{name}' is '{status['state']}'."
|
|
162
|
-
)
|
|
163
|
-
return {
|
|
164
|
-
"targetName": name,
|
|
165
|
-
"action": action,
|
|
166
|
-
"fromState": status["state"],
|
|
167
|
-
"toPlatformVersion": PLATFORM_VERSION,
|
|
168
|
-
"distribution": PLATFORM_DISTRIBUTION,
|
|
169
|
-
"platformRoot": PLATFORM_ROOT,
|
|
170
|
-
"preserves": ["route-conf.d", "route-registry", "caddy-data", "caddy-config", "shared-network"],
|
|
171
|
-
"doesNotOwn": ["application-containers", "application-data", "application-routes"],
|
|
172
|
-
"statusSnapshot": _snapshot(status),
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def apply_platform_change(plan: dict[str, Any]) -> dict[str, Any]:
|
|
177
|
-
required = {
|
|
178
|
-
"targetName",
|
|
179
|
-
"action",
|
|
180
|
-
"fromState",
|
|
181
|
-
"toPlatformVersion",
|
|
182
|
-
"distribution",
|
|
183
|
-
"platformRoot",
|
|
184
|
-
"preserves",
|
|
185
|
-
"doesNotOwn",
|
|
186
|
-
"statusSnapshot",
|
|
187
|
-
}
|
|
188
|
-
if not isinstance(plan, dict) or set(plan) != required:
|
|
189
|
-
raise DeploymentKitError("Malformed host-platform change plan.")
|
|
190
|
-
name = plan["targetName"]
|
|
191
|
-
action = plan["action"]
|
|
192
|
-
if action not in {"install", "upgrade"}:
|
|
193
|
-
raise DeploymentKitError("Malformed host-platform change action.")
|
|
194
|
-
current = platform_status(name)
|
|
195
|
-
if _snapshot(current) != plan["statusSnapshot"]:
|
|
196
|
-
raise DeploymentKitError(
|
|
197
|
-
"Host-platform state changed after the plan was reviewed; create and authorize a fresh plan."
|
|
198
|
-
)
|
|
199
|
-
profile = load_profile(name)
|
|
200
|
-
evidence = verify_access(profile)
|
|
201
|
-
_run_remote(profile, evidence["address"], action, sudo=True)
|
|
202
|
-
final = platform_status(name)
|
|
203
|
-
if final["state"] != "ready":
|
|
204
|
-
raise DeploymentKitError(
|
|
205
|
-
f"Host-platform {action} completed but readiness is '{final['state']}', not 'ready'."
|
|
206
|
-
)
|
|
207
|
-
final["mutated"] = True
|
|
208
|
-
return final
|
|
1
|
+
"""Shared Ubuntu box platform ownership and lifecycle operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import shlex
|
|
9
|
+
import subprocess
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .box_platform_assets import (
|
|
13
|
+
AVAHI_SERVICE,
|
|
14
|
+
CADDYFILE,
|
|
15
|
+
CADDY_CONTAINER,
|
|
16
|
+
CADDY_IMAGE,
|
|
17
|
+
COMPOSE_YAML,
|
|
18
|
+
HOST_PLATFORM_WRITE_PHRASE,
|
|
19
|
+
NETWORK_NAME,
|
|
20
|
+
PLATFORM_DISTRIBUTION,
|
|
21
|
+
PLATFORM_ROOT,
|
|
22
|
+
PLATFORM_SCHEMA_VERSION,
|
|
23
|
+
PLATFORM_VERSION,
|
|
24
|
+
PUBLISH_ALIAS_SH,
|
|
25
|
+
)
|
|
26
|
+
from .box_platform_remote import REMOTE_SCRIPT
|
|
27
|
+
from .errors import DeploymentKitError
|
|
28
|
+
from .profiles import load_profile
|
|
29
|
+
from .ssh import ssh_args, verify_access
|
|
30
|
+
|
|
31
|
+
_BOOL_KEYS = {
|
|
32
|
+
"markerPresent",
|
|
33
|
+
"prerequisitesReady",
|
|
34
|
+
"networkPresent",
|
|
35
|
+
"caddyRunning",
|
|
36
|
+
"avahiRunning",
|
|
37
|
+
"port80InUse",
|
|
38
|
+
}
|
|
39
|
+
_EXPECTED_KEYS = {
|
|
40
|
+
"state",
|
|
41
|
+
"markerPresent",
|
|
42
|
+
"markerSchemaVersion",
|
|
43
|
+
"markerDistribution",
|
|
44
|
+
"markerPlatformVersion",
|
|
45
|
+
"expectedPlatformVersion",
|
|
46
|
+
"prerequisitesReady",
|
|
47
|
+
"networkPresent",
|
|
48
|
+
"caddyRunning",
|
|
49
|
+
"avahiRunning",
|
|
50
|
+
"port80InUse",
|
|
51
|
+
"platformRoot",
|
|
52
|
+
"networkName",
|
|
53
|
+
"caddyContainer",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_remote_script() -> str:
|
|
58
|
+
replacements = {
|
|
59
|
+
"__PLATFORM_ROOT__": PLATFORM_ROOT,
|
|
60
|
+
"__NETWORK_NAME__": NETWORK_NAME,
|
|
61
|
+
"__CADDY_CONTAINER__": CADDY_CONTAINER,
|
|
62
|
+
"__CADDY_IMAGE__": CADDY_IMAGE,
|
|
63
|
+
"__SCHEMA_VERSION__": str(PLATFORM_SCHEMA_VERSION),
|
|
64
|
+
"__PLATFORM_VERSION__": PLATFORM_VERSION,
|
|
65
|
+
"__DISTRIBUTION__": PLATFORM_DISTRIBUTION,
|
|
66
|
+
"__COMPOSE_B64__": base64.b64encode(COMPOSE_YAML.encode()).decode("ascii"),
|
|
67
|
+
"__CADDYFILE_B64__": base64.b64encode(CADDYFILE.encode()).decode("ascii"),
|
|
68
|
+
"__PUBLISH_ALIAS_B64__": base64.b64encode(PUBLISH_ALIAS_SH.encode()).decode("ascii"),
|
|
69
|
+
"__AVAHI_SERVICE_B64__": base64.b64encode(AVAHI_SERVICE.encode()).decode("ascii"),
|
|
70
|
+
}
|
|
71
|
+
script = REMOTE_SCRIPT
|
|
72
|
+
for placeholder, value in replacements.items():
|
|
73
|
+
script = script.replace(placeholder, value)
|
|
74
|
+
if "__" in script:
|
|
75
|
+
raise DeploymentKitError("Internal host-platform script contains an unresolved placeholder.")
|
|
76
|
+
return script
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _parse_status(output: str) -> dict[str, Any]:
|
|
80
|
+
result: dict[str, Any] = {}
|
|
81
|
+
for raw in output.splitlines():
|
|
82
|
+
if not raw.strip():
|
|
83
|
+
continue
|
|
84
|
+
if "=" not in raw:
|
|
85
|
+
raise DeploymentKitError(f"Malformed host-platform status line: {raw!r}")
|
|
86
|
+
key, value = raw.split("=", 1)
|
|
87
|
+
if key not in _EXPECTED_KEYS or key in result:
|
|
88
|
+
raise DeploymentKitError(f"Unexpected or duplicate host-platform status key: {key!r}")
|
|
89
|
+
if key in _BOOL_KEYS:
|
|
90
|
+
if value not in {"true", "false"}:
|
|
91
|
+
raise DeploymentKitError(f"Invalid boolean host-platform status value for {key}.")
|
|
92
|
+
result[key] = value == "true"
|
|
93
|
+
else:
|
|
94
|
+
result[key] = value
|
|
95
|
+
missing = _EXPECTED_KEYS - result.keys()
|
|
96
|
+
if missing:
|
|
97
|
+
raise DeploymentKitError(
|
|
98
|
+
"Host-platform status omitted required keys: " + ", ".join(sorted(missing))
|
|
99
|
+
)
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _run_remote(profile: dict[str, Any], address: str, action: str, *, sudo: bool) -> str:
|
|
104
|
+
if action not in {"status", "install", "upgrade"}:
|
|
105
|
+
raise DeploymentKitError(f"Unsupported host-platform action: {action}")
|
|
106
|
+
remote_command = "bash -s -- " + shlex.quote(action)
|
|
107
|
+
if sudo:
|
|
108
|
+
remote_command = "sudo -n " + remote_command
|
|
109
|
+
command = ssh_args(profile, address, command=remote_command)
|
|
110
|
+
completed = subprocess.run(
|
|
111
|
+
command,
|
|
112
|
+
input=build_remote_script().encode("utf-8"),
|
|
113
|
+
check=False,
|
|
114
|
+
capture_output=True,
|
|
115
|
+
)
|
|
116
|
+
stdout = completed.stdout.decode("utf-8", errors="replace")
|
|
117
|
+
stderr = completed.stderr.decode("utf-8", errors="replace")
|
|
118
|
+
if completed.returncode != 0:
|
|
119
|
+
detail = stderr.strip() or stdout.strip() or f"exit {completed.returncode}"
|
|
120
|
+
raise DeploymentKitError(f"Remote host-platform {action} failed: {detail}")
|
|
121
|
+
return stdout
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def platform_status(name: str) -> dict[str, Any]:
|
|
125
|
+
profile = load_profile(name)
|
|
126
|
+
evidence = verify_access(profile)
|
|
127
|
+
status = _parse_status(_run_remote(profile, evidence["address"], "status", sudo=True))
|
|
128
|
+
status["targetName"] = name
|
|
129
|
+
status["verifiedAddress"] = evidence["address"]
|
|
130
|
+
return status
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def platform_doctor(name: str) -> dict[str, Any]:
|
|
134
|
+
status = platform_status(name)
|
|
135
|
+
next_actions = {
|
|
136
|
+
"installable": "platform-install",
|
|
137
|
+
"upgrade-required": "platform-upgrade",
|
|
138
|
+
"drifted": "inspect-platform-drift",
|
|
139
|
+
"blocked": "prepare-host-prerequisites",
|
|
140
|
+
"blocked-port-80": "identify-existing-port-80-owner",
|
|
141
|
+
"incompatible": "manual-platform-recovery",
|
|
142
|
+
"ready": "none",
|
|
143
|
+
}
|
|
144
|
+
status["nextAction"] = next_actions.get(status["state"], "manual-review")
|
|
145
|
+
status["mutated"] = False
|
|
146
|
+
return status
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _snapshot(status: dict[str, Any]) -> str:
|
|
150
|
+
selected = {key: status[key] for key in sorted(_EXPECTED_KEYS)}
|
|
151
|
+
return hashlib.sha256(json.dumps(selected, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def plan_platform_change(name: str, action: str) -> dict[str, Any]:
|
|
155
|
+
if action not in {"install", "upgrade"}:
|
|
156
|
+
raise DeploymentKitError("Platform change action must be install or upgrade.")
|
|
157
|
+
status = platform_status(name)
|
|
158
|
+
expected_state = "installable" if action == "install" else "upgrade-required"
|
|
159
|
+
if status["state"] != expected_state:
|
|
160
|
+
raise DeploymentKitError(
|
|
161
|
+
f"Platform {action} requires state '{expected_state}', but target '{name}' is '{status['state']}'."
|
|
162
|
+
)
|
|
163
|
+
return {
|
|
164
|
+
"targetName": name,
|
|
165
|
+
"action": action,
|
|
166
|
+
"fromState": status["state"],
|
|
167
|
+
"toPlatformVersion": PLATFORM_VERSION,
|
|
168
|
+
"distribution": PLATFORM_DISTRIBUTION,
|
|
169
|
+
"platformRoot": PLATFORM_ROOT,
|
|
170
|
+
"preserves": ["route-conf.d", "route-registry", "caddy-data", "caddy-config", "shared-network"],
|
|
171
|
+
"doesNotOwn": ["application-containers", "application-data", "application-routes"],
|
|
172
|
+
"statusSnapshot": _snapshot(status),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def apply_platform_change(plan: dict[str, Any]) -> dict[str, Any]:
|
|
177
|
+
required = {
|
|
178
|
+
"targetName",
|
|
179
|
+
"action",
|
|
180
|
+
"fromState",
|
|
181
|
+
"toPlatformVersion",
|
|
182
|
+
"distribution",
|
|
183
|
+
"platformRoot",
|
|
184
|
+
"preserves",
|
|
185
|
+
"doesNotOwn",
|
|
186
|
+
"statusSnapshot",
|
|
187
|
+
}
|
|
188
|
+
if not isinstance(plan, dict) or set(plan) != required:
|
|
189
|
+
raise DeploymentKitError("Malformed host-platform change plan.")
|
|
190
|
+
name = plan["targetName"]
|
|
191
|
+
action = plan["action"]
|
|
192
|
+
if action not in {"install", "upgrade"}:
|
|
193
|
+
raise DeploymentKitError("Malformed host-platform change action.")
|
|
194
|
+
current = platform_status(name)
|
|
195
|
+
if _snapshot(current) != plan["statusSnapshot"]:
|
|
196
|
+
raise DeploymentKitError(
|
|
197
|
+
"Host-platform state changed after the plan was reviewed; create and authorize a fresh plan."
|
|
198
|
+
)
|
|
199
|
+
profile = load_profile(name)
|
|
200
|
+
evidence = verify_access(profile)
|
|
201
|
+
_run_remote(profile, evidence["address"], action, sudo=True)
|
|
202
|
+
final = platform_status(name)
|
|
203
|
+
if final["state"] != "ready":
|
|
204
|
+
raise DeploymentKitError(
|
|
205
|
+
f"Host-platform {action} completed but readiness is '{final['state']}', not 'ready'."
|
|
206
|
+
)
|
|
207
|
+
final["mutated"] = True
|
|
208
|
+
return final
|
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
"""Static shared-box platform assets and contract constants."""
|
|
2
|
-
|
|
3
|
-
PLATFORM_SCHEMA_VERSION = 1
|
|
4
|
-
PLATFORM_VERSION = "2"
|
|
5
|
-
PLATFORM_DISTRIBUTION = "trusted-lan-ubuntu-compose"
|
|
6
|
-
PLATFORM_ROOT = "/srv/lean-poc-platform"
|
|
7
|
-
NETWORK_NAME = "caddy-net"
|
|
8
|
-
CADDY_CONTAINER = "lean-poc-caddy"
|
|
9
|
-
CADDY_IMAGE = "caddy:2.8.4-alpine@sha256:af32e97399febea808609119bb21544d0265c58a02836576e32a2d082c262c17"
|
|
10
|
-
HOST_PLATFORM_WRITE_PHRASE = "[HOST PLATFORM WRITE]"
|
|
11
|
-
|
|
12
|
-
COMPOSE_YAML = f"""services:
|
|
13
|
-
caddy:
|
|
14
|
-
image: {CADDY_IMAGE}
|
|
15
|
-
container_name: {CADDY_CONTAINER}
|
|
16
|
-
restart: unless-stopped
|
|
17
|
-
ports:
|
|
18
|
-
- \"80:80\"
|
|
19
|
-
volumes:
|
|
20
|
-
- {PLATFORM_ROOT}/proxy/Caddyfile:/etc/caddy/Caddyfile:ro
|
|
21
|
-
- {PLATFORM_ROOT}/proxy/conf.d:/etc/caddy/conf.d:ro
|
|
22
|
-
- {PLATFORM_ROOT}/proxy/data:/data
|
|
23
|
-
- {PLATFORM_ROOT}/proxy/config:/config
|
|
24
|
-
networks:
|
|
25
|
-
- {NETWORK_NAME}
|
|
26
|
-
logging:
|
|
27
|
-
driver: json-file
|
|
28
|
-
options:
|
|
29
|
-
max-size: \"10m\"
|
|
30
|
-
max-file: \"3\"
|
|
31
|
-
|
|
32
|
-
networks:
|
|
33
|
-
{NETWORK_NAME}:
|
|
34
|
-
external: true
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
CADDYFILE = """{
|
|
38
|
-
auto_https off
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
import /etc/caddy/conf.d/*.caddy
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
PUBLISH_ALIAS_SH = """#!/usr/bin/env bash
|
|
45
|
-
set -Eeuo pipefail
|
|
46
|
-
|
|
47
|
-
: \"${ALIAS_HOSTNAME:?ALIAS_HOSTNAME is required}\"
|
|
48
|
-
|
|
49
|
-
current_ip() {
|
|
50
|
-
local ip
|
|
51
|
-
ip=\"$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\") print $(i+1)}')\"
|
|
52
|
-
if [[ -z \"$ip\" ]]; then
|
|
53
|
-
ip=\"$(ip -4 addr show scope global | grep -v 'docker' | grep -v 'br-' | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1)\"
|
|
54
|
-
fi
|
|
55
|
-
printf '%s\\n' \"$ip\"
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
CURRENT_IP=\"$(current_ip)\"
|
|
59
|
-
if [[ -z \"$CURRENT_IP\" ]]; then
|
|
60
|
-
echo \"lean-poc alias publisher: could not determine host LAN IPv4 address.\" >&2
|
|
61
|
-
exit 1
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
/usr/bin/avahi-publish -a -R \"$ALIAS_HOSTNAME\" \"$CURRENT_IP\" &
|
|
65
|
-
AVAHI_PID=$!
|
|
66
|
-
|
|
67
|
-
cleanup() {
|
|
68
|
-
kill \"$AVAHI_PID\" 2>/dev/null || true
|
|
69
|
-
wait \"$AVAHI_PID\" 2>/dev/null || true
|
|
70
|
-
}
|
|
71
|
-
trap cleanup SIGTERM SIGINT EXIT
|
|
72
|
-
|
|
73
|
-
while kill -0 \"$AVAHI_PID\" 2>/dev/null; do
|
|
74
|
-
sleep 10
|
|
75
|
-
NEW_IP=\"$(current_ip)\"
|
|
76
|
-
if [[ -n \"$NEW_IP\" && \"$NEW_IP\" != \"$CURRENT_IP\" ]]; then
|
|
77
|
-
cleanup
|
|
78
|
-
exit 0
|
|
79
|
-
fi
|
|
80
|
-
done
|
|
81
|
-
wait \"$AVAHI_PID\"
|
|
82
|
-
"""
|
|
83
|
-
|
|
84
|
-
AVAHI_SERVICE = """[Unit]
|
|
85
|
-
Description=Lean POC mDNS alias %i
|
|
86
|
-
After=network-online.target avahi-daemon.service
|
|
87
|
-
Wants=network-online.target
|
|
88
|
-
Requires=avahi-daemon.service
|
|
89
|
-
|
|
90
|
-
[Service]
|
|
91
|
-
Type=simple
|
|
92
|
-
EnvironmentFile=/etc/lean-poc-platform/aliases/%i.env
|
|
93
|
-
ExecStart=/usr/local/lib/lean-poc-platform/publish-alias.sh
|
|
94
|
-
Restart=always
|
|
95
|
-
RestartSec=2
|
|
96
|
-
NoNewPrivileges=true
|
|
97
|
-
|
|
98
|
-
[Install]
|
|
99
|
-
WantedBy=multi-user.target
|
|
100
|
-
"""
|
|
1
|
+
"""Static shared-box platform assets and contract constants."""
|
|
2
|
+
|
|
3
|
+
PLATFORM_SCHEMA_VERSION = 1
|
|
4
|
+
PLATFORM_VERSION = "2"
|
|
5
|
+
PLATFORM_DISTRIBUTION = "trusted-lan-ubuntu-compose"
|
|
6
|
+
PLATFORM_ROOT = "/srv/lean-poc-platform"
|
|
7
|
+
NETWORK_NAME = "caddy-net"
|
|
8
|
+
CADDY_CONTAINER = "lean-poc-caddy"
|
|
9
|
+
CADDY_IMAGE = "caddy:2.8.4-alpine@sha256:af32e97399febea808609119bb21544d0265c58a02836576e32a2d082c262c17"
|
|
10
|
+
HOST_PLATFORM_WRITE_PHRASE = "[HOST PLATFORM WRITE]"
|
|
11
|
+
|
|
12
|
+
COMPOSE_YAML = f"""services:
|
|
13
|
+
caddy:
|
|
14
|
+
image: {CADDY_IMAGE}
|
|
15
|
+
container_name: {CADDY_CONTAINER}
|
|
16
|
+
restart: unless-stopped
|
|
17
|
+
ports:
|
|
18
|
+
- \"80:80\"
|
|
19
|
+
volumes:
|
|
20
|
+
- {PLATFORM_ROOT}/proxy/Caddyfile:/etc/caddy/Caddyfile:ro
|
|
21
|
+
- {PLATFORM_ROOT}/proxy/conf.d:/etc/caddy/conf.d:ro
|
|
22
|
+
- {PLATFORM_ROOT}/proxy/data:/data
|
|
23
|
+
- {PLATFORM_ROOT}/proxy/config:/config
|
|
24
|
+
networks:
|
|
25
|
+
- {NETWORK_NAME}
|
|
26
|
+
logging:
|
|
27
|
+
driver: json-file
|
|
28
|
+
options:
|
|
29
|
+
max-size: \"10m\"
|
|
30
|
+
max-file: \"3\"
|
|
31
|
+
|
|
32
|
+
networks:
|
|
33
|
+
{NETWORK_NAME}:
|
|
34
|
+
external: true
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
CADDYFILE = """{
|
|
38
|
+
auto_https off
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
import /etc/caddy/conf.d/*.caddy
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
PUBLISH_ALIAS_SH = """#!/usr/bin/env bash
|
|
45
|
+
set -Eeuo pipefail
|
|
46
|
+
|
|
47
|
+
: \"${ALIAS_HOSTNAME:?ALIAS_HOSTNAME is required}\"
|
|
48
|
+
|
|
49
|
+
current_ip() {
|
|
50
|
+
local ip
|
|
51
|
+
ip=\"$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\") print $(i+1)}')\"
|
|
52
|
+
if [[ -z \"$ip\" ]]; then
|
|
53
|
+
ip=\"$(ip -4 addr show scope global | grep -v 'docker' | grep -v 'br-' | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1)\"
|
|
54
|
+
fi
|
|
55
|
+
printf '%s\\n' \"$ip\"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
CURRENT_IP=\"$(current_ip)\"
|
|
59
|
+
if [[ -z \"$CURRENT_IP\" ]]; then
|
|
60
|
+
echo \"lean-poc alias publisher: could not determine host LAN IPv4 address.\" >&2
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
/usr/bin/avahi-publish -a -R \"$ALIAS_HOSTNAME\" \"$CURRENT_IP\" &
|
|
65
|
+
AVAHI_PID=$!
|
|
66
|
+
|
|
67
|
+
cleanup() {
|
|
68
|
+
kill \"$AVAHI_PID\" 2>/dev/null || true
|
|
69
|
+
wait \"$AVAHI_PID\" 2>/dev/null || true
|
|
70
|
+
}
|
|
71
|
+
trap cleanup SIGTERM SIGINT EXIT
|
|
72
|
+
|
|
73
|
+
while kill -0 \"$AVAHI_PID\" 2>/dev/null; do
|
|
74
|
+
sleep 10
|
|
75
|
+
NEW_IP=\"$(current_ip)\"
|
|
76
|
+
if [[ -n \"$NEW_IP\" && \"$NEW_IP\" != \"$CURRENT_IP\" ]]; then
|
|
77
|
+
cleanup
|
|
78
|
+
exit 0
|
|
79
|
+
fi
|
|
80
|
+
done
|
|
81
|
+
wait \"$AVAHI_PID\"
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
AVAHI_SERVICE = """[Unit]
|
|
85
|
+
Description=Lean POC mDNS alias %i
|
|
86
|
+
After=network-online.target avahi-daemon.service
|
|
87
|
+
Wants=network-online.target
|
|
88
|
+
Requires=avahi-daemon.service
|
|
89
|
+
|
|
90
|
+
[Service]
|
|
91
|
+
Type=simple
|
|
92
|
+
EnvironmentFile=/etc/lean-poc-platform/aliases/%i.env
|
|
93
|
+
ExecStart=/usr/local/lib/lean-poc-platform/publish-alias.sh
|
|
94
|
+
Restart=always
|
|
95
|
+
RestartSec=2
|
|
96
|
+
NoNewPrivileges=true
|
|
97
|
+
|
|
98
|
+
[Install]
|
|
99
|
+
WantedBy=multi-user.target
|
|
100
|
+
"""
|