78 lines
3.5 KiB
Python
78 lines
3.5 KiB
Python
"""Deploy the opt-in native Alloy service using shared host/guest configs."""
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import tempfile
|
|
|
|
from engine.descriptor import Flag, Meta
|
|
|
|
METADATA = Meta(summary="Configure native Alloy from configs/alloy/config.json.",
|
|
args=[Flag("--dry-run", "Validate the deployment plan without changes.")])
|
|
|
|
|
|
def deployment(ctx):
|
|
settings = Path(ctx.paths.configs) / "alloy/config.json"
|
|
if not settings.exists():
|
|
return None
|
|
values = json.loads(settings.read_text())
|
|
role = values.get("role")
|
|
if role not in ("host", "guest"):
|
|
raise ValueError("Alloy role must be host or guest")
|
|
env_file = values.get("environment_file", "")
|
|
if not re.fullmatch(r"/[A-Za-z0-9_./-]+", env_file):
|
|
raise ValueError("Alloy environment_file must be an absolute path without whitespace")
|
|
source = Path(ctx.paths.commons) / "etc/alloy" / (
|
|
"native-host.alloy" if role == "host" else "native.alloy")
|
|
config = source.read_text()
|
|
extra = Path(ctx.paths.configs) / "alloy/extra.alloy"
|
|
if extra.is_file():
|
|
config += "\n" + extra.read_text()
|
|
unit = ("[Service]\nEnvironmentFile=" + env_file + "\n"
|
|
'ExecStart=\nExecStart=/usr/bin/alloy run --storage.path=/var/lib/alloy --server.http.listen-addr=127.0.0.1:12346 /etc/alloy/config.alloy\n'
|
|
+ ("User=root\nGroup=root\n" if role == "guest" else "") +
|
|
"SupplementaryGroups=systemd-journal" + (" docker" if role == "guest" else "") + "\n")
|
|
return config, unit, env_file
|
|
|
|
|
|
def run(ctx, args):
|
|
plan = deployment(ctx)
|
|
if plan is None:
|
|
ctx.log.info("Native Alloy is not configured for this node; skipping.")
|
|
return
|
|
config, unit, env_file = plan
|
|
if args.dry_run:
|
|
ctx.log.info("Native Alloy deployment plan validated; no changes made.")
|
|
return
|
|
ctx.sys.sudo(["test", "-f", env_file])
|
|
# Validate with the actual service environment without printing credentials.
|
|
validator = '''import os, stat, subprocess, sys
|
|
from pathlib import Path
|
|
# Environment files deliberately use plain KEY=value, with no shell evaluation.
|
|
secret = Path(sys.argv[1])
|
|
info = secret.stat()
|
|
if info.st_uid != 0 or stat.S_IMODE(info.st_mode) & 0o077:
|
|
raise RuntimeError('Alloy environment file must be root-owned with mode 0600 or stricter')
|
|
for line in secret.read_text().splitlines():
|
|
if not line.strip() or line.lstrip().startswith('#'): continue
|
|
key, value = line.split('=', 1)
|
|
os.environ[key] = value
|
|
subprocess.run(['alloy', 'validate', sys.argv[2]], check=True)
|
|
'''
|
|
with tempfile.TemporaryDirectory(prefix="apex-alloy-") as directory:
|
|
config_path = Path(directory) / "config.alloy"
|
|
unit_path = Path(directory) / "apex.conf"
|
|
config_path.write_text(config)
|
|
unit_path.write_text(unit)
|
|
ctx.sys.sudo(["python3", "-c", validator, env_file, str(config_path)])
|
|
ctx.sys.sudo(["install", "-d", "-m", "0755", "/etc/alloy",
|
|
"/etc/systemd/system/alloy.service.d"])
|
|
ctx.sys.sudo(["install", "-m", "0644", str(config_path), "/etc/alloy/config.alloy"])
|
|
ctx.sys.sudo(["install", "-m", "0644", str(unit_path),
|
|
"/etc/systemd/system/alloy.service.d/apex.conf"])
|
|
ctx.sys.sudo(["systemctl", "unmask", "alloy.service"])
|
|
ctx.sys.daemon_reload()
|
|
ctx.sys.sudo(["systemctl", "enable", "alloy.service"])
|
|
ctx.sys.sudo(["systemctl", "restart", "alloy.service"])
|
|
ctx.sys.sudo(["systemctl", "is-active", "--quiet", "alloy.service"])
|
|
ctx.log.success("Configured native Alloy.")
|