certapi 1.1.5__tar.gz → 1.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {certapi-1.1.5/src/certapi.egg-info → certapi-1.1.7}/PKG-INFO +2 -2
- {certapi-1.1.5 → certapi-1.1.7}/setup.py +2 -2
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/client/renewal_manager.py +87 -55
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/issuers/AcmeCertIssuer.py +1 -1
- {certapi-1.1.5 → certapi-1.1.7/src/certapi.egg-info}/PKG-INFO +2 -2
- certapi-1.1.7/src/certapi.egg-info/requires.txt +2 -0
- {certapi-1.1.5 → certapi-1.1.7}/tests/test_renewal_manager.py +31 -49
- certapi-1.1.5/src/certapi.egg-info/requires.txt +0 -2
- {certapi-1.1.5 → certapi-1.1.7}/MANIFEST.in +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/README.md +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/pyproject.toml +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/setup.cfg +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/__init__.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/acme/Acme.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/acme/AcmeError.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/acme/Challenge.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/acme/Order.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/acme/__init__.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/acme/http.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/challenge_solver/ChallengeSolver.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/challenge_solver/FileSystemChallengeSolver.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/challenge_solver/InmemoryChallengeSolver.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/challenge_solver/__init__.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/challenge_solver/dns/__init__.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/challenge_solver/dns/cloudflare/__init__.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/challenge_solver/dns/digitalocean/__init__.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/cli.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/client/__init__.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/client/cert_manager_client.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/crypto/__init__.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/crypto/crypto.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/crypto/crypto_classes.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/domain_batching.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/errors.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/http/HttpClientBase.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/http/__init__.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/http/types.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/issuers/SelfCertIssuer.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/issuers/__init__.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/issuers/abstract_certissuer.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/keystore/FileSystemKeyStore.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/keystore/KeyStore.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/keystore/RemoteKeyStore.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/keystore/SqliteKeyStore.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/keystore/__init__.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/manager/__init__.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/manager/acme_cert_manager.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/server/__init__.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/server/api.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/server/cert_api.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/server/key_api.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/util.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi/utils.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi.egg-info/SOURCES.txt +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi.egg-info/dependency_links.txt +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi.egg-info/entry_points.txt +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/src/certapi.egg-info/top_level.txt +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/tests/test_acme_cert_manager_batching.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/tests/test_acme_error_handling.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/tests/test_cert_issuer_generic.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/tests/test_certs_with_key_types.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/tests/test_cli.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/tests/test_domain_batching_boulder_cases.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/tests/test_http_error_handling.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/tests/test_keystores.py +0 -0
- {certapi-1.1.5 → certapi-1.1.7}/tests/test_obtain_interface.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: certapi
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.7
|
|
4
4
|
Summary: Python Package for managing keys, request SSL certificates from ACME.
|
|
5
5
|
Home-page: https://github.com/mesudip/certapi
|
|
6
6
|
Author: Sudip Bhattarai
|
|
@@ -10,7 +10,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Requires-Python: >=3.6
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: cryptography
|
|
13
|
+
Requires-Dist: cryptography>=39.0.0
|
|
14
14
|
Requires-Dist: requests
|
|
15
15
|
Dynamic: author
|
|
16
16
|
Dynamic: author-email
|
|
@@ -2,11 +2,11 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="certapi",
|
|
5
|
-
version="1.1.
|
|
5
|
+
version="1.1.7",
|
|
6
6
|
packages=find_packages(where="src"),
|
|
7
7
|
package_dir={"": "src"},
|
|
8
8
|
install_requires=[
|
|
9
|
-
"cryptography",
|
|
9
|
+
"cryptography>=39.0.0",
|
|
10
10
|
"requests",
|
|
11
11
|
],
|
|
12
12
|
include_package_data=True,
|
|
@@ -18,9 +18,13 @@ class RenewalManager:
|
|
|
18
18
|
callers publish the complete desired domain set through
|
|
19
19
|
:meth:`update_watch_domains`. Publishing a domain set is synchronous: the call
|
|
20
20
|
returns after certapi has processed the set and attempted any due
|
|
21
|
-
obtain/renew work.
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
obtain/renew work.
|
|
22
|
+
|
|
23
|
+
The integration owns domain discovery. Certapi's background renewal cycle
|
|
24
|
+
only invokes ``renewal_callback`` when it is time to refresh external state;
|
|
25
|
+
that callback is responsible for calling :meth:`update_watch_domains` with
|
|
26
|
+
the current complete domain set. ``update_watch_domains`` is the only method
|
|
27
|
+
that mutates watched domains and performs due renewal work.
|
|
24
28
|
|
|
25
29
|
Algorithm:
|
|
26
30
|
- Maintain a watched domain set and cache each domain's certificate expiry.
|
|
@@ -48,7 +52,7 @@ class RenewalManager:
|
|
|
48
52
|
def __init__(
|
|
49
53
|
self,
|
|
50
54
|
cert_manager_client,
|
|
51
|
-
|
|
55
|
+
renewal_callback: Optional[Callable[[], None]] = None,
|
|
52
56
|
renew_threshold_days: Optional[int] = None,
|
|
53
57
|
min_renew_threshold_days: int = 10,
|
|
54
58
|
sleep_slack_seconds: int = 300,
|
|
@@ -75,11 +79,10 @@ class RenewalManager:
|
|
|
75
79
|
cert_manager_client: Backend object with an ``obtain(domains,
|
|
76
80
|
**kwargs)`` method. Local managers and ``CertManagerClient`` are
|
|
77
81
|
both supported.
|
|
78
|
-
|
|
79
|
-
renewal
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
nested renewal pass.
|
|
82
|
+
renewal_callback: Optional callback invoked by background or forced
|
|
83
|
+
renewal cycles. Integrations should use this to recompute their
|
|
84
|
+
domain list and call :meth:`update_watch_domains`. The callback
|
|
85
|
+
itself should not perform certificate issuance directly.
|
|
83
86
|
renew_threshold_days: Renew certificates when they expire within
|
|
84
87
|
this many days. Defaults to ``CERT_RENEW_THRESHOLD_DAYS`` or the
|
|
85
88
|
certapi default.
|
|
@@ -113,7 +116,7 @@ class RenewalManager:
|
|
|
113
116
|
certs.
|
|
114
117
|
"""
|
|
115
118
|
self.cert_manager_client = cert_manager_client
|
|
116
|
-
self.
|
|
119
|
+
self.renewal_callback = renewal_callback
|
|
117
120
|
self.key_type = key_type
|
|
118
121
|
self.expiry_days = expiry_days
|
|
119
122
|
self.batch_domains = batch_domains
|
|
@@ -154,6 +157,10 @@ class RenewalManager:
|
|
|
154
157
|
self._cycle_running = False
|
|
155
158
|
self._cycle_thread_id: Optional[int] = None
|
|
156
159
|
self._cycle_generation = 0
|
|
160
|
+
self._renewal_running = False
|
|
161
|
+
self._renewal_requested = False
|
|
162
|
+
self._renewal_thread_id: Optional[int] = None
|
|
163
|
+
self._renewal_generation = 0
|
|
157
164
|
self._blacklist: Dict[str, datetime] = {}
|
|
158
165
|
self._last_error_message: Optional[str] = None
|
|
159
166
|
self._last_error_timestamp: Optional[datetime] = None
|
|
@@ -166,30 +173,20 @@ class RenewalManager:
|
|
|
166
173
|
Empty values are ignored. Cache and blacklist entries for domains no
|
|
167
174
|
longer being watched are dropped.
|
|
168
175
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
176
|
+
This method immediately runs or queues a renewal pass and blocks until
|
|
177
|
+
certapi has attempted any due obtain/renew work for the latest published
|
|
178
|
+
set. Concurrent calls are serialized inside the manager; if a renewal
|
|
179
|
+
pass is already running, the current pass is allowed to finish and a
|
|
180
|
+
follow-up pass processes the newest watch set before callers return.
|
|
174
181
|
"""
|
|
175
182
|
new_watch_set = {x for x in domains if x}
|
|
176
183
|
with self._lock:
|
|
177
184
|
self._watch_domains = new_watch_set
|
|
178
185
|
self._cache = {d: expiry for d, expiry in self._cache.items() if d in self._watch_domains}
|
|
179
186
|
self._blacklist = {d: exp for d, exp in self._blacklist.items() if d in self._watch_domains}
|
|
180
|
-
if self._cycle_thread_id == threading.get_ident():
|
|
181
|
-
self._lock.notify_all()
|
|
182
|
-
return
|
|
183
|
-
if self._running:
|
|
184
|
-
target_generation = self._cycle_generation + (2 if self._cycle_running else 1)
|
|
185
|
-
self._cycle_requested = True
|
|
186
|
-
self._lock.notify_all()
|
|
187
|
-
while self._running and self._cycle_generation < target_generation:
|
|
188
|
-
self._lock.wait()
|
|
189
|
-
return
|
|
190
187
|
self._lock.notify_all()
|
|
191
188
|
|
|
192
|
-
self.
|
|
189
|
+
self._run_renewal_pass(force=False)
|
|
193
190
|
|
|
194
191
|
def start(self):
|
|
195
192
|
"""
|
|
@@ -227,15 +224,13 @@ class RenewalManager:
|
|
|
227
224
|
|
|
228
225
|
def trigger_now(self):
|
|
229
226
|
"""
|
|
230
|
-
Run
|
|
227
|
+
Run the renewal trigger immediately.
|
|
231
228
|
|
|
232
|
-
When
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
Most integrations should call :meth:`update_watch_domains` instead. This
|
|
238
|
-
method is for forcing a pass without changing the watched domain set.
|
|
229
|
+
When a renewal callback is configured, this method invokes that callback
|
|
230
|
+
and waits for it to return. The callback should call
|
|
231
|
+
:meth:`update_watch_domains`, which performs due renewal work. Without a
|
|
232
|
+
callback, this method performs a direct forced renewal pass over the
|
|
233
|
+
existing watch set.
|
|
239
234
|
|
|
240
235
|
If called from inside the renewal cycle itself, the method only requests
|
|
241
236
|
a follow-up pass and returns immediately to avoid deadlock.
|
|
@@ -321,14 +316,6 @@ class RenewalManager:
|
|
|
321
316
|
# Testing hook: allows deterministic no-op sleep behavior.
|
|
322
317
|
self.sleep_fn(wait_seconds)
|
|
323
318
|
|
|
324
|
-
def _sync_watch_domains(self):
|
|
325
|
-
if self.sync_watch_domains is None:
|
|
326
|
-
return
|
|
327
|
-
try:
|
|
328
|
-
self.sync_watch_domains()
|
|
329
|
-
except Exception as e:
|
|
330
|
-
self._set_error(e)
|
|
331
|
-
|
|
332
319
|
def _due_window_secs(self) -> float:
|
|
333
320
|
return max(self.update_threshold_secs, self.cert_min_renew_threshold_secs)
|
|
334
321
|
|
|
@@ -366,7 +353,49 @@ class RenewalManager:
|
|
|
366
353
|
self._lock.notify_all()
|
|
367
354
|
|
|
368
355
|
def _run_cycle_body(self, force: bool = False) -> int:
|
|
369
|
-
self.
|
|
356
|
+
if self.renewal_callback is None:
|
|
357
|
+
return self._run_renewal_pass(force=force)
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
self.renewal_callback()
|
|
361
|
+
except Exception as e:
|
|
362
|
+
self._set_error(e)
|
|
363
|
+
return 0
|
|
364
|
+
|
|
365
|
+
def _run_renewal_pass(self, force: bool = False) -> int:
|
|
366
|
+
count = 0
|
|
367
|
+
current_thread_id = threading.get_ident()
|
|
368
|
+
with self._lock:
|
|
369
|
+
if self._renewal_thread_id == current_thread_id:
|
|
370
|
+
return 0
|
|
371
|
+
if self._renewal_running:
|
|
372
|
+
target_generation = self._renewal_generation + 2
|
|
373
|
+
self._renewal_requested = True
|
|
374
|
+
self._lock.notify_all()
|
|
375
|
+
while self._renewal_generation < target_generation:
|
|
376
|
+
self._lock.wait()
|
|
377
|
+
return 0
|
|
378
|
+
|
|
379
|
+
self._renewal_running = True
|
|
380
|
+
self._renewal_thread_id = current_thread_id
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
while True:
|
|
384
|
+
count += self._run_renewal_pass_body(force=force)
|
|
385
|
+
with self._lock:
|
|
386
|
+
self._renewal_generation += 1
|
|
387
|
+
if not self._renewal_requested:
|
|
388
|
+
return count
|
|
389
|
+
self._renewal_requested = False
|
|
390
|
+
force = False
|
|
391
|
+
self._lock.notify_all()
|
|
392
|
+
finally:
|
|
393
|
+
with self._lock:
|
|
394
|
+
self._renewal_running = False
|
|
395
|
+
self._renewal_thread_id = None
|
|
396
|
+
self._lock.notify_all()
|
|
397
|
+
|
|
398
|
+
def _run_renewal_pass_body(self, force: bool = False) -> int:
|
|
370
399
|
self._seed_cache_from_local_keystore()
|
|
371
400
|
now = self.clock_fn()
|
|
372
401
|
self._clean_blacklist(now)
|
|
@@ -390,8 +419,8 @@ class RenewalManager:
|
|
|
390
419
|
if (expiry - now).total_seconds() < due_window:
|
|
391
420
|
due_domains.append(domain)
|
|
392
421
|
|
|
393
|
-
|
|
394
|
-
self.
|
|
422
|
+
if due_domains:
|
|
423
|
+
self._renew_domains(due_domains, now)
|
|
395
424
|
|
|
396
425
|
return len(due_domains)
|
|
397
426
|
|
|
@@ -434,10 +463,12 @@ class RenewalManager:
|
|
|
434
463
|
self._cache[domain] = expiry
|
|
435
464
|
self._lock.notify_all()
|
|
436
465
|
|
|
437
|
-
def
|
|
438
|
-
|
|
466
|
+
def _renew_domains(self, domains: List[str], now: datetime):
|
|
467
|
+
domains_with_cached_entries = []
|
|
439
468
|
with self._lock:
|
|
440
|
-
|
|
469
|
+
for domain in domains:
|
|
470
|
+
if domain in self._cache:
|
|
471
|
+
domains_with_cached_entries.append(domain)
|
|
441
472
|
|
|
442
473
|
try:
|
|
443
474
|
renew_threshold_days = self.cert_min_renew_threshold_secs // (24 * 3600)
|
|
@@ -446,7 +477,7 @@ class RenewalManager:
|
|
|
446
477
|
"expiry_days": self.expiry_days,
|
|
447
478
|
"renew_threshold_days": renew_threshold_days,
|
|
448
479
|
"skip_failing": False,
|
|
449
|
-
"batch_domains":
|
|
480
|
+
"batch_domains": True,
|
|
450
481
|
"self_verify": self.self_verify,
|
|
451
482
|
"country": self.country,
|
|
452
483
|
"state": self.state,
|
|
@@ -456,7 +487,7 @@ class RenewalManager:
|
|
|
456
487
|
}
|
|
457
488
|
res = self._call_certificate_backend(
|
|
458
489
|
self.cert_manager_client.obtain,
|
|
459
|
-
|
|
490
|
+
domains,
|
|
460
491
|
obtain_options,
|
|
461
492
|
)
|
|
462
493
|
if res is None:
|
|
@@ -464,11 +495,12 @@ class RenewalManager:
|
|
|
464
495
|
self._update_expiry_cache(res.issued + res.existing)
|
|
465
496
|
except Exception as e:
|
|
466
497
|
self._set_error(e)
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
self.
|
|
470
|
-
|
|
471
|
-
|
|
498
|
+
for domain in domains:
|
|
499
|
+
self._add_to_blacklist(domain, now)
|
|
500
|
+
if domain in domains_with_cached_entries or self._has_local_certificate(domain):
|
|
501
|
+
self._schedule_existing_certificate_retry(domain, now)
|
|
502
|
+
else:
|
|
503
|
+
self._register_self_signed(domain)
|
|
472
504
|
|
|
473
505
|
def _schedule_existing_certificate_retry(self, domain: str, now: datetime):
|
|
474
506
|
retry_interval_seconds = min(self.renew_retry_interval_seconds, 24 * 3600)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: certapi
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.7
|
|
4
4
|
Summary: Python Package for managing keys, request SSL certificates from ACME.
|
|
5
5
|
Home-page: https://github.com/mesudip/certapi
|
|
6
6
|
Author: Sudip Bhattarai
|
|
@@ -10,7 +10,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Requires-Python: >=3.6
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: cryptography
|
|
13
|
+
Requires-Dist: cryptography>=39.0.0
|
|
14
14
|
Requires-Dist: requests
|
|
15
15
|
Dynamic: author
|
|
16
16
|
Dynamic: author-email
|
|
@@ -25,7 +25,7 @@ class DummyClient:
|
|
|
25
25
|
|
|
26
26
|
def obtain(self, hosts, **kwargs):
|
|
27
27
|
host = hosts[0] if isinstance(hosts, list) else hosts
|
|
28
|
-
self.calls.append({"host": host, "kwargs": kwargs})
|
|
28
|
+
self.calls.append({"host": host, "hosts": hosts, "kwargs": kwargs})
|
|
29
29
|
handler = self._handlers.get(host)
|
|
30
30
|
if handler is None:
|
|
31
31
|
return CertificateResponse()
|
|
@@ -47,12 +47,12 @@ class DummyClientWithObtain(DummyClient):
|
|
|
47
47
|
|
|
48
48
|
def obtain(self, hosts, **kwargs):
|
|
49
49
|
host = hosts[0] if isinstance(hosts, list) else hosts
|
|
50
|
-
self.obtain_calls.append({"host": host, "kwargs": kwargs})
|
|
50
|
+
self.obtain_calls.append({"host": host, "hosts": hosts, "kwargs": kwargs})
|
|
51
51
|
return super().obtain(hosts, **kwargs)
|
|
52
52
|
|
|
53
53
|
def issue_certificate(self, hosts, **kwargs):
|
|
54
54
|
host = hosts[0] if isinstance(hosts, list) else hosts
|
|
55
|
-
self.issue_calls.append({"host": host, "kwargs": kwargs})
|
|
55
|
+
self.issue_calls.append({"host": host, "hosts": hosts, "kwargs": kwargs})
|
|
56
56
|
return super().obtain(hosts, **kwargs)
|
|
57
57
|
|
|
58
58
|
|
|
@@ -325,7 +325,7 @@ def test_retry_deferral_suppresses_immediate_retry_and_force_when_blacklisted():
|
|
|
325
325
|
assert len(client.calls) == 1
|
|
326
326
|
|
|
327
327
|
|
|
328
|
-
def
|
|
328
|
+
def test_watch_domain_replacement_drops_unwatched_cache_and_external_update_processes_new_set():
|
|
329
329
|
now = datetime(2026, 1, 1, tzinfo=UTC)
|
|
330
330
|
client = DummyClient()
|
|
331
331
|
cert_c = _make_cert_pem("c.example.com", now, valid_for_days=60)
|
|
@@ -334,12 +334,7 @@ def test_watch_domain_replacement_drops_unwatched_cache_and_sync_callback_replac
|
|
|
334
334
|
CertificateResponse(issued=[IssuedCert(cert=cert_c, domains=["c.example.com"])], existing=[]),
|
|
335
335
|
)
|
|
336
336
|
|
|
337
|
-
mgr =
|
|
338
|
-
|
|
339
|
-
def sync_watch_domains():
|
|
340
|
-
mgr.update_watch_domains(["c.example.com"])
|
|
341
|
-
|
|
342
|
-
mgr = RenewalManager(client, sync_watch_domains=sync_watch_domains, clock_fn=lambda: now)
|
|
337
|
+
mgr = RenewalManager(client, clock_fn=lambda: now)
|
|
343
338
|
mgr.update_watch_domains(["a.example.com", "b.example.com"])
|
|
344
339
|
with mgr._lock:
|
|
345
340
|
mgr._cache["a.example.com"] = now + timedelta(days=20)
|
|
@@ -349,8 +344,9 @@ def test_watch_domain_replacement_drops_unwatched_cache_and_sync_callback_replac
|
|
|
349
344
|
with mgr._lock:
|
|
350
345
|
assert "b.example.com" not in mgr._cache
|
|
351
346
|
|
|
352
|
-
#
|
|
353
|
-
|
|
347
|
+
# Integrations can publish new domain state directly, outside the timed
|
|
348
|
+
# renewal callback path.
|
|
349
|
+
mgr.update_watch_domains(["c.example.com"])
|
|
354
350
|
state = mgr.get_state()
|
|
355
351
|
assert state["watched_domains"] == ["c.example.com"]
|
|
356
352
|
with mgr._lock:
|
|
@@ -527,21 +523,6 @@ def test_new_domain_failure_selfsigns_and_blacklists():
|
|
|
527
523
|
assert len(client.calls) == 2
|
|
528
524
|
|
|
529
525
|
|
|
530
|
-
def test_sync_callback_exception_sets_state_error():
|
|
531
|
-
now = datetime(2026, 1, 1, tzinfo=UTC)
|
|
532
|
-
mgr = RenewalManager(
|
|
533
|
-
DummyClient(),
|
|
534
|
-
sync_watch_domains=lambda: (_ for _ in ()).throw(RuntimeError("sync failed")),
|
|
535
|
-
clock_fn=lambda: now,
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
mgr.trigger_now()
|
|
539
|
-
|
|
540
|
-
state = mgr.get_state()
|
|
541
|
-
assert state["last_error_message"] == "sync failed"
|
|
542
|
-
assert state["last_error_timestamp"] == now.isoformat()
|
|
543
|
-
|
|
544
|
-
|
|
545
526
|
def test_update_watch_domains_blocks_until_running_worker_processes_domains():
|
|
546
527
|
now = datetime(2026, 1, 1, tzinfo=UTC)
|
|
547
528
|
started = threading.Event()
|
|
@@ -572,7 +553,7 @@ def test_update_watch_domains_blocks_until_running_worker_processes_domains():
|
|
|
572
553
|
assert client.calls[0]["host"] == "requested.example.com"
|
|
573
554
|
|
|
574
555
|
|
|
575
|
-
def
|
|
556
|
+
def test_renewal_cycle_invokes_callback_and_callback_updates_domains():
|
|
576
557
|
now = datetime(2026, 1, 1, tzinfo=UTC)
|
|
577
558
|
client = DummyClient()
|
|
578
559
|
cert = _make_cert_pem("from-callback.example.com", now, valid_for_days=60)
|
|
@@ -582,26 +563,22 @@ def test_update_watch_domains_from_cycle_thread_does_not_request_extra_cycle():
|
|
|
582
563
|
)
|
|
583
564
|
mgr = None
|
|
584
565
|
|
|
585
|
-
def
|
|
566
|
+
def renewal_callback():
|
|
586
567
|
mgr.update_watch_domains(["from-callback.example.com"])
|
|
587
568
|
|
|
588
|
-
mgr = RenewalManager(client,
|
|
589
|
-
|
|
590
|
-
mgr._running = True
|
|
591
|
-
|
|
592
|
-
mgr._run_cycle(force=False)
|
|
569
|
+
mgr = RenewalManager(client, renewal_callback=renewal_callback, clock_fn=lambda: now)
|
|
570
|
+
mgr.trigger_now()
|
|
593
571
|
|
|
594
572
|
with mgr._lock:
|
|
573
|
+
assert mgr._watch_domains == {"from-callback.example.com"}
|
|
574
|
+
assert "from-callback.example.com" in mgr._cache
|
|
595
575
|
assert mgr._force_trigger is False
|
|
596
576
|
assert mgr._cycle_requested is False
|
|
597
577
|
assert len(client.calls) == 1
|
|
598
578
|
|
|
599
579
|
|
|
600
|
-
def
|
|
580
|
+
def test_running_trigger_now_blocks_until_callback_update_finishes():
|
|
601
581
|
now = datetime(2026, 1, 1, tzinfo=UTC)
|
|
602
|
-
sync_count = 0
|
|
603
|
-
sync_ready = threading.Event()
|
|
604
|
-
sync_enabled = threading.Event()
|
|
605
582
|
obtain_started = threading.Event()
|
|
606
583
|
release_obtain = threading.Event()
|
|
607
584
|
client = DummyClient()
|
|
@@ -613,20 +590,13 @@ def test_running_trigger_now_blocks_until_synced_cycle_finishes():
|
|
|
613
590
|
release_obtain.wait(timeout=2)
|
|
614
591
|
return CertificateResponse(issued=[IssuedCert(cert=cert, domains=[host])], existing=[])
|
|
615
592
|
|
|
616
|
-
def
|
|
617
|
-
|
|
618
|
-
sync_count += 1
|
|
619
|
-
sync_ready.set()
|
|
620
|
-
if sync_enabled.is_set():
|
|
621
|
-
mgr.update_watch_domains(["blocking.example.com"])
|
|
593
|
+
def renewal_callback():
|
|
594
|
+
mgr.update_watch_domains(["blocking.example.com"])
|
|
622
595
|
|
|
623
596
|
client.set_handler("blocking.example.com", slow_handler)
|
|
624
|
-
mgr = RenewalManager(client,
|
|
597
|
+
mgr = RenewalManager(client, renewal_callback=renewal_callback, clock_fn=lambda: now)
|
|
625
598
|
|
|
626
599
|
mgr.start()
|
|
627
|
-
assert sync_ready.wait(timeout=2)
|
|
628
|
-
|
|
629
|
-
sync_enabled.set()
|
|
630
600
|
trigger_thread = threading.Thread(target=mgr.trigger_now)
|
|
631
601
|
trigger_thread.start()
|
|
632
602
|
|
|
@@ -637,7 +607,6 @@ def test_running_trigger_now_blocks_until_synced_cycle_finishes():
|
|
|
637
607
|
|
|
638
608
|
mgr.stop()
|
|
639
609
|
assert not trigger_thread.is_alive()
|
|
640
|
-
assert sync_count >= 2
|
|
641
610
|
assert len(client.calls) == 1
|
|
642
611
|
assert client.calls[0]["host"] == "blocking.example.com"
|
|
643
612
|
|
|
@@ -733,3 +702,16 @@ def test_stop_does_not_wait_for_hung_remote_request_thread():
|
|
|
733
702
|
|
|
734
703
|
assert not mgr._thread.is_alive()
|
|
735
704
|
release.set()
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def test_renewal_manager_batches_due_domains():
|
|
708
|
+
now = datetime(2026, 1, 1, tzinfo=UTC)
|
|
709
|
+
client = DummyClientWithObtain()
|
|
710
|
+
client.set_handler("domain1.com", CertificateResponse())
|
|
711
|
+
|
|
712
|
+
mgr = RenewalManager(client, renew_threshold_days=30, clock_fn=lambda: now, batch_domains=True)
|
|
713
|
+
mgr.update_watch_domains(["domain1.com", "domain2.com"])
|
|
714
|
+
|
|
715
|
+
assert len(client.obtain_calls) == 1
|
|
716
|
+
assert "domain1.com" in client.obtain_calls[0]["hosts"]
|
|
717
|
+
assert "domain2.com" in client.obtain_calls[0]["hosts"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{certapi-1.1.5 → certapi-1.1.7}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{certapi-1.1.5 → certapi-1.1.7}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|