83 lines
3.4 KiB
Python
83 lines
3.4 KiB
Python
"""Export synthesized APEX values or bridge a composition's environment for Compose."""
|
|
import os
|
|
import shlex
|
|
|
|
from engine.descriptor import Flag, Meta, Opt
|
|
|
|
METADATA = Meta(
|
|
summary="Export or sync synthesized APEX_* variables for shell or docker compose.",
|
|
args=[
|
|
Flag("--export", "Format as shell export statements (default)."),
|
|
Flag("--dotenv", "Print quoted generated APEX_* dotenv values only."),
|
|
Opt("--write", "Write a standalone generated dotenv file (0600).", default=None),
|
|
Flag("--sync", "Preserve user .env text and refresh the core Compose managed block."),
|
|
],
|
|
)
|
|
|
|
_START = "# BEGIN APEX MANAGED ENV"
|
|
_END = "# END APEX MANAGED ENV"
|
|
|
|
|
|
def _dotenv_value(value):
|
|
"""Quote a literal using Compose dotenv escapes, including interpolation dollars."""
|
|
value = str(value)
|
|
for old, new in (("\\", "\\\\"), ('"', '\\"'), ("$", "$$"),
|
|
("\r", "\\r"), ("\n", "\\n"), ("\t", "\\t")):
|
|
value = value.replace(old, new)
|
|
return f'"{value}"'
|
|
|
|
|
|
def _read_optional(path):
|
|
try:
|
|
with open(path, encoding="utf-8", newline="") as source:
|
|
return source.read()
|
|
except FileNotFoundError:
|
|
return ""
|
|
|
|
|
|
def _sync_body(existing, public, generated):
|
|
if any(marker in public or marker in generated for marker in (_START, _END)):
|
|
raise ValueError("Managed environment marker collides with source content.")
|
|
lines = existing.splitlines(keepends=True)
|
|
markers = [(index, line.rstrip("\r\n")) for index, line in enumerate(lines)
|
|
if _START in line or _END in line]
|
|
if markers:
|
|
if (len(markers) != 2 or markers[0][1] != _START or markers[1][1] != _END):
|
|
raise ValueError("Malformed or duplicate managed environment markers; .env unchanged.")
|
|
start, end = markers[0][0], markers[1][0]
|
|
existing = "".join(lines[:start] + lines[end + 1:])
|
|
# Keep user bytes in order, but place managed values last to preserve precedence.
|
|
separator = "" if not existing or existing.endswith("\n") else "\n"
|
|
public_separator = "" if not public or public.endswith("\n") else "\n"
|
|
return (existing + separator + _START + "\n" + public + public_separator
|
|
+ generated + _END + "\n")
|
|
|
|
|
|
def run(ctx, args):
|
|
modes = (args.export, args.dotenv, args.write is not None, args.sync)
|
|
if sum(bool(mode) for mode in modes) > 1:
|
|
ctx.log.error("Choose only one of --export, --dotenv, --write, or --sync.")
|
|
raise SystemExit(64)
|
|
variables = {k: v for k, v in ctx.vars().items() if k.startswith("APEX_")}
|
|
generated = "".join(f"{key}={_dotenv_value(value)}\n"
|
|
for key, value in sorted(variables.items()))
|
|
target = args.write
|
|
if args.sync:
|
|
core = os.path.join(ctx.paths.compositions, "apex")
|
|
target = os.path.join(core, ".env")
|
|
try:
|
|
generated = _sync_body(_read_optional(target),
|
|
_read_optional(os.path.join(core, "apex.env")), generated)
|
|
except ValueError as error:
|
|
ctx.log.error(str(error))
|
|
raise SystemExit(65) from error
|
|
if target is not None:
|
|
ctx.host.write_file(target, generated, mode="0600")
|
|
ctx.log.success(f"Wrote {len(variables)} synthesized variables to {target}.")
|
|
elif args.dotenv:
|
|
print(generated, end="")
|
|
else:
|
|
for key, value in sorted(variables.items()):
|
|
print(f"export {key}={shlex.quote(str(value))}")
|
|
return 0
|