Recover interrupted certificate replacement
This commit is contained in:
@@ -414,6 +414,183 @@ class ExecutionTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
def test_restart_recovery_rebinds_before_planned_retirement(self):
|
||||
endpoint = Endpoint(
|
||||
"npm",
|
||||
"192.168.1.4:81",
|
||||
)
|
||||
|
||||
adapter = FakeAdapter(
|
||||
endpoint,
|
||||
capabilities(
|
||||
CERTIFICATES,
|
||||
PROXY_HOSTS,
|
||||
),
|
||||
apply_local_keys={
|
||||
(
|
||||
PROXY_HOSTS,
|
||||
"app.example.com",
|
||||
): "12",
|
||||
},
|
||||
)
|
||||
|
||||
old_certificate = NormalizedObject(
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
local_key="67",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"valid_from": "2026-08-01T00:00:00+00:00",
|
||||
"expires_on": "2026-11-01T00:00:00+00:00",
|
||||
},
|
||||
)
|
||||
|
||||
new_certificate = NormalizedObject(
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
local_key="84",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"valid_from": "2026-09-01T00:00:00+00:00",
|
||||
"expires_on": "2026-12-01T00:00:00+00:00",
|
||||
},
|
||||
)
|
||||
|
||||
old_proxy = NormalizedObject(
|
||||
object_type=PROXY_HOSTS,
|
||||
logical_key="app.example.com",
|
||||
local_key="12",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"forward_scheme": "https",
|
||||
"forward_host": "backend.internal",
|
||||
"forward_port": 443,
|
||||
"enabled": True,
|
||||
},
|
||||
references=(
|
||||
ObjectReference(
|
||||
relation="certificate",
|
||||
object_type=CERTIFICATES,
|
||||
local_key="67",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
desired_certificate = NormalizedObject(
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
payload=dict(new_certificate.payload),
|
||||
)
|
||||
|
||||
desired_proxy = NormalizedObject(
|
||||
object_type=PROXY_HOSTS,
|
||||
logical_key="app.example.com",
|
||||
payload=dict(old_proxy.payload),
|
||||
references=(
|
||||
ObjectReference(
|
||||
relation="certificate",
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
plan = EndpointPlan(
|
||||
endpoint.identity,
|
||||
(
|
||||
PlanAction(
|
||||
endpoint_id=endpoint.identity,
|
||||
operation=PlanOperation.NOOP,
|
||||
desired=desired_certificate,
|
||||
current=new_certificate,
|
||||
),
|
||||
PlanAction(
|
||||
endpoint_id=endpoint.identity,
|
||||
operation=PlanOperation.UPDATE,
|
||||
desired=desired_proxy,
|
||||
current=old_proxy,
|
||||
reason=(
|
||||
"referenced local object is being retired"
|
||||
),
|
||||
),
|
||||
),
|
||||
retirements=(
|
||||
RetireRequest(
|
||||
object=old_certificate,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
with SQLiteStateStore(":memory:") as store:
|
||||
store.replace_observations(
|
||||
endpoint.identity,
|
||||
CERTIFICATES,
|
||||
[
|
||||
Observation(
|
||||
endpoint.identity,
|
||||
old_certificate,
|
||||
NOW,
|
||||
),
|
||||
Observation(
|
||||
endpoint.identity,
|
||||
new_certificate,
|
||||
NOW,
|
||||
),
|
||||
],
|
||||
NOW,
|
||||
)
|
||||
|
||||
store.replace_observations(
|
||||
endpoint.identity,
|
||||
PROXY_HOSTS,
|
||||
[
|
||||
Observation(
|
||||
endpoint.identity,
|
||||
old_proxy,
|
||||
NOW,
|
||||
)
|
||||
],
|
||||
NOW,
|
||||
)
|
||||
|
||||
execute_endpoint_plans(
|
||||
[adapter],
|
||||
[plan],
|
||||
store,
|
||||
executed_at=NOW,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
tuple(event[0] for event in adapter.events),
|
||||
(
|
||||
"apply",
|
||||
"verify",
|
||||
"retire",
|
||||
),
|
||||
)
|
||||
|
||||
proxy_action = adapter.applied_actions[0]
|
||||
|
||||
self.assertEqual(
|
||||
proxy_action.desired.references,
|
||||
(
|
||||
ObjectReference(
|
||||
relation="certificate",
|
||||
object_type=CERTIFICATES,
|
||||
local_key="84",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
adapter.retired,
|
||||
[
|
||||
RetireRequest(
|
||||
object=old_certificate,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def test_failed_verification_is_persisted_and_raises(self):
|
||||
endpoint = Endpoint("verizon", "192.168.1.1:443")
|
||||
adapter = FakeAdapter(
|
||||
|
||||
@@ -615,6 +615,217 @@ class EndpointPlanningTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
def test_restart_duplicate_certificate_uses_desired_copy_and_rebinds_proxy(self):
|
||||
endpoint = Endpoint(
|
||||
"generic",
|
||||
"node.example:443",
|
||||
)
|
||||
|
||||
generic_adapter = adapter(
|
||||
"generic",
|
||||
"node.example:443",
|
||||
CERTIFICATES,
|
||||
PROXY_HOSTS,
|
||||
)
|
||||
|
||||
desired_certificate = NormalizedObject(
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"provider": "letsencrypt",
|
||||
"valid_from": "2026-09-20T00:00:00+00:00",
|
||||
"expires_on": "2026-12-20T00:00:00+00:00",
|
||||
},
|
||||
)
|
||||
|
||||
desired_proxy = NormalizedObject(
|
||||
object_type=PROXY_HOSTS,
|
||||
logical_key="app.example.com",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"forward_scheme": "https",
|
||||
"forward_host": "backend.internal",
|
||||
"forward_port": 443,
|
||||
"enabled": True,
|
||||
},
|
||||
references=(
|
||||
ObjectReference(
|
||||
relation="certificate",
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
old_certificate = NormalizedObject(
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
local_key="67",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"provider": "other",
|
||||
"valid_from": "2026-08-20T00:00:00+00:00",
|
||||
"expires_on": "2026-11-20T00:00:00+00:00",
|
||||
},
|
||||
)
|
||||
|
||||
new_certificate = NormalizedObject(
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
local_key="84",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"provider": "other",
|
||||
"valid_from": "2026-09-20T00:00:00+00:00",
|
||||
"expires_on": "2026-12-20T00:00:00+00:00",
|
||||
},
|
||||
)
|
||||
|
||||
current_proxy = NormalizedObject(
|
||||
object_type=PROXY_HOSTS,
|
||||
logical_key="app.example.com",
|
||||
local_key="12",
|
||||
payload=dict(desired_proxy.payload),
|
||||
references=(
|
||||
ObjectReference(
|
||||
relation="certificate",
|
||||
object_type=CERTIFICATES,
|
||||
local_key="67",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
plans = build_endpoint_plans(
|
||||
[generic_adapter],
|
||||
[
|
||||
desired_certificate,
|
||||
desired_proxy,
|
||||
],
|
||||
[
|
||||
observed(
|
||||
endpoint.identity,
|
||||
old_certificate,
|
||||
),
|
||||
observed(
|
||||
endpoint.identity,
|
||||
new_certificate,
|
||||
),
|
||||
observed(
|
||||
endpoint.identity,
|
||||
current_proxy,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
self.assertEqual(len(plans), 1)
|
||||
|
||||
plan = plans[0]
|
||||
|
||||
by_type = {
|
||||
action.desired.object_type: action
|
||||
for action in plan.actions
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
by_type[CERTIFICATES].operation,
|
||||
PlanOperation.NOOP,
|
||||
)
|
||||
self.assertEqual(
|
||||
by_type[CERTIFICATES].current.local_key,
|
||||
"84",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
by_type[PROXY_HOSTS].operation,
|
||||
PlanOperation.UPDATE,
|
||||
)
|
||||
self.assertEqual(
|
||||
by_type[PROXY_HOSTS].reason,
|
||||
"referenced local object is being retired",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
tuple(
|
||||
retirement.object.local_key
|
||||
for retirement in plan.retirements
|
||||
),
|
||||
("67",),
|
||||
)
|
||||
|
||||
def test_equivalent_duplicate_preserves_letsencrypt_copy(self):
|
||||
endpoint = Endpoint(
|
||||
"generic",
|
||||
"node.example:443",
|
||||
)
|
||||
|
||||
generic_adapter = adapter(
|
||||
"generic",
|
||||
"node.example:443",
|
||||
CERTIFICATES,
|
||||
)
|
||||
|
||||
desired = NormalizedObject(
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"valid_from": "2026-09-20T00:00:00+00:00",
|
||||
"expires_on": "2026-12-20T00:00:00+00:00",
|
||||
},
|
||||
)
|
||||
|
||||
imported = NormalizedObject(
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
local_key="90",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"provider": "other",
|
||||
"valid_from": "2026-09-20T00:00:00+00:00",
|
||||
"expires_on": "2026-12-20T00:00:00+00:00",
|
||||
},
|
||||
)
|
||||
|
||||
native = NormalizedObject(
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
local_key="12",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"provider": "letsencrypt",
|
||||
"valid_from": "2026-09-20T00:00:00+00:00",
|
||||
"expires_on": "2026-12-20T00:00:00+00:00",
|
||||
},
|
||||
)
|
||||
|
||||
plans = build_endpoint_plans(
|
||||
[generic_adapter],
|
||||
[desired],
|
||||
[
|
||||
observed(endpoint.identity, imported),
|
||||
observed(endpoint.identity, native),
|
||||
],
|
||||
)
|
||||
|
||||
action = plans[0].actions[0]
|
||||
|
||||
self.assertEqual(
|
||||
action.operation,
|
||||
PlanOperation.NOOP,
|
||||
)
|
||||
self.assertEqual(
|
||||
action.current.local_key,
|
||||
"12",
|
||||
)
|
||||
self.assertEqual(
|
||||
tuple(
|
||||
retirement.object.local_key
|
||||
for retirement in plans[0].retirements
|
||||
),
|
||||
("90",),
|
||||
)
|
||||
|
||||
def test_duplicate_endpoint_objects_fail_without_payload_output(self):
|
||||
desired = [
|
||||
dns_object("app.example.com", "192.168.1.101")
|
||||
|
||||
Reference in New Issue
Block a user