From ff32c4350d67cf35316be35c286bf62e10280a9f Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Thu, 2 Apr 2026 10:37:43 +0000 Subject: [PATCH] Add ip mode to external-services for static IP endpoints ExternalName services only support DNS names (CNAME records), not raw IP addresses. Add an ip mode that creates a headless Service + Endpoints with a static IP, enabling routing to host-network services like Kind gateway IPs or bare-metal endpoints. Spec format: external-services: my-service: ip: 172.18.0.1 port: 8899 Co-Authored-By: Claude Opus 4.6 --- stack_orchestrator/deploy/k8s/cluster_info.py | 21 +++++++++- stack_orchestrator/deploy/k8s/deploy_k8s.py | 42 +++++++++++++++++++ stack_orchestrator/deploy/spec.py | 2 + 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/stack_orchestrator/deploy/k8s/cluster_info.py b/stack_orchestrator/deploy/k8s/cluster_info.py index 36df515d..d2fd396a 100644 --- a/stack_orchestrator/deploy/k8s/cluster_info.py +++ b/stack_orchestrator/deploy/k8s/cluster_info.py @@ -1094,8 +1094,9 @@ class ClusterInfo: def get_external_service_resources(self) -> List: """Build k8s Services (and Endpoints) for external-services in spec. - Two modes: + Three modes: - host mode: ExternalName Service (DNS CNAME to external host) + - ip mode: headless Service + Endpoints with a static IP - selector mode: headless Service + Endpoints (cross-namespace routing to a mock pod, IP discovered at deploy time) @@ -1124,6 +1125,24 @@ class ClusterInfo: ) resources.append(svc) + elif "ip" in config: + # Static IP: headless Service + Endpoints with fixed address. + # Useful for routing to host-network services (e.g. Kind + # host gateway) or any endpoint reachable by raw IP. + svc = client.V1Service( + metadata=client.V1ObjectMeta( + name=name, + labels={"app": self.app_name}, + ), + spec=client.V1ServiceSpec( + cluster_ip="None", + ports=[client.V1ServicePort(port=port, name=f"port-{port}")], + ), + ) + resources.append(svc) + # Endpoints are created in deploy_k8s.py using the + # static IP — no pod discovery needed. + elif "selector" in config and "namespace" in config: # Cross-namespace headless Service + Endpoints. # The Endpoints IP is populated in deploy_k8s.py at deploy diff --git a/stack_orchestrator/deploy/k8s/deploy_k8s.py b/stack_orchestrator/deploy/k8s/deploy_k8s.py index 0b2dd7ae..a9a227fc 100644 --- a/stack_orchestrator/deploy/k8s/deploy_k8s.py +++ b/stack_orchestrator/deploy/k8s/deploy_k8s.py @@ -431,6 +431,7 @@ class K8sDeployer(Deployer): """Create k8s Services for external-services declared in the spec. For host mode: ExternalName Service (DNS CNAME). + For ip mode: headless Service + Endpoints with static IP. For selector mode: headless Service + Endpoints with pod IPs discovered from the target namespace. """ @@ -461,6 +462,47 @@ class K8sDeployer(Deployer): else: raise + # Create Endpoints for ip-mode services (static IP) + for name, svc_config in ext_services.items(): + if "ip" not in svc_config: + continue + if opts.o.dry_run: + continue + + ip = svc_config["ip"] + port = svc_config.get("port", 443) + + endpoints = client.V1Endpoints( + metadata=client.V1ObjectMeta( + name=name, + labels={"app": self.cluster_info.app_name}, + ), + subsets=[ + client.V1EndpointSubset( + addresses=[client.V1EndpointAddress(ip=ip)], + ports=[ + client.CoreV1EndpointPort(port=port, name=f"port-{port}") + ], + ) + ], + ) + + try: + self.core_api.create_namespaced_endpoints( + body=endpoints, namespace=self.k8s_namespace + ) + print(f"Created endpoints for '{name}' → {ip}:{port}") + except ApiException as e: + if e.status == 409: + self.core_api.replace_namespaced_endpoints( + name=name, + namespace=self.k8s_namespace, + body=endpoints, + ) + print(f"Updated endpoints for '{name}' → {ip}:{port}") + else: + raise + # Create Endpoints for selector-mode services for name, svc_config in ext_services.items(): if "selector" not in svc_config or "namespace" not in svc_config: diff --git a/stack_orchestrator/deploy/spec.py b/stack_orchestrator/deploy/spec.py index a798fdab..5ac61662 100644 --- a/stack_orchestrator/deploy/spec.py +++ b/stack_orchestrator/deploy/spec.py @@ -319,6 +319,8 @@ class Spec: Each entry maps a service name to its routing config: - host mode: {host: "example.com", port: 443} → ExternalName k8s Service (DNS CNAME) + - ip mode: {ip: "172.18.0.1", port: 8899} + → Headless Service + Endpoints with static IP address - selector mode: {selector: {app: "foo"}, namespace: "ns", port: 443} → Headless Service + Endpoints (cross-namespace routing to mock pod) """