Skip to content

Commit 465214e

Browse files
committed
feat(madmail): print admin token and info, and introduce "--with-admin" option
1 parent e2f4c8e commit 465214e

5 files changed

Lines changed: 119 additions & 12 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ The `--source` argument controls where the code comes from:
6161

6262
cmlxc deploy-cmdeploy --source @main cm0
6363
cmlxc deploy-madmail --source @main mad1
64+
cmlxc deploy-madmail --source @main --with-webadmin mad1
6465
cmlxc deploy-madmail --source @main --ipv4-only mad1
6566

67+
6668
| Form | Meaning |
6769
|---------|---------|
6870
| `@ref` | Clone default remote at branch/tag `ref` |

src/cmlxc/cli.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
)
2222
from cmlxc.driver_base import __version__
2323
from cmlxc.driver_cmdeploy import CmdeployDriver
24-
from cmlxc.driver_madmail import MadmailDriver
24+
from cmlxc.driver_madmail import MadmailDriver, print_admin_info
2525
from cmlxc.incus import Incus, _is_ip_address, check_cgroup_compat
2626
from cmlxc.output import Out
2727

@@ -435,6 +435,9 @@ def _print_container_status(out, c, ix):
435435
detail_out.print(f"source: {source_ref}")
436436
detail_out.print(f" {status}")
437437
detail_out.print(f"builder: {repo_path}")
438+
if driver == "madmail":
439+
ct = ix.get_relay_container(cname)
440+
print_admin_info(detail_out, ct, ip)
438441

439442
elif cname == BUILDER_CONTAINER_NAME and is_running:
440443
_print_builder_repos(detail_out, BuilderContainer(ix))

src/cmlxc/container.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,6 @@ def setup_repo(self, dest, out, source):
488488
self.bash(f"rm -rf {dest}")
489489
out.print(f" Initial clone of {name} ...")
490490
self.bash(f"git clone {source.url} {dest}")
491-
self.bash(f"git config --global --add safe.directory {dest}")
492491

493492
def get_repo_status(self, repo_path):
494493
"""Return a one-line string describing the git repo at repo_path."""

src/cmlxc/driver_base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ def add_cli_options(cls, parser, completer=None):
153153
help="Create containers without IPv6 connectivity.",
154154
)
155155

156+
157+
def configure_from_args(self, args):
158+
"""Apply driver-specific CLI arguments. Override in subclasses."""
159+
156160
# ------------------------------------------------------------------
157161
# Deploy protocol (override in subclasses)
158162
# ------------------------------------------------------------------
@@ -191,6 +195,8 @@ def get_git_main_path(self):
191195
@classmethod
192196
def prep_builder(cls, ix, out, bld_ct):
193197
"""Hook called by ``cmlxc init`` to prepare toolchains and main checkout."""
198+
# Trust all repo paths inside the builder (ownership differs from host).
199+
bld_ct.bash("git config --global --add safe.directory '*'", check=False)
194200
tmp_dest = f"/root/{cls.REPO_NAME}-git-main"
195201
if bld_ct.bash(f"test -d {tmp_dest}", check=False) is None:
196202
source = parse_source("@main", cls.DEFAULT_SOURCE_URL)
@@ -275,6 +281,8 @@ def cmd(args, out):
275281
if not driver.check_local_source(source):
276282
return 1
277283

284+
driver.configure_from_args(args)
285+
278286
out.print(f"cmlxc {__version__}")
279287
with out.section(f"Preparing {cls.CLI_NAME} source in builder"):
280288
out.print(f" Source: {source.description}")

src/cmlxc/driver_madmail.py

Lines changed: 105 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@ class MadmailDriver(Driver):
2323
REPO_NAME = MADMAIL
2424
REQUIRED_SOURCE_PATHS = ["go.mod", "Makefile"]
2525

26+
@classmethod
27+
def add_cli_options(cls, parser, completer=None):
28+
"""Register madmail-specific deploy options."""
29+
super().add_cli_options(parser, completer=completer)
30+
parser.add_argument(
31+
"--with-webadmin",
32+
action="store_true",
33+
help=(
34+
"Build and enable the embedded admin web UI at /admin. "
35+
"Disabled by default."
36+
),
37+
)
38+
39+
def configure_from_args(self, args):
40+
self.with_admin = bool(args.with_webadmin)
41+
2642
@classmethod
2743
def on_prep_builder(cls, out, bld_ct, tmp_dest):
2844
"""Hook called by ``prep_builder`` to ensure the Go toolchain is ready."""
@@ -31,19 +47,56 @@ def on_prep_builder(cls, out, bld_ct, tmp_dest):
3147

3248
def on_init_relay(self, repo_path):
3349
"""Hook called by ``init_builder`` to build the maddy binary."""
34-
with self.out.section(f"Building maddy binary for {self.ct.shortname}"):
35-
self.bld_ct.bash(f"""
36-
if [ -f '{repo_path}/admin-web/package.json' ]; then
37-
cd '{repo_path}/admin-web' && bun install
38-
fi
39-
""")
40-
self.out.print(f"Compiling maddy in {repo_path} (make build) ...")
41-
ret = self.out.shell(
42-
f"incus exec {self.bld_ct.name} -- bash -c 'cd {repo_path} && make build'"
43-
)
50+
mode = "with admin web UI" if self.with_admin else "without admin web UI"
51+
with self.out.section(
52+
f"Building maddy binary for {self.ct.shortname} ({mode})"
53+
):
54+
if self.with_admin:
55+
# Ensure admin-web submodule is populated and dependencies installed;
56+
# build.sh copy_admin_web() handles the actual SPA build.
57+
self.bld_ct.bash(f"""
58+
if [ ! -f '{repo_path}/admin-web/package.json' ]; then
59+
cd '{repo_path}' && git submodule update --init admin-web
60+
fi
61+
cd '{repo_path}/admin-web'
62+
if command -v bun >/dev/null 2>&1; then
63+
bun install
64+
elif command -v npm >/dev/null 2>&1; then
65+
npm install
66+
fi
67+
""")
68+
else:
69+
# Hide package.json so build.sh creates a placeholder instead.
70+
self.bld_ct.bash(f"""
71+
PKG='{repo_path}/admin-web/package.json'
72+
BAK='{repo_path}/admin-web/package.json.cmlxc-disabled'
73+
if [ -f "$PKG" ]; then mv "$PKG" "$BAK"; fi
74+
""")
75+
76+
try:
77+
ret = self.out.shell(
78+
f"incus exec {self.bld_ct.name} -- bash -c "
79+
f"'cd {repo_path} && make clean build'"
80+
)
81+
finally:
82+
# Restore package.json if we hid it.
83+
self.bld_ct.bash(f"""
84+
BAK='{repo_path}/admin-web/package.json.cmlxc-disabled'
85+
PKG='{repo_path}/admin-web/package.json'
86+
if [ -f "$BAK" ] && [ ! -f "$PKG" ]; then mv "$BAK" "$PKG"; fi
87+
""")
88+
4489
if ret:
4590
raise SetupError(f"maddy build failed in {repo_path} (exit {ret})")
4691

92+
if self.with_admin:
93+
check = self.bld_ct.bash(
94+
f"test -f {repo_path}/internal/adminweb/build/index.html",
95+
check=False,
96+
)
97+
if check is None:
98+
raise SetupError("admin-web build produced no index.html")
99+
47100
def get_test_domain_or_ip(self):
48101
if not self.ct.ipv4:
49102
self.ct.wait_ready()
@@ -94,15 +147,57 @@ def deploy(self, source=None):
94147
self.ct.bash("systemctl daemon-reload")
95148
self.ct.bash("systemctl enable madmail")
96149
self.ct.bash("systemctl start madmail")
150+
151+
if self.with_admin:
152+
self.out.print("Configuring admin web interface at /admin ...")
153+
self.ct.bash("madmail admin-web path /admin")
154+
self.ct.bash("madmail admin-web enable")
155+
# Path changes are applied at startup.
156+
self.ct.bash("systemctl restart madmail")
157+
else:
158+
self.out.print("Disabling admin web interface ...")
159+
self.ct.bash("madmail admin-web disable")
160+
97161
self.ct.bash("rm -f /tmp/madmail")
98162

99163
self.ct.write_deploy_state(MADMAIL, source=source)
100164
self.out.green(f"madmail deployed to {self.ct.shortname} ({ip})")
165+
print_admin_info(self.out, self.ct, ip)
101166

102167
elapsed = time.time() - t_total
103168
self.out.section_line(f"deploy madmail complete ({elapsed:.1f}s)")
104169

105170

171+
def print_admin_info(out, ct, ip):
172+
"""Print admin API token and admin-web endpoint state."""
173+
try:
174+
token = ct.bash("madmail admin-token --raw", check=False).strip()
175+
if token:
176+
out.print(f"admin-token: {token}")
177+
178+
status = ct.bash("madmail admin-web status", check=False) or ""
179+
enabled, path = _parse_admin_web_status(status)
180+
if enabled and path:
181+
out.print(f"admin-url: https://{ip}{path}/")
182+
else:
183+
out.print("admin-url: disabled")
184+
except Exception:
185+
pass
186+
187+
188+
def _parse_admin_web_status(status):
189+
enabled = "Admin Web Dashboard: enabled" in status
190+
path = None
191+
for line in status.splitlines():
192+
if "Admin Web Path:" in line:
193+
_, _, value = line.partition("Admin Web Path:")
194+
value = value.strip()
195+
if value.startswith("/"):
196+
path = value.rstrip("/")
197+
break
198+
return enabled, path
199+
200+
106201
def prepare_build_container(bld_ct, go_mod_path):
107202
"""Install or update Go inside the builder according to go.mod."""
108203
bld_ct.bash("""

0 commit comments

Comments
 (0)