Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class KafkaAccessSpec {
private KafkaReference kafka;
private KafkaUserReference user;
private String secretName;
private KafkaAccessTemplate template;

/**
* Gets the KafkaReference instance
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}

Original file line number Diff line number Diff line change
@@ -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<String, String> labels;
private Map<String, String> annotations;

/**
* Gets the labels
*
* @return A map of labels
*/
public Map<String, String> getLabels() {
return labels;
}

/**
* Sets the labels
*
* @param labels A map of labels
*/
public void setLabels(final Map<String, String> labels) {
this.labels = labels;
}

/**
* Gets the annotations
*
* @return A map of annotations
*/
public Map<String, String> getAnnotations() {
return annotations;
}

/**
* Sets the annotations
*
* @param annotations A map of annotations
*/
public void setAnnotations(final Map<String, String> annotations) {
this.annotations = annotations;
}
}

Original file line number Diff line number Diff line change
@@ -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;
}
}

30 changes: 30 additions & 0 deletions examples/kafka-access-with-reflector.yaml
Original file line number Diff line number Diff line change
@@ -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

8 changes: 8 additions & 0 deletions examples/kafka-access-with-user.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
17 changes: 17 additions & 0 deletions install/040-Crd-kafkaaccess.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
Comment on lines +20 to +26
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] This explicit no-argument constructor is unnecessary and can be removed. Java automatically provides a default no-argument constructor when no constructors are explicitly defined. Since this class only contains static methods, the constructor is never called directly. The Javadoc plugin warning (if any) should be configured to ignore classes that don't need explicit constructor documentation, rather than adding unnecessary constructors.

Suggested change
/**
* Creates a new KafkaAccessOperator instance.
* Explicit constructor added to satisfy Javadoc plugin warning on default constructor.
*/
public KafkaAccessOperator() {
// Intentionally empty.
}

Copilot uses AI. Check for mistakes.

private static final Logger LOGGER = LoggerFactory.getLogger(KafkaAccessOperator.class);
private static final int HEALTH_CHECK_PORT = 8080;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ public class KafkaAccessReconciler implements Reconciler<KafkaAccess> {
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) {
Expand Down Expand Up @@ -104,14 +106,38 @@ private void createOrUpdateSecret(final Map<String, String> data, final KafkaAcc
if (kafkaAccessSecretEventSource == null) {
throw new IllegalStateException("Event source for Kafka Access Secret not initialized, cannot reconcile");
}

final Map<String, String> templateAnnotations = getTemplateAnnotations(kafkaAccess);
final Map<String, String> templateLabels = getTemplateLabels(kafkaAccess);

kafkaAccessSecretEventSource.get(new ResourceID(secretName, kafkaAccessNamespace))
.ifPresentOrElse(secret -> {
final Map<String, String> currentData = secret.getData();
if (!data.equals(currentData)) {
final Map<String, String> currentAnnotations = Optional.ofNullable(secret.getMetadata().getAnnotations()).orElse(new HashMap<>());
final Map<String, String> currentLabels = Optional.ofNullable(secret.getMetadata().getLabels()).orElse(new HashMap<>());

// Merge template annotations/labels with existing ones (template takes precedence)
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "Merge template annotations/labels with existing ones (template takes precedence)" is slightly misleading. The merge behavior actually preserves ALL existing annotations/labels and adds/overwrites only the template-specified ones. This means:

  1. Manually added annotations/labels are preserved across reconciliations
  2. Removing an annotation/label from the template will NOT remove it from an existing secret
  3. Template values take precedence only for keys that exist in both maps

Consider clarifying the comment to explicitly state this behavior, as it has important implications for users who might expect the secret metadata to exactly match the template specification:

// Merge template annotations/labels with existing ones.
// Template values override matching keys, but all other existing metadata is preserved.
// Note: Removing a key from the template will not remove it from existing secrets.
Suggested change
// Merge template annotations/labels with existing ones (template takes precedence)
// Merge template annotations/labels with existing ones.
// Template values override matching keys, but all other existing metadata is preserved.
// Note: Removing a key from the template will not remove it from existing secrets.

Copilot uses AI. Check for mistakes.
final Map<String, String> mergedAnnotations = new HashMap<>(currentAnnotations);
mergedAnnotations.putAll(templateAnnotations);

final Map<String, String> 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()
Expand All @@ -121,7 +147,8 @@ private void createOrUpdateSecret(final Map<String, String> data, final KafkaAcc
.withType(SECRET_TYPE)
.withNewMetadata()
.withName(secretName)
.withLabels(commonSecretLabels)
.withLabels(templateLabels)
.withAnnotations(templateAnnotations)
.withOwnerReferences(
new OwnerReferenceBuilder()
.withApiVersion(kafkaAccess.getApiVersion())
Expand All @@ -140,6 +167,39 @@ private void createOrUpdateSecret(final Map<String, String> 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<String, String> 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<String, String> getTemplateLabels(final KafkaAccess kafkaAccess) {
final Map<String, String> 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.
Expand Down
Loading
Loading