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 <noreply@anthropic.com>
pull/740/head
Prathamesh Musale 2026-04-02 10:37:43 +00:00
parent 185ebf17f9
commit ff32c4350d
3 changed files with 64 additions and 1 deletions

View File

@ -1094,8 +1094,9 @@ class ClusterInfo:
def get_external_service_resources(self) -> List: def get_external_service_resources(self) -> List:
"""Build k8s Services (and Endpoints) for external-services in spec. """Build k8s Services (and Endpoints) for external-services in spec.
Two modes: Three modes:
- host mode: ExternalName Service (DNS CNAME to external host) - 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 - selector mode: headless Service + Endpoints (cross-namespace
routing to a mock pod, IP discovered at deploy time) routing to a mock pod, IP discovered at deploy time)
@ -1124,6 +1125,24 @@ class ClusterInfo:
) )
resources.append(svc) 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: elif "selector" in config and "namespace" in config:
# Cross-namespace headless Service + Endpoints. # Cross-namespace headless Service + Endpoints.
# The Endpoints IP is populated in deploy_k8s.py at deploy # The Endpoints IP is populated in deploy_k8s.py at deploy

View File

@ -431,6 +431,7 @@ class K8sDeployer(Deployer):
"""Create k8s Services for external-services declared in the spec. """Create k8s Services for external-services declared in the spec.
For host mode: ExternalName Service (DNS CNAME). 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 For selector mode: headless Service + Endpoints with pod IPs
discovered from the target namespace. discovered from the target namespace.
""" """
@ -461,6 +462,47 @@ class K8sDeployer(Deployer):
else: else:
raise 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 # Create Endpoints for selector-mode services
for name, svc_config in ext_services.items(): for name, svc_config in ext_services.items():
if "selector" not in svc_config or "namespace" not in svc_config: if "selector" not in svc_config or "namespace" not in svc_config:

View File

@ -319,6 +319,8 @@ class Spec:
Each entry maps a service name to its routing config: Each entry maps a service name to its routing config:
- host mode: {host: "example.com", port: 443} - host mode: {host: "example.com", port: 443}
ExternalName k8s Service (DNS CNAME) 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} - selector mode: {selector: {app: "foo"}, namespace: "ns", port: 443}
Headless Service + Endpoints (cross-namespace routing to mock pod) Headless Service + Endpoints (cross-namespace routing to mock pod)
""" """