Skip to content

Commit 6f41bdb

Browse files
committed
Add cert-manager support to KafkaReconciler
Signed-off-by: Kate Stanley <[email protected]>
1 parent ad355ac commit 6f41bdb

File tree

16 files changed

+501
-120
lines changed

16 files changed

+501
-120
lines changed

.checkstyle/import-control.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ We also control imports only in production classes and not in tests. This is con
7070
<allow pkg="io.strimzi.platform" />
7171
<allow pkg="io.strimzi.certs" />
7272
<allow pkg="io.strimzi.kafka.config.model" />
73+
<allow pkg="io.strimzi.operator.common.auth" />
7374
<allow pkg="io.strimzi.operator.common.model" />
7475
<allow pkg="io.strimzi.operator.cluster.model" />
7576
<allow class="io.strimzi.operator.common.Reconciliation" />
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright Strimzi authors.
3+
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
4+
*/
5+
package io.strimzi.operator.cluster.model;
6+
7+
import io.fabric8.certmanager.api.model.v1.Certificate;
8+
import io.fabric8.certmanager.api.model.v1.CertificateBuilder;
9+
import io.fabric8.kubernetes.api.model.OwnerReference;
10+
import io.fabric8.kubernetes.api.model.Secret;
11+
import io.strimzi.operator.common.Annotations;
12+
import io.strimzi.operator.common.model.Ca;
13+
import io.strimzi.operator.common.model.ClientsCa;
14+
import io.strimzi.operator.common.model.Labels;
15+
16+
import java.util.HashMap;
17+
import java.util.Map;
18+
import java.util.Objects;
19+
20+
/**
21+
* cert-manager utility methods
22+
*/
23+
public class CertManagerUtils {
24+
private static final String CERT_MANAGER_SECRET_SUFFIX = "-cm";
25+
/**
26+
* Get the name of the Secret managed by cert-manager, given a Strimzi managed Secret
27+
*
28+
* @param strimziSecretName Name of the Secret managed by Strimzi
29+
* @return Secret name to use for cert-manager managed Secret
30+
*/
31+
public static String certManagerSecretName(String strimziSecretName) {
32+
return strimziSecretName + CERT_MANAGER_SECRET_SUFFIX;
33+
}
34+
35+
/**
36+
* Get the name of the Secret managed by Strimzi, given a cert-manager managed Secret
37+
*
38+
* @param certManagerSecretName Name of the Secret managed by cert-manager
39+
* @return Secret name to use for Strimzi managed Secret
40+
*/
41+
public static String strimziSecretName(String certManagerSecretName) {
42+
if (!matchesCertManagerSecretNaming(certManagerSecretName)) {
43+
throw new RuntimeException("Supplied Secret does not match expected format for cert-manager Secret name");
44+
}
45+
return certManagerSecretName.substring(0, certManagerSecretName.length() - CERT_MANAGER_SECRET_SUFFIX.length());
46+
}
47+
48+
/**
49+
* Returns whether the supplied Secret name has the same format as a Secret created by cert-manager
50+
*
51+
* @param secretName Secret name to check
52+
* @return Whether the Secret name matches the format of a Secret created by cert-manager
53+
*/
54+
public static boolean matchesCertManagerSecretNaming(String secretName) {
55+
return secretName.endsWith(CERT_MANAGER_SECRET_SUFFIX);
56+
}
57+
58+
/**
59+
* Build Certificate object to give to cert-manager to generate certificate
60+
*
61+
* @param namespace Namespace for the Certificate
62+
* @param name Name for the Certificate
63+
* @param initialCertificate Certificate to use as a base
64+
* @param labels Labels for Certificate
65+
* @param ownerReference Owner reference for Certificate
66+
* @return Certificate object
67+
*/
68+
public static Certificate buildCertManagerCertificate(String namespace,
69+
String name, Certificate initialCertificate,
70+
Labels labels, OwnerReference ownerReference) {
71+
String secretName = certManagerSecretName(name);
72+
if (ownerReference == null) {
73+
return new CertificateBuilder(initialCertificate)
74+
.withNewMetadata()
75+
.withName(name)
76+
.withNamespace(namespace)
77+
.endMetadata()
78+
.editSpec()
79+
.withSecretName(secretName)
80+
.withNewSecretTemplate()
81+
.withLabels(labels.toMap())
82+
.endSecretTemplate()
83+
.endSpec()
84+
.build();
85+
} else {
86+
return new CertificateBuilder(initialCertificate)
87+
.withNewMetadata()
88+
.withName(name)
89+
.withNamespace(namespace)
90+
.withOwnerReferences(ownerReference)
91+
.endMetadata()
92+
.editSpec()
93+
.withSecretName(secretName)
94+
.withNewSecretTemplate()
95+
.withLabels(labels.toMap())
96+
.endSecretTemplate()
97+
.endSpec()
98+
.build();
99+
}
100+
}
101+
102+
/**
103+
* Builds a clusterCa certificate Secret for the different Strimzi components (TO, UO, KE, ...).
104+
* Used when cert-manager has issued the CA Secret.
105+
*
106+
* @param clusterCa The Cluster CA
107+
* @param clientsCa The Clients CA. If this is not null the Clients CA generation is added
108+
* @param certManagerSecret cert-manager Secret containing the Cluster CA
109+
* @param namespace Namespace for the Secret
110+
* @param secretName Name of the Secret
111+
* @param keyCertName Key under which the certificate will be stored in the new Secret
112+
* @param labels Labels
113+
* @param ownerReference Owner reference
114+
* @return Newly built Secret
115+
*/
116+
public static Secret buildTrustedCertificateSecretFromCertManager(ClusterCa clusterCa, ClientsCa clientsCa, Secret certManagerSecret, String namespace,
117+
String secretName, String keyCertName, Labels labels,
118+
OwnerReference ownerReference) {
119+
String certHash = CertUtils.getCertificateThumbprint(certManagerSecret, "tls.crt");
120+
Objects.requireNonNull(certHash);
121+
122+
Map<String, String> annotations = new HashMap<>();
123+
annotations.put(Annotations.ANNO_STRIMZI_SERVER_CERT_HASH, certHash);
124+
Map.Entry<String, String> clusterCaGeneration = clusterCa.caCertGenerationFullAnnotation();
125+
annotations.put(clusterCaGeneration.getKey(), clusterCaGeneration.getValue());
126+
127+
if (clientsCa != null) {
128+
Map.Entry<String, String> clientsCaGeneration = clientsCa.caCertGenerationFullAnnotation();
129+
annotations.put(clientsCaGeneration.getKey(), clientsCaGeneration.getValue());
130+
}
131+
132+
return ModelUtils.createSecret(secretName, namespace, labels, ownerReference,
133+
Map.of(
134+
Ca.SecretEntry.CRT.asKey(keyCertName), certManagerSecret.getData().get("tls.crt"),
135+
Ca.SecretEntry.KEY.asKey(keyCertName), certManagerSecret.getData().get("tls.key")
136+
),
137+
annotations,
138+
Map.of());
139+
}
140+
141+
/**
142+
* Checks if the hashes of certs in a cert Secret have changed
143+
* @param existingCertSecret Existing cert Secret
144+
* @param newCertSecret New cert Secret
145+
* @return Whether the cert has been updated in the new Secret
146+
*/
147+
public static boolean certManagerCertUpdated(Secret existingCertSecret, Secret newCertSecret) {
148+
String existingCertHash = Annotations.stringAnnotation(existingCertSecret, Annotations.ANNO_STRIMZI_SERVER_CERT_HASH, null);
149+
String newCertHash = Annotations.stringAnnotation(newCertSecret, Annotations.ANNO_STRIMZI_SERVER_CERT_HASH, null);
150+
if (existingCertHash == null || newCertHash == null) {
151+
throw new RuntimeException(String.format("Failed to find server-cert-hash annotation for Secret %s/%s", existingCertSecret.getMetadata().getNamespace(), existingCertSecret.getMetadata().getName()));
152+
}
153+
return !existingCertHash.equals(newCertHash);
154+
}
155+
}

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

Lines changed: 0 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@
44
*/
55
package io.strimzi.operator.cluster.model;
66

7-
import io.fabric8.certmanager.api.model.v1.Certificate;
8-
import io.fabric8.certmanager.api.model.v1.CertificateBuilder;
97
import io.fabric8.kubernetes.api.model.OwnerReference;
108
import io.fabric8.kubernetes.api.model.Secret;
119
import io.fabric8.kubernetes.api.model.Volume;
1210
import io.fabric8.kubernetes.api.model.VolumeMount;
1311
import io.strimzi.api.kafka.model.common.CertSecretSource;
1412
import io.strimzi.certs.CertAndKey;
15-
import io.strimzi.operator.common.Annotations;
1613
import io.strimzi.operator.common.Reconciliation;
1714
import io.strimzi.operator.common.ReconciliationLogger;
1815
import io.strimzi.operator.common.Util;
@@ -38,7 +35,6 @@
3835
import java.util.HashMap;
3936
import java.util.List;
4037
import java.util.Map;
41-
import java.util.Objects;
4238
import java.util.Set;
4339

4440
import static java.util.Collections.emptyMap;
@@ -142,78 +138,6 @@ public static Secret buildTrustedCertificateSecret(Reconciliation reconciliation
142138
return ModelUtils.createSecret(secretName, namespace, labels, ownerReference, secretData, Map.ofEntries(clusterCa.caCertGenerationFullAnnotation()), emptyMap());
143139
}
144140

145-
/**
146-
* Build Certificate object to give to cert-manager to generate certificate
147-
*
148-
* @param clusterCa Cluster CA
149-
* @param namespace Namespace for the Certificate
150-
* @param name Name for the Certificate
151-
* @param commonName Common name for certificates
152-
* @param labels Labels for Certificate
153-
* @param ownerReference Owner reference for Certificate
154-
* @return Certificate object
155-
*/
156-
public static Certificate buildCertManagerCertificate(ClusterCa clusterCa, String namespace,
157-
String name, String commonName,
158-
Labels labels, OwnerReference ownerReference) {
159-
Certificate certificate = clusterCa.getCertManagerCert(commonName, Ca.IO_STRIMZI);
160-
String secretName = name + "cm";
161-
if (ownerReference == null) {
162-
return new CertificateBuilder(certificate)
163-
.withNewMetadata()
164-
.withName(name)
165-
.withNamespace(namespace)
166-
.endMetadata()
167-
.editSpec()
168-
.withSecretName(secretName)
169-
.withNewSecretTemplate()
170-
.withLabels(labels.toMap())
171-
.endSecretTemplate()
172-
.endSpec()
173-
.build();
174-
} else {
175-
return new CertificateBuilder(certificate)
176-
.withNewMetadata()
177-
.withName(name)
178-
.withNamespace(namespace)
179-
.withOwnerReferences(ownerReference)
180-
.endMetadata()
181-
.editSpec()
182-
.withSecretName(secretName)
183-
.withNewSecretTemplate()
184-
.withLabels(labels.toMap())
185-
.endSecretTemplate()
186-
.endSpec()
187-
.build();
188-
}
189-
}
190-
191-
/**
192-
* Builds a clusterCa certificate Secret for the different Strimzi components (TO, UO, KE, ...).
193-
* Used when cert-manager has issued the CA Secret.
194-
* @param clusterCa The Cluster CA
195-
* @param certManagerSecret cert-manager Secret containing the Cluster CA
196-
* @param namespace Namspace for the Secret
197-
* @param secretName Name of the Secret
198-
* @param keyCertName Key under which the certificate will be stored in the new Secret
199-
* @param labels Labels
200-
* @param ownerReference Owner reference
201-
* @return Newly built Secret
202-
*/
203-
public static Secret buildTrustedCertificateSecretFromCertManager(ClusterCa clusterCa, Secret certManagerSecret, String namespace,
204-
String secretName, String keyCertName, Labels labels, OwnerReference ownerReference) {
205-
String certHash = getCertificateShortThumbprint(certManagerSecret, "tls.crt");
206-
Objects.requireNonNull(certHash);
207-
208-
return ModelUtils.createSecret(secretName, namespace, labels, ownerReference,
209-
Map.of(
210-
Ca.SecretEntry.CRT.asKey(keyCertName), certManagerSecret.getData().get("tls.crt"),
211-
Ca.SecretEntry.KEY.asKey(keyCertName), certManagerSecret.getData().get("tls.key")
212-
),
213-
Map.ofEntries(clusterCa.caCertGenerationFullAnnotation(), Map.entry(Annotations.ANNO_STRIMZI_SERVER_CERT_HASH, certHash)),
214-
Map.of());
215-
}
216-
217141
/**
218142
* Constructs a Map containing the provided certificates to be stored in a Kubernetes Secret.
219143
*

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

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
package io.strimzi.operator.cluster.model;
66

7+
import io.fabric8.certmanager.api.model.v1.Certificate;
78
import io.fabric8.kubernetes.api.model.Secret;
89
import io.strimzi.api.kafka.model.common.CertificateAuthority;
910
import io.strimzi.api.kafka.model.common.CertificateExpirationPolicy;
@@ -178,7 +179,43 @@ protected Map<String, CertAndKey> generateBrokerCerts(
178179
Map<Integer, Set<String>> externalAddresses,
179180
boolean isMaintenanceTimeWindowsSatisfied
180181
) throws IOException {
181-
Function<NodeRef, Subject> subjectFn = node -> {
182+
LOGGER.debugCr(reconciliation, "{}: Reconciling kafka broker certificates", this);
183+
return maybeCopyOrGenerateCerts(
184+
reconciliation,
185+
nodes,
186+
kafkaNodeCertsSubjectFn(namespace, clusterName, externalBootstrapAddresses, externalAddresses),
187+
existingCertificates,
188+
isMaintenanceTimeWindowsSatisfied
189+
);
190+
}
191+
192+
/**
193+
* Prepares the Certificate objects for the Kafka nodes.
194+
* Only used when cert-manager is issuing certificates.
195+
*
196+
* @param namespace Namespace of the Kafka cluster
197+
* @param clusterName Name of the Kafka cluster
198+
* @param nodes Nodes that are part of the Kafka cluster
199+
* @param externalBootstrapAddresses List of external bootstrap addresses (used for certificate SANs)
200+
* @param externalAddresses Map with external listener addresses for the different nodes (used for certificate SANs)
201+
*
202+
* @return Map of Certificate resources keyed on the node id
203+
*/
204+
public Map<String, Certificate> generateKafkaNodeCertificateResources(String namespace, String clusterName, Set<NodeRef> nodes,
205+
Set<String> externalBootstrapAddresses,
206+
Map<Integer, Set<String>> externalAddresses) {
207+
Map<String, Certificate> certificates = new HashMap<>();
208+
for (NodeRef node : nodes) {
209+
certificates.put(node.podName(), getCertManagerCert(kafkaNodeCertsSubjectFn(namespace, clusterName, externalBootstrapAddresses, externalAddresses).apply(node)));
210+
}
211+
return certificates;
212+
}
213+
214+
private Function<NodeRef, Subject> kafkaNodeCertsSubjectFn(String namespace, String clusterName,
215+
Set<String> externalBootstrapAddresses,
216+
Map<Integer, Set<String>> externalAddresses
217+
) {
218+
return node -> {
182219
Subject.Builder subject = new Subject.Builder()
183220
.withOrganizationName("io.strimzi")
184221
.withCommonName(KafkaResources.kafkaComponentName(clusterName));
@@ -215,16 +252,6 @@ protected Map<String, CertAndKey> generateBrokerCerts(
215252

216253
return subject.build();
217254
};
218-
219-
LOGGER.debugCr(reconciliation, "{}: Reconciling kafka broker certificates", this);
220-
221-
return maybeCopyOrGenerateCerts(
222-
reconciliation,
223-
nodes,
224-
subjectFn,
225-
existingCertificates,
226-
isMaintenanceTimeWindowsSatisfied
227-
);
228255
}
229256

230257
@Override
@@ -378,7 +405,7 @@ public void maybeDeleteOldCerts() {
378405
}
379406

380407
@Override
381-
public void updateCertAndGenerations(String caCert, X509Certificate endEntityCertificate) {
408+
public void updateCertAndIncrementGenerations(String caCert, X509Certificate endEntityCertificate) {
382409
if (endEntityCertificate == null) {
383410
// Cluster operator certificate is missing, so no cert path validation to perform
384411
LOGGER.warnCr(reconciliation, "Strimzi CA cert Secret containing custom cert has been created, but operator Secret is missing");

0 commit comments

Comments
 (0)