Skip to content

Commit 5b28288

Browse files
bdchathamclaude
andauthored
refactor: derive Gateway routes from node mode with listener-based hostnames (#69)
* refactor: derive Gateway routes from node mode with listener-based hostname pattern Replace baseDomain/hostnames CRD fields with mode-derived route generation. The Gateway owns the protocol topology — deployments just attach to it. Key changes: - Remove Hostnames, BaseDomain from GatewayRouteConfig (CRD simplification) - Merge evm-rpc + evm-ws into single "evm" listener (industry standard) - Derive listeners from node mode via NodePortsForMode(): full/archive → evm, rpc, rest, grpc (4 routes) validator → none (no public traffic) - Hostnames follow {deployment}.{protocol}.{domain} pattern - Add SEI_GATEWAY_DOMAIN platform env var for hostname construction - Gateway config on CRD just needs to exist (non-nil) to enable routing Example: SeiNodeDeployment "pacific-1" with domain "prod.platform.sei.io": pacific-1.evm.prod.platform.sei.io → :8545 pacific-1.rpc.prod.platform.sei.io → :26657 pacific-1.rest.prod.platform.sei.io → :1317 pacific-1.grpc.prod.platform.sei.io → :9090 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: always generate HTTPRoutes from mode, make gateway field optional Address PR review feedback: - Routes are now always generated when networking is configured and the node mode has public ports. The gateway field is only needed for custom annotations on HTTPRoutes. - Remove CEL validation requiring gateway when service is configured - Remove gateway: {} from sample manifest (no longer needed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0218260 commit 5b28288

12 files changed

Lines changed: 202 additions & 207 deletions

File tree

api/v1alpha1/networking_types.go

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,16 @@ const (
1919
// Routing uses the Kubernetes Gateway API exclusively; the platform must
2020
// install the Gateway API CRDs (v1+) and a Gateway implementation such
2121
// as Istio before HTTPRoute resources will take effect.
22-
// +kubebuilder:validation:XValidation:rule="!has(self.gateway) || has(self.service)",message="gateway requires service to be configured"
2322
type NetworkingConfig struct {
2423
// Service creates a non-headless Service shared across all replicas.
2524
// Each SeiNode still gets its own headless Service for pod DNS.
2625
// +optional
2726
Service *ExternalServiceConfig `json:"service,omitempty"`
2827

29-
// Gateway creates a gateway.networking.k8s.io/v1 HTTPRoute
30-
// targeting a shared Gateway (e.g. Istio ingress gateway).
28+
// Gateway provides optional annotations for generated HTTPRoute resources.
29+
// HTTPRoutes are generated automatically when the node mode has public
30+
// ports and the platform Gateway env vars are configured. This field is
31+
// only needed to add custom annotations to the HTTPRoute metadata.
3132
// +optional
3233
Gateway *GatewayRouteConfig `json:"gateway,omitempty"`
3334

@@ -55,20 +56,11 @@ type ExternalServiceConfig struct {
5556
// targeting the platform Gateway (configured via SEI_GATEWAY_NAME and
5657
// SEI_GATEWAY_NAMESPACE environment variables on the controller).
5758
//
58-
// +kubebuilder:validation:XValidation:rule=”(has(self.hostnames) && size(self.hostnames) > 0) || (has(self.baseDomain) && size(self.baseDomain) > 0)”,message=”at least one of hostnames or baseDomain must be set”
59+
// Hostnames are derived automatically from the deployment name, protocol,
60+
// and the platform domain (SEI_GATEWAY_DOMAIN). Which protocols get
61+
// HTTPRoutes is determined by the node mode via seiconfig.NodePortsForMode.
5962
type GatewayRouteConfig struct {
60-
// Hostnames routes all listed hostnames to the RPC port (26657).
61-
// For multi-protocol routing, use BaseDomain instead.
62-
// +optional
63-
Hostnames []string `json:"hostnames,omitempty"`
64-
65-
// BaseDomain generates HTTPRoutes for all standard Sei protocols
66-
// using conventional subdomain prefixes (rpc.*, rest.*, grpc.*,
67-
// evm-rpc.*, evm-ws.*), each routing to the correct backend port.
68-
// +optional
69-
BaseDomain string `json:"baseDomain,omitempty"`
70-
71-
// Annotations are merged onto the HTTPRoute metadata.
63+
// Annotations are merged onto HTTPRoute metadata.
7264
// +optional
7365
Annotations map[string]string `json:"annotations,omitempty"`
7466
}

api/v1alpha1/seinodedeployment_types.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,6 @@ const (
316316
// +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=`.status.replicas`
317317
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
318318
// +kubebuilder:printcolumn:name="Revision",type=string,JSONPath=`.status.deployment.entrantRevision`,priority=1
319-
// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.networking.gateway.hostnames[0]`,priority=1
320319
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
321320

322321
// SeiNodeDeployment is the Schema for the seinodedeployments API.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 0 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func main() {
134134
GenesisRegion: os.Getenv("SEI_GENESIS_REGION"),
135135
GatewayName: os.Getenv("SEI_GATEWAY_NAME"),
136136
GatewayNamespace: os.Getenv("SEI_GATEWAY_NAMESPACE"),
137+
GatewayDomain: os.Getenv("SEI_GATEWAY_DOMAIN"),
137138
}
138139

139140
if err := platformCfg.Validate(); err != nil {
@@ -185,6 +186,7 @@ func main() {
185186
ControllerSA: controllerSA,
186187
GatewayName: platformCfg.GatewayName,
187188
GatewayNamespace: platformCfg.GatewayNamespace,
189+
GatewayDomain: platformCfg.GatewayDomain,
188190
PlanExecutor: &planner.Executor[*seiv1alpha1.SeiNodeDeployment]{
189191
Client: kc,
190192
ConfigFor: func(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) task.ExecutionConfig {

config/crd/sei.io_seinodedeployments.yaml

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ spec:
3030
name: Revision
3131
priority: 1
3232
type: string
33-
- jsonPath: .spec.networking.gateway.hostnames[0]
34-
name: Host
35-
priority: 1
36-
type: string
3733
- jsonPath: .metadata.creationTimestamp
3834
name: Age
3935
type: date
@@ -192,32 +188,17 @@ spec:
192188
properties:
193189
gateway:
194190
description: |-
195-
Gateway creates a gateway.networking.k8s.io/v1 HTTPRoute
196-
targeting a shared Gateway (e.g. Istio ingress gateway).
191+
Gateway provides optional annotations for generated HTTPRoute resources.
192+
HTTPRoutes are generated automatically when the node mode has public
193+
ports and the platform Gateway env vars are configured. This field is
194+
only needed to add custom annotations to the HTTPRoute metadata.
197195
properties:
198196
annotations:
199197
additionalProperties:
200198
type: string
201-
description: Annotations are merged onto the HTTPRoute metadata.
199+
description: Annotations are merged onto HTTPRoute metadata.
202200
type: object
203-
baseDomain:
204-
description: |-
205-
BaseDomain generates HTTPRoutes for all standard Sei protocols
206-
using conventional subdomain prefixes (rpc.*, rest.*, grpc.*,
207-
evm-rpc.*, evm-ws.*), each routing to the correct backend port.
208-
type: string
209-
hostnames:
210-
description: |-
211-
Hostnames routes all listed hostnames to the RPC port (26657).
212-
For multi-protocol routing, use BaseDomain instead.
213-
items:
214-
type: string
215-
type: array
216201
type: object
217-
x-kubernetes-validations:
218-
- message: at least one of hostnames or baseDomain must be set
219-
rule: '(has(self.hostnames) && size(self.hostnames) > 0) ||
220-
(has(self.baseDomain) && size(self.baseDomain) > 0)'
221202
isolation:
222203
description: Isolation configures network-level access control
223204
for node pods.
@@ -280,9 +261,6 @@ spec:
280261
type: string
281262
type: object
282263
type: object
283-
x-kubernetes-validations:
284-
- message: gateway requires service to be configured
285-
rule: '!has(self.gateway) || has(self.service)'
286264
replicas:
287265
default: 1
288266
description: Replicas is the number of SeiNode instances to create.

internal/controller/nodedeployment/controller.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ type SeiNodeDeploymentReconciler struct {
4040
// controller can always reach the seictl sidecar.
4141
ControllerSA string
4242

43-
// GatewayName and GatewayNamespace identify the platform Gateway for
44-
// HTTPRoute parentRefs. Read from SEI_GATEWAY_NAME / SEI_GATEWAY_NAMESPACE.
43+
// GatewayName, GatewayNamespace, and GatewayDomain identify the platform
44+
// Gateway for HTTPRoute parentRefs and hostname derivation.
45+
// Read from SEI_GATEWAY_NAME / SEI_GATEWAY_NAMESPACE / SEI_GATEWAY_DOMAIN.
4546
GatewayName string
4647
GatewayNamespace string
48+
GatewayDomain string
4749

4850
// PlanExecutor drives group-level task plans (e.g. genesis assembly).
4951
PlanExecutor planner.PlanExecutor[*seiv1alpha1.SeiNodeDeployment]

internal/controller/nodedeployment/networking.go

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,10 @@ var seiProtocolRoutes = []struct {
2626
Prefix string
2727
Port int32
2828
}{
29+
{"evm", seiconfig.PortEVMHTTP},
2930
{"rpc", seiconfig.PortRPC},
3031
{"rest", seiconfig.PortREST},
3132
{"grpc", seiconfig.PortGRPC},
32-
{"evm-rpc", seiconfig.PortEVMHTTP},
33-
{"evm-ws", seiconfig.PortEVMWS},
3433
}
3534

3635
type effectiveRoute struct {
@@ -204,11 +203,12 @@ func portsForMode(mode seiconfig.NodeMode) []corev1.ServicePort {
204203
}
205204

206205
func (r *SeiNodeDeploymentReconciler) reconcileRoute(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) error {
207-
if group.Spec.Networking.Gateway == nil {
206+
routes := resolveEffectiveRoutes(group, r.GatewayDomain)
207+
if len(routes) == 0 {
208208
removeCondition(group, seiv1alpha1.ConditionRouteReady)
209209
return r.deleteHTTPRoutesByLabel(ctx, group)
210210
}
211-
return r.reconcileHTTPRoute(ctx, group)
211+
return r.reconcileHTTPRoutes(ctx, group, routes)
212212
}
213213

214214
func (r *SeiNodeDeploymentReconciler) deleteHTTPRoutesByLabel(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) error {
@@ -229,29 +229,36 @@ func (r *SeiNodeDeploymentReconciler) deleteHTTPRoutesByLabel(ctx context.Contex
229229
return nil
230230
}
231231

232-
func resolveEffectiveRoutes(group *seiv1alpha1.SeiNodeDeployment) []effectiveRoute {
233-
cfg := group.Spec.Networking.Gateway
234-
if cfg.BaseDomain != "" {
235-
routes := make([]effectiveRoute, len(seiProtocolRoutes))
236-
for i, p := range seiProtocolRoutes {
237-
routes[i] = effectiveRoute{
238-
Name: fmt.Sprintf("%s-%s", group.Name, p.Prefix),
239-
Hostnames: []string{fmt.Sprintf("%s.%s", p.Prefix, cfg.BaseDomain)},
240-
Port: p.Port,
241-
}
232+
func resolveEffectiveRoutes(group *seiv1alpha1.SeiNodeDeployment, domain string) []effectiveRoute {
233+
modePorts := seiconfig.NodePortsForMode(groupMode(group))
234+
235+
activePorts := make(map[string]bool, len(modePorts))
236+
for _, p := range modePorts {
237+
activePorts[p.Name] = true
238+
}
239+
240+
var routes []effectiveRoute
241+
for _, proto := range seiProtocolRoutes {
242+
if !isProtocolActiveForMode(proto.Prefix, activePorts) {
243+
continue
242244
}
243-
return routes
245+
routes = append(routes, effectiveRoute{
246+
Name: fmt.Sprintf("%s-%s", group.Name, proto.Prefix),
247+
Hostnames: []string{fmt.Sprintf("%s.%s.%s", group.Name, proto.Prefix, domain)},
248+
Port: proto.Port,
249+
})
244250
}
245-
return []effectiveRoute{{
246-
Name: group.Name,
247-
Hostnames: cfg.Hostnames,
248-
Port: seiconfig.PortRPC,
249-
}}
251+
return routes
250252
}
251253

252-
func (r *SeiNodeDeploymentReconciler) reconcileHTTPRoute(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) error {
253-
routes := resolveEffectiveRoutes(group)
254+
func isProtocolActiveForMode(prefix string, activePorts map[string]bool) bool {
255+
if prefix == "evm" {
256+
return activePorts["evm-rpc"]
257+
}
258+
return activePorts[prefix]
259+
}
254260

261+
func (r *SeiNodeDeploymentReconciler) reconcileHTTPRoutes(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment, routes []effectiveRoute) error {
255262
desiredNames := make(map[string]bool, len(routes))
256263
for _, er := range routes {
257264
desiredNames[er.Name] = true

0 commit comments

Comments
 (0)