Skip to content

Commit 6dec055

Browse files
authored
Merge pull request #1 from codeanker/feature/api-k8s-prefer-pod-ip
feat(api): dial Kubernetes-discovered orchestrators on their pod IP w…
2 parents 7a69f45 + 73e7418 commit 6dec055

5 files changed

Lines changed: 70 additions & 6 deletions

File tree

‎docs/ARCHITECTURE.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ The control-plane entry point (Gin, OpenAPI-generated from `spec/openapi.yml`, p
166166
running-sandbox (Redis) and paused-snapshot (Postgres) state so they survive pause/resume and
167167
orchestrator re-sync; a fork starts a new workload and does not inherit them.
168168
- **Placement**: keeps a live map of orchestrator nodes (discovered via Nomad, Kubernetes, or a
169-
static list). Chooses a node per sandbox with a **best-of-K** algorithm
169+
static list; Kubernetes-discovered pods are dialed on their node's address, or on their pod
170+
IP with `K8S_PREFER_POD_IP=true` for orchestrators that keep their own network namespace).
171+
Chooses a node per sandbox with a **best-of-K** algorithm
170172
(`internal/orchestrator/placement/`): sample K ready nodes, score by the
171173
higher of CPU load and hugepage-pool load, pick the lowest; retry on
172174
exhausted nodes. A node that refuses a create (`ResourceExhausted`) stays

‎packages/api/internal/cfg/model.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ type Config struct {
111111
K8sNamespace string `env:"K8S_NAMESPACE" envDefault:"default"`
112112
K8sOrchestratorPodLabelSelector string `env:"K8S_ORCHESTRATOR_POD_LABEL_SELECTOR" envDefault:"app.kubernetes.io/name=orchestrator"`
113113
K8sTemplateManagerPodLabelSelector string `env:"K8S_TEMPLATE_MANAGER_POD_LABEL_SELECTOR" envDefault:"app.kubernetes.io/name=template-manager"`
114+
// K8sPreferPodIP dials the discovered orchestrator and template-manager
115+
// pods on their pod IP instead of their node's. Off, the API dials
116+
// status.hostIP, which is where a pod with host networking listens (the
117+
// reference deploy). On, for a cluster whose orchestrator pods keep their
118+
// own network namespace and are reachable only through the pod network,
119+
// such as edge nodes behind an overlay that carries pod CIDRs but not node
120+
// addresses.
121+
K8sPreferPodIP bool `env:"K8S_PREFER_POD_IP"`
114122

115123
PostgresConnectionString string `env:"POSTGRES_CONNECTION_STRING,required,notEmpty"`
116124
DBMaxOpenConnections int32 `env:"DB_MAX_OPEN_CONNECTIONS" envDefault:"40"`

‎packages/api/internal/handlers/service_discovery_test.go‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,50 @@ func TestNewServiceDiscovery_ExplicitProviderSurvivesALocalEnvironment(t *testin
247247

248248
// The clusters registry owns orchestrator nodes only when the resolved provider
249249
// is the static local one — there both planes are the same single instance.
250+
// A pod with host networking reports the same address twice, so the fixture
251+
// pulls them apart: the node's address is where the reference deploy listens,
252+
// the pod's where an orchestrator with its own network namespace does.
253+
func TestNewServiceDiscovery_KubernetesDialsTheNodeUnlessPreferPodIP(t *testing.T) {
254+
t.Parallel()
255+
256+
const (
257+
nodeIP = "10.0.0.5"
258+
podIP = "10.42.1.3"
259+
)
260+
261+
tests := map[string]struct {
262+
preferPodIP bool
263+
want string
264+
}{
265+
"the default dials the node, where a host-network pod listens": {
266+
preferPodIP: false, want: nodeIP,
267+
},
268+
"K8S_PREFER_POD_IP dials the pod network": {
269+
preferPodIP: true, want: podIP,
270+
},
271+
}
272+
273+
for name, tt := range tests {
274+
t.Run(name, func(t *testing.T) {
275+
t.Parallel()
276+
277+
orchestrator := discoveredPod("orchestrator-1", "orchestrator", nodeIP)
278+
orchestrator.Status.PodIP = podIP
279+
builder := discoveredPod("template-manager-1", "template-manager", nodeIP)
280+
builder.Status.PodIP = podIP
281+
282+
config := discoveryConfig(cfg.ServiceDiscoveryProviderKubernetes, "")
283+
config.K8sPreferPodIP = tt.preferPodIP
284+
285+
sd, err := newServiceDiscovery(t.Context(), config, fixedKubeClient(orchestrator, builder), cfg.ServiceDiscoveryProviderKubernetes)
286+
require.NoError(t, err)
287+
288+
assert.Equal(t, []string{tt.want}, planeIPs(t, sd.nodes))
289+
assert.Equal(t, []string{tt.want}, planeIPs(t, sd.templateBuilders))
290+
})
291+
}
292+
}
293+
250294
// Deriving it from the environment instead builds a Nomad or Kubernetes node
251295
// plane that neither the periodic loop nor on-demand discovery ever consults,
252296
// so an orchestrator that is not also a template builder never registers.

‎packages/api/internal/handlers/store.go‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"github.com/e2b-dev/infra/packages/db/pkg/pool"
4545
"github.com/e2b-dev/infra/packages/shared/pkg/apierrors"
4646
sharedclusters "github.com/e2b-dev/infra/packages/shared/pkg/clusters/discovery"
47+
"github.com/e2b-dev/infra/packages/shared/pkg/consts"
4748
"github.com/e2b-dev/infra/packages/shared/pkg/env"
4849
"github.com/e2b-dev/infra/packages/shared/pkg/featureflags"
4950
managementv1 "github.com/e2b-dev/infra/packages/shared/pkg/grpc/contracts/secretsstore/management"
@@ -134,16 +135,23 @@ func newKubernetesServiceDiscovery(ctx context.Context, config cfg.Config, newKu
134135
return serviceDiscovery{}, fmt.Errorf("kubernetes client: %w", err)
135136
}
136137

138+
// Both planes dial the well-known orchestrator port. Which address they
139+
// dial it on follows K8sPreferPodIP: the node's by default, the pod's when
140+
// the orchestrator pods do not share their node's network namespace.
137141
return serviceDiscovery{
138-
nodes: kube.NewPods(
142+
nodes: kube.NewPodsOnPort(
139143
client,
140144
config.K8sNamespace,
141145
config.K8sOrchestratorPodLabelSelector,
146+
consts.OrchestratorAPIPort,
147+
!config.K8sPreferPodIP,
142148
),
143-
templateBuilders: kube.NewPods(
149+
templateBuilders: kube.NewPodsOnPort(
144150
client,
145151
config.K8sNamespace,
146152
config.K8sTemplateManagerPodLabelSelector,
153+
consts.OrchestratorAPIPort,
154+
!config.K8sPreferPodIP,
147155
),
148156
}, nil
149157
}

‎packages/shared/pkg/servicediscovery/kube/pods.go‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ var tracer = otel.Tracer("github.com/e2b-dev/infra/packages/shared/pkg/servicedi
1818
// k8sDiscovery lists pods of the orchestrator DaemonSet via the K8s API
1919
// server.
2020
//
21-
// Those pods run with host networking, so each pod's status.HostIP equals
22-
// status.PodIP and is the address the orchestrator gRPC server listens on. A
23-
// pod is returned while it is Running and has an IP; the Ready condition is
21+
// In the reference deploy those pods run with host networking, so each pod's
22+
// status.HostIP equals status.PodIP and is the address the orchestrator gRPC
23+
// server listens on; a consumer whose orchestrators keep their own network
24+
// namespace asks for the pod IP instead (NewPodsOnPort with preferHostIP
25+
// false). A pod is returned while it is Running and has an IP; the Ready condition is
2426
// deliberately not consulted, because it is false for instances that still
2527
// have to be reachable — self-reported unhealthy, not yet probed, on a
2628
// NotReady node. Liveness is in-band and routing gates on that instead.

0 commit comments

Comments
 (0)