jailbee 1.0.0__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.
- jailbee/__init__.py +12 -0
- jailbee/__main__.py +8 -0
- jailbee/apply.py +400 -0
- jailbee/autostart.py +324 -0
- jailbee/background.py +299 -0
- jailbee/branch_config.py +425 -0
- jailbee/chrome_pool.py +336 -0
- jailbee/claude_skills.py +63 -0
- jailbee/cli.py +6170 -0
- jailbee/completion.py +268 -0
- jailbee/config.py +2146 -0
- jailbee/config_init.py +357 -0
- jailbee/dashboard.py +740 -0
- jailbee/db/__init__.py +130 -0
- jailbee/db/gui_state.py +55 -0
- jailbee/db/models.py +143 -0
- jailbee/destroy_guard.py +189 -0
- jailbee/device_groups.py +86 -0
- jailbee/docker_daemon.py +131 -0
- jailbee/doctor.py +648 -0
- jailbee/egress.py +160 -0
- jailbee/egress_pool.py +533 -0
- jailbee/entry.py +22 -0
- jailbee/git.py +938 -0
- jailbee/git_status.py +494 -0
- jailbee/global_config.py +178 -0
- jailbee/golden.py +393 -0
- jailbee/gui.py +166 -0
- jailbee/hosts.py +177 -0
- jailbee/incus.py +597 -0
- jailbee/init_command.py +365 -0
- jailbee/jobs.py +169 -0
- jailbee/lifecycle.py +2091 -0
- jailbee/loose_revert.py +148 -0
- jailbee/macos.py +191 -0
- jailbee/maintenance.py +155 -0
- jailbee/migrate.py +818 -0
- jailbee/mounts.py +29 -0
- jailbee/network.py +199 -0
- jailbee/paths.py +88 -0
- jailbee/pr.py +474 -0
- jailbee/pr_ai.py +267 -0
- jailbee/profiles.py +353 -0
- jailbee/provision/__init__.py +0 -0
- jailbee/provision/ensure-claude.sh +65 -0
- jailbee/provision/install.d/05-extra-apt.sh +14 -0
- jailbee/provision/install.d/10-locale.sh +11 -0
- jailbee/provision/install.d/15-prompt.sh +39 -0
- jailbee/provision/install.d/60-gui-libs.sh +25 -0
- jailbee/provision/install.d/75-github-cli.sh +23 -0
- jailbee/provision/install.d.available/20-corretto.sh +16 -0
- jailbee/provision/install.d.available/20-openjdk.sh +11 -0
- jailbee/provision/install.d.available/30-nodejs.sh +38 -0
- jailbee/provision/install.d.available/40-python.sh +13 -0
- jailbee/provision/install.d.available/50-docker.sh +47 -0
- jailbee/provision/install.d.available/80-ecr-helper.sh +10 -0
- jailbee/provision/install.d.available/90-registry-mirror-ca.sh +16 -0
- jailbee/provision/install.sh +179 -0
- jailbee/provision/registry-mirror/install.sh +43 -0
- jailbee/provision/registry-mirror/jailbee-registry-proxy.container +18 -0
- jailbee/qtui/__init__.py +1 -0
- jailbee/qtui/actions.py +96 -0
- jailbee/qtui/app.py +404 -0
- jailbee/qtui/cards.py +469 -0
- jailbee/qtui/flow_layout.py +89 -0
- jailbee/qtui/model.py +167 -0
- jailbee/qtui/refresh.py +129 -0
- jailbee/qtui/terminal.py +71 -0
- jailbee/qtui/window.py +299 -0
- jailbee/registry.py +602 -0
- jailbee/retry.py +113 -0
- jailbee/runtime_mounts.py +195 -0
- jailbee/skills/jailbee-repo-setup/SKILL.md +309 -0
- jailbee/skills/jailbee-repo-setup/references/config-schema.md +626 -0
- jailbee/skills/jailbee-usage/SKILL.md +463 -0
- jailbee/skills/jailbee-usage/references/commands.md +483 -0
- jailbee/snapshots.py +37 -0
- jailbee/ssh_seed.py +52 -0
- jailbee/submodules.py +851 -0
- jailbee/sync.py +2513 -0
- jailbee/table_format.py +196 -0
- jailbee/templates/__init__.py +1 -0
- jailbee/templates/systemd/__init__.py +1 -0
- jailbee/templates/systemd/jailbee-net-refresh.service +10 -0
- jailbee/templates/systemd/jailbee-net-refresh.timer +12 -0
- jailbee/tmux.py +242 -0
- jailbee/tui.py +573 -0
- jailbee-1.0.0.dist-info/METADATA +179 -0
- jailbee-1.0.0.dist-info/RECORD +92 -0
- jailbee-1.0.0.dist-info/WHEEL +4 -0
- jailbee-1.0.0.dist-info/entry_points.txt +4 -0
- jailbee-1.0.0.dist-info/licenses/LICENSE +674 -0
jailbee/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""jailbee — manage isolated dev environments using Incus.
|
|
2
|
+
|
|
3
|
+
Originally written for the GISGRO codebase; now generic — every repo
|
|
4
|
+
provides its own ``.jailbee/config.yaml``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
__version__ = version("jailbee")
|
|
11
|
+
except PackageNotFoundError: # running from a source tree without an install
|
|
12
|
+
__version__ = "0.0.0+unknown"
|
jailbee/__main__.py
ADDED
jailbee/apply.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""`jailbee apply` — re-apply config to profiles, ACL, and live container state.
|
|
2
|
+
|
|
3
|
+
Orchestrates the steps that take a repo from "config edited" to "every
|
|
4
|
+
container reflects the new config", or makes clear which manual step is
|
|
5
|
+
needed. Replaces `jailbee init --reapply` and `jailbee net refresh`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
from sqlmodel import Session
|
|
16
|
+
|
|
17
|
+
from jailbee.config import Config
|
|
18
|
+
from jailbee.db import get_engine
|
|
19
|
+
from jailbee.egress_pool import refresh_pool, register_repo
|
|
20
|
+
from jailbee.global_config import GlobalConfig
|
|
21
|
+
from jailbee.incus import Incus
|
|
22
|
+
from jailbee.profiles import (
|
|
23
|
+
base_profile_yaml,
|
|
24
|
+
binds_profile_yaml,
|
|
25
|
+
net_profile_yaml,
|
|
26
|
+
profile_names,
|
|
27
|
+
)
|
|
28
|
+
from jailbee.tui import ConfirmFn, default_confirm
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from jailbee.lifecycle import ContainerInfo
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class ApplyResult:
|
|
36
|
+
profiles_changed: list[str]
|
|
37
|
+
profiles_unchanged: list[str]
|
|
38
|
+
acl_changed: bool
|
|
39
|
+
hosts_repinned: list[str]
|
|
40
|
+
docker_proxy_reapplied: list[str]
|
|
41
|
+
restarted: list[str]
|
|
42
|
+
restart_failures: list[tuple[str, str]]
|
|
43
|
+
# Containers moved off the removed `<prefix>-net-offline` profile by
|
|
44
|
+
# this run. Empty on every apply after the first one.
|
|
45
|
+
offline_migrated: list[str] = field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def fully_successful(self) -> bool:
|
|
49
|
+
return not self.restart_failures
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _profile_differs(incus: Incus, name: str, new_yaml: str) -> bool:
|
|
53
|
+
"""True if the rendered YAML differs semantically from Incus's stored profile.
|
|
54
|
+
|
|
55
|
+
Compares ``config`` and ``devices`` keys as parsed dicts. Ignores any
|
|
56
|
+
other keys Incus surfaces in `profile show` (e.g. project, used_by).
|
|
57
|
+
"""
|
|
58
|
+
return _yaml_subset_differs(incus.profile_show(name), new_yaml, keys=("config", "devices"))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _acl_differs(incus: Incus, name: str, new_yaml: str) -> bool:
|
|
62
|
+
"""True if the rendered ACL YAML differs from Incus's stored ACL."""
|
|
63
|
+
return _yaml_subset_differs(
|
|
64
|
+
incus.network_acl_show(name),
|
|
65
|
+
new_yaml,
|
|
66
|
+
keys=("egress", "ingress", "config"),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _yaml_subset_differs(existing: str, candidate: str, *, keys: tuple[str, ...]) -> bool:
|
|
71
|
+
existing_parsed = yaml.safe_load(existing) or {}
|
|
72
|
+
candidate_parsed = yaml.safe_load(candidate) or {}
|
|
73
|
+
for key in keys:
|
|
74
|
+
if existing_parsed.get(key) != candidate_parsed.get(key):
|
|
75
|
+
return True
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# The `offline` network mode was removed. Containers created before that
|
|
80
|
+
# still carry `<prefix>-net-offline`; `jailbee apply` moves them to strict and
|
|
81
|
+
# deletes the profile. Spelled as a suffix rather than read from
|
|
82
|
+
# `ProfileNames` because the attribute no longer exists.
|
|
83
|
+
_STALE_NET_OFFLINE_SUFFIX = "-net-offline"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _drop_offline_net_profile(cfg: Config, incus: Incus) -> list[str]:
|
|
87
|
+
"""Move any container off the removed offline net profile, then delete it.
|
|
88
|
+
|
|
89
|
+
Returns the migrated container names.
|
|
90
|
+
|
|
91
|
+
Deliberately not via ``switch_network``: that helper requires a
|
|
92
|
+
*recognised* net profile to be attached, and `<prefix>-net-offline` is no
|
|
93
|
+
longer in ``net_by_mode`` — it would raise "has no network profile
|
|
94
|
+
attached — cannot switch" on exactly the containers being migrated.
|
|
95
|
+
|
|
96
|
+
``/etc/hosts`` is not re-pinned here either. The migrated containers then
|
|
97
|
+
report ``network == "strict"``, so ``run_apply``'s running-container sweep
|
|
98
|
+
re-pins them a few lines further down.
|
|
99
|
+
"""
|
|
100
|
+
from jailbee.incus import IncusError
|
|
101
|
+
from jailbee.tui import info, warn
|
|
102
|
+
|
|
103
|
+
names = profile_names(cfg)
|
|
104
|
+
stale = f"{cfg.container_prefix}{_STALE_NET_OFFLINE_SUFFIX}"
|
|
105
|
+
|
|
106
|
+
migrated: list[str] = []
|
|
107
|
+
for raw in incus.list_containers():
|
|
108
|
+
# A container mid-destroy can be reported with "profiles": null,
|
|
109
|
+
# which bypasses a `.get(..., [])` default (key present, value None).
|
|
110
|
+
profiles = raw.get("profiles") or []
|
|
111
|
+
if stale not in profiles:
|
|
112
|
+
continue
|
|
113
|
+
info(f" Migrating {raw['name']} off {stale} → {names.net_strict}...")
|
|
114
|
+
incus.profile_assign(
|
|
115
|
+
raw["name"],
|
|
116
|
+
[names.net_strict if p == stale else p for p in profiles],
|
|
117
|
+
)
|
|
118
|
+
migrated.append(raw["name"])
|
|
119
|
+
|
|
120
|
+
if incus.profile_exists(stale):
|
|
121
|
+
try:
|
|
122
|
+
incus.profile_delete(stale)
|
|
123
|
+
info(f" Deleted stale profile {stale}")
|
|
124
|
+
except IncusError as e:
|
|
125
|
+
warn(f"Could not delete stale profile {stale}: {e}")
|
|
126
|
+
|
|
127
|
+
return migrated
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def run_apply(
|
|
131
|
+
cfg: Config,
|
|
132
|
+
incus: Incus,
|
|
133
|
+
gcfg: GlobalConfig,
|
|
134
|
+
*,
|
|
135
|
+
assume_yes: bool = False,
|
|
136
|
+
no_restart: bool = False,
|
|
137
|
+
confirm_fn: ConfirmFn | None = None,
|
|
138
|
+
) -> ApplyResult:
|
|
139
|
+
"""Apply current config to profiles, ACL, and live container state."""
|
|
140
|
+
from jailbee.lifecycle import short_name
|
|
141
|
+
from jailbee.tui import info
|
|
142
|
+
|
|
143
|
+
info("Applying configuration...")
|
|
144
|
+
|
|
145
|
+
# Resolve everything that could fail outside Incus before we mutate
|
|
146
|
+
# anything. Either of these raises and apply aborts cleanly with no
|
|
147
|
+
# partial profile / ACL update.
|
|
148
|
+
info("Refreshing egress pool + ACL + /etc/hosts...")
|
|
149
|
+
mirror_endpoint = _compute_mirror_endpoint_or_abort(incus, gcfg)
|
|
150
|
+
mirror_ca_pem = _read_mirror_ca_or_warn(gcfg) if mirror_endpoint else None
|
|
151
|
+
|
|
152
|
+
with Session(get_engine()) as session:
|
|
153
|
+
register_repo(session, cfg)
|
|
154
|
+
refresh_result = refresh_pool(
|
|
155
|
+
cfg,
|
|
156
|
+
gcfg,
|
|
157
|
+
incus,
|
|
158
|
+
session,
|
|
159
|
+
now=datetime.now(UTC),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if refresh_result.status == "dns_error":
|
|
163
|
+
from jailbee.egress import NetworkResolveError
|
|
164
|
+
|
|
165
|
+
raise NetworkResolveError(
|
|
166
|
+
"egress refresh",
|
|
167
|
+
Exception(refresh_result.error or "DNS failure"),
|
|
168
|
+
)
|
|
169
|
+
if refresh_result.status == "acl_error":
|
|
170
|
+
from jailbee.incus import IncusError
|
|
171
|
+
|
|
172
|
+
raise IncusError(refresh_result.error or "ACL write failed")
|
|
173
|
+
if refresh_result.status == "partial":
|
|
174
|
+
from jailbee.tui import warn
|
|
175
|
+
|
|
176
|
+
warn(f"Some hostnames failed to resolve: {refresh_result.error}")
|
|
177
|
+
|
|
178
|
+
if refresh_result.added or refresh_result.removed:
|
|
179
|
+
info(f" Pool: +{len(refresh_result.added)} added, -{len(refresh_result.removed)} evicted")
|
|
180
|
+
|
|
181
|
+
# refresh_pool wrote the ACL whenever there was anything to resolve.
|
|
182
|
+
# The added/removed counters are the closest "did anything change"
|
|
183
|
+
# signal we have from the new pipeline.
|
|
184
|
+
acl_changed_flag = bool(refresh_result.added or refresh_result.removed)
|
|
185
|
+
|
|
186
|
+
# Ensure shared-dir tree exists. `jailbee init` would have created it,
|
|
187
|
+
# but a user enabling claude/jetbrains after the initial init (or a repo
|
|
188
|
+
# initialised before a given integration's mounts were added) needs the
|
|
189
|
+
# integration subdirs to exist before the binds profile mounts them —
|
|
190
|
+
# otherwise Incus rejects the profile edit/assign with "Missing source
|
|
191
|
+
# path ...". `_ensure_integration_shared_dirs` is the shared source of
|
|
192
|
+
# truth with `jailbee init` so the two can't drift. Idempotent — cheap to re-run.
|
|
193
|
+
from jailbee.init_command import (
|
|
194
|
+
_ensure_integration_shared_dirs,
|
|
195
|
+
_ensure_shared_dirs,
|
|
196
|
+
_ensure_user_shared_dirs,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
assert cfg.shared_dir is not None # set by load_config
|
|
200
|
+
_ensure_shared_dirs(cfg.shared_dir)
|
|
201
|
+
_ensure_user_shared_dirs(cfg)
|
|
202
|
+
_ensure_integration_shared_dirs(cfg)
|
|
203
|
+
|
|
204
|
+
# Refresh jailbee's bundled skills in the shared ~/.claude/skills so existing
|
|
205
|
+
# containers pick up a newer jailbee without recreation. Non-fatal.
|
|
206
|
+
from jailbee.claude_skills import sync_jailbee_skills
|
|
207
|
+
from jailbee.tui import warn
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
sync_jailbee_skills(cfg)
|
|
211
|
+
except Exception as e: # non-fatal
|
|
212
|
+
warn(f"jailbee-skills sync failed (continuing): {e}")
|
|
213
|
+
|
|
214
|
+
names = profile_names(cfg)
|
|
215
|
+
profile_yamls = {
|
|
216
|
+
names.base: base_profile_yaml(cfg),
|
|
217
|
+
names.binds: binds_profile_yaml(cfg),
|
|
218
|
+
names.net_strict: net_profile_yaml(cfg, "strict"),
|
|
219
|
+
names.net_loose: net_profile_yaml(cfg, "loose"),
|
|
220
|
+
}
|
|
221
|
+
offline_migrated = _drop_offline_net_profile(cfg, incus)
|
|
222
|
+
|
|
223
|
+
info("Checking profiles...")
|
|
224
|
+
profiles_changed: list[str] = []
|
|
225
|
+
profiles_unchanged: list[str] = []
|
|
226
|
+
for name, new_yaml in profile_yamls.items():
|
|
227
|
+
if _profile_differs(incus, name, new_yaml):
|
|
228
|
+
info(f" Updating profile {name}...")
|
|
229
|
+
incus.profile_set_yaml(name, new_yaml)
|
|
230
|
+
profiles_changed.append(name)
|
|
231
|
+
else:
|
|
232
|
+
profiles_unchanged.append(name)
|
|
233
|
+
|
|
234
|
+
_ensure_acl_attached_to_bridge(cfg, incus)
|
|
235
|
+
|
|
236
|
+
# Push the repo's extra upstream registries into the mirror once,
|
|
237
|
+
# before re-applying per-container dockerd proxy. apply_mirror_registries
|
|
238
|
+
# is idempotent and a no-op when the list is empty or already covered.
|
|
239
|
+
if mirror_endpoint is not None and cfg.docker_registry_mirror.extra_registries:
|
|
240
|
+
info("Syncing extra registries into mirror...")
|
|
241
|
+
from jailbee.registry import apply_mirror_registries
|
|
242
|
+
|
|
243
|
+
apply_mirror_registries(incus, cfg.docker_registry_mirror.extra_registries)
|
|
244
|
+
|
|
245
|
+
info("Listing running containers...")
|
|
246
|
+
hosts_repinned: list[str] = []
|
|
247
|
+
docker_proxy_reapplied: list[str] = []
|
|
248
|
+
mirror_port = mirror_endpoint[1] if mirror_endpoint else None
|
|
249
|
+
|
|
250
|
+
running_names: list[str] = []
|
|
251
|
+
for ci in _list_containers(cfg, incus):
|
|
252
|
+
if ci.state != "Running":
|
|
253
|
+
continue
|
|
254
|
+
running_names.append(ci.name)
|
|
255
|
+
short = short_name(cfg, ci.name)
|
|
256
|
+
if cfg.host_devices:
|
|
257
|
+
# Re-ensure dev is in each host_devices group (e.g. kvm). Idempotent;
|
|
258
|
+
# picks up entries added since the container was created. New shells
|
|
259
|
+
# see the group; an already-open `jailbee shell` must be reopened.
|
|
260
|
+
from jailbee.device_groups import ensure_device_groups
|
|
261
|
+
|
|
262
|
+
ensure_device_groups(cfg, incus, ci.name)
|
|
263
|
+
if ci.network == "strict":
|
|
264
|
+
info(f" Re-pinning /etc/hosts on {short}...")
|
|
265
|
+
from jailbee.hosts import apply_hosts
|
|
266
|
+
|
|
267
|
+
apply_hosts(cfg, incus, ci.name, mirror_endpoint=mirror_endpoint)
|
|
268
|
+
hosts_repinned.append(ci.name)
|
|
269
|
+
if mirror_endpoint is not None and mirror_ca_pem is not None and mirror_port is not None:
|
|
270
|
+
info(f" Re-applying dockerd HTTPS_PROXY on {short}...")
|
|
271
|
+
from jailbee.docker_daemon import apply_docker_proxy
|
|
272
|
+
|
|
273
|
+
# apply_docker_proxy reads the mirror endpoint via MIRROR_DNS_NAME
|
|
274
|
+
# internally; we only need to pass CA + port.
|
|
275
|
+
apply_docker_proxy(incus, ci.name, mirror_ca_pem, mirror_port)
|
|
276
|
+
docker_proxy_reapplied.append(ci.name)
|
|
277
|
+
|
|
278
|
+
restarted: list[str] = []
|
|
279
|
+
restart_failures: list[tuple[str, str]] = []
|
|
280
|
+
should_restart = bool(profiles_changed) and bool(running_names) and not no_restart
|
|
281
|
+
if should_restart and not assume_yes:
|
|
282
|
+
prompt = (
|
|
283
|
+
f"\n{len(running_names)} running container(s) need restart "
|
|
284
|
+
f"to pick up profile changes:\n "
|
|
285
|
+
f"{', '.join(short_name(cfg, n) for n in running_names)}\nRestart now?"
|
|
286
|
+
)
|
|
287
|
+
fn = confirm_fn or default_confirm
|
|
288
|
+
if not fn(prompt):
|
|
289
|
+
should_restart = False
|
|
290
|
+
|
|
291
|
+
if should_restart:
|
|
292
|
+
for name in running_names:
|
|
293
|
+
info(f" Restarting {short_name(cfg, name)}...")
|
|
294
|
+
try:
|
|
295
|
+
_restart_one(cfg, incus, name, mirror_endpoint=mirror_endpoint)
|
|
296
|
+
restarted.append(name)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
restart_failures.append((name, str(e)))
|
|
299
|
+
|
|
300
|
+
return ApplyResult(
|
|
301
|
+
profiles_changed=profiles_changed,
|
|
302
|
+
profiles_unchanged=profiles_unchanged,
|
|
303
|
+
acl_changed=acl_changed_flag,
|
|
304
|
+
hosts_repinned=hosts_repinned,
|
|
305
|
+
docker_proxy_reapplied=docker_proxy_reapplied,
|
|
306
|
+
restarted=restarted,
|
|
307
|
+
restart_failures=restart_failures,
|
|
308
|
+
offline_migrated=offline_migrated,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _apply_acl_with_nft_quirk(incus: Incus, name: str, acl_yaml: str) -> None:
|
|
313
|
+
"""Thin delegator to egress_pool._apply_acl_with_nft_quirk.
|
|
314
|
+
|
|
315
|
+
Logic was moved to egress_pool so the refresh timer and `jailbee apply`
|
|
316
|
+
share one nft-quirk-aware ACL writer. This wrapper stays for the
|
|
317
|
+
apply.py code path's clarity (and for tests that may want to patch
|
|
318
|
+
on apply rather than egress_pool).
|
|
319
|
+
"""
|
|
320
|
+
from jailbee.egress_pool import _apply_acl_with_nft_quirk as impl
|
|
321
|
+
|
|
322
|
+
impl(incus, name, acl_yaml)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _ensure_acl_attached_to_bridge(cfg: Config, incus: Incus) -> None:
|
|
326
|
+
"""Idempotent. Wraps ``init_command.ensure_acl_attached_to_bridge``."""
|
|
327
|
+
from jailbee.init_command import ensure_acl_attached_to_bridge
|
|
328
|
+
|
|
329
|
+
ensure_acl_attached_to_bridge(cfg, incus)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _compute_mirror_endpoint_or_abort(incus: Incus, gcfg: GlobalConfig) -> tuple[str, int] | None:
|
|
333
|
+
"""Resolve mirror endpoint, return None if disabled. Aborts on ValueError."""
|
|
334
|
+
if not gcfg.docker_registry_mirror.enabled:
|
|
335
|
+
return None
|
|
336
|
+
from jailbee.docker_daemon import compute_mirror_endpoint
|
|
337
|
+
|
|
338
|
+
return compute_mirror_endpoint(incus, gcfg)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _read_mirror_ca_or_warn(gcfg: GlobalConfig) -> str | None:
|
|
342
|
+
"""Return CA cert PEM string, or None if not present (with a warning)."""
|
|
343
|
+
from jailbee.tui import warn
|
|
344
|
+
|
|
345
|
+
ca_path = gcfg.docker_registry_mirror.data_dir / "ca" / "ca.crt"
|
|
346
|
+
if ca_path.is_file():
|
|
347
|
+
return ca_path.read_text()
|
|
348
|
+
warn(f"Mirror CA cert not found at {ca_path}; skipping per-container docker-proxy reapply.")
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _list_containers(cfg: Config, incus: Incus) -> list[ContainerInfo]:
|
|
353
|
+
"""Wrap ``lifecycle.list_containers`` so tests can patch one symbol."""
|
|
354
|
+
from jailbee.lifecycle import list_containers
|
|
355
|
+
|
|
356
|
+
return list_containers(cfg, incus)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _restart_one(
|
|
360
|
+
cfg: Config,
|
|
361
|
+
incus: Incus,
|
|
362
|
+
name: str,
|
|
363
|
+
*,
|
|
364
|
+
mirror_endpoint: tuple[str, int] | None = None,
|
|
365
|
+
) -> None:
|
|
366
|
+
"""Restart one container, re-pin /etc/hosts, and run on_start autostart.
|
|
367
|
+
|
|
368
|
+
Wrapper called only by ``run_apply``. The GUI auto-launch part of
|
|
369
|
+
``cli._post_start_actions`` is intentionally skipped — `jailbee apply`
|
|
370
|
+
should not pop up IDE/Chrome on every container — but the autostart
|
|
371
|
+
shell steps (docker daemon, services, etc.) MUST run, otherwise the
|
|
372
|
+
container comes back from restart with no services and is unusable.
|
|
373
|
+
"""
|
|
374
|
+
from jailbee.autostart import (
|
|
375
|
+
AutostartTrigger,
|
|
376
|
+
inject_github_token,
|
|
377
|
+
run_autostart,
|
|
378
|
+
)
|
|
379
|
+
from jailbee.hosts import apply_hosts
|
|
380
|
+
from jailbee.lifecycle import (
|
|
381
|
+
container_repo_dir,
|
|
382
|
+
current_network_mode,
|
|
383
|
+
restart_container,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
restart_container(cfg, incus, name)
|
|
387
|
+
if current_network_mode(cfg, incus, name) == "strict":
|
|
388
|
+
apply_hosts(cfg, incus, name, mirror_endpoint=mirror_endpoint)
|
|
389
|
+
repo_dir = container_repo_dir(cfg, incus, name)
|
|
390
|
+
# Re-inject GH_TOKEN (infrastructure, not a user autostart step) so a
|
|
391
|
+
# rotated PAT is picked up on `jailbee apply`.
|
|
392
|
+
inject_github_token(cfg, incus, name, repo_dir, mirror_endpoint=mirror_endpoint)
|
|
393
|
+
run_autostart(
|
|
394
|
+
cfg,
|
|
395
|
+
incus,
|
|
396
|
+
name,
|
|
397
|
+
AutostartTrigger.ON_START,
|
|
398
|
+
repo_dir=repo_dir,
|
|
399
|
+
mirror_endpoint=mirror_endpoint,
|
|
400
|
+
)
|