Skip to content

Commit a4953e0

Browse files
authored
Fix CC not restarted when API secret changes (strimzi#9616)
Signed-off-by: Federico Valeri <fedevaleri@gmail.com>
1 parent d916d4d commit a4953e0

File tree

7 files changed

+77
-12
lines changed

7 files changed

+77
-12
lines changed

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

+8-4
Original file line numberDiff line numberDiff line change
@@ -415,10 +415,11 @@ protected List<EnvVar> getEnvVars() {
415415
/**
416416
* Creates Cruise Control API auth usernames, passwords, and credentials file
417417
*
418+
* @param passwordGenerator The password generator for API users
419+
*
418420
* @return Map containing Cruise Control API auth credentials
419421
*/
420-
public static Map<String, String> generateCruiseControlApiCredentials() {
421-
PasswordGenerator passwordGenerator = new PasswordGenerator(16);
422+
public static Map<String, String> generateCruiseControlApiCredentials(PasswordGenerator passwordGenerator) {
422423
String apiAdminPassword = passwordGenerator.generate();
423424
String apiUserPassword = passwordGenerator.generate();
424425

@@ -441,10 +442,13 @@ public static Map<String, String> generateCruiseControlApiCredentials() {
441442
/**
442443
* Generate the Secret containing the Cruise Control API auth credentials.
443444
*
445+
* @param passwordGenerator The password generator for API users
446+
*
444447
* @return The generated Secret.
445448
*/
446-
public Secret generateApiSecret() {
447-
return ModelUtils.createSecret(CruiseControlResources.apiSecretName(cluster), namespace, labels, ownerReference, generateCruiseControlApiCredentials(), Collections.emptyMap(), Collections.emptyMap());
449+
public Secret generateApiSecret(PasswordGenerator passwordGenerator) {
450+
return ModelUtils.createSecret(CruiseControlResources.apiSecretName(cluster), namespace, labels, ownerReference,
451+
generateCruiseControlApiCredentials(passwordGenerator), Collections.emptyMap(), Collections.emptyMap());
448452
}
449453

450454
/**

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

+15-6
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
import io.strimzi.operator.cluster.model.KafkaVersion;
2121
import io.strimzi.operator.cluster.model.NodeRef;
2222
import io.strimzi.operator.cluster.operator.resource.ResourceOperatorSupplier;
23+
import io.strimzi.operator.common.Annotations;
2324
import io.strimzi.operator.common.Reconciliation;
2425
import io.strimzi.operator.common.ReconciliationLogger;
2526
import io.strimzi.operator.common.Util;
2627
import io.strimzi.operator.common.model.Ca;
2728
import io.strimzi.operator.common.model.Labels;
29+
import io.strimzi.operator.common.model.PasswordGenerator;
2830
import io.strimzi.operator.common.operator.resource.ConfigMapOperator;
2931
import io.strimzi.operator.common.operator.resource.DeploymentOperator;
3032
import io.strimzi.operator.common.operator.resource.NetworkPolicyOperator;
@@ -40,7 +42,6 @@
4042
import java.util.Map;
4143
import java.util.Set;
4244

43-
4445
/**
4546
* Class used for reconciliation of Cruise Control. This class contains both the steps of the Cruise Control
4647
* reconciliation pipeline and is also used to store the state between them.
@@ -63,18 +64,21 @@ public class CruiseControlReconciler {
6364
private final ServiceOperator serviceOperator;
6465
private final NetworkPolicyOperator networkPolicyOperator;
6566
private final ConfigMapOperator configMapOperator;
67+
private final PasswordGenerator passwordGenerator;
6668

6769
private boolean existingCertsChanged = false;
6870

6971
private String serverConfigurationHash = "";
7072
private String capacityConfigurationHash = "";
71-
73+
private String apiSecretHash = "";
74+
7275
/**
7376
* Constructs the Cruise Control reconciler
7477
*
7578
* @param reconciliation Reconciliation marker
7679
* @param config Cluster Operator Configuration
7780
* @param supplier Supplier with Kubernetes Resource Operators
81+
* @param passwordGenerator The password generator for API users
7882
* @param kafkaAssembly The Kafka custom resource
7983
* @param versions The supported Kafka versions
8084
* @param kafkaBrokerNodes List of the broker nodes which are part of the Kafka cluster
@@ -87,6 +91,7 @@ public CruiseControlReconciler(
8791
Reconciliation reconciliation,
8892
ClusterOperatorConfig config,
8993
ResourceOperatorSupplier supplier,
94+
PasswordGenerator passwordGenerator,
9095
Kafka kafkaAssembly,
9196
KafkaVersion.Lookup versions,
9297
Set<NodeRef> kafkaBrokerNodes,
@@ -102,7 +107,8 @@ public CruiseControlReconciler(
102107
this.operatorNamespace = config.getOperatorNamespace();
103108
this.operatorNamespaceLabels = config.getOperatorNamespaceLabels();
104109
this.isNetworkPolicyGeneration = config.isNetworkPolicyGeneration();
105-
110+
this.passwordGenerator = passwordGenerator;
111+
106112
this.deploymentOperator = supplier.deploymentOperations;
107113
this.secretOperator = supplier.secretOperations;
108114
this.serviceAccountOperator = supplier.serviceAccountOperations;
@@ -240,15 +246,17 @@ protected Future<Void> apiSecret() {
240246
if (cruiseControl != null) {
241247
return secretOperator.getAsync(reconciliation.namespace(), CruiseControlResources.apiSecretName(reconciliation.name()))
242248
.compose(oldSecret -> {
243-
Secret newSecret = cruiseControl.generateApiSecret();
249+
Secret newSecret = cruiseControl.generateApiSecret(passwordGenerator);
244250

245251
if (oldSecret != null) {
246252
// The credentials should not change with every release
247253
// So if the secret with credentials already exists, we re-use the values
248254
// But we use the new secret to update labels etc. if needed
249255
newSecret.setData(oldSecret.getData());
250256
}
251-
257+
258+
this.apiSecretHash = ReconcilerUtils.hashSecretContent(newSecret);
259+
252260
return secretOperator.reconcile(reconciliation, reconciliation.namespace(), CruiseControlResources.apiSecretName(reconciliation.name()), newSecret)
253261
.map((Void) null);
254262
});
@@ -285,7 +293,8 @@ protected Future<Void> deployment(boolean isOpenShift, ImagePullPolicy imagePull
285293
podAnnotations.put(Ca.ANNO_STRIMZI_IO_CLUSTER_CA_KEY_GENERATION, String.valueOf(clusterCa.caKeyGeneration()));
286294
podAnnotations.put(CruiseControl.ANNO_STRIMZI_SERVER_CONFIGURATION_HASH, serverConfigurationHash);
287295
podAnnotations.put(CruiseControl.ANNO_STRIMZI_CAPACITY_CONFIGURATION_HASH, capacityConfigurationHash);
288-
296+
podAnnotations.put(Annotations.ANNO_STRIMZI_AUTH_HASH, apiSecretHash);
297+
289298
Deployment deployment = cruiseControl.generateDeployment(podAnnotations, isOpenShift, imagePullPolicy, imagePullSecrets);
290299

291300
return deploymentOperator

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

+1
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ CruiseControlReconciler cruiseControlReconciler() {
699699
reconciliation,
700700
config,
701701
supplier,
702+
passwordGenerator,
702703
kafkaAssembly,
703704
versions,
704705
kafkaBrokerNodes,

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

+23
Original file line numberDiff line numberDiff line change
@@ -381,4 +381,27 @@ public static boolean nodePoolsEnabled(Kafka kafka) {
381381
public static boolean kraftEnabled(Kafka kafka) {
382382
return KafkaCluster.ENABLED_VALUE_STRIMZI_IO_KRAFT.equals(Annotations.stringAnnotation(kafka, Annotations.ANNO_STRIMZI_IO_KRAFT, "disabled").toLowerCase(Locale.ENGLISH));
383383
}
384+
385+
/**
386+
* Creates a hash from Secret's content.
387+
*
388+
* @param secret Secret with content.
389+
* @return Hash of the secret content.
390+
*/
391+
public static String hashSecretContent(Secret secret) {
392+
if (secret == null) {
393+
throw new RuntimeException("Secret not found");
394+
}
395+
396+
if (secret.getData() == null || secret.getData().isEmpty()) {
397+
throw new RuntimeException("Empty secret");
398+
}
399+
400+
StringBuilder sb = new StringBuilder();
401+
secret.getData().entrySet().stream()
402+
.sorted(Map.Entry.comparingByKey())
403+
.forEach(entry -> sb.append(entry.getKey()).append(entry.getValue()));
404+
405+
return Util.hashStub(sb.toString());
406+
}
384407
}

cluster-operator/src/test/java/io/strimzi/operator/cluster/operator/assembly/CruiseControlReconcilerTest.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.strimzi.operator.cluster.model.KafkaVersion;
2626
import io.strimzi.operator.cluster.model.NodeRef;
2727
import io.strimzi.operator.cluster.operator.resource.ResourceOperatorSupplier;
28+
import io.strimzi.operator.common.Annotations;
2829
import io.strimzi.operator.common.Reconciliation;
2930
import io.strimzi.operator.common.model.PasswordGenerator;
3031
import io.strimzi.operator.common.operator.MockCertManager;
@@ -78,7 +79,8 @@ public void reconcileEnabledCruiseControl(VertxTestContext context) {
7879
ServiceOperator mockServiceOps = supplier.serviceOperations;
7980
NetworkPolicyOperator mockNetPolicyOps = supplier.networkPolicyOperator;
8081
ConfigMapOperator mockCmOps = supplier.configMapOperations;
81-
82+
PasswordGenerator mockPasswordGenerator = new PasswordGenerator(10, "a", "a");
83+
8284
ArgumentCaptor<ServiceAccount> saCaptor = ArgumentCaptor.forClass(ServiceAccount.class);
8385
when(mockSaOps.reconcile(any(), eq(NAMESPACE), eq(CruiseControlResources.serviceAccountName(NAME)), saCaptor.capture())).thenReturn(Future.succeededFuture());
8486

@@ -119,6 +121,7 @@ public void reconcileEnabledCruiseControl(VertxTestContext context) {
119121
Reconciliation.DUMMY_RECONCILIATION,
120122
ResourceUtils.dummyClusterOperatorConfig(),
121123
supplier,
124+
mockPasswordGenerator,
122125
kafka,
123126
VERSIONS,
124127
NODES,
@@ -151,6 +154,7 @@ public void reconcileEnabledCruiseControl(VertxTestContext context) {
151154
assertThat(deployCaptor.getValue(), is(notNullValue()));
152155
assertThat(deployCaptor.getAllValues().get(0).getSpec().getTemplate().getMetadata().getAnnotations().get(CruiseControl.ANNO_STRIMZI_SERVER_CONFIGURATION_HASH), is("f6dc41c7"));
153156
assertThat(deployCaptor.getAllValues().get(0).getSpec().getTemplate().getMetadata().getAnnotations().get(CruiseControl.ANNO_STRIMZI_CAPACITY_CONFIGURATION_HASH), is("1eb49220"));
157+
assertThat(deployCaptor.getAllValues().get(0).getSpec().getTemplate().getMetadata().getAnnotations().get(Annotations.ANNO_STRIMZI_AUTH_HASH), is("27ada64b"));
154158

155159
async.flag();
156160
})));
@@ -202,6 +206,7 @@ public void reconcileDisabledCruiseControl(VertxTestContext context) {
202206
Reconciliation.DUMMY_RECONCILIATION,
203207
ResourceUtils.dummyClusterOperatorConfig(),
204208
supplier,
209+
new PasswordGenerator(16),
205210
kafka,
206211
VERSIONS,
207212
NODES,

cluster-operator/src/test/java/io/strimzi/operator/cluster/operator/assembly/ReconcilerUtilsTest.java

+22
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,28 @@ public void testEnabledJmxWithAuthWithExistingSecret(VertxTestContext context) {
290290
async.flag();
291291
})));
292292
}
293+
294+
@Test
295+
public void testHashSecretContent() {
296+
Secret secret = new SecretBuilder()
297+
.addToData(Map.of("username", "foo"))
298+
.addToData(Map.of("password", "changeit"))
299+
.build();
300+
assertThat(ReconcilerUtils.hashSecretContent(secret), is("756937ae"));
301+
}
302+
303+
@Test
304+
public void testHashSecretContentWithNoData() {
305+
Secret secret = new SecretBuilder().build();
306+
RuntimeException ex = assertThrows(RuntimeException.class, () -> ReconcilerUtils.hashSecretContent(secret));
307+
assertThat(ex.getMessage(), is("Empty secret"));
308+
}
309+
310+
@Test
311+
public void testHashSecretContentWithNoSecret() {
312+
RuntimeException ex = assertThrows(RuntimeException.class, () -> ReconcilerUtils.hashSecretContent(null));
313+
assertThat(ex.getMessage(), is("Secret not found"));
314+
}
293315

294316
static class MockJmxCluster implements SupportsJmx {
295317
private final JmxModel jmx;

cluster-operator/src/test/java/io/strimzi/operator/cluster/operator/resource/cruisecontrol/MockCruiseControl.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import io.strimzi.operator.cluster.model.CruiseControl;
1313
import io.strimzi.operator.cluster.model.ModelUtils;
1414
import io.strimzi.operator.common.model.Labels;
15+
import io.strimzi.operator.common.model.PasswordGenerator;
1516
import io.strimzi.operator.common.model.cruisecontrol.CruiseControlEndpoints;
1617
import io.strimzi.operator.common.model.cruisecontrol.CruiseControlParameters;
1718
import io.strimzi.operator.common.operator.MockCertManager;
@@ -79,7 +80,7 @@ public class MockCruiseControl {
7980
.addToData("cruise-control.crt", MockCertManager.clusterCaCert())
8081
.build();
8182
public static final Secret CC_API_SECRET = ModelUtils.createSecret(CruiseControlResources.apiSecretName(CLUSTER), NAMESPACE, Labels.EMPTY, null,
82-
CruiseControl.generateCruiseControlApiCredentials(), Collections.emptyMap(), Collections.emptyMap());
83+
CruiseControl.generateCruiseControlApiCredentials(new PasswordGenerator(16)), Collections.emptyMap(), Collections.emptyMap());
8384

8485
private static final Header AUTH_HEADER = convertToHeader(CruiseControlApiImpl.getAuthHttpHeader(true, CC_API_SECRET));
8586

0 commit comments

Comments
 (0)