From 1c947dbe7238866969e1c2f605c3e42e798ab3bb Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Tue, 21 Jul 2026 15:41:17 -0400 Subject: [PATCH 1/5] Add support for Console pod node affinity configurations Signed-off-by: Michael Edgar --- .../api/v1alpha1/spec/ConsoleSpec.java | 16 ++++ .../spec/template/DeploymentTemplate.java | 26 ++++++ .../v1alpha1/spec/template/PodTemplate.java | 74 ++++++++++++++++ .../console/dependents/ConsoleDeployment.java | 9 ++ .../ConsoleReconcilerTemplateTest.java | 87 +++++++++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentTemplate.java create mode 100644 operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodTemplate.java create mode 100644 operator/src/test/java/com/github/streamshub/console/ConsoleReconcilerTemplateTest.java diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/ConsoleSpec.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/ConsoleSpec.java index f15476793..81c593e44 100644 --- a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/ConsoleSpec.java +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/ConsoleSpec.java @@ -8,6 +8,7 @@ import com.github.streamshub.console.api.v1alpha1.spec.containers.Containers; import com.github.streamshub.console.api.v1alpha1.spec.metrics.MetricsSource; import com.github.streamshub.console.api.v1alpha1.spec.security.GlobalSecurity; +import com.github.streamshub.console.api.v1alpha1.spec.template.DeploymentTemplate; import io.fabric8.kubernetes.api.model.EnvVar; import io.sundr.builder.annotations.Buildable; @@ -33,6 +34,13 @@ public class ConsoleSpec { """) Tls tls; + @JsonPropertyDescription(""" + Template for the Console Deployment and its pod. Allows configuration \ + of scheduling constraints such as affinity, tolerations, topology spread \ + constraints, and node selectors. + """) + DeploymentTemplate deployment; + @JsonPropertyDescription(""" Templates for Console instance containers. The templates allow \ users to specify how the Kubernetes resources are generated. @@ -77,6 +85,14 @@ public void setTls(Tls tls) { this.tls = tls; } + public DeploymentTemplate getDeployment() { + return deployment; + } + + public void setDeployment(DeploymentTemplate deployment) { + this.deployment = deployment; + } + public Containers getContainers() { return containers; } diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentTemplate.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentTemplate.java new file mode 100644 index 000000000..5e7f26fc7 --- /dev/null +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentTemplate.java @@ -0,0 +1,26 @@ +package com.github.streamshub.console.api.v1alpha1.spec.template; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +import io.sundr.builder.annotations.Buildable; + +@Buildable(editableEnabled = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DeploymentTemplate { + + @JsonPropertyDescription(""" + Template for the console pod. Allows configuration of scheduling \ + constraints such as affinity, tolerations, topology spread constraints, \ + and node selectors. + """) + private PodTemplate pod; + + public PodTemplate getPod() { + return pod; + } + + public void setPod(PodTemplate pod) { + this.pod = pod; + } +} diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodTemplate.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodTemplate.java new file mode 100644 index 000000000..588a53092 --- /dev/null +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodTemplate.java @@ -0,0 +1,74 @@ +package com.github.streamshub.console.api.v1alpha1.spec.template; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +import io.fabric8.kubernetes.api.model.Affinity; +import io.fabric8.kubernetes.api.model.Toleration; +import io.fabric8.kubernetes.api.model.TopologySpreadConstraint; +import io.sundr.builder.annotations.Buildable; + +@Buildable(editableEnabled = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PodTemplate { + + @JsonPropertyDescription(""" + The pod's affinity rules. Allows constraining which nodes the console \ + pod is eligible to be scheduled on, based on node labels or the labels \ + of other pods already running on a node. + """) + private Affinity affinity; + + @JsonPropertyDescription(""" + The pod's tolerations. Allows the console pod to be scheduled onto \ + nodes that have matching taints. + """) + private List tolerations; + + @JsonPropertyDescription(""" + The pod's topology spread constraints. Controls how the console pod \ + is spread across failure-domains such as regions, zones, or nodes. + """) + private List topologySpreadConstraints; + + @JsonPropertyDescription(""" + A selector which must match a node's labels for the console pod \ + to be scheduled on that node. + """) + private Map nodeSelector; + + public Affinity getAffinity() { + return affinity; + } + + public void setAffinity(Affinity affinity) { + this.affinity = affinity; + } + + public List getTolerations() { + return tolerations; + } + + public void setTolerations(List tolerations) { + this.tolerations = tolerations; + } + + public List getTopologySpreadConstraints() { + return topologySpreadConstraints; + } + + public void setTopologySpreadConstraints(List topologySpreadConstraints) { + this.topologySpreadConstraints = topologySpreadConstraints; + } + + public Map getNodeSelector() { + return nodeSelector; + } + + public void setNodeSelector(Map nodeSelector) { + this.nodeSelector = nodeSelector; + } +} diff --git a/operator/src/main/java/com/github/streamshub/console/dependents/ConsoleDeployment.java b/operator/src/main/java/com/github/streamshub/console/dependents/ConsoleDeployment.java index 93f61acce..2d72a3e45 100644 --- a/operator/src/main/java/com/github/streamshub/console/dependents/ConsoleDeployment.java +++ b/operator/src/main/java/com/github/streamshub/console/dependents/ConsoleDeployment.java @@ -16,6 +16,8 @@ import com.github.streamshub.console.api.v1alpha1.spec.containers.ContainerSpec; import com.github.streamshub.console.api.v1alpha1.spec.containers.ContainerTemplateSpec; import com.github.streamshub.console.api.v1alpha1.spec.containers.Containers; +import com.github.streamshub.console.api.v1alpha1.spec.template.DeploymentTemplate; +import com.github.streamshub.console.api.v1alpha1.spec.template.PodTemplate; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.IntOrString; @@ -52,6 +54,9 @@ protected Deployment desired(Console primary, Context context) { String name = instanceName(primary); String configSecretName = secret.instanceName(primary); + var podTemplate = Optional.ofNullable(primary.getSpec().getDeployment()) + .map(DeploymentTemplate::getPod); + var containers = Optional.ofNullable(primary.getSpec().getContainers()); var templateAPI = containers.map(Containers::getApi).map(ContainerTemplateSpec::getSpec); // deprecated @@ -88,6 +93,10 @@ protected Deployment desired(Console primary, Context context) { serializeDigest(context, "console-digest")) .endMetadata() .editSpec() + .withAffinity(podTemplate.map(PodTemplate::getAffinity).orElse(null)) + .withTolerations(podTemplate.map(PodTemplate::getTolerations).orElse(null)) + .withTopologySpreadConstraints(podTemplate.map(PodTemplate::getTopologySpreadConstraints).orElse(null)) + .withNodeSelector(podTemplate.map(PodTemplate::getNodeSelector).orElse(null)) .withServiceAccountName(serviceAccount.instanceName(primary)) .editMatchingVolume(vol -> "config".equals(vol.getName())) .editSecret() diff --git a/operator/src/test/java/com/github/streamshub/console/ConsoleReconcilerTemplateTest.java b/operator/src/test/java/com/github/streamshub/console/ConsoleReconcilerTemplateTest.java new file mode 100644 index 000000000..690fb74a2 --- /dev/null +++ b/operator/src/test/java/com/github/streamshub/console/ConsoleReconcilerTemplateTest.java @@ -0,0 +1,87 @@ +package com.github.streamshub.console; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.github.streamshub.console.api.v1alpha1.Console; +import com.github.streamshub.console.api.v1alpha1.ConsoleBuilder; +import com.github.streamshub.console.dependents.ConsoleDeployment; +import com.github.streamshub.console.dependents.PrometheusDeployment; + +import io.fabric8.kubernetes.api.model.AffinityBuilder; +import io.fabric8.kubernetes.api.model.NodeAffinityBuilder; +import io.fabric8.kubernetes.api.model.NodeSelectorRequirementBuilder; +import io.fabric8.kubernetes.api.model.NodeSelectorTermBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.TolerationBuilder; +import io.quarkus.test.junit.QuarkusTest; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@QuarkusTest +class ConsoleReconcilerTemplateTest extends ConsoleReconcilerTestBase { + + @Test + void testConsoleReconciliationWithDeploymentTemplate() { + var affinity = new AffinityBuilder() + .withNodeAffinity(new NodeAffinityBuilder() + .addNewPreferredDuringSchedulingIgnoredDuringExecution() + .withWeight(1) + .withPreference(new NodeSelectorTermBuilder() + .addToMatchExpressions(new NodeSelectorRequirementBuilder() + .withKey("topology.kubernetes.io/zone") + .withOperator("In") + .withValues("us-east-1a") + .build()) + .build()) + .endPreferredDuringSchedulingIgnoredDuringExecution() + .build()) + .build(); + var toleration = new TolerationBuilder() + .withKey("dedicated") + .withOperator("Equal") + .withValue("console") + .withEffect("NoSchedule") + .build(); + + Console consoleCR = new ConsoleBuilder() + .withMetadata(new ObjectMetaBuilder() + .withName(CONSOLE_NAME) + .withNamespace(CONSOLE_NS) + .build()) + .withNewSpec() + .withHostname("console.example.com") + .withNewDeployment() + .withNewPod() + .withAffinity(affinity) + .withTolerations(toleration) + .withNodeSelector(Map.of("disktype", "ssd")) + .endPod() + .endDeployment() + .addNewKafkaCluster() + .withName(kafkaCR.getMetadata().getName()) + .withNamespace(kafkaCR.getMetadata().getNamespace()) + .withListener(kafkaCR.getSpec().getKafka().getListeners().get(0).getName()) + .endKafkaCluster() + .endSpec() + .build(); + + client.resource(consoleCR).create(); + + awaitDependentsNotReady(consoleCR, "ConsoleDeployment", "PrometheusDeployment"); + setDeploymentReady(consoleCR, PrometheusDeployment.NAME); + awaitDependentsNotReady(consoleCR, "ConsoleDeployment"); + var consoleDeployment = setDeploymentReady(consoleCR, ConsoleDeployment.NAME); + awaitDependentsNotReady(consoleCR, "ConsoleIngress"); + setConsoleIngressReady(consoleCR); + + var podSpec = consoleDeployment.getSpec().getTemplate().getSpec(); + assertEquals(affinity, podSpec.getAffinity()); + assertEquals(List.of(toleration), podSpec.getTolerations()); + assertEquals(Map.of("disktype", "ssd"), podSpec.getNodeSelector()); + + awaitReady(consoleCR); + } +} From e029a40b72b9ce73265eb0fc5d3588ca185e2565 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Fri, 24 Jul 2026 16:37:16 -0400 Subject: [PATCH 2/5] Refine template structure, include container and deprecate old configs Signed-off-by: Michael Edgar --- .../streamshub/console/ConsoleReconciler.java | 3 + .../api/v1alpha1/spec/ConsoleSpec.java | 9 +- .../spec/template/ContainerTemplate.java | 60 +++++++ .../spec/template/DeploymentSpecTemplate.java | 27 +++ .../spec/template/DeploymentTemplate.java | 30 ++-- .../spec/template/MetadataTemplate.java | 32 ++++ .../spec/template/PodSpecTemplate.java | 88 ++++++++++ .../v1alpha1/spec/template/PodTemplate.java | 65 ++----- .../api/v1alpha1/status/Condition.java | 21 ++- .../api/v1alpha1/status/ConsoleStatus.java | 2 +- .../console/dependents/ConsoleDeployment.java | 161 +++++++++++++++--- .../ConsoleReconcilerTemplateTest.java | 14 +- 12 files changed, 412 insertions(+), 100 deletions(-) create mode 100644 operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/ContainerTemplate.java create mode 100644 operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentSpecTemplate.java create mode 100644 operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/MetadataTemplate.java create mode 100644 operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodSpecTemplate.java diff --git a/operator/src/main/java/com/github/streamshub/console/ConsoleReconciler.java b/operator/src/main/java/com/github/streamshub/console/ConsoleReconciler.java index cef8153bc..9c0fe0620 100644 --- a/operator/src/main/java/com/github/streamshub/console/ConsoleReconciler.java +++ b/operator/src/main/java/com/github/streamshub/console/ConsoleReconciler.java @@ -12,6 +12,8 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import org.jboss.logging.Logger; + import com.github.streamshub.console.api.v1alpha1.Console; import com.github.streamshub.console.api.v1alpha1.status.Condition; import com.github.streamshub.console.api.v1alpha1.status.ConditionBuilder; @@ -227,6 +229,7 @@ public ErrorStatusUpdateControl updateErrorStatus(Console resource, Context context, Exception e) { + Logger.getLogger(getClass()).debug("Exception during reconcile", e); var result = context.managedWorkflowAndDependentResourceContext().getWorkflowReconcileResult(); determineConditions(resource, result, e); diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/ConsoleSpec.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/ConsoleSpec.java index 81c593e44..37cfaf35b 100644 --- a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/ConsoleSpec.java +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/ConsoleSpec.java @@ -42,14 +42,15 @@ public class ConsoleSpec { DeploymentTemplate deployment; @JsonPropertyDescription(""" - Templates for Console instance containers. The templates allow \ - users to specify how the Kubernetes resources are generated. + DEPRECATED: Templates for Console instance containers. The templates allow \ + users to specify how the Kubernetes resources are generated. \ + Use `deployment.spec.template.spec.serverContainer` property instead. """) Containers containers; @JsonPropertyDescription(""" DEPRECATED: Image overrides to be used for the API and UI servers. \ - Use `containers` property instead. + Use `deployment.spec.template.spec.serverContainer.image` property instead. """) Images images; @@ -65,7 +66,7 @@ public class ConsoleSpec { @JsonPropertyDescription(""" DEPRECATED: Environment variables which should be applied to the API container. \ - Use `containers` property instead. + Use `deployment.spec.template.spec.serverContainer.env` property instead. """) List env; diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/ContainerTemplate.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/ContainerTemplate.java new file mode 100644 index 000000000..3465e7819 --- /dev/null +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/ContainerTemplate.java @@ -0,0 +1,60 @@ +package com.github.streamshub.console.api.v1alpha1.spec.template; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.ResourceRequirements; +import io.sundr.builder.annotations.Buildable; + +@Buildable(editableEnabled = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ContainerTemplate { + + @JsonPropertyDescription("Container image to be used for the container") + private String image; + + @JsonPropertyDescription("Image pull policy to be used for the container image") + private String imagePullPolicy; + + @JsonPropertyDescription("CPU and memory resources to reserve.") + private ResourceRequirements resources; + + @JsonPropertyDescription("Environment variables which should be applied to the container.") + private List env; + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public String getImagePullPolicy() { + return imagePullPolicy; + } + + public void setImagePullPolicy(String imagePullPolicy) { + this.imagePullPolicy = imagePullPolicy; + } + + public ResourceRequirements getResources() { + return resources; + } + + public void setResources(ResourceRequirements resources) { + this.resources = resources; + } + + public List getEnv() { + return env; + } + + public void setEnv(List env) { + this.env = env; + } + +} diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentSpecTemplate.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentSpecTemplate.java new file mode 100644 index 000000000..fec3dfd5e --- /dev/null +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentSpecTemplate.java @@ -0,0 +1,27 @@ +package com.github.streamshub.console.api.v1alpha1.spec.template; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +import io.sundr.builder.annotations.Buildable; + +@Buildable(editableEnabled = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DeploymentSpecTemplate { + + @JsonPropertyDescription(""" + Template for the console pod. Allows configuration of scheduling \ + constraints such as affinity, tolerations, topology spread constraints, \ + and node selectors. + """) + private PodTemplate template; + + public PodTemplate getTemplate() { + return template; + } + + public void setTemplate(PodTemplate template) { + this.template = template; + } + +} diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentTemplate.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentTemplate.java index 5e7f26fc7..cbeea866a 100644 --- a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentTemplate.java +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentTemplate.java @@ -9,18 +9,26 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class DeploymentTemplate { - @JsonPropertyDescription(""" - Template for the console pod. Allows configuration of scheduling \ - constraints such as affinity, tolerations, topology spread constraints, \ - and node selectors. - """) - private PodTemplate pod; - - public PodTemplate getPod() { - return pod; + @JsonPropertyDescription("Metadata for the deployment template.") + private MetadataTemplate metadata; + + @JsonPropertyDescription("Spec for the deployment template.") + private DeploymentSpecTemplate spec; + + public MetadataTemplate getMetadata() { + return metadata; + } + + public void setMetadata(MetadataTemplate metadata) { + this.metadata = metadata; } - public void setPod(PodTemplate pod) { - this.pod = pod; + public DeploymentSpecTemplate getSpec() { + return spec; } + + public void setSpec(DeploymentSpecTemplate spec) { + this.spec = spec; + } + } diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/MetadataTemplate.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/MetadataTemplate.java new file mode 100644 index 000000000..e2ee601e9 --- /dev/null +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/MetadataTemplate.java @@ -0,0 +1,32 @@ +package com.github.streamshub.console.api.v1alpha1.spec.template; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import io.sundr.builder.annotations.Buildable; + +@Buildable(editableEnabled = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MetadataTemplate { + + private Map labels; + private Map annotations; + + public Map getLabels() { + return labels; + } + + public void setLabels(Map labels) { + this.labels = labels; + } + + public Map getAnnotations() { + return annotations; + } + + public void setAnnotations(Map annotations) { + this.annotations = annotations; + } + +} diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodSpecTemplate.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodSpecTemplate.java new file mode 100644 index 000000000..c044e050e --- /dev/null +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodSpecTemplate.java @@ -0,0 +1,88 @@ +package com.github.streamshub.console.api.v1alpha1.spec.template; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +import io.fabric8.kubernetes.api.model.Affinity; +import io.fabric8.kubernetes.api.model.Toleration; +import io.fabric8.kubernetes.api.model.TopologySpreadConstraint; +import io.sundr.builder.annotations.Buildable; + +@Buildable(editableEnabled = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PodSpecTemplate { + + @JsonPropertyDescription(""" + The pod's affinity rules. Allows constraining which nodes the console \ + pod is eligible to be scheduled on, based on node labels or the labels \ + of other pods already running on a node. + """) + private Affinity affinity; + + @JsonPropertyDescription(""" + The pod's tolerations. Allows the console pod to be scheduled onto \ + nodes that have matching taints. + """) + private List tolerations; + + @JsonPropertyDescription(""" + The pod's topology spread constraints. Controls how the console pod \ + is spread across failure-domains such as regions, zones, or nodes. + """) + private List topologySpreadConstraints; + + @JsonPropertyDescription(""" + A selector which must match a node's labels for the console pod \ + to be scheduled on that node. + """) + private Map nodeSelector; + + @JsonPropertyDescription(""" + Template for the console container. Allows configuration image, \ + pull policy, resources, and environment variables. + """) + private ContainerTemplate serverContainer; + + public Affinity getAffinity() { + return affinity; + } + + public void setAffinity(Affinity affinity) { + this.affinity = affinity; + } + + public List getTolerations() { + return tolerations; + } + + public void setTolerations(List tolerations) { + this.tolerations = tolerations; + } + + public List getTopologySpreadConstraints() { + return topologySpreadConstraints; + } + + public void setTopologySpreadConstraints(List topologySpreadConstraints) { + this.topologySpreadConstraints = topologySpreadConstraints; + } + + public Map getNodeSelector() { + return nodeSelector; + } + + public void setNodeSelector(Map nodeSelector) { + this.nodeSelector = nodeSelector; + } + + public ContainerTemplate getServerContainer() { + return serverContainer; + } + + public void setServerContainer(ContainerTemplate serverContainer) { + this.serverContainer = serverContainer; + } +} diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodTemplate.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodTemplate.java index 588a53092..322dc8411 100644 --- a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodTemplate.java +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodTemplate.java @@ -1,74 +1,33 @@ package com.github.streamshub.console.api.v1alpha1.spec.template; -import java.util.List; -import java.util.Map; - import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonPropertyDescription; -import io.fabric8.kubernetes.api.model.Affinity; -import io.fabric8.kubernetes.api.model.Toleration; -import io.fabric8.kubernetes.api.model.TopologySpreadConstraint; import io.sundr.builder.annotations.Buildable; @Buildable(editableEnabled = false) @JsonInclude(JsonInclude.Include.NON_NULL) public class PodTemplate { - @JsonPropertyDescription(""" - The pod's affinity rules. Allows constraining which nodes the console \ - pod is eligible to be scheduled on, based on node labels or the labels \ - of other pods already running on a node. - """) - private Affinity affinity; - - @JsonPropertyDescription(""" - The pod's tolerations. Allows the console pod to be scheduled onto \ - nodes that have matching taints. - """) - private List tolerations; - - @JsonPropertyDescription(""" - The pod's topology spread constraints. Controls how the console pod \ - is spread across failure-domains such as regions, zones, or nodes. - """) - private List topologySpreadConstraints; - - @JsonPropertyDescription(""" - A selector which must match a node's labels for the console pod \ - to be scheduled on that node. - """) - private Map nodeSelector; - - public Affinity getAffinity() { - return affinity; - } + @JsonPropertyDescription("Metadata for the pod template.") + private MetadataTemplate metadata; - public void setAffinity(Affinity affinity) { - this.affinity = affinity; - } - - public List getTolerations() { - return tolerations; - } - - public void setTolerations(List tolerations) { - this.tolerations = tolerations; - } + @JsonPropertyDescription("Spec for the pod template.") + private PodSpecTemplate spec; - public List getTopologySpreadConstraints() { - return topologySpreadConstraints; + public MetadataTemplate getMetadata() { + return metadata; } - public void setTopologySpreadConstraints(List topologySpreadConstraints) { - this.topologySpreadConstraints = topologySpreadConstraints; + public void setMetadata(MetadataTemplate metadata) { + this.metadata = metadata; } - public Map getNodeSelector() { - return nodeSelector; + public PodSpecTemplate getSpec() { + return spec; } - public void setNodeSelector(Map nodeSelector) { - this.nodeSelector = nodeSelector; + public void setSpec(PodSpecTemplate spec) { + this.spec = spec; } } diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/status/Condition.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/status/Condition.java index 29c51f7bc..01da3dfeb 100644 --- a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/status/Condition.java +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/status/Condition.java @@ -1,5 +1,7 @@ package com.github.streamshub.console.api.v1alpha1.status; +import java.util.Comparator; +import java.util.Locale; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -121,9 +123,22 @@ public static final class Types { private Types() { } - public static final String READY = "Ready"; - public static final String WARNING = "Warning"; - public static final String ERROR = "Error"; + enum ConditionType { + READY("Ready"), WARNING("Warning"), ERROR("Error"); + final String display; + ConditionType(String display) { + this.display = display; + } + } + + public static final String READY = ConditionType.READY.display; + public static final String WARNING = ConditionType.WARNING.display; + public static final String ERROR = ConditionType.ERROR.display; + + static final Comparator COMPARATOR = (t1, t2) -> + // declaration order + ConditionType.valueOf(t1.toUpperCase(Locale.ROOT)) + .compareTo(ConditionType.valueOf(t2.toUpperCase(Locale.ROOT))); } /** diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/status/ConsoleStatus.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/status/ConsoleStatus.java index a24168ce8..f557f51fa 100644 --- a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/status/ConsoleStatus.java +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/status/ConsoleStatus.java @@ -18,7 +18,7 @@ public class ConsoleStatus { private long observedGeneration; private final Set conditions = new TreeSet<>(Comparator - .comparing(Condition::getType).reversed() + .comparing(Condition::getType, Condition.Types.COMPARATOR) .thenComparing(Condition::getLastTransitionTime) .thenComparing(Condition::getStatus, Comparator.nullsLast(String::compareTo)) .thenComparing(Condition::getReason, Comparator.nullsLast(String::compareTo)) diff --git a/operator/src/main/java/com/github/streamshub/console/dependents/ConsoleDeployment.java b/operator/src/main/java/com/github/streamshub/console/dependents/ConsoleDeployment.java index 2d72a3e45..e9949a58e 100644 --- a/operator/src/main/java/com/github/streamshub/console/dependents/ConsoleDeployment.java +++ b/operator/src/main/java/com/github/streamshub/console/dependents/ConsoleDeployment.java @@ -1,7 +1,10 @@ package com.github.streamshub.console.dependents; +import java.time.Instant; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -16,8 +19,15 @@ import com.github.streamshub.console.api.v1alpha1.spec.containers.ContainerSpec; import com.github.streamshub.console.api.v1alpha1.spec.containers.ContainerTemplateSpec; import com.github.streamshub.console.api.v1alpha1.spec.containers.Containers; +import com.github.streamshub.console.api.v1alpha1.spec.template.ContainerTemplate; +import com.github.streamshub.console.api.v1alpha1.spec.template.DeploymentSpecTemplate; import com.github.streamshub.console.api.v1alpha1.spec.template.DeploymentTemplate; +import com.github.streamshub.console.api.v1alpha1.spec.template.MetadataTemplate; +import com.github.streamshub.console.api.v1alpha1.spec.template.PodSpecTemplate; import com.github.streamshub.console.api.v1alpha1.spec.template.PodTemplate; +import com.github.streamshub.console.api.v1alpha1.status.Condition.Reasons; +import com.github.streamshub.console.api.v1alpha1.status.Condition.Types; +import com.github.streamshub.console.api.v1alpha1.status.ConditionBuilder; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.IntOrString; @@ -54,21 +64,33 @@ protected Deployment desired(Console primary, Context context) { String name = instanceName(primary); String configSecretName = secret.instanceName(primary); - var podTemplate = Optional.ofNullable(primary.getSpec().getDeployment()) - .map(DeploymentTemplate::getPod); + var template = Optional.ofNullable(primary.getSpec().getDeployment()); + var metaTemplate = template.map(DeploymentTemplate::getMetadata); + var podTemplate = template.map(DeploymentTemplate::getSpec).map(DeploymentSpecTemplate::getTemplate); + var podMetaTemplate = podTemplate.map(PodTemplate::getMetadata); + var podSpecTemplate = podTemplate.map(PodTemplate::getSpec); + var podRequiredLabels = Map.of(INSTANCE_LABEL, name); + var podRequiredAnnotations = Map.of("streamshub.github.com/dependency-digest", + serializeDigest(context, "console-digest")); + var containerTemplate = podSpecTemplate.map(PodSpecTemplate::getServerContainer); + + // deprecated var containers = Optional.ofNullable(primary.getSpec().getContainers()); var templateAPI = containers.map(Containers::getApi).map(ContainerTemplateSpec::getSpec); - // deprecated var images = Optional.ofNullable(primary.getSpec().getImages()); + var envs = Optional.ofNullable(primary.getSpec().getEnv()); + warnDeprecations(primary, containers, images, envs); - String imageAPI = templateAPI.map(ContainerSpec::getImage) + String image = containerTemplate.map(ContainerTemplate::getImage) + .or(() -> templateAPI.map(ContainerSpec::getImage)) .or(() -> images.map(Images::getApi)) .orElse(defaultAPIImage); - - List envVars = new ArrayList<>(); - envVars.addAll(coalesce(primary.getSpec().getEnv(), Collections::emptyList)); - envVars.addAll(templateAPI.map(ContainerSpec::getEnv).orElseGet(Collections::emptyList)); + String imagePullPolicy = containerTemplate.map(ContainerTemplate::getImagePullPolicy) + .orElseGet(() -> pullPolicy(image)); + var containerResources = containerTemplate.map(ContainerTemplate::getResources) + .or(() -> templateAPI.map(ContainerSpec::getResources)) + .orElse(null); boolean tls = primary.getSpec().getTls() != null; String portName = tls ? "https" : "http"; @@ -79,24 +101,41 @@ protected Deployment desired(Console primary, Context context) { .editMetadata() .withName(name) .withNamespace(primary.getMetadata().getNamespace()) - .withLabels(commonLabels("console")) + .withLabels(merge( + commonLabels("console"), + metaTemplate.map(MetadataTemplate::getLabels) + )) + .addToAnnotations(metaTemplate.map(MetadataTemplate::getAnnotations) + .orElseGet(Collections::emptyMap)) .endMetadata() .editSpec() .editSelector() - .withMatchLabels(Map.of(INSTANCE_LABEL, name)) + .withMatchLabels(podRequiredLabels) .endSelector() .editTemplate() .editMetadata() - .addToLabels(Map.of(INSTANCE_LABEL, name)) - .addToAnnotations( - "streamshub.github.com/dependency-digest", - serializeDigest(context, "console-digest")) + .addToLabels(merge( + podRequiredLabels, + podMetaTemplate.map(MetadataTemplate::getLabels) + )) + .addToAnnotations(merge( + podRequiredAnnotations, + podMetaTemplate.map(MetadataTemplate::getAnnotations) + )) .endMetadata() .editSpec() - .withAffinity(podTemplate.map(PodTemplate::getAffinity).orElse(null)) - .withTolerations(podTemplate.map(PodTemplate::getTolerations).orElse(null)) - .withTopologySpreadConstraints(podTemplate.map(PodTemplate::getTopologySpreadConstraints).orElse(null)) - .withNodeSelector(podTemplate.map(PodTemplate::getNodeSelector).orElse(null)) + .withAffinity(podSpecTemplate + .map(PodSpecTemplate::getAffinity) + .orElse(null)) + .withTolerations(podSpecTemplate + .map(PodSpecTemplate::getTolerations) + .orElse(null)) + .withTopologySpreadConstraints(podSpecTemplate + .map(PodSpecTemplate::getTopologySpreadConstraints) + .orElse(null)) + .withNodeSelector(podSpecTemplate + .map(PodSpecTemplate::getNodeSelector) + .orElse(null)) .withServiceAccountName(serviceAccount.instanceName(primary)) .editMatchingVolume(vol -> "config".equals(vol.getName())) .editSecret() @@ -105,10 +144,16 @@ protected Deployment desired(Console primary, Context context) { .endVolume() // Set API container image and port options .editMatchingContainer(c -> "console-api".equals(c.getName())) - .withImage(imageAPI) - .withImagePullPolicy(pullPolicy(imageAPI)) - .withResources(templateAPI.map(ContainerSpec::getResources).orElse(null)) - .addAllToEnv(envVars) + .withImage(image) + .withImagePullPolicy(imagePullPolicy) + .withResources(containerResources) + .withEnv(buildEnvVars(desired, List.of( + // new way, preferred + containerTemplate.map(ContainerTemplate::getEnv), + // deprecated, lower precedence + templateAPI.map(ContainerSpec::getEnv), + envs) + )) .editFirstPort() .withName(portName) .withContainerPort(containerPort) @@ -138,7 +183,77 @@ protected Deployment desired(Console primary, Context context) { .build(); } - private String pullPolicy(String image) { + private static Map merge(Map required, Optional> configured) { + Map result = new LinkedHashMap<>(required); + configured.ifPresent(values -> values.forEach(result::putIfAbsent)); + return result; + } + + private static List buildEnvVars(Deployment base, List>> configured) { + List envVars = new ArrayList<>(base.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); + + configured.stream() + .filter(Optional::isPresent) + .map(Optional::get) + .flatMap(Collection::stream) + .forEach(envVar -> { + String configuredName = envVar.getName(); + + if (envVars.stream().map(EnvVar::getName).noneMatch(configuredName::equals)) { + envVars.add(envVar); + } + }); + + return envVars; + } + + private static String pullPolicy(String image) { return image.contains("sha256:") ? "IfNotPresent" : "Always"; } + + private static void warnDeprecations(Console primary, Optional containers, Optional images, Optional envs) { + StringBuilder deprecationWarnings = new StringBuilder(); + + if (containers.isPresent()) { + deprecationWarnings.append(""" + spec.containers is deprecated and will be removed \ + in a future release. Please use \ + spec.deployment.spec.template.spec.serverContainer \ + to configure the server container."""); + } + + if (images.isPresent()) { + if (!deprecationWarnings.isEmpty()) { + deprecationWarnings.append(' '); + } + + deprecationWarnings.append(""" + spec.images is deprecated and will be removed \ + in a future release. Please use \ + spec.deployment.spec.template.spec.serverContainer.image \ + to configure the server container image."""); + } + + if (envs.isPresent()) { + if (!deprecationWarnings.isEmpty()) { + deprecationWarnings.append(' '); + } + + deprecationWarnings.append(""" + spec.env is deprecated and will be removed \ + in a future release. Please use \ + spec.deployment.spec.template.spec.serverContainer.env \ + to configure the server container."""); + } + + if (!deprecationWarnings.isEmpty()) { + primary.getOrCreateStatus().updateCondition(new ConditionBuilder() + .withType(Types.WARNING) + .withStatus("True") + .withLastTransitionTime(Instant.now().toString()) + .withReason(Reasons.DEPRECATED_CONFIGURATION) + .withMessage(deprecationWarnings.toString()) + .build()); + } + } } diff --git a/operator/src/test/java/com/github/streamshub/console/ConsoleReconcilerTemplateTest.java b/operator/src/test/java/com/github/streamshub/console/ConsoleReconcilerTemplateTest.java index 690fb74a2..3cf2e344d 100644 --- a/operator/src/test/java/com/github/streamshub/console/ConsoleReconcilerTemplateTest.java +++ b/operator/src/test/java/com/github/streamshub/console/ConsoleReconcilerTemplateTest.java @@ -54,11 +54,15 @@ void testConsoleReconciliationWithDeploymentTemplate() { .withNewSpec() .withHostname("console.example.com") .withNewDeployment() - .withNewPod() - .withAffinity(affinity) - .withTolerations(toleration) - .withNodeSelector(Map.of("disktype", "ssd")) - .endPod() + .withNewSpec() + .withNewTemplate() + .withNewSpec() + .withAffinity(affinity) + .withTolerations(toleration) + .withNodeSelector(Map.of("disktype", "ssd")) + .endSpec() + .endTemplate() + .endSpec() .endDeployment() .addNewKafkaCluster() .withName(kafkaCR.getMetadata().getName()) From 2240cd44754b2c169ce9ed88851543b51aafccf1 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Wed, 29 Jul 2026 15:33:19 -0400 Subject: [PATCH 3/5] Improve spec descriptions, use enum for container image pull policy Signed-off-by: Michael Edgar --- .../api/v1alpha1/spec/ConsoleSpec.java | 7 +-- .../spec/template/ContainerTemplate.java | 43 ++++++++++++++++--- .../spec/template/DeploymentSpecTemplate.java | 7 +-- .../spec/template/DeploymentTemplate.java | 10 ++++- .../spec/template/MetadataTemplate.java | 4 ++ .../v1alpha1/spec/template/PodTemplate.java | 11 ++++- .../console/dependents/ConsoleDeployment.java | 6 ++- 7 files changed, 72 insertions(+), 16 deletions(-) diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/ConsoleSpec.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/ConsoleSpec.java index 37cfaf35b..7d2e5d1aa 100644 --- a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/ConsoleSpec.java +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/ConsoleSpec.java @@ -35,9 +35,10 @@ public class ConsoleSpec { Tls tls; @JsonPropertyDescription(""" - Template for the Console Deployment and its pod. Allows configuration \ - of scheduling constraints such as affinity, tolerations, topology spread \ - constraints, and node selectors. + Template for the Console Deployment. Allows customisation of the Deployment \ + and pod metadata, scheduling constraints (affinity, tolerations, topology \ + spread constraints, and node selector), and the server container image, \ + pull policy, resources, and environment variables. """) DeploymentTemplate deployment; diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/ContainerTemplate.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/ContainerTemplate.java index 3465e7819..d979f5adf 100644 --- a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/ContainerTemplate.java +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/ContainerTemplate.java @@ -2,8 +2,10 @@ import java.util.List; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.fasterxml.jackson.annotation.JsonValue; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.ResourceRequirements; @@ -13,11 +15,16 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class ContainerTemplate { - @JsonPropertyDescription("Container image to be used for the container") + @JsonPropertyDescription("Container image to be used for the container.") private String image; - @JsonPropertyDescription("Image pull policy to be used for the container image") - private String imagePullPolicy; + @JsonPropertyDescription(""" + Image pull policy to be used for the container image. \ + One of Always, IfNotPresent, or Never. \ + Defaults to Always when the image tag is not a digest, \ + and IfNotPresent when a digest is specified. + """) + private ImagePullPolicy imagePullPolicy; @JsonPropertyDescription("CPU and memory resources to reserve.") private ResourceRequirements resources; @@ -33,11 +40,11 @@ public void setImage(String image) { this.image = image; } - public String getImagePullPolicy() { + public ImagePullPolicy getImagePullPolicy() { return imagePullPolicy; } - public void setImagePullPolicy(String imagePullPolicy) { + public void setImagePullPolicy(ImagePullPolicy imagePullPolicy) { this.imagePullPolicy = imagePullPolicy; } @@ -57,4 +64,30 @@ public void setEnv(List env) { this.env = env; } + public enum ImagePullPolicy { + ALWAYS("Always"), + IF_NOT_PRESENT("IfNotPresent"), + NEVER("Never"); + + private final String value; + + ImagePullPolicy(String value) { + this.value = value; + } + + @JsonValue + public String value() { + return value; + } + + @JsonCreator + public static ImagePullPolicy fromValue(String value) { + for (var policy : values()) { + if (policy.value.equals(value)) { + return policy; + } + } + throw new IllegalArgumentException("Invalid imagePullPolicy: " + value); + } + } } diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentSpecTemplate.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentSpecTemplate.java index fec3dfd5e..0934e0838 100644 --- a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentSpecTemplate.java +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentSpecTemplate.java @@ -10,9 +10,10 @@ public class DeploymentSpecTemplate { @JsonPropertyDescription(""" - Template for the console pod. Allows configuration of scheduling \ - constraints such as affinity, tolerations, topology spread constraints, \ - and node selectors. + Template for the console pod. Allows configuration of pod metadata, \ + scheduling constraints (affinity, tolerations, topology spread constraints, \ + and node selector), and the server container image, pull policy, resources, \ + and environment variables. """) private PodTemplate template; diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentTemplate.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentTemplate.java index cbeea866a..fa96a10a3 100644 --- a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentTemplate.java +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/DeploymentTemplate.java @@ -9,10 +9,16 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class DeploymentTemplate { - @JsonPropertyDescription("Metadata for the deployment template.") + @JsonPropertyDescription(""" + Labels and annotations to apply to the Console Deployment resource. + """) private MetadataTemplate metadata; - @JsonPropertyDescription("Spec for the deployment template.") + @JsonPropertyDescription(""" + Spec for the Console Deployment. Provides access to the pod template \ + to configure scheduling constraints, pod metadata, the server container, \ + and other pod-level settings. + """) private DeploymentSpecTemplate spec; public MetadataTemplate getMetadata() { diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/MetadataTemplate.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/MetadataTemplate.java index e2ee601e9..44f983e56 100644 --- a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/MetadataTemplate.java +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/MetadataTemplate.java @@ -3,6 +3,7 @@ import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; import io.sundr.builder.annotations.Buildable; @@ -10,7 +11,10 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class MetadataTemplate { + @JsonPropertyDescription("Additional labels to apply to the resource.") private Map labels; + + @JsonPropertyDescription("Additional annotations to apply to the resource.") private Map annotations; public Map getLabels() { diff --git a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodTemplate.java b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodTemplate.java index 322dc8411..f39910bbb 100644 --- a/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodTemplate.java +++ b/operator/src/main/java/com/github/streamshub/console/api/v1alpha1/spec/template/PodTemplate.java @@ -9,10 +9,17 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class PodTemplate { - @JsonPropertyDescription("Metadata for the pod template.") + @JsonPropertyDescription(""" + Labels and annotations to apply to the console pod. + """) private MetadataTemplate metadata; - @JsonPropertyDescription("Spec for the pod template.") + @JsonPropertyDescription(""" + Spec for the console pod. Allows configuration of scheduling constraints \ + (affinity, tolerations, topology spread constraints, and node selector) \ + and the server container image, pull policy, resources, and environment \ + variables. + """) private PodSpecTemplate spec; public MetadataTemplate getMetadata() { diff --git a/operator/src/main/java/com/github/streamshub/console/dependents/ConsoleDeployment.java b/operator/src/main/java/com/github/streamshub/console/dependents/ConsoleDeployment.java index e9949a58e..a0d6cb956 100644 --- a/operator/src/main/java/com/github/streamshub/console/dependents/ConsoleDeployment.java +++ b/operator/src/main/java/com/github/streamshub/console/dependents/ConsoleDeployment.java @@ -20,6 +20,7 @@ import com.github.streamshub.console.api.v1alpha1.spec.containers.ContainerTemplateSpec; import com.github.streamshub.console.api.v1alpha1.spec.containers.Containers; import com.github.streamshub.console.api.v1alpha1.spec.template.ContainerTemplate; +import com.github.streamshub.console.api.v1alpha1.spec.template.ContainerTemplate.ImagePullPolicy; import com.github.streamshub.console.api.v1alpha1.spec.template.DeploymentSpecTemplate; import com.github.streamshub.console.api.v1alpha1.spec.template.DeploymentTemplate; import com.github.streamshub.console.api.v1alpha1.spec.template.MetadataTemplate; @@ -87,6 +88,7 @@ protected Deployment desired(Console primary, Context context) { .or(() -> images.map(Images::getApi)) .orElse(defaultAPIImage); String imagePullPolicy = containerTemplate.map(ContainerTemplate::getImagePullPolicy) + .map(ImagePullPolicy::value) .orElseGet(() -> pullPolicy(image)); var containerResources = containerTemplate.map(ContainerTemplate::getResources) .or(() -> templateAPI.map(ContainerSpec::getResources)) @@ -208,7 +210,9 @@ private static List buildEnvVars(Deployment base, List containers, Optional images, Optional envs) { From e61ad9bba0856e26f3303b4510c2b095823ebbe6 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Wed, 29 Jul 2026 15:56:37 -0400 Subject: [PATCH 4/5] Add documentation for deployment customization via the operator and CR Assisted-by: IBM Bob Signed-off-by: Michael Edgar --- .../assemblies/assembly-deploying.adoc | 2 + .../ref-deployment-configuration.adoc | 152 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 docs/sources/modules/deploying/ref-deployment-configuration.adoc diff --git a/docs/sources/assemblies/assembly-deploying.adoc b/docs/sources/assemblies/assembly-deploying.adoc index 6eea85cbb..1d3ab1069 100644 --- a/docs/sources/assemblies/assembly-deploying.adoc +++ b/docs/sources/assemblies/assembly-deploying.adoc @@ -40,6 +40,8 @@ include::../modules/deploying/proc-deploying-operator-crd.adoc[leveloffset=+2] include::../modules/deploying/proc-connecting-console.adoc[leveloffset=+1] //TLS configuration for the console server include::../modules/deploying/ref-tls-configuration.adoc[leveloffset=+2] +//Deployment configuration (scheduling, container overrides) +include::../modules/deploying/ref-deployment-configuration.adoc[leveloffset=+2] //virtual kafka clusters include::../modules/deploying/ref-virtual-kafka-clusters.adoc[leveloffset=+2] //cluster connection authentication options diff --git a/docs/sources/modules/deploying/ref-deployment-configuration.adoc b/docs/sources/modules/deploying/ref-deployment-configuration.adoc new file mode 100644 index 000000000..70b7d8229 --- /dev/null +++ b/docs/sources/modules/deploying/ref-deployment-configuration.adoc @@ -0,0 +1,152 @@ +:_mod-docs-content-type: REFERENCE + +// Module included in the following assemblies: +// +// assembly-deploying.adoc + +[id='ref-deployment-configuration-{context}'] += Configuring the console deployment + +[role="_abstract"] +You can customize how the operator deploys the console by configuring the `spec.deployment` field of the `Console` custom resource. +This field provides a template that mirrors the structure of a Kubernetes `Deployment` manifest, giving you control over pod scheduling and container configuration. + +`spec.deployment` is optional. +When omitted, the operator deploys the console using default settings. + +== Deployment template properties + +`spec.deployment.metadata`:: Optional labels and annotations to apply to the `Deployment` resource itself. +`spec.deployment.metadata.labels`:: Additional labels to add to the `Deployment`. +`spec.deployment.metadata.annotations`:: Additional annotations to add to the `Deployment`. +`spec.deployment.spec.template`:: Template applied to the console pod. +`spec.deployment.spec.template.metadata`:: Optional labels and annotations to apply to the console pod. +`spec.deployment.spec.template.metadata.labels`:: Additional labels to add to the pod. +`spec.deployment.spec.template.metadata.annotations`:: Additional annotations to add to the pod. +`spec.deployment.spec.template.spec`:: Spec for the console pod. Configures scheduling constraints and the server container. + +=== Pod scheduling properties + +`spec.deployment.spec.template.spec.affinity`:: The pod's affinity rules. +Constrains which node the console pod is eligible to be scheduled on, based on node labels or the labels of other pods already running on a node. +See the https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity[Kubernetes affinity documentation] for details. +`spec.deployment.spec.template.spec.tolerations`:: The pod's tolerations. +Allows the console pod to be scheduled onto a node that has matching taints. +See the https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/[Kubernetes taints and tolerations documentation] for details. +`spec.deployment.spec.template.spec.topologySpreadConstraints`:: The pod's topology spread constraints. +Controls how the console pod is spread across failure-domains such as regions, zones, or nodes. +See the https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/[Kubernetes topology spread constraints documentation] for details. +`spec.deployment.spec.template.spec.nodeSelector`:: A label selector that must match a node's labels for the console pod to be scheduled on that node. +Specified as a map of key-value pairs. + +=== Server container properties + +`spec.deployment.spec.template.spec.serverContainer`:: Template for the console server container. +`spec.deployment.spec.template.spec.serverContainer.image`:: Container image to use for the console server. +When omitted, the operator uses its built-in default image. +`spec.deployment.spec.template.spec.serverContainer.imagePullPolicy`:: Image pull policy for the console server image. +Accepted values are `Always`, `IfNotPresent`, and `Never`. +When omitted, the operator defaults to `IfNotPresent` for images specified by digest and `Always` for all other images. +`spec.deployment.spec.template.spec.serverContainer.resources`:: CPU and memory resource requests and limits for the console server container. +`spec.deployment.spec.template.spec.serverContainer.env`:: Additional environment variables to set on the console server container. + +== Examples + +=== Scheduling the console pod on specific nodes + +The following example uses a `nodeSelector` to schedule the console pod only on a node labeled `disktype: ssd`, and adds a toleration so that the pod can be placed on a node tainted `dedicated=console:NoSchedule`. + +[source,yaml] +---- +apiVersion: console.streamshub.github.com/v1alpha1 +kind: Console +metadata: + name: my-console +spec: + hostname: my-console. + deployment: + spec: + template: + spec: + nodeSelector: + disktype: ssd + tolerations: + - key: dedicated + operator: Equal + value: console + effect: NoSchedule + kafkaClusters: + - name: console-kafka + namespace: kafka + listener: secure + credentials: + kafkaUser: + name: console-kafka-user1 +---- + +=== Pinning the console to a specific availability zone using node affinity + +The following example uses a preferred node affinity rule to favor nodes in the `us-east-1a` availability zone. + +[source,yaml] +---- +apiVersion: console.streamshub.github.com/v1alpha1 +kind: Console +metadata: + name: my-console +spec: + hostname: my-console. + deployment: + spec: + template: + spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 1 + preference: + matchExpressions: + - key: topology.kubernetes.io/zone + operator: In + values: + - us-east-1a + kafkaClusters: + - name: console-kafka + namespace: kafka + listener: secure + credentials: + kafkaUser: + name: console-kafka-user1 +---- + +=== Overriding the server container image and resources + +[source,yaml] +---- +apiVersion: console.streamshub.github.com/v1alpha1 +kind: Console +metadata: + name: my-console +spec: + hostname: my-console. + deployment: + spec: + template: + spec: + serverContainer: + image: quay.io/my-org/console-api:latest + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + kafkaClusters: + - name: console-kafka + namespace: kafka + listener: secure + credentials: + kafkaUser: + name: console-kafka-user1 +---- From a3769d919bb5190a8e4cf61d6090a71db9c3395f Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Wed, 29 Jul 2026 16:08:56 -0400 Subject: [PATCH 5/5] Add test cases for several deployment template scenarios Assisted-by: IBM Bob Signed-off-by: Michael Edgar --- .../ConsoleReconcilerTemplateTest.java | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/operator/src/test/java/com/github/streamshub/console/ConsoleReconcilerTemplateTest.java b/operator/src/test/java/com/github/streamshub/console/ConsoleReconcilerTemplateTest.java index 3cf2e344d..8efcbf8fd 100644 --- a/operator/src/test/java/com/github/streamshub/console/ConsoleReconcilerTemplateTest.java +++ b/operator/src/test/java/com/github/streamshub/console/ConsoleReconcilerTemplateTest.java @@ -8,9 +8,11 @@ import com.github.streamshub.console.api.v1alpha1.Console; import com.github.streamshub.console.api.v1alpha1.ConsoleBuilder; import com.github.streamshub.console.dependents.ConsoleDeployment; +import com.github.streamshub.console.dependents.ConsoleResource; import com.github.streamshub.console.dependents.PrometheusDeployment; import io.fabric8.kubernetes.api.model.AffinityBuilder; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; import io.fabric8.kubernetes.api.model.NodeAffinityBuilder; import io.fabric8.kubernetes.api.model.NodeSelectorRequirementBuilder; import io.fabric8.kubernetes.api.model.NodeSelectorTermBuilder; @@ -19,6 +21,8 @@ import io.quarkus.test.junit.QuarkusTest; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; @QuarkusTest class ConsoleReconcilerTemplateTest extends ConsoleReconcilerTestBase { @@ -88,4 +92,184 @@ void testConsoleReconciliationWithDeploymentTemplate() { awaitReady(consoleCR); } + + @Test + void testDeploymentAndPodMetadataTemplateApplied() { + Console consoleCR = new ConsoleBuilder() + .withMetadata(new ObjectMetaBuilder() + .withName(CONSOLE_NAME) + .withNamespace(CONSOLE_NS) + .build()) + .withNewSpec() + .withHostname("console.example.com") + .withNewDeployment() + .withNewMetadata() + .withLabels(Map.of("custom-deploy-label", "deploy-value")) + .withAnnotations(Map.of("custom-deploy-annotation", "deploy-ann-value")) + .endMetadata() + .withNewSpec() + .withNewTemplate() + .withNewMetadata() + .withLabels(Map.of("custom-pod-label", "pod-value")) + .withAnnotations(Map.of("custom-pod-annotation", "pod-ann-value")) + .endMetadata() + .endTemplate() + .endSpec() + .endDeployment() + .addNewKafkaCluster() + .withName(kafkaCR.getMetadata().getName()) + .withNamespace(kafkaCR.getMetadata().getNamespace()) + .withListener(kafkaCR.getSpec().getKafka().getListeners().get(0).getName()) + .endKafkaCluster() + .endSpec() + .build(); + + client.resource(consoleCR).create(); + + awaitDependentsNotReady(consoleCR, "ConsoleDeployment", "PrometheusDeployment"); + setDeploymentReady(consoleCR, PrometheusDeployment.NAME); + awaitDependentsNotReady(consoleCR, "ConsoleDeployment"); + var consoleDeployment = setDeploymentReady(consoleCR, ConsoleDeployment.NAME); + awaitDependentsNotReady(consoleCR, "ConsoleIngress"); + setConsoleIngressReady(consoleCR); + + // Deployment metadata — user labels merged alongside required system labels + var deployLabels = consoleDeployment.getMetadata().getLabels(); + assertEquals("deploy-value", deployLabels.get("custom-deploy-label")); + // System labels must not be overwritten + assertEquals(ConsoleResource.MANAGER, deployLabels.get("app.kubernetes.io/managed-by")); + assertEquals("console", deployLabels.get("app.kubernetes.io/name")); + + var deployAnnotations = consoleDeployment.getMetadata().getAnnotations(); + assertEquals("deploy-ann-value", deployAnnotations.get("custom-deploy-annotation")); + + // Pod template metadata — user labels merged alongside required instance label + var podLabels = consoleDeployment.getSpec().getTemplate().getMetadata().getLabels(); + assertEquals("pod-value", podLabels.get("custom-pod-label")); + // Required instance label must be present and not overwritten + String expectedInstanceLabel = CONSOLE_NAME + "-" + ConsoleDeployment.NAME; + assertEquals(expectedInstanceLabel, podLabels.get("app.kubernetes.io/instance")); + + var podAnnotations = consoleDeployment.getSpec().getTemplate().getMetadata().getAnnotations(); + assertEquals("pod-ann-value", podAnnotations.get("custom-pod-annotation")); + + awaitReady(consoleCR); + } + + @Test + void testSystemLabelsCannotBeOverwrittenByDeploymentTemplate() { + Console consoleCR = new ConsoleBuilder() + .withMetadata(new ObjectMetaBuilder() + .withName(CONSOLE_NAME) + .withNamespace(CONSOLE_NS) + .build()) + .withNewSpec() + .withHostname("console.example.com") + .withNewDeployment() + .withNewMetadata() + // Attempt to overwrite system-managed labels + .withLabels(Map.of( + "app.kubernetes.io/managed-by", "my-own-operator", + "app.kubernetes.io/name", "my-app")) + .endMetadata() + .withNewSpec() + .withNewTemplate() + .withNewMetadata() + // Attempt to overwrite the required instance label on the pod + .withLabels(Map.of("app.kubernetes.io/instance", "tampered")) + .endMetadata() + .endTemplate() + .endSpec() + .endDeployment() + .addNewKafkaCluster() + .withName(kafkaCR.getMetadata().getName()) + .withNamespace(kafkaCR.getMetadata().getNamespace()) + .withListener(kafkaCR.getSpec().getKafka().getListeners().get(0).getName()) + .endKafkaCluster() + .endSpec() + .build(); + + client.resource(consoleCR).create(); + + awaitDependentsNotReady(consoleCR, "ConsoleDeployment", "PrometheusDeployment"); + setDeploymentReady(consoleCR, PrometheusDeployment.NAME); + awaitDependentsNotReady(consoleCR, "ConsoleDeployment"); + var consoleDeployment = setDeploymentReady(consoleCR, ConsoleDeployment.NAME); + awaitDependentsNotReady(consoleCR, "ConsoleIngress"); + setConsoleIngressReady(consoleCR); + + var deployLabels = consoleDeployment.getMetadata().getLabels(); + assertEquals(ConsoleResource.MANAGER, deployLabels.get("app.kubernetes.io/managed-by")); + assertEquals("console", deployLabels.get("app.kubernetes.io/name")); + + String expectedInstanceLabel = CONSOLE_NAME + "-" + ConsoleDeployment.NAME; + var podLabels = consoleDeployment.getSpec().getTemplate().getMetadata().getLabels(); + assertEquals(expectedInstanceLabel, podLabels.get("app.kubernetes.io/instance")); + + awaitReady(consoleCR); + } + + @Test + void testServerContainerEnvOverridePreventedForSystemVar() { + Console consoleCR = new ConsoleBuilder() + .withMetadata(new ObjectMetaBuilder() + .withName(CONSOLE_NAME) + .withNamespace(CONSOLE_NS) + .build()) + .withNewSpec() + .withHostname("console.example.com") + .withNewDeployment() + .withNewSpec() + .withNewTemplate() + .withNewSpec() + .withNewServerContainer() + // Attempt to override the system-seeded CONSOLE_CONFIG_PATH + .addToEnv(new EnvVarBuilder() + .withName("CONSOLE_CONFIG_PATH") + .withValue("/tampered/path") + .build()) + // A custom var that should be added normally + .addToEnv(new EnvVarBuilder() + .withName("CUSTOM_VAR") + .withValue("custom-value") + .build()) + .endServerContainer() + .endSpec() + .endTemplate() + .endSpec() + .endDeployment() + .addNewKafkaCluster() + .withName(kafkaCR.getMetadata().getName()) + .withNamespace(kafkaCR.getMetadata().getNamespace()) + .withListener(kafkaCR.getSpec().getKafka().getListeners().get(0).getName()) + .endKafkaCluster() + .endSpec() + .build(); + + client.resource(consoleCR).create(); + + awaitDependentsNotReady(consoleCR, "ConsoleDeployment", "PrometheusDeployment"); + setDeploymentReady(consoleCR, PrometheusDeployment.NAME); + awaitDependentsNotReady(consoleCR, "ConsoleDeployment"); + var consoleDeployment = setDeploymentReady(consoleCR, ConsoleDeployment.NAME); + awaitDependentsNotReady(consoleCR, "ConsoleIngress"); + setConsoleIngressReady(consoleCR); + + var env = consoleDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv(); + + // The system-seeded value must not be overwritten + var configPath = env.stream() + .filter(e -> "CONSOLE_CONFIG_PATH".equals(e.getName())) + .findFirst() + .orElseThrow(); + assertNotEquals("/tampered/path", configPath.getValue(), + "CONSOLE_CONFIG_PATH must not be overrideable by the deployment template"); + + // The custom var must be present + assertTrue(env.stream().anyMatch(e -> "CUSTOM_VAR".equals(e.getName()) + && "custom-value".equals(e.getValue())), + "CUSTOM_VAR should be added to the container env"); + + awaitReady(consoleCR); + } }