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/operator/src/main/java/io/strimzi/kafka/access/KafkaAccessOperator.java b/operator/src/main/java/io/strimzi/kafka/access/KafkaAccessOperator.java index 72a6030..cf9b290 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 b2e0a3a..bb43a36 100644 --- a/operator/src/main/java/io/strimzi/kafka/access/KafkaAccessReconciler.java +++ b/operator/src/main/java/io/strimzi/kafka/access/KafkaAccessReconciler.java @@ -59,6 +59,8 @@ public class KafkaAccessReconciler implements Reconciler { 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) { @@ -104,14 +106,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() @@ -121,7 +147,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()) @@ -140,6 +167,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. * It configures the JOSDK framework with resources the operator needs to watch. 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: