From 31a387581f03c1706475e98b2d83795f346c389d Mon Sep 17 00:00:00 2001 From: Vaishnav88sk Date: Tue, 26 May 2026 17:18:35 +0530 Subject: [PATCH 1/4] feat: migrate proxy sidecar injection and add Deployment proxy reconciler Signed-off-by: Vaishnav88sk --- .../admission/AdmissionControllers.java | 61 ++++++++++- .../operator/DeploymentProxyReconciler.java | 103 ++++++++++++++++++ 2 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 operator/src/main/java/io/reshapr/kubernetes/operator/DeploymentProxyReconciler.java diff --git a/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java b/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java index 7619bf1..7349e93 100644 --- a/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java +++ b/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java @@ -15,6 +15,8 @@ */ package io.reshapr.kubernetes.admission; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.Pod; import io.javaoperatorsdk.webhook.admission.AdmissionController; import io.javaoperatorsdk.webhook.admission.NotAllowedException; @@ -22,12 +24,23 @@ import io.javaoperatorsdk.webhook.admission.mutation.Mutator; import java.util.HashMap; +import java.util.Map; +import java.util.ArrayList; /** * @author laurent */ public class AdmissionControllers { + public static final String INJECT_ANNOTATION = "io.reshapr/inject"; + public static final String CONTROL_PLANE_URL_ANNOTATION = "io.reshapr/control-plane-url"; + public static final String TOKEN_SECRET_NAME_ANNOTATION = "io.reshapr/token-secret-name"; + + public static final String PROXY_INJECTED_LABEL = "reshapr.io/proxy-injected"; + + public static final String PROXY_CONTAINER_NAME = "reshapr-proxy"; + public static final String DEFAULT_PROXY_IMAGE = "quay.io/reshapr/reshapr-proxy:latest"; + private AdmissionControllers() { // Private constructor to prevent instantiation. } @@ -37,17 +50,55 @@ public static AdmissionController mutatingController() { } /** - * + * Mutates Pods to inject the Reshapr Proxy container if annotated. */ public static class PodMutator implements Mutator { @Override public Pod mutate(Pod resource, Operation operation) throws NotAllowedException { - // Example mutation: add a label to the Pod - if (resource.getMetadata().getLabels() == null) { - resource.getMetadata().setLabels(new HashMap<>()); + Map annotations = resource.getMetadata().getAnnotations(); + if (annotations != null && "true".equalsIgnoreCase(annotations.get(INJECT_ANNOTATION))) { + + // Check if already injected + if (resource.getSpec().getContainers() != null && resource.getSpec().getContainers().stream().anyMatch(c -> PROXY_CONTAINER_NAME.equals(c.getName()))) { + return resource; + } + + ContainerBuilder proxyBuilder = new ContainerBuilder() + .withName(PROXY_CONTAINER_NAME) + .withImage(DEFAULT_PROXY_IMAGE); + + // Inject control plane URL env + String controlPlaneUrl = annotations.get(CONTROL_PLANE_URL_ANNOTATION); + if (controlPlaneUrl != null && !controlPlaneUrl.isBlank()) { + proxyBuilder.addNewEnv() + .withName("RESHAPR_CONTROL_PLANE_URL") + .withValue(controlPlaneUrl) + .endEnv(); + } + + // Inject secret as envFrom + String secretName = annotations.get(TOKEN_SECRET_NAME_ANNOTATION); + if (secretName != null && !secretName.isBlank()) { + proxyBuilder.addNewEnvFrom() + .withNewSecretRef() + .withName(secretName) + .endSecretRef() + .endEnvFrom(); + } + + // Add the container to the Pod + if (resource.getSpec().getContainers() == null) { + resource.getSpec().setContainers(new ArrayList<>()); + } + resource.getSpec().getContainers().add(proxyBuilder.build()); + + // Add the routing label + if (resource.getMetadata().getLabels() == null) { + resource.getMetadata().setLabels(new HashMap<>()); + } + resource.getMetadata().getLabels().put(PROXY_INJECTED_LABEL, "true"); } - resource.getMetadata().getLabels().put("mutated", "true"); return resource; } } diff --git a/operator/src/main/java/io/reshapr/kubernetes/operator/DeploymentProxyReconciler.java b/operator/src/main/java/io/reshapr/kubernetes/operator/DeploymentProxyReconciler.java new file mode 100644 index 0000000..b17e5fc --- /dev/null +++ b/operator/src/main/java/io/reshapr/kubernetes/operator/DeploymentProxyReconciler.java @@ -0,0 +1,103 @@ +/* + * Copyright The Reshapr Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.reshapr.kubernetes.operator; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import org.jboss.logging.Logger; + +import java.util.HashMap; +import java.util.Map; + +@ControllerConfiguration +public class DeploymentProxyReconciler implements Reconciler { + + private static final Logger logger = Logger.getLogger(DeploymentProxyReconciler.class); + + public static final String INJECT_ANNOTATION = "io.reshapr/inject"; + public static final String PROXY_INJECTED_LABEL = "reshapr.io/proxy-injected"; + public static final int DEFAULT_PROXY_PORT = 8080; + + private final KubernetesClient client; + + public DeploymentProxyReconciler(KubernetesClient client) { + this.client = client; + } + + @Override + public UpdateControl reconcile(Deployment deployment, Context context) { + Map annotations = deployment.getMetadata().getAnnotations(); + String namespace = deployment.getMetadata().getNamespace(); + String deploymentName = deployment.getMetadata().getName(); + String serviceName = "reshapr-proxy-" + deploymentName; + + if (annotations == null || !"true".equalsIgnoreCase(annotations.get(INJECT_ANNOTATION))) { + Service existingService = client.services().inNamespace(namespace).withName(serviceName).get(); + if (existingService != null) { + logger.infof("Injection annotation absent/removed. Deleting dedicated Service %s in namespace %s", serviceName, namespace); + client.services().inNamespace(namespace).withName(serviceName).delete(); + } + return UpdateControl.noUpdate(); + } + + logger.infof("Reconciling Deployment %s/%s for Reshapr Proxy Service", namespace, deploymentName); + + // Check if service already exists + Service existingService = client.services().inNamespace(namespace).withName(serviceName).get(); + if (existingService == null) { + logger.infof("Creating dedicated Service %s in namespace %s", serviceName, namespace); + + Map serviceSelector = new HashMap<>(); + serviceSelector.put(PROXY_INJECTED_LABEL, "true"); + if (deployment.getSpec() != null && deployment.getSpec().getSelector() != null && deployment.getSpec().getSelector().getMatchLabels() != null) { + serviceSelector.putAll(deployment.getSpec().getSelector().getMatchLabels()); + } + + Service newService = new ServiceBuilder() + .withNewMetadata() + .withName(serviceName) + .withNamespace(namespace) + // Add owner reference so it's deleted when deployment is deleted + .addNewOwnerReference() + .withApiVersion("apps/v1") + .withKind("Deployment") + .withName(deploymentName) + .withUid(deployment.getMetadata().getUid()) + .endOwnerReference() + .endMetadata() + .withNewSpec() + .withSessionAffinity("ClientIP") + .withSelector(serviceSelector) + .addNewPort() + .withName("proxy") + .withPort(DEFAULT_PROXY_PORT) + .withNewTargetPort(DEFAULT_PROXY_PORT) + .endPort() + .endSpec() + .build(); + + client.services().inNamespace(namespace).resource(newService).create(); + } + + return UpdateControl.noUpdate(); + } +} From d2e593618a0163f3416c9eef49f91b2fa0f1b8e0 Mon Sep 17 00:00:00 2001 From: Vaishnav88sk Date: Tue, 26 May 2026 17:51:03 +0530 Subject: [PATCH 2/4] feat: add configmap fallback and gateway labels injection Signed-off-by: Vaishnav88sk --- .../admission/AdmissionControllerConfig.java | 7 ++- .../admission/AdmissionControllers.java | 54 ++++++++++++++++--- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllerConfig.java b/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllerConfig.java index 17161b9..757ea1d 100644 --- a/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllerConfig.java +++ b/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllerConfig.java @@ -16,7 +16,9 @@ package io.reshapr.kubernetes.admission; import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.webhook.admission.AdmissionController; +import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.inject.Singleton; @@ -27,9 +29,12 @@ public class AdmissionControllerConfig { public static final String MUTATING_CONTROLLER = "mutatingController"; + @Inject + KubernetesClient client; + @Singleton @Named(MUTATING_CONTROLLER) public AdmissionController mutatingController() { - return AdmissionControllers.mutatingController(); + return AdmissionControllers.mutatingController(client); } } diff --git a/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java b/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java index 7349e93..252652a 100644 --- a/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java +++ b/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java @@ -18,6 +18,7 @@ import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.webhook.admission.AdmissionController; import io.javaoperatorsdk.webhook.admission.NotAllowedException; import io.javaoperatorsdk.webhook.admission.Operation; @@ -45,8 +46,8 @@ private AdmissionControllers() { // Private constructor to prevent instantiation. } - public static AdmissionController mutatingController() { - return new AdmissionController<>(new PodMutator()); + public static AdmissionController mutatingController(KubernetesClient client) { + return new AdmissionController<>(new PodMutator(client)); } /** @@ -54,6 +55,12 @@ public static AdmissionController mutatingController() { */ public static class PodMutator implements Mutator { + private final KubernetesClient client; + + public PodMutator(KubernetesClient client) { + this.client = client; + } + @Override public Pod mutate(Pod resource, Operation operation) throws NotAllowedException { Map annotations = resource.getMetadata().getAnnotations(); @@ -64,12 +71,32 @@ public Pod mutate(Pod resource, Operation operation) throws NotAllowedException return resource; } + // Fetch defaults from ConfigMap + String namespace = resource.getMetadata().getNamespace(); + if (namespace == null) { + // If namespace is not set on the resource, it defaults to "default" in Kubernetes + namespace = "default"; + } + + Map configMapData = new HashMap<>(); + try { + var configMap = client.configMaps().inNamespace(namespace).withName("reshapr-injection-config").get(); + if (configMap != null && configMap.getData() != null) { + configMapData = configMap.getData(); + } + } catch (Exception e) { + // Ignore if ConfigMap doesn't exist or client fails + } + ContainerBuilder proxyBuilder = new ContainerBuilder() .withName(PROXY_CONTAINER_NAME) .withImage(DEFAULT_PROXY_IMAGE); - // Inject control plane URL env - String controlPlaneUrl = annotations.get(CONTROL_PLANE_URL_ANNOTATION); + // 1. Inject control plane URL + String controlPlaneUrl = annotations.containsKey(CONTROL_PLANE_URL_ANNOTATION) + ? annotations.get(CONTROL_PLANE_URL_ANNOTATION) + : configMapData.get("control-plane-url"); + if (controlPlaneUrl != null && !controlPlaneUrl.isBlank()) { proxyBuilder.addNewEnv() .withName("RESHAPR_CONTROL_PLANE_URL") @@ -77,8 +104,11 @@ public Pod mutate(Pod resource, Operation operation) throws NotAllowedException .endEnv(); } - // Inject secret as envFrom - String secretName = annotations.get(TOKEN_SECRET_NAME_ANNOTATION); + // 2. Inject secret as envFrom + String secretName = annotations.containsKey(TOKEN_SECRET_NAME_ANNOTATION) + ? annotations.get(TOKEN_SECRET_NAME_ANNOTATION) + : configMapData.get("token-secret-name"); + if (secretName != null && !secretName.isBlank()) { proxyBuilder.addNewEnvFrom() .withNewSecretRef() @@ -86,6 +116,18 @@ public Pod mutate(Pod resource, Operation operation) throws NotAllowedException .endSecretRef() .endEnvFrom(); } + + // 3. Inject Gateway Labels + String gatewayLabels = annotations.containsKey("io.reshapr/gateway-labels") + ? annotations.get("io.reshapr/gateway-labels") + : configMapData.get("gateway-labels"); + + if (gatewayLabels != null && !gatewayLabels.isBlank()) { + proxyBuilder.addNewEnv() + .withName("RESHAPR_GATEWAY_LABELS") + .withValue(gatewayLabels) + .endEnv(); + } // Add the container to the Pod if (resource.getSpec().getContainers() == null) { From b7cac9e4841f322faf8b2c18801b271ff1f4979e Mon Sep 17 00:00:00 2001 From: Vaishnav88sk Date: Tue, 26 May 2026 17:55:16 +0530 Subject: [PATCH 3/4] refactor: add gateway instance config and expose proxy port Signed-off-by: Vaishnav88sk --- .../admission/AdmissionControllers.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java b/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java index 252652a..8416784 100644 --- a/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java +++ b/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java @@ -90,7 +90,11 @@ public Pod mutate(Pod resource, Operation operation) throws NotAllowedException ContainerBuilder proxyBuilder = new ContainerBuilder() .withName(PROXY_CONTAINER_NAME) - .withImage(DEFAULT_PROXY_IMAGE); + .withImage(DEFAULT_PROXY_IMAGE) + .addNewPort() + .withContainerPort(8080) + .withName("proxy") + .endPort(); // 1. Inject control plane URL String controlPlaneUrl = annotations.containsKey(CONTROL_PLANE_URL_ANNOTATION) @@ -129,6 +133,18 @@ public Pod mutate(Pod resource, Operation operation) throws NotAllowedException .endEnv(); } + // 4. Inject Gateway Instance (Control Plane Binding) + String gatewayInstance = annotations.containsKey("io.reshapr/instance") + ? annotations.get("io.reshapr/instance") + : configMapData.get("instance"); + + if (gatewayInstance != null && !gatewayInstance.isBlank()) { + proxyBuilder.addNewEnv() + .withName("RESHAPR_GATEWAY_INSTANCE") + .withValue(gatewayInstance) + .endEnv(); + } + // Add the container to the Pod if (resource.getSpec().getContainers() == null) { resource.getSpec().setContainers(new ArrayList<>()); From 9696f667c8ce22c3c0228d0b2f0dd84f957c848f Mon Sep 17 00:00:00 2001 From: Vaishnav88sk Date: Mon, 8 Jun 2026 19:22:28 +0530 Subject: [PATCH 4/4] refactor: address review feedback on default namespace and proxy port Signed-off-by: Vaishnav88sk --- .../admission/AdmissionControllers.java | 17 +++++++++--- .../operator/DeploymentProxyReconciler.java | 27 ++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java b/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java index 8416784..806e3de 100644 --- a/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java +++ b/admission/src/main/java/io/reshapr/kubernetes/admission/AdmissionControllers.java @@ -74,8 +74,8 @@ public Pod mutate(Pod resource, Operation operation) throws NotAllowedException // Fetch defaults from ConfigMap String namespace = resource.getMetadata().getNamespace(); if (namespace == null) { - // If namespace is not set on the resource, it defaults to "default" in Kubernetes - namespace = "default"; + // If namespace is not set on the resource, it defaults to "reshapr-system" in Kubernetes + namespace = "reshapr-system"; } Map configMapData = new HashMap<>(); @@ -88,11 +88,22 @@ public Pod mutate(Pod resource, Operation operation) throws NotAllowedException // Ignore if ConfigMap doesn't exist or client fails } + // 0. Resolve proxy port + int proxyPort = 7777; + String portStr = annotations.containsKey("io.reshapr/proxy-port") + ? annotations.get("io.reshapr/proxy-port") + : configMapData.get("proxy-port"); + if (portStr != null && !portStr.isBlank()) { + try { + proxyPort = Integer.parseInt(portStr); + } catch (NumberFormatException ignored) {} + } + ContainerBuilder proxyBuilder = new ContainerBuilder() .withName(PROXY_CONTAINER_NAME) .withImage(DEFAULT_PROXY_IMAGE) .addNewPort() - .withContainerPort(8080) + .withContainerPort(proxyPort) .withName("proxy") .endPort(); diff --git a/operator/src/main/java/io/reshapr/kubernetes/operator/DeploymentProxyReconciler.java b/operator/src/main/java/io/reshapr/kubernetes/operator/DeploymentProxyReconciler.java index b17e5fc..392c2cb 100644 --- a/operator/src/main/java/io/reshapr/kubernetes/operator/DeploymentProxyReconciler.java +++ b/operator/src/main/java/io/reshapr/kubernetes/operator/DeploymentProxyReconciler.java @@ -35,7 +35,7 @@ public class DeploymentProxyReconciler implements Reconciler { public static final String INJECT_ANNOTATION = "io.reshapr/inject"; public static final String PROXY_INJECTED_LABEL = "reshapr.io/proxy-injected"; - public static final int DEFAULT_PROXY_PORT = 8080; + public static final int DEFAULT_PROXY_PORT = 7777; private final KubernetesClient client; @@ -47,6 +47,10 @@ public DeploymentProxyReconciler(KubernetesClient client) { public UpdateControl reconcile(Deployment deployment, Context context) { Map annotations = deployment.getMetadata().getAnnotations(); String namespace = deployment.getMetadata().getNamespace(); + if (namespace == null) { + namespace = "reshapr-system"; + } + String deploymentName = deployment.getMetadata().getName(); String serviceName = "reshapr-proxy-" + deploymentName; @@ -66,6 +70,23 @@ public UpdateControl reconcile(Deployment deployment, Context serviceSelector = new HashMap<>(); serviceSelector.put(PROXY_INJECTED_LABEL, "true"); if (deployment.getSpec() != null && deployment.getSpec().getSelector() != null && deployment.getSpec().getSelector().getMatchLabels() != null) { @@ -89,8 +110,8 @@ public UpdateControl reconcile(Deployment deployment, Context