mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
The swarm branch passed values through raw, so a value with interior whitespace (MAIL_FROM_NAME='Infinito.Nexus by ...') landed unquoted in the rendered .env and broke the env parser: the snipe-it service crashlooped at 0/3 until the converge wait failed in every variant. Quote (and escape backslash/double-quote) only when the value contains space/tab/newline; everything else stays raw, so the swarm invariants hold: no compose $$-doubling, raw $ and double quotes preserved, and the playwright --env-file path (docker CLI keeps quotes literal) is unaffected because no playwright env value can contain whitespace. Compose quoting is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
80 lines
2.3 KiB
Python
80 lines
2.3 KiB
Python
# Dotenv-safe quoting for .env files.
|
|
#
|
|
# Behaviour is mode-aware:
|
|
#
|
|
# * Compose: wraps value in double quotes, escapes ``$`` as ``$$`` (compose
|
|
# parses interpolation), escapes ``\\`` and ``"``. Compose then strips
|
|
# the outer quotes back off when loading the env-file.
|
|
#
|
|
# * Swarm: ``docker stack deploy`` does NOT strip outer quotes and does NOT
|
|
# apply ``$$`` interpolation - the value is passed verbatim to the
|
|
# container. Wrapping the value would leak literal quotes into
|
|
# ``$KEY`` ("\"password\""), breaking DB connect strings, URL parsers,
|
|
# and any ``int(os.environ['KEY'])`` consumer.
|
|
# So in swarm mode the filter returns the value unchanged.
|
|
#
|
|
# The mode is read from ``DEPLOYMENT_MODE`` in the rendering context.
|
|
# When the context is missing (e.g. unit tests calling the filter
|
|
# directly), the compose-style quote is used as a safe default - the same
|
|
# behaviour the filter had before the swarm split.
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import jinja2
|
|
|
|
_MISSING: Any = object()
|
|
|
|
|
|
def _quote_compose_style(value: Any) -> str:
|
|
if value is None:
|
|
return '""'
|
|
|
|
s = str(value)
|
|
|
|
s = s.replace("$", "$$")
|
|
|
|
s = s.replace("\\", "\\\\")
|
|
s = s.replace('"', '\\"')
|
|
|
|
return f'"{s}"'
|
|
|
|
|
|
def _passthrough_swarm(value: Any) -> str:
|
|
if value is None:
|
|
return ""
|
|
s = str(value)
|
|
if any(c in s for c in " \t\n"):
|
|
s = s.replace("\\", "\\\\").replace('"', '\\"')
|
|
return f'"{s}"'
|
|
return s
|
|
|
|
|
|
@jinja2.pass_context
|
|
def dotenv_quote(ctx: jinja2.runtime.Context, value: Any = _MISSING) -> str:
|
|
"""Dotenv-safe quote, switched on DEPLOYMENT_MODE from the render context.
|
|
|
|
Also callable directly from Python (unit tests, helpers) without a
|
|
Jinja context: in that case the only positional argument is the
|
|
value and compose-style quoting is used.
|
|
"""
|
|
if value is _MISSING:
|
|
return _quote_compose_style(ctx)
|
|
|
|
mode = ""
|
|
try:
|
|
mode = str(ctx.resolve("DEPLOYMENT_MODE") or "").strip()
|
|
except Exception: # pragma: no cover
|
|
mode = ""
|
|
|
|
if mode == "swarm":
|
|
return _passthrough_swarm(value)
|
|
return _quote_compose_style(value)
|
|
|
|
|
|
class FilterModule:
|
|
def filters(self) -> dict[str, Any]:
|
|
return {
|
|
"dotenv_quote": dotenv_quote,
|
|
}
|