fix: roll out rotated traefik certificate secrets
This commit is contained in:
@@ -55,7 +55,29 @@ def wildcard_secret_version(cert_dir: str = CERT_DIR) -> str:
|
||||
return "v" + digest[:16]
|
||||
|
||||
|
||||
def _traefik_setup(recipe: str, domain: str, version: str) -> None:
|
||||
def _traefik_requires_certificate_rollout(domain: str, secret_version: str) -> bool:
|
||||
"""Whether Traefik's active service still references an older cert version."""
|
||||
stack = lifecycle._stack_name(domain) # noqa: SLF001
|
||||
service = f"{stack}_app"
|
||||
result = _run(
|
||||
[
|
||||
"docker",
|
||||
"service",
|
||||
"inspect",
|
||||
service,
|
||||
"--format",
|
||||
"{{range .Spec.TaskTemplate.ContainerSpec.Secrets}}{{.SecretName}} {{end}}",
|
||||
],
|
||||
timeout=30,
|
||||
)
|
||||
expected = {
|
||||
f"{stack}_ssl_cert_{secret_version}",
|
||||
f"{stack}_ssl_key_{secret_version}",
|
||||
}
|
||||
return not expected.issubset(set(result.stdout.split()))
|
||||
|
||||
|
||||
def _traefik_setup(recipe: str, domain: str, version: str) -> bool:
|
||||
"""Per-app config for the traefik reverse-proxy reconcile — preserves EXACTLY what the prior
|
||||
proxy.nix bash reconcile did (wildcard/file-provider mode serving the pre-issued cert as
|
||||
ssl_cert/ssl_key swarm secrets; NO ACME). Uses the proven abra.env_set (newline-safe, unlike the
|
||||
@@ -118,6 +140,7 @@ def _traefik_setup(recipe: str, domain: str, version: str) -> None:
|
||||
timeout=120,
|
||||
check=True,
|
||||
)
|
||||
return _traefik_requires_certificate_rollout(domain, secret_version)
|
||||
|
||||
|
||||
SPECS: dict[str, dict] = {
|
||||
@@ -476,8 +499,9 @@ def reconcile(app: str) -> str:
|
||||
# Per-app config/secrets: a spec may provide its own `setup` (traefik's cert/file-provider wiring);
|
||||
# otherwise the default keycloak-shaped path (app new + DOMAIN/LETS_ENCRYPT + generate secrets).
|
||||
setup = spec.get("setup")
|
||||
setup_needs_rollout = False
|
||||
if setup:
|
||||
setup(recipe, domain, latest)
|
||||
setup_needs_rollout = bool(setup(recipe, domain, latest))
|
||||
else:
|
||||
ensure_app_config(recipe, domain, latest)
|
||||
ensure_secrets(domain)
|
||||
@@ -495,6 +519,20 @@ def reconcile(app: str) -> str:
|
||||
write_last_good(recipe, target)
|
||||
return f"deployed-fresh:{target}"
|
||||
|
||||
# A certificate rotation changes Traefik's immutable Swarm secrets but
|
||||
# must not be held hostage by an unrelated recipe-major upgrade policy.
|
||||
# Redeploy the current recipe version so its compose spec references the
|
||||
# just-created cert/key secret pair, then apply the usual health gate.
|
||||
if setup_needs_rollout:
|
||||
if not current:
|
||||
raise RuntimeError(f"{app} has services but no current version")
|
||||
print(f"[{app}] certificate changed → redeploy {current}", flush=True)
|
||||
deploy_version(recipe, domain, current, dt)
|
||||
if not wait_healthy(spec):
|
||||
raise RuntimeError(f"{app} certificate rollout {current} did not become healthy")
|
||||
write_last_good(recipe, current)
|
||||
return f"certificate-rolled-out:{current}"
|
||||
|
||||
# Deployed & already on latest → converge to a no-op (commit last-good if healthy).
|
||||
if current == latest:
|
||||
if wait_healthy(spec, timeout=60):
|
||||
|
||||
Reference in New Issue
Block a user