fix(dstack-ingress): renew one lineage per run, and size the timeout to the wait - #111
Conversation
|
On CI: the workflows here trigger on In the meantime I ran the same checks locally on this branch:
Reviewing just the second commit ( |
…to the wait Two problems that compound each other. `certbot renew` renews *every* lineage in /etc/letsencrypt/renewal unless given --cert-name, and we never gave it one. run_pass calls this once per domain, so N domains meant N runs each covering all N lineages -- and renew_certificate then reported the result against whichever domain happened to ask, so domain A could be recorded as renewed because domain B was. Scope each run to the lineage it is named for. The run timeout was a hard-coded 300s. The dominant term inside a run is the DNS propagation wait, which the plugin sleeps through in-process -- and linode's own CERTBOT_PROPAGATION_SECONDS is 300, so on linode the cap could never be met and dns-01 timed out every time, on issuance as well as renewal. Before --cert-name it was worse still: the cap covered all lineages at once, so three cloudflare domains (120s each) could not fit either. Size it from the wait, and let CERTBOT_TIMEOUT override. --cert-name does not change the no-op wording renew_certificate parses: "No renewals were attempted." comes from _renew_describe_results, which runs for `renew` whether or not the lineage set was filtered. Checked against certbot 5.7.0, the version the image installs. Tested: 11 new cases in scripts/tests/test_certman.py covering scope (plain, wildcard, delegation, and certonly staying on -d) and the timeout (per-provider, delegation, override, invalid override, and a provider with no propagation setting). All five that assert the new behaviour fail against the previous code.
Review follow-up, and a regression this PR introduced. route53 declares CERTBOT_PROPAGATION_SECONDS = None, so sizing purely from the wait gave it 0 + 180 = 180s where the fixed cap had allowed 300s. A route53 renewal that used to complete in between would now be killed. The point of sizing was to raise the cap where a provider needs more than 300s, not to lower it anywhere. Floor the derived value at the 300s it replaced. Nothing gets a smaller budget than before; linode goes 300 -> 480. An explicit CERTBOT_TIMEOUT is still taken literally, floor included, since that one is the operator's call. Tested: the route53 case now asserts the floor rather than the bare headroom, and a new case walks every propagation value we ship (None, 0, 30, 120, 300) asserting none of them ends up under 300. 24 cases in test_certman.py.
6f2fe45 to
4d62c57
Compare
|
Independent review found a regression this PR introduced. Fixed in Taken — route53's budget was cut from 300 s to 180 sroute53 declares Floored at the 300 s it replaced:
An explicit Refuted — one propagation wait per lineage, not per identifierThe concern was that a multi-SAN lineage might sleep once per identifier, making for achall in achalls:
...
self._perform(domain, validation_domain_name, validation)
responses.append(...)
# one sleep, after the loop
sleep(self.conf('propagation-seconds'))
Refuted —
|
Two long-standing problems (both present since v1.0) that compound each other.
1.
certbot renewwas never scopedcertbot renewrenews every lineage in/etc/letsencrypt/renewalunless given--cert-name, and we never gave it one:run_passcalls this once per domain, so N domains meant N runs each covering all N lineages.renew_certificatethen reported the result against whichever domain asked, so domain A could be recorded as renewed because domain B was — and that flag is what drives evidence regeneration and the haproxy reload.Each run is now scoped to the lineage it is named for.
2. The run timeout was a fixed 300 s
The dominant term inside a certbot run is the DNS propagation wait, which the plugin sleeps through in-process. A single fixed cap cannot fit every provider:
CERTBOT_PROPAGATION_SECONDS--cert-name, three domains could not fitThe timeout is now derived from the wait (
propagation + 180 sheadroom, orDELEGATION_PROPAGATION_SECONDSunder delegation) andCERTBOT_TIMEOUToverrides it. The renew path also gets a dedicatedTimeoutExpiredbranch that names the knob instead of falling through to a generic handler.Why
--cert-nameis safe hererenew_certificatedetects a no-op by looking for"No renewals were attempted". That string comes from_renew_describe_results, which runs for therenewsubcommand whether or not the lineage set was filtered —--cert-nameonly narrows the candidates. Verified against certbot 5.7.0, the version the image installs:So the existing reload-only-on-actual-renewal behaviour (#102) is preserved. Parsing prose is still fragile — a
--deploy-hookmarker, as the lego path already uses, would be better, but that is a separate change.Testing
11 new cases in
scripts/tests/test_certman.py(20 total in the file, all green, plustest_dnsguide.py27 and the sanitizer suite):certonlystill using-drather than--cert-nameChecked both ways — the five cases that assert the new behaviour fail against the previous code:
How it turned up
Testing 2.3. Two dns-01 renewals hit the 300 s cap; on dns-01 that fails the bootstrap pass, which runs before haproxy serves anything, so the container exits. My case was one cloudflare domain, so the arithmetic alone does not explain it — concurrent load on the same zone probably contributed — but chasing it surfaced both defects above, which are structural rather than incidental.