From 4306e927e062ad3f1dfecd78f9cc70f754159681 Mon Sep 17 00:00:00 2001 From: CG Date: Sat, 15 Nov 2025 17:27:31 +0800 Subject: [PATCH 1/2] feat: support annotations Signed-off-by: CG --- .../kafka/access/model/KafkaAccessSpec.java | 19 ++++++ .../access/model/KafkaAccessTemplate.java | 39 +++++++++++ .../kafka/access/model/MetadataTemplate.java | 60 +++++++++++++++++ .../kafka/access/model/SecretTemplate.java | 39 +++++++++++ examples/kafka-access-with-reflector.yaml | 30 +++++++++ examples/kafka-access-with-user.yaml | 8 +++ .../crds/040-Crd-kafkaaccess.yaml | 17 +++++ install/040-Crd-kafkaaccess.yaml | 17 +++++ .../kafka/access/KafkaAccessOperator.java | 11 +++- .../kafka/access/KafkaAccessReconciler.java | 66 ++++++++++++++++++- .../access/internal/KafkaAccessMapper.java | 10 ++- .../kafka/access/internal/KafkaParser.java | 10 ++- .../kafka/access/server/HealthServlet.java | 11 +++- .../examples/kafka-access-with-user.yaml | 8 +++ .../crds/040-Crd-kafkaaccess.yaml | 17 +++++ packaging/install/040-Crd-kafkaaccess.yaml | 17 +++++ 16 files changed, 372 insertions(+), 7 deletions(-) create mode 100644 api/src/main/java/io/strimzi/kafka/access/model/KafkaAccessTemplate.java create mode 100644 api/src/main/java/io/strimzi/kafka/access/model/MetadataTemplate.java create mode 100644 api/src/main/java/io/strimzi/kafka/access/model/SecretTemplate.java create mode 100644 examples/kafka-access-with-reflector.yaml diff --git a/api/src/main/java/io/strimzi/kafka/access/model/KafkaAccessSpec.java b/api/src/main/java/io/strimzi/kafka/access/model/KafkaAccessSpec.java index 2735c3e..94b25d5 100644 --- a/api/src/main/java/io/strimzi/kafka/access/model/KafkaAccessSpec.java +++ b/api/src/main/java/io/strimzi/kafka/access/model/KafkaAccessSpec.java @@ -21,6 +21,7 @@ public class KafkaAccessSpec { private KafkaReference kafka; private KafkaUserReference user; private String secretName; + private KafkaAccessTemplate template; /** * Gets the KafkaReference instance @@ -75,4 +76,22 @@ public String getSecretName() { public void setSecretName(String secretName) { this.secretName = secretName; } + + /** + * Gets the template for customizing generated resources + * + * @return The KafkaAccessTemplate instance + */ + public KafkaAccessTemplate getTemplate() { + return template; + } + + /** + * Sets the template for customizing generated resources + * + * @param template The KafkaAccessTemplate model + */ + public void setTemplate(final KafkaAccessTemplate template) { + this.template = template; + } } diff --git a/api/src/main/java/io/strimzi/kafka/access/model/KafkaAccessTemplate.java b/api/src/main/java/io/strimzi/kafka/access/model/KafkaAccessTemplate.java new file mode 100644 index 0000000..83bde83 --- /dev/null +++ b/api/src/main/java/io/strimzi/kafka/access/model/KafkaAccessTemplate.java @@ -0,0 +1,39 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +package io.strimzi.kafka.access.model; + +import io.strimzi.api.kafka.model.common.Constants; +import io.sundr.builder.annotations.Buildable; + +/** + * Template for KafkaAccess resources (e.g., Secret template) + */ +@Buildable( + editableEnabled = false, + builderPackage = Constants.FABRIC8_KUBERNETES_API +) +public class KafkaAccessTemplate { + + private SecretTemplate secret; + + /** + * Gets the Secret template + * + * @return The SecretTemplate instance + */ + public SecretTemplate getSecret() { + return secret; + } + + /** + * Sets the Secret template + * + * @param secret The SecretTemplate model + */ + public void setSecret(final SecretTemplate secret) { + this.secret = secret; + } +} + diff --git a/api/src/main/java/io/strimzi/kafka/access/model/MetadataTemplate.java b/api/src/main/java/io/strimzi/kafka/access/model/MetadataTemplate.java new file mode 100644 index 0000000..1b789c7 --- /dev/null +++ b/api/src/main/java/io/strimzi/kafka/access/model/MetadataTemplate.java @@ -0,0 +1,60 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +package io.strimzi.kafka.access.model; + +import io.strimzi.api.kafka.model.common.Constants; +import io.sundr.builder.annotations.Buildable; + +import java.util.Map; + +/** + * Template for Kubernetes resource metadata (labels, annotations) + */ +@Buildable( + editableEnabled = false, + builderPackage = Constants.FABRIC8_KUBERNETES_API +) +public class MetadataTemplate { + + private Map labels; + private Map annotations; + + /** + * Gets the labels + * + * @return A map of labels + */ + public Map getLabels() { + return labels; + } + + /** + * Sets the labels + * + * @param labels A map of labels + */ + public void setLabels(final Map labels) { + this.labels = labels; + } + + /** + * Gets the annotations + * + * @return A map of annotations + */ + public Map getAnnotations() { + return annotations; + } + + /** + * Sets the annotations + * + * @param annotations A map of annotations + */ + public void setAnnotations(final Map annotations) { + this.annotations = annotations; + } +} + diff --git a/api/src/main/java/io/strimzi/kafka/access/model/SecretTemplate.java b/api/src/main/java/io/strimzi/kafka/access/model/SecretTemplate.java new file mode 100644 index 0000000..e3e2f00 --- /dev/null +++ b/api/src/main/java/io/strimzi/kafka/access/model/SecretTemplate.java @@ -0,0 +1,39 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +package io.strimzi.kafka.access.model; + +import io.strimzi.api.kafka.model.common.Constants; +import io.sundr.builder.annotations.Buildable; + +/** + * Template for Secret metadata (labels, annotations) + */ +@Buildable( + editableEnabled = false, + builderPackage = Constants.FABRIC8_KUBERNETES_API +) +public class SecretTemplate { + + private MetadataTemplate metadata; + + /** + * Gets the metadata template + * + * @return The MetadataTemplate instance + */ + public MetadataTemplate getMetadata() { + return metadata; + } + + /** + * Sets the metadata template + * + * @param metadata The MetadataTemplate model + */ + public void setMetadata(final MetadataTemplate metadata) { + this.metadata = metadata; + } +} + diff --git a/examples/kafka-access-with-reflector.yaml b/examples/kafka-access-with-reflector.yaml new file mode 100644 index 0000000..e3b0cb0 --- /dev/null +++ b/examples/kafka-access-with-reflector.yaml @@ -0,0 +1,30 @@ +# Example KafkaAccess using reflector/replicator annotations for secret replication +# This demonstrates how to use the template field to add annotations for +# Kubernetes secret replication tools like reflector and replicator +apiVersion: access.strimzi.io/v1alpha1 +kind: KafkaAccess +metadata: + name: my-replicated-kafka-access +spec: + kafka: + name: data-stream-serv + namespace: kafka + user: + kind: KafkaUser + apiGroup: kafka.strimzi.io + name: my-app-user + namespace: kafka + template: + secret: + metadata: + annotations: + # Replicator annotations (mittwald) + replicator.v1.mittwald.de/replicate-to: "namespace1,namespace2,namespace3" + # Reflector annotations (emberstack) + reflector.v1.k8s.emberstack.com/reflection-allowed: "true" + reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true" + reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "namespace1,namespace2,namespace3" + labels: + environment: production + team: platform + diff --git a/examples/kafka-access-with-user.yaml b/examples/kafka-access-with-user.yaml index 16eb148..b8f0fb9 100644 --- a/examples/kafka-access-with-user.yaml +++ b/examples/kafka-access-with-user.yaml @@ -18,3 +18,11 @@ spec: apiGroup: kafka.strimzi.io name: my-user namespace: kafka + # Optional: template to customize the generated Secret + template: + secret: + metadata: + annotations: + example.com/custom-annotation: "value" + labels: + example.com/custom-label: "value" diff --git a/helm-charts/helm3/strimzi-access-operator/crds/040-Crd-kafkaaccess.yaml b/helm-charts/helm3/strimzi-access-operator/crds/040-Crd-kafkaaccess.yaml index 046fc2f..70d9b40 100644 --- a/helm-charts/helm3/strimzi-access-operator/crds/040-Crd-kafkaaccess.yaml +++ b/helm-charts/helm3/strimzi-access-operator/crds/040-Crd-kafkaaccess.yaml @@ -53,6 +53,23 @@ spec: type: object secretName: type: string + template: + properties: + secret: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + type: object + type: object user: properties: apiGroup: diff --git a/install/040-Crd-kafkaaccess.yaml b/install/040-Crd-kafkaaccess.yaml index 046fc2f..70d9b40 100644 --- a/install/040-Crd-kafkaaccess.yaml +++ b/install/040-Crd-kafkaaccess.yaml @@ -53,6 +53,23 @@ spec: type: object secretName: type: string + template: + properties: + secret: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + type: object + type: object user: properties: apiGroup: diff --git a/operator/src/main/java/io/strimzi/kafka/access/KafkaAccessOperator.java b/operator/src/main/java/io/strimzi/kafka/access/KafkaAccessOperator.java index 360826c..20a58d4 100644 --- a/operator/src/main/java/io/strimzi/kafka/access/KafkaAccessOperator.java +++ b/operator/src/main/java/io/strimzi/kafka/access/KafkaAccessOperator.java @@ -12,10 +12,19 @@ import org.slf4j.LoggerFactory; /** - * The main operator class for Strimzi Access Operator + * The main operator class for Strimzi Access Operator. + * This class initializes and runs the Kubernetes operator for managing KafkaAccess resources. */ public class KafkaAccessOperator { + /** + * Creates a new KafkaAccessOperator instance. + * Explicit constructor added to satisfy Javadoc plugin warning on default constructor. + */ + public KafkaAccessOperator() { + // Intentionally empty. + } + private static final Logger LOGGER = LoggerFactory.getLogger(KafkaAccessOperator.class); private static final int HEALTH_CHECK_PORT = 8080; diff --git a/operator/src/main/java/io/strimzi/kafka/access/KafkaAccessReconciler.java b/operator/src/main/java/io/strimzi/kafka/access/KafkaAccessReconciler.java index b8f240d..e909d55 100644 --- a/operator/src/main/java/io/strimzi/kafka/access/KafkaAccessReconciler.java +++ b/operator/src/main/java/io/strimzi/kafka/access/KafkaAccessReconciler.java @@ -60,6 +60,8 @@ public class KafkaAccessReconciler implements Reconciler, EventSour public static final String KAFKA_USER_SECRET_EVENT_SOURCE = "KAFKA_USER_SECRET_EVENT_SOURCE"; /** + * Constructs a new KafkaAccessReconciler with the specified Kubernetes client. + * * @param kubernetesClient The Kubernetes client */ public KafkaAccessReconciler(final KubernetesClient kubernetesClient) { @@ -105,14 +107,38 @@ private void createOrUpdateSecret(final Map data, final KafkaAcc if (kafkaAccessSecretEventSource == null) { throw new IllegalStateException("Event source for Kafka Access Secret not initialized, cannot reconcile"); } + + final Map templateAnnotations = getTemplateAnnotations(kafkaAccess); + final Map templateLabels = getTemplateLabels(kafkaAccess); + kafkaAccessSecretEventSource.get(new ResourceID(secretName, kafkaAccessNamespace)) .ifPresentOrElse(secret -> { final Map currentData = secret.getData(); - if (!data.equals(currentData)) { + final Map currentAnnotations = Optional.ofNullable(secret.getMetadata().getAnnotations()).orElse(new HashMap<>()); + final Map currentLabels = Optional.ofNullable(secret.getMetadata().getLabels()).orElse(new HashMap<>()); + + // Merge template annotations/labels with existing ones (template takes precedence) + final Map mergedAnnotations = new HashMap<>(currentAnnotations); + mergedAnnotations.putAll(templateAnnotations); + + final Map mergedLabels = new HashMap<>(currentLabels); + mergedLabels.putAll(templateLabels); + + final boolean dataChanged = !data.equals(currentData); + final boolean annotationsChanged = !mergedAnnotations.equals(currentAnnotations); + final boolean labelsChanged = !mergedLabels.equals(currentLabels); + + if (dataChanged || annotationsChanged || labelsChanged) { kubernetesClient.secrets() .inNamespace(kafkaAccessNamespace) .withName(secretName) - .edit(s -> new SecretBuilder(s).withData(data).build()); + .edit(s -> new SecretBuilder(s) + .withData(data) + .editOrNewMetadata() + .withAnnotations(mergedAnnotations) + .withLabels(mergedLabels) + .endMetadata() + .build()); } }, () -> kubernetesClient .secrets() @@ -122,7 +148,8 @@ private void createOrUpdateSecret(final Map data, final KafkaAcc .withType(SECRET_TYPE) .withNewMetadata() .withName(secretName) - .withLabels(commonSecretLabels) + .withLabels(templateLabels) + .withAnnotations(templateAnnotations) .withOwnerReferences( new OwnerReferenceBuilder() .withApiVersion(kafkaAccess.getApiVersion()) @@ -141,6 +168,39 @@ private void createOrUpdateSecret(final Map data, final KafkaAcc ); } + /** + * Extracts annotations from the KafkaAccess spec template that should be applied to the Secret. + * + * @param kafkaAccess The KafkaAccess custom resource. + * @return A map of annotations to apply to the Secret. + */ + private Map getTemplateAnnotations(final KafkaAccess kafkaAccess) { + return Optional.ofNullable(kafkaAccess.getSpec().getTemplate()) + .map(template -> template.getSecret()) + .map(secret -> secret.getMetadata()) + .map(metadata -> metadata.getAnnotations()) + .orElse(new HashMap<>()); + } + + /** + * Extracts labels from the KafkaAccess spec template that should be applied to the Secret. + * These are merged with the common secret labels. + * + * @param kafkaAccess The KafkaAccess custom resource. + * @return A map of labels to apply to the Secret (includes common labels). + */ + private Map getTemplateLabels(final KafkaAccess kafkaAccess) { + final Map labels = new HashMap<>(commonSecretLabels); + + Optional.ofNullable(kafkaAccess.getSpec().getTemplate()) + .map(template -> template.getSecret()) + .map(secret -> secret.getMetadata()) + .map(metadata -> metadata.getLabels()) + .ifPresent(labels::putAll); + + return labels; + } + /** * Prepares the event sources required for triggering the reconciliation * diff --git a/operator/src/main/java/io/strimzi/kafka/access/internal/KafkaAccessMapper.java b/operator/src/main/java/io/strimzi/kafka/access/internal/KafkaAccessMapper.java index 3038c87..068c99c 100644 --- a/operator/src/main/java/io/strimzi/kafka/access/internal/KafkaAccessMapper.java +++ b/operator/src/main/java/io/strimzi/kafka/access/internal/KafkaAccessMapper.java @@ -29,10 +29,18 @@ import java.util.stream.Stream; /** - * Maps Strimzi and Kuberentes resources to and from KafkaAccess resources + * Maps Strimzi and Kubernetes resources to and from KafkaAccess resources. + * This utility class provides mapping functions for primary and secondary resources. */ public class KafkaAccessMapper { + /** + * Utility class - prevent instantiation. + */ + private KafkaAccessMapper() { + // Intentionally empty. + } + /** * The constant for managed-by label */ diff --git a/operator/src/main/java/io/strimzi/kafka/access/internal/KafkaParser.java b/operator/src/main/java/io/strimzi/kafka/access/internal/KafkaParser.java index 5f34ca4..2b0d850 100644 --- a/operator/src/main/java/io/strimzi/kafka/access/internal/KafkaParser.java +++ b/operator/src/main/java/io/strimzi/kafka/access/internal/KafkaParser.java @@ -27,10 +27,18 @@ import java.util.stream.Stream; /** - * Representation of a Kafka parser that gets the relevant Kafka listener from different sources + * Representation of a Kafka parser that gets the relevant Kafka listener from different sources. + * This utility class parses Kafka resources and extracts listener configuration. */ public class KafkaParser { + /** + * Utility class - prevent instantiation. + */ + private KafkaParser() { + // Intentionally empty. + } + /** * The constant for listener authentication type */ diff --git a/operator/src/main/java/io/strimzi/kafka/access/server/HealthServlet.java b/operator/src/main/java/io/strimzi/kafka/access/server/HealthServlet.java index b1c5028..9da3ea4 100644 --- a/operator/src/main/java/io/strimzi/kafka/access/server/HealthServlet.java +++ b/operator/src/main/java/io/strimzi/kafka/access/server/HealthServlet.java @@ -11,10 +11,19 @@ import java.io.Serial; /** - * Servlet class for health checking of the operator + * Servlet class for health checking of the operator. + * This servlet provides a simple health check endpoint. */ public class HealthServlet extends HttpServlet { + /** + * Creates a new HealthServlet. + * This explicit constructor documents the default servlet instantiation. + */ + public HealthServlet() { + super(); + } + @Serial private static final long serialVersionUID = 1L; diff --git a/packaging/examples/kafka-access-with-user.yaml b/packaging/examples/kafka-access-with-user.yaml index 16eb148..b8f0fb9 100644 --- a/packaging/examples/kafka-access-with-user.yaml +++ b/packaging/examples/kafka-access-with-user.yaml @@ -18,3 +18,11 @@ spec: apiGroup: kafka.strimzi.io name: my-user namespace: kafka + # Optional: template to customize the generated Secret + template: + secret: + metadata: + annotations: + example.com/custom-annotation: "value" + labels: + example.com/custom-label: "value" diff --git a/packaging/helm-charts/helm3/strimzi-access-operator/crds/040-Crd-kafkaaccess.yaml b/packaging/helm-charts/helm3/strimzi-access-operator/crds/040-Crd-kafkaaccess.yaml index 046fc2f..70d9b40 100644 --- a/packaging/helm-charts/helm3/strimzi-access-operator/crds/040-Crd-kafkaaccess.yaml +++ b/packaging/helm-charts/helm3/strimzi-access-operator/crds/040-Crd-kafkaaccess.yaml @@ -53,6 +53,23 @@ spec: type: object secretName: type: string + template: + properties: + secret: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + type: object + type: object user: properties: apiGroup: diff --git a/packaging/install/040-Crd-kafkaaccess.yaml b/packaging/install/040-Crd-kafkaaccess.yaml index 046fc2f..70d9b40 100644 --- a/packaging/install/040-Crd-kafkaaccess.yaml +++ b/packaging/install/040-Crd-kafkaaccess.yaml @@ -53,6 +53,23 @@ spec: type: object secretName: type: string + template: + properties: + secret: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + type: object + type: object user: properties: apiGroup: From 3ff33be992c03d457812b6ee6712ab52ebba0dff Mon Sep 17 00:00:00 2001 From: CG Date: Sat, 15 Nov 2025 23:16:53 +0800 Subject: [PATCH 2/2] chore: remove changes to contents in examples, install and helm-chart folders --- examples/kafka-access-with-reflector.yaml | 30 ------------------- examples/kafka-access-with-user.yaml | 8 ----- .../crds/040-Crd-kafkaaccess.yaml | 17 ----------- install/040-Crd-kafkaaccess.yaml | 17 ----------- 4 files changed, 72 deletions(-) delete mode 100644 examples/kafka-access-with-reflector.yaml diff --git a/examples/kafka-access-with-reflector.yaml b/examples/kafka-access-with-reflector.yaml deleted file mode 100644 index e3b0cb0..0000000 --- a/examples/kafka-access-with-reflector.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# Example KafkaAccess using reflector/replicator annotations for secret replication -# This demonstrates how to use the template field to add annotations for -# Kubernetes secret replication tools like reflector and replicator -apiVersion: access.strimzi.io/v1alpha1 -kind: KafkaAccess -metadata: - name: my-replicated-kafka-access -spec: - kafka: - name: data-stream-serv - namespace: kafka - user: - kind: KafkaUser - apiGroup: kafka.strimzi.io - name: my-app-user - namespace: kafka - template: - secret: - metadata: - annotations: - # Replicator annotations (mittwald) - replicator.v1.mittwald.de/replicate-to: "namespace1,namespace2,namespace3" - # Reflector annotations (emberstack) - reflector.v1.k8s.emberstack.com/reflection-allowed: "true" - reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true" - reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "namespace1,namespace2,namespace3" - labels: - environment: production - team: platform - diff --git a/examples/kafka-access-with-user.yaml b/examples/kafka-access-with-user.yaml index b8f0fb9..16eb148 100644 --- a/examples/kafka-access-with-user.yaml +++ b/examples/kafka-access-with-user.yaml @@ -18,11 +18,3 @@ spec: apiGroup: kafka.strimzi.io name: my-user namespace: kafka - # Optional: template to customize the generated Secret - template: - secret: - metadata: - annotations: - example.com/custom-annotation: "value" - labels: - example.com/custom-label: "value" diff --git a/helm-charts/helm3/strimzi-access-operator/crds/040-Crd-kafkaaccess.yaml b/helm-charts/helm3/strimzi-access-operator/crds/040-Crd-kafkaaccess.yaml index 70d9b40..046fc2f 100644 --- a/helm-charts/helm3/strimzi-access-operator/crds/040-Crd-kafkaaccess.yaml +++ b/helm-charts/helm3/strimzi-access-operator/crds/040-Crd-kafkaaccess.yaml @@ -53,23 +53,6 @@ spec: type: object secretName: type: string - template: - properties: - secret: - properties: - metadata: - properties: - annotations: - additionalProperties: - type: string - type: object - labels: - additionalProperties: - type: string - type: object - type: object - type: object - type: object user: properties: apiGroup: diff --git a/install/040-Crd-kafkaaccess.yaml b/install/040-Crd-kafkaaccess.yaml index 70d9b40..046fc2f 100644 --- a/install/040-Crd-kafkaaccess.yaml +++ b/install/040-Crd-kafkaaccess.yaml @@ -53,23 +53,6 @@ spec: type: object secretName: type: string - template: - properties: - secret: - properties: - metadata: - properties: - annotations: - additionalProperties: - type: string - type: object - labels: - additionalProperties: - type: string - type: object - type: object - type: object - type: object user: properties: apiGroup: