Add rollback-safe NPM certificate create
This commit is contained in:
@@ -3,11 +3,14 @@ from typing import Any
|
||||
from ...adapters import (
|
||||
CERTIFICATES,
|
||||
PROXY_HOSTS,
|
||||
ApplyResult,
|
||||
Capability,
|
||||
CapabilityOperation,
|
||||
DiscoveredObject,
|
||||
NormalizedObject,
|
||||
ObjectReference,
|
||||
PlanAction,
|
||||
PlanOperation,
|
||||
)
|
||||
from ...certificates.material import CertificateMaterial
|
||||
from ...npm_client import NpmClient
|
||||
@@ -312,9 +315,142 @@ class Plugin:
|
||||
|
||||
return baseline
|
||||
|
||||
def apply(self, action):
|
||||
def apply(
|
||||
self,
|
||||
action: PlanAction,
|
||||
) -> ApplyResult | None:
|
||||
desired = action.desired
|
||||
|
||||
if desired is None:
|
||||
raise RuntimeError(
|
||||
"NPM apply requires desired state"
|
||||
)
|
||||
|
||||
if action.endpoint_id != self.endpoint.identity:
|
||||
raise RuntimeError(
|
||||
"NPM apply action endpoint does not match adapter"
|
||||
)
|
||||
|
||||
if (
|
||||
desired.object_type == CERTIFICATES
|
||||
and action.operation == PlanOperation.CREATE
|
||||
):
|
||||
return self._create_certificate(desired)
|
||||
|
||||
raise RuntimeError(
|
||||
f"{self.endpoint.identity} does not support apply yet"
|
||||
f"{self.endpoint.identity} does not support "
|
||||
f"{action.operation} for {desired.object_type}"
|
||||
)
|
||||
|
||||
def _create_certificate(
|
||||
self,
|
||||
desired: NormalizedObject,
|
||||
) -> ApplyResult:
|
||||
material = self._certificate_material_source(
|
||||
desired
|
||||
)
|
||||
|
||||
domains = desired.payload.get("domain_names")
|
||||
|
||||
if (
|
||||
not isinstance(domains, list)
|
||||
or not domains
|
||||
or not all(
|
||||
isinstance(value, str) and value
|
||||
for value in domains
|
||||
)
|
||||
):
|
||||
raise RuntimeError(
|
||||
"NPM certificate create requires domain_names"
|
||||
)
|
||||
|
||||
nice_name = desired.payload.get("nice_name")
|
||||
|
||||
if not isinstance(nice_name, str):
|
||||
nice_name = ""
|
||||
|
||||
nice_name = nice_name.strip()
|
||||
|
||||
if not nice_name:
|
||||
nice_name = (
|
||||
"dns-sync: "
|
||||
+ ", ".join(domains)
|
||||
)
|
||||
|
||||
created = self.client.create_certificate(
|
||||
{
|
||||
# This node is receiving existing certificate
|
||||
# material. It must not independently perform ACME
|
||||
# issuance.
|
||||
"provider": "other",
|
||||
"domain_names": list(domains),
|
||||
"nice_name": nice_name,
|
||||
"meta": {
|
||||
"cluster_managed": True,
|
||||
"acme_renew": False,
|
||||
"dns_challenge": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
created_id = created.get("id")
|
||||
|
||||
if (
|
||||
isinstance(created_id, bool)
|
||||
or not isinstance(created_id, int)
|
||||
or created_id < 1
|
||||
):
|
||||
raise RuntimeError(
|
||||
"NPM certificate create returned invalid id"
|
||||
)
|
||||
|
||||
local_key = str(created_id)
|
||||
|
||||
try:
|
||||
self.client.upload_certificate(
|
||||
local_key,
|
||||
certificate=material.certificate,
|
||||
certificate_key=material.certificate_key,
|
||||
intermediate_certificate=(
|
||||
material.intermediate_certificate
|
||||
),
|
||||
)
|
||||
|
||||
row = self.client.certificate(local_key)
|
||||
|
||||
actual = normalize_discovered_object(
|
||||
self._certificate_from_row(row)
|
||||
)
|
||||
|
||||
if actual.local_key != local_key:
|
||||
raise RuntimeError(
|
||||
"Created NPM certificate returned "
|
||||
"unexpected local id"
|
||||
)
|
||||
|
||||
if not self._portable_certificate_matches(
|
||||
actual,
|
||||
desired,
|
||||
):
|
||||
raise RuntimeError(
|
||||
"Created NPM certificate does not match "
|
||||
"desired portable certificate state"
|
||||
)
|
||||
|
||||
except Exception:
|
||||
try:
|
||||
self.client.delete_certificate(local_key)
|
||||
except Exception as rollback_error:
|
||||
raise RuntimeError(
|
||||
"NPM certificate creation failed and "
|
||||
"rollback also failed"
|
||||
) from rollback_error
|
||||
|
||||
raise
|
||||
|
||||
return ApplyResult(
|
||||
object=actual,
|
||||
detail="created certificate",
|
||||
)
|
||||
|
||||
def verify(self, object: NormalizedObject) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user