Skip to content

Commit eb6068e

Browse files
authored
Refactor Ca and CaReconciler (strimzi#11400)
Signed-off-by: Katherine Stanley <11195226+katheris@users.noreply.github.com>
1 parent 6d32d44 commit eb6068e

7 files changed

Lines changed: 282 additions & 236 deletions

File tree

cluster-operator/src/main/java/io/strimzi/operator/cluster/model/ClusterCa.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,9 +362,9 @@ private List<String> getSubjectAltNames(byte[] certificate) {
362362
public void maybeDeleteOldCerts() {
363363
// the operator doesn't have to touch Secret provided by the user with his own custom CA certificate
364364
if (this.generateCa) {
365-
this.caCertsRemoved = removeCerts(this.caCertSecret.getData(), entry -> OLD_CA_CERT_PATTERN.matcher(entry.getKey()).matches()) > 0;
366-
if (this.caCertsRemoved) {
365+
if (removeCerts(this.caCertData, entry -> OLD_CA_CERT_PATTERN.matcher(entry.getKey()).matches())) {
367366
LOGGER.infoCr(reconciliation, "{}: Old CA certificates removed", this);
367+
this.caCertsRemoved = true;
368368
}
369369
}
370370
}

cluster-operator/src/main/java/io/strimzi/operator/cluster/operator/assembly/CaReconciler.java

Lines changed: 125 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import io.fabric8.kubernetes.api.model.OwnerReferenceBuilder;
99
import io.fabric8.kubernetes.api.model.Pod;
1010
import io.fabric8.kubernetes.api.model.Secret;
11+
import io.fabric8.kubernetes.api.model.SecretBuilder;
12+
import io.strimzi.api.ResourceAnnotations;
1113
import io.strimzi.api.kafka.model.common.CertificateAuthority;
1214
import io.strimzi.api.kafka.model.kafka.Kafka;
1315
import io.strimzi.api.kafka.model.kafka.KafkaResources;
@@ -52,11 +54,17 @@
5254

5355
import java.time.Clock;
5456
import java.util.ArrayList;
57+
import java.util.HashMap;
5558
import java.util.List;
5659
import java.util.Map;
5760
import java.util.Set;
5861
import java.util.stream.Collectors;
5962

63+
import static io.strimzi.operator.common.model.Ca.ANNO_STRIMZI_IO_CA_CERT_GENERATION;
64+
import static io.strimzi.operator.common.model.Ca.ANNO_STRIMZI_IO_CA_KEY_GENERATION;
65+
import static java.util.Collections.emptyList;
66+
import static java.util.Collections.singletonList;
67+
6068
/**
6169
* Class used for reconciliation of Cluster and Client CAs. This class contains both the steps of the CA reconciliation
6270
* pipeline and is also used to store the state between them.
@@ -91,6 +99,7 @@ public class CaReconciler {
9199
// Fields used to store state during the reconciliation
92100
private ClusterCa clusterCa;
93101
private ClientsCa clientsCa;
102+
private Secret clusterCaCertSecret;
94103
private Secret coSecret;
95104

96105
/* test */ boolean isClusterCaNeedFullTrust;
@@ -225,64 +234,79 @@ Future<Void> reconcileCas(Clock clock) {
225234

226235
return secretOperator.listAsync(reconciliation.namespace(), Labels.EMPTY.withStrimziKind(reconciliation.kind()).withStrimziCluster(reconciliation.name()))
227236
.compose(clusterSecrets -> {
228-
Secret clusterCaCertSecret = null;
229-
Secret clusterCaKeySecret = null;
230-
Secret clientsCaCertSecret = null;
231-
Secret clientsCaKeySecret = null;
237+
Secret existingClusterCaCertSecret = null;
238+
Secret existingClusterCaKeySecret = null;
239+
Secret existingClientsCaCertSecret = null;
240+
Secret existingClientsCaKeySecret = null;
232241

233242
for (Secret secret : clusterSecrets) {
234243
String secretName = secret.getMetadata().getName();
235244
if (secretName.equals(clusterCaCertName)) {
236-
clusterCaCertSecret = secret;
245+
existingClusterCaCertSecret = secret;
237246
} else if (secretName.equals(clusterCaKeyName)) {
238-
clusterCaKeySecret = secret;
247+
existingClusterCaKeySecret = secret;
239248
} else if (secretName.equals(clientsCaCertName)) {
240-
clientsCaCertSecret = secret;
249+
existingClientsCaCertSecret = secret;
241250
} else if (secretName.equals(clientsCaKeyName)) {
242-
clientsCaKeySecret = secret;
251+
existingClientsCaKeySecret = secret;
243252
}
244253
}
245254

246-
clusterCa = new ClusterCa(reconciliation, certManager, passwordGenerator, reconciliation.name(), clusterCaCertSecret,
247-
clusterCaKeySecret,
255+
boolean generateClusterCa = clusterCaConfig == null || clusterCaConfig.isGenerateCertificateAuthority();
256+
boolean generateClientsCa = clientsCaConfig == null || clientsCaConfig.isGenerateCertificateAuthority();
257+
258+
clusterCa = new ClusterCa(reconciliation, certManager, passwordGenerator,
259+
reconciliation.name(),
260+
existingClusterCaCertSecret,
261+
existingClusterCaKeySecret,
248262
ModelUtils.getCertificateValidity(clusterCaConfig),
249263
ModelUtils.getRenewalDays(clusterCaConfig),
250-
clusterCaConfig == null || clusterCaConfig.isGenerateCertificateAuthority(), clusterCaConfig != null ? clusterCaConfig.getCertificateExpirationPolicy() : null);
251-
clusterCa.createRenewOrReplace(
252-
reconciliation.namespace(), caLabels,
253-
clusterCaCertLabels, clusterCaCertAnnotations,
254-
clusterCaConfig != null && !clusterCaConfig.isGenerateSecretOwnerReference() ? null : ownerRef,
255-
Util.isMaintenanceTimeWindowsSatisfied(reconciliation, maintenanceWindows, clock.instant()));
256-
257-
clientsCa = new ClientsCa(reconciliation, certManager,
258-
passwordGenerator, clientsCaCertName,
259-
clientsCaCertSecret, clientsCaKeyName,
260-
clientsCaKeySecret,
264+
generateClusterCa,
265+
clusterCaConfig != null ? clusterCaConfig.getCertificateExpirationPolicy() : null);
266+
267+
268+
clientsCa = new ClientsCa(reconciliation, certManager, passwordGenerator,
269+
clientsCaCertName, existingClientsCaCertSecret,
270+
clientsCaKeyName, existingClientsCaKeySecret,
261271
ModelUtils.getCertificateValidity(clientsCaConfig),
262272
ModelUtils.getRenewalDays(clientsCaConfig),
263-
clientsCaConfig == null || clientsCaConfig.isGenerateCertificateAuthority(),
273+
generateClientsCa,
264274
clientsCaConfig != null ? clientsCaConfig.getCertificateExpirationPolicy() : null);
265-
clientsCa.createRenewOrReplace(reconciliation.namespace(),
266-
caLabels, Map.of(), Map.of(),
267-
clientsCaConfig != null && !clientsCaConfig.isGenerateSecretOwnerReference() ? null : ownerRef,
268-
Util.isMaintenanceTimeWindowsSatisfied(reconciliation, maintenanceWindows, clock.instant()));
269-
270-
Promise<Void> caUpdatePromise = Promise.promise();
271275

272276
List<Future<ReconcileResult<Secret>>> secretReconciliations = new ArrayList<>(2);
273277

274-
if (clusterCaConfig == null || clusterCaConfig.isGenerateCertificateAuthority()) {
275-
Future<ReconcileResult<Secret>> clusterSecretReconciliation = secretOperator.reconcile(reconciliation, reconciliation.namespace(), clusterCaCertName, clusterCa.caCertSecret())
276-
.compose(ignored -> secretOperator.reconcile(reconciliation, reconciliation.namespace(), clusterCaKeyName, clusterCa.caKeySecret()));
278+
if (generateClusterCa) {
279+
clusterCa.createRenewOrReplace(Util.isMaintenanceTimeWindowsSatisfied(reconciliation, maintenanceWindows, clock.instant()),
280+
isForceReplace(existingClusterCaKeySecret),
281+
isForceRenew(existingClusterCaCertSecret));
282+
283+
OwnerReference ownerReference = clusterCaConfig != null && !clusterCaConfig.isGenerateSecretOwnerReference() ? null : ownerRef;
284+
clusterCaCertSecret = createCaCertSecret(clusterCaCertName, clusterCaCertLabels, clusterCaCertAnnotations, ownerReference, clusterCa, existingClusterCaCertSecret);
285+
Secret clusterCaKeySecret = createCaKeySecret(clusterCaKeyName, ownerReference, clusterCa, existingClusterCaKeySecret);
286+
287+
Future<ReconcileResult<Secret>> clusterSecretReconciliation = secretOperator.reconcile(reconciliation, reconciliation.namespace(), clusterCaCertName, clusterCaCertSecret)
288+
.compose(ignored -> secretOperator.reconcile(reconciliation, reconciliation.namespace(), clusterCaKeyName, clusterCaKeySecret));
277289
secretReconciliations.add(clusterSecretReconciliation);
290+
} else {
291+
clusterCaCertSecret = existingClusterCaCertSecret;
278292
}
279293

280-
if (clientsCaConfig == null || clientsCaConfig.isGenerateCertificateAuthority()) {
281-
Future<ReconcileResult<Secret>> clientsSecretReconciliation = secretOperator.reconcile(reconciliation, reconciliation.namespace(), clientsCaCertName, clientsCa.caCertSecret())
282-
.compose(ignored -> secretOperator.reconcile(reconciliation, reconciliation.namespace(), clientsCaKeyName, clientsCa.caKeySecret()));
294+
if (generateClientsCa) {
295+
clientsCa.createRenewOrReplace(Util.isMaintenanceTimeWindowsSatisfied(reconciliation, maintenanceWindows, clock.instant()),
296+
isForceReplace(existingClientsCaKeySecret),
297+
isForceRenew(existingClientsCaCertSecret));
298+
299+
OwnerReference ownerReference = clientsCaConfig != null && !clientsCaConfig.isGenerateSecretOwnerReference() ? null : ownerRef;
300+
Secret clientsCaCertSecret = createCaCertSecret(clientsCaCertName, Map.of(), Map.of(), ownerReference, clientsCa, existingClientsCaCertSecret);
301+
Secret clientsCaKeySecret = createCaKeySecret(clientsCaKeyName, ownerReference, clientsCa, existingClientsCaKeySecret);
302+
303+
Future<ReconcileResult<Secret>> clientsSecretReconciliation = secretOperator.reconcile(reconciliation, reconciliation.namespace(), clientsCaCertName, clientsCaCertSecret)
304+
.compose(ignored -> secretOperator.reconcile(reconciliation, reconciliation.namespace(), clientsCaKeyName, clientsCaKeySecret));
283305
secretReconciliations.add(clientsSecretReconciliation);
284306
}
285307

308+
Promise<Void> caUpdatePromise = Promise.promise();
309+
286310
Future.join(secretReconciliations).onComplete(res -> {
287311
if (res.succeeded()) {
288312
caUpdatePromise.complete();
@@ -334,7 +358,7 @@ Future<Void> reconcileClusterOperatorSecret(Clock clock) {
334358
* Maybe perform a rolling update of the cluster to update the CA certificates in component truststores.
335359
* This is only necessary when the Cluster CA certificate has changed due to a new CA key.
336360
* It is not necessary when the CA certificate is renewed while retaining the existing key.
337-
*
361+
* <p>
338362
* If Strimzi did not replace the CA key during the current reconciliation, {@code isClusterCaNeedFullTrust} is used to:
339363
* * continue from a previous CA key replacement which didn't end successfully (i.e. CO stopped)
340364
* * track key replacements when the user is managing the CA
@@ -345,7 +369,7 @@ Future<Void> reconcileClusterOperatorSecret(Clock clock) {
345369
Future<Void> maybeRollingUpdateForNewClusterCaKey() {
346370
if (clusterCa.keyReplaced() || isClusterCaNeedFullTrust) {
347371
RestartReason restartReason = RestartReason.CLUSTER_CA_CERT_KEY_REPLACED;
348-
TlsPemIdentity coTlsPemIdentity = new TlsPemIdentity(new PemTrustSet(clusterCa.caCertSecret()), PemAuthIdentity.clusterOperator(coSecret));
372+
TlsPemIdentity coTlsPemIdentity = new TlsPemIdentity(new PemTrustSet(clusterCaCertSecret), PemAuthIdentity.clusterOperator(coSecret));
349373
return patchClusterCaKeyGenerationAndReturnNodes()
350374
.compose(nodes -> rollKafkaBrokers(nodes, RestartReasons.of(restartReason), coTlsPemIdentity))
351375
.compose(i -> rollDeploymentIfExists(KafkaResources.entityOperatorDeploymentName(reconciliation.name()), restartReason))
@@ -358,11 +382,11 @@ Future<Void> maybeRollingUpdateForNewClusterCaKey() {
358382

359383
/**
360384
* Gather the Kafka related components pods for checking Cluster CA key trust and Cluster CA certificate usage to sign servers certificate.
361-
*
385+
* <p>
362386
* Verify that all the pods are already trusting the new CA certificate signed by a new CA key.
363387
* It checks each pod's CA key generation, compared with the new CA key generation.
364388
* When the trusting phase is not completed (i.e. because CO stopped), it needs to be recovered from where it was left.
365-
*
389+
* <p>
366390
* Verify that all pods are already using the new CA certificate to sign server certificates.
367391
* It checks each pod's CA certificate generation, compared with the new CA certificate generation.
368392
* When the new CA certificate is used everywhere, the old CA certificate can be removed.
@@ -519,7 +543,7 @@ Future<Void> maybeRollingUpdateForNewClusterCaKey() {
519543
clusterCa.maybeDeleteOldCerts();
520544

521545
if (clusterCa.certsRemoved()) {
522-
return secretOperator.reconcile(reconciliation, reconciliation.namespace(), AbstractModel.clusterCaCertSecretName(reconciliation.name()), clusterCa.caCertSecret())
546+
return secretOperator.reconcile(reconciliation, reconciliation.namespace(), AbstractModel.clusterCaCertSecretName(reconciliation.name()), clusterCaCertSecret)
523547
.mapEmpty();
524548
} else {
525549
return Future.succeededFuture();
@@ -529,6 +553,68 @@ Future<Void> maybeRollingUpdateForNewClusterCaKey() {
529553
}
530554
}
531555

556+
private boolean isForceReplace(Secret caSecret) {
557+
if (caSecret != null && caSecret.getMetadata() != null &&
558+
Annotations.hasAnnotation(caSecret, ResourceAnnotations.ANNO_STRIMZI_IO_FORCE_REPLACE)) {
559+
return Annotations.booleanAnnotation(caSecret, ResourceAnnotations.ANNO_STRIMZI_IO_FORCE_REPLACE, false);
560+
} else {
561+
return false;
562+
}
563+
}
564+
565+
private boolean isForceRenew(Secret caSecret) {
566+
if (caSecret != null && caSecret.getMetadata() != null &&
567+
Annotations.hasAnnotation(caSecret, ResourceAnnotations.ANNO_STRIMZI_IO_FORCE_RENEW)) {
568+
return Annotations.booleanAnnotation(caSecret, ResourceAnnotations.ANNO_STRIMZI_IO_FORCE_RENEW, false);
569+
} else {
570+
return false;
571+
}
572+
}
573+
574+
private Secret createCaCertSecret(String secretName, Map<String, String> additionalLabels, Map<String, String> additionalAnnotations,
575+
OwnerReference ownerReference, Ca ca, Secret existingCaCertSecret) {
576+
Map<String, String> certAnnotations = new HashMap<>(2);
577+
certAnnotations.put(ANNO_STRIMZI_IO_CA_CERT_GENERATION, String.valueOf(ca.caCertGeneration()));
578+
579+
if (ca.postponed()
580+
&& existingCaCertSecret != null
581+
&& Annotations.hasAnnotation(existingCaCertSecret, Annotations.ANNO_STRIMZI_IO_FORCE_RENEW)) {
582+
certAnnotations.put(Annotations.ANNO_STRIMZI_IO_FORCE_RENEW, Annotations.stringAnnotation(existingCaCertSecret, Annotations.ANNO_STRIMZI_IO_FORCE_RENEW, "false"));
583+
}
584+
return createCaSecret(secretName, ca.caCertData(), Util.mergeLabelsOrAnnotations(caLabels, additionalLabels),
585+
Util.mergeLabelsOrAnnotations(certAnnotations, additionalAnnotations), ownerReference);
586+
587+
}
588+
589+
private Secret createCaKeySecret(String secretName, OwnerReference ownerReference, Ca ca, Secret existingCaKeySecret) {
590+
Map<String, String> keyAnnotations = new HashMap<>(2);
591+
keyAnnotations.put(ANNO_STRIMZI_IO_CA_KEY_GENERATION, String.valueOf(ca.caKeyGeneration()));
592+
593+
if (ca.postponed()
594+
&& existingCaKeySecret != null
595+
&& Annotations.hasAnnotation(existingCaKeySecret, Annotations.ANNO_STRIMZI_IO_FORCE_REPLACE)) {
596+
keyAnnotations.put(Annotations.ANNO_STRIMZI_IO_FORCE_REPLACE, Annotations.stringAnnotation(existingCaKeySecret, Annotations.ANNO_STRIMZI_IO_FORCE_REPLACE, "false"));
597+
}
598+
return createCaSecret(secretName, ca.caKeyData(), caLabels, keyAnnotations, ownerReference);
599+
600+
}
601+
602+
private Secret createCaSecret(String name, Map<String, String> data,
603+
Map<String, String> labels, Map<String, String> annotations, OwnerReference ownerReference) {
604+
List<OwnerReference> or = ownerReference != null ? singletonList(ownerReference) : emptyList();
605+
return new SecretBuilder()
606+
.withNewMetadata()
607+
.withName(name)
608+
.withNamespace(reconciliation.namespace())
609+
.withLabels(labels)
610+
.withAnnotations(annotations)
611+
.withOwnerReferences(or)
612+
.endMetadata()
613+
.withType("Opaque")
614+
.withData(data)
615+
.build();
616+
}
617+
532618
/**
533619
* Helper class to pass both Cluster and Clients CA as a result of the reconciliation
534620
*

0 commit comments

Comments
 (0)