From af46fde8e08c599c7a09fd9059598863cdddbc10 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Thu, 9 Apr 2026 10:17:41 -0700 Subject: [PATCH 1/2] refactor: derive Gateway routes from node mode with listener-based hostname pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- api/v1alpha1/networking_types.go | 17 +- api/v1alpha1/seinodedeployment_types.go | 1 - api/v1alpha1/zz_generated.deepcopy.go | 5 - cmd/main.go | 2 + config/crd/sei.io_seinodedeployments.yaml | 23 +- .../controller/nodedeployment/controller.go | 6 +- .../controller/nodedeployment/networking.go | 46 ++-- .../nodedeployment/networking_test.go | 250 ++++++++++-------- internal/platform/platform.go | 2 + internal/platform/platformtest/config.go | 1 + .../pacific-1-rpc-group.yaml | 3 +- manifests/sei.io_seinodedeployments.yaml | 23 +- 12 files changed, 189 insertions(+), 190 deletions(-) diff --git a/api/v1alpha1/networking_types.go b/api/v1alpha1/networking_types.go index f52a5eb..cd8a67c 100644 --- a/api/v1alpha1/networking_types.go +++ b/api/v1alpha1/networking_types.go @@ -55,20 +55,11 @@ type ExternalServiceConfig struct { // targeting the platform Gateway (configured via SEI_GATEWAY_NAME and // SEI_GATEWAY_NAMESPACE environment variables on the controller). // -// +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” +// Hostnames are derived automatically from the deployment name, protocol, +// and the platform domain (SEI_GATEWAY_DOMAIN). Which protocols get +// HTTPRoutes is determined by the node mode via seiconfig.NodePortsForMode. type GatewayRouteConfig struct { - // Hostnames routes all listed hostnames to the RPC port (26657). - // For multi-protocol routing, use BaseDomain instead. - // +optional - Hostnames []string `json:"hostnames,omitempty"` - - // BaseDomain generates HTTPRoutes for all standard Sei protocols - // using conventional subdomain prefixes (rpc.*, rest.*, grpc.*, - // evm-rpc.*, evm-ws.*), each routing to the correct backend port. - // +optional - BaseDomain string `json:"baseDomain,omitempty"` - - // Annotations are merged onto the HTTPRoute metadata. + // Annotations are merged onto HTTPRoute metadata. // +optional Annotations map[string]string `json:"annotations,omitempty"` } diff --git a/api/v1alpha1/seinodedeployment_types.go b/api/v1alpha1/seinodedeployment_types.go index 047cbb3..db5f792 100644 --- a/api/v1alpha1/seinodedeployment_types.go +++ b/api/v1alpha1/seinodedeployment_types.go @@ -316,7 +316,6 @@ const ( // +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=`.status.replicas` // +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Revision",type=string,JSONPath=`.status.deployment.entrantRevision`,priority=1 -// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.networking.gateway.hostnames[0]`,priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // SeiNodeDeployment is the Schema for the seinodedeployments API. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index cd8404c..c51d624 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -243,11 +243,6 @@ func (in *FullNodeSpec) DeepCopy() *FullNodeSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GatewayRouteConfig) DeepCopyInto(out *GatewayRouteConfig) { *out = *in - if in.Hostnames != nil { - in, out := &in.Hostnames, &out.Hostnames - *out = make([]string, len(*in)) - copy(*out, *in) - } if in.Annotations != nil { in, out := &in.Annotations, &out.Annotations *out = make(map[string]string, len(*in)) diff --git a/cmd/main.go b/cmd/main.go index 4b9f107..ce15ea6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -134,6 +134,7 @@ func main() { GenesisRegion: os.Getenv("SEI_GENESIS_REGION"), GatewayName: os.Getenv("SEI_GATEWAY_NAME"), GatewayNamespace: os.Getenv("SEI_GATEWAY_NAMESPACE"), + GatewayDomain: os.Getenv("SEI_GATEWAY_DOMAIN"), } if err := platformCfg.Validate(); err != nil { @@ -185,6 +186,7 @@ func main() { ControllerSA: controllerSA, GatewayName: platformCfg.GatewayName, GatewayNamespace: platformCfg.GatewayNamespace, + GatewayDomain: platformCfg.GatewayDomain, PlanExecutor: &planner.Executor[*seiv1alpha1.SeiNodeDeployment]{ Client: kc, ConfigFor: func(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) task.ExecutionConfig { diff --git a/config/crd/sei.io_seinodedeployments.yaml b/config/crd/sei.io_seinodedeployments.yaml index c2abbb2..8777cbb 100644 --- a/config/crd/sei.io_seinodedeployments.yaml +++ b/config/crd/sei.io_seinodedeployments.yaml @@ -30,10 +30,6 @@ spec: name: Revision priority: 1 type: string - - jsonPath: .spec.networking.gateway.hostnames[0] - name: Host - priority: 1 - type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -198,26 +194,9 @@ spec: annotations: additionalProperties: type: string - description: Annotations are merged onto the HTTPRoute metadata. + description: Annotations are merged onto HTTPRoute metadata. type: object - baseDomain: - description: |- - BaseDomain generates HTTPRoutes for all standard Sei protocols - using conventional subdomain prefixes (rpc.*, rest.*, grpc.*, - evm-rpc.*, evm-ws.*), each routing to the correct backend port. - type: string - hostnames: - description: |- - Hostnames routes all listed hostnames to the RPC port (26657). - For multi-protocol routing, use BaseDomain instead. - items: - type: string - type: array type: object - x-kubernetes-validations: - - message: at least one of hostnames or baseDomain must be set - rule: '(has(self.hostnames) && size(self.hostnames) > 0) || - (has(self.baseDomain) && size(self.baseDomain) > 0)' isolation: description: Isolation configures network-level access control for node pods. diff --git a/internal/controller/nodedeployment/controller.go b/internal/controller/nodedeployment/controller.go index 0eb5eaa..5b4c6a0 100644 --- a/internal/controller/nodedeployment/controller.go +++ b/internal/controller/nodedeployment/controller.go @@ -40,10 +40,12 @@ type SeiNodeDeploymentReconciler struct { // controller can always reach the seictl sidecar. ControllerSA string - // GatewayName and GatewayNamespace identify the platform Gateway for - // HTTPRoute parentRefs. Read from SEI_GATEWAY_NAME / SEI_GATEWAY_NAMESPACE. + // GatewayName, GatewayNamespace, and GatewayDomain identify the platform + // Gateway for HTTPRoute parentRefs and hostname derivation. + // Read from SEI_GATEWAY_NAME / SEI_GATEWAY_NAMESPACE / SEI_GATEWAY_DOMAIN. GatewayName string GatewayNamespace string + GatewayDomain string // PlanExecutor drives group-level task plans (e.g. genesis assembly). PlanExecutor planner.PlanExecutor[*seiv1alpha1.SeiNodeDeployment] diff --git a/internal/controller/nodedeployment/networking.go b/internal/controller/nodedeployment/networking.go index 389f3ca..5d695bc 100644 --- a/internal/controller/nodedeployment/networking.go +++ b/internal/controller/nodedeployment/networking.go @@ -26,11 +26,10 @@ var seiProtocolRoutes = []struct { Prefix string Port int32 }{ + {"evm", seiconfig.PortEVMHTTP}, {"rpc", seiconfig.PortRPC}, {"rest", seiconfig.PortREST}, {"grpc", seiconfig.PortGRPC}, - {"evm-rpc", seiconfig.PortEVMHTTP}, - {"evm-ws", seiconfig.PortEVMWS}, } type effectiveRoute struct { @@ -229,28 +228,37 @@ func (r *SeiNodeDeploymentReconciler) deleteHTTPRoutesByLabel(ctx context.Contex return nil } -func resolveEffectiveRoutes(group *seiv1alpha1.SeiNodeDeployment) []effectiveRoute { - cfg := group.Spec.Networking.Gateway - if cfg.BaseDomain != "" { - routes := make([]effectiveRoute, len(seiProtocolRoutes)) - for i, p := range seiProtocolRoutes { - routes[i] = effectiveRoute{ - Name: fmt.Sprintf("%s-%s", group.Name, p.Prefix), - Hostnames: []string{fmt.Sprintf("%s.%s", p.Prefix, cfg.BaseDomain)}, - Port: p.Port, - } +func resolveEffectiveRoutes(group *seiv1alpha1.SeiNodeDeployment, domain string) []effectiveRoute { + modePorts := seiconfig.NodePortsForMode(groupMode(group)) + + activePorts := make(map[string]bool, len(modePorts)) + for _, p := range modePorts { + activePorts[p.Name] = true + } + + var routes []effectiveRoute + for _, proto := range seiProtocolRoutes { + if !isProtocolActiveForMode(proto.Prefix, activePorts) { + continue } - return routes + routes = append(routes, effectiveRoute{ + Name: fmt.Sprintf("%s-%s", group.Name, proto.Prefix), + Hostnames: []string{fmt.Sprintf("%s.%s.%s", group.Name, proto.Prefix, domain)}, + Port: proto.Port, + }) + } + return routes +} + +func isProtocolActiveForMode(prefix string, activePorts map[string]bool) bool { + if prefix == "evm" { + return activePorts["evm-rpc"] } - return []effectiveRoute{{ - Name: group.Name, - Hostnames: cfg.Hostnames, - Port: seiconfig.PortRPC, - }} + return activePorts[prefix] } func (r *SeiNodeDeploymentReconciler) reconcileHTTPRoute(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) error { - routes := resolveEffectiveRoutes(group) + routes := resolveEffectiveRoutes(group, r.GatewayDomain) desiredNames := make(map[string]bool, len(routes)) for _, er := range routes { diff --git a/internal/controller/nodedeployment/networking_test.go b/internal/controller/nodedeployment/networking_test.go index baefa8b..cf42623 100644 --- a/internal/controller/nodedeployment/networking_test.go +++ b/internal/controller/nodedeployment/networking_test.go @@ -98,23 +98,139 @@ func TestGenerateExternalService_NoPublishNotReady(t *testing.T) { g.Expect(svc.Spec.PublishNotReadyAddresses).To(BeFalse()) } -// --- HTTPRoute --- +func TestGenerateExternalService_FullModeIncludesAllPorts(t *testing.T) { + g := NewWithT(t) + group := newTestGroup("pacific-1-rpc", "sei") + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ + Service: &seiv1alpha1.ExternalServiceConfig{}, + } + + svc := generateExternalService(group) + portNames := make([]string, len(svc.Spec.Ports)) + for i, p := range svc.Spec.Ports { + portNames[i] = p.Name + } + g.Expect(portNames).To(ConsistOf("evm-rpc", "evm-ws", "grpc", "rest", "p2p", "rpc", "metrics")) +} + +// --- Effective Routes --- + +func TestResolveEffectiveRoutes_FullMode_FourRoutes(t *testing.T) { + g := NewWithT(t) + group := newTestGroup("pacific-1-rpc", "sei") + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ + Service: &seiv1alpha1.ExternalServiceConfig{}, + Gateway: &seiv1alpha1.GatewayRouteConfig{}, + } + + routes := resolveEffectiveRoutes(group, "prod.platform.sei.io") + g.Expect(routes).To(HaveLen(4)) + + type routeExpectation struct { + Name string + Hostname string + Port int32 + } + expected := []routeExpectation{ + {"pacific-1-rpc-evm", "pacific-1-rpc.evm.prod.platform.sei.io", 8545}, + {"pacific-1-rpc-rpc", "pacific-1-rpc.rpc.prod.platform.sei.io", 26657}, + {"pacific-1-rpc-rest", "pacific-1-rpc.rest.prod.platform.sei.io", 1317}, + {"pacific-1-rpc-grpc", "pacific-1-rpc.grpc.prod.platform.sei.io", 9090}, + } + for i, exp := range expected { + g.Expect(routes[i].Name).To(Equal(exp.Name)) + g.Expect(routes[i].Hostnames).To(Equal([]string{exp.Hostname})) + g.Expect(routes[i].Port).To(Equal(exp.Port)) + } +} + +func TestResolveEffectiveRoutes_ArchiveMode_FourRoutes(t *testing.T) { + g := NewWithT(t) + group := newTestGroup("pacific-1-archive", "sei") + group.Spec.Template.Spec.FullNode = nil + group.Spec.Template.Spec.Archive = &seiv1alpha1.ArchiveSpec{} + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ + Service: &seiv1alpha1.ExternalServiceConfig{}, + Gateway: &seiv1alpha1.GatewayRouteConfig{}, + } + + routes := resolveEffectiveRoutes(group, "prod.platform.sei.io") + g.Expect(routes).To(HaveLen(4)) +} + +func TestResolveEffectiveRoutes_ValidatorMode_NoRoutes(t *testing.T) { + g := NewWithT(t) + group := newTestGroup("pacific-1-val", "sei") + group.Spec.Template.Spec.FullNode = nil + group.Spec.Template.Spec.Validator = &seiv1alpha1.ValidatorSpec{} + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ + Service: &seiv1alpha1.ExternalServiceConfig{}, + Gateway: &seiv1alpha1.GatewayRouteConfig{}, + } + + routes := resolveEffectiveRoutes(group, "prod.platform.sei.io") + g.Expect(routes).To(BeEmpty()) +} + +func TestGenerateHTTPRoute_HostnamePattern(t *testing.T) { + g := NewWithT(t) + group := newTestGroup("pacific-1-rpc", "sei") + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ + Service: &seiv1alpha1.ExternalServiceConfig{}, + Gateway: &seiv1alpha1.GatewayRouteConfig{}, + } + + routes := resolveEffectiveRoutes(group, "prod.platform.sei.io") + g.Expect(routes).NotTo(BeEmpty()) + + for _, er := range routes { + route := generateHTTPRoute(group, er, "sei-gateway", "istio-system") + spec := route.Object["spec"].(map[string]any) + hostnames := spec["hostnames"].([]any) + g.Expect(hostnames).To(HaveLen(1)) + g.Expect(hostnames[0]).To(MatchRegexp(`^pacific-1-rpc\.\w+\.prod\.platform\.sei\.io$`)) + } +} + +func TestGenerateHTTPRoute_EVMMerged(t *testing.T) { + g := NewWithT(t) + group := newTestGroup("pacific-1-rpc", "sei") + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ + Service: &seiv1alpha1.ExternalServiceConfig{}, + Gateway: &seiv1alpha1.GatewayRouteConfig{}, + } + + routes := resolveEffectiveRoutes(group, "prod.platform.sei.io") + + evmCount := 0 + for _, r := range routes { + if r.Name == "pacific-1-rpc-evm" { + evmCount++ + g.Expect(r.Port).To(Equal(int32(8545))) + } + } + g.Expect(evmCount).To(Equal(1), "expected exactly one merged EVM route") + + for _, r := range routes { + g.Expect(r.Name).NotTo(ContainSubstring("evm-rpc")) + g.Expect(r.Name).NotTo(ContainSubstring("evm-ws")) + } +} + +// --- HTTPRoute Generation --- func TestGenerateHTTPRoute_BasicFields(t *testing.T) { g := NewWithT(t) group := newTestGroup("archive-rpc", "sei") group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{ - Hostnames: []string{"rpc.pacific-1.sei.io"}, - }, + Gateway: &seiv1alpha1.GatewayRouteConfig{}, } - routes := resolveEffectiveRoutes(group) - g.Expect(routes).To(HaveLen(1)) + routes := resolveEffectiveRoutes(group, "prod.platform.sei.io") + g.Expect(routes).NotTo(BeEmpty()) route := generateHTTPRoute(group, routes[0], "sei-gateway", "istio-system") - g.Expect(route.GetName()).To(Equal("archive-rpc")) g.Expect(route.GetNamespace()).To(Equal("sei")) spec := route.Object["spec"].(map[string]any) @@ -124,9 +240,6 @@ func TestGenerateHTTPRoute_BasicFields(t *testing.T) { ref := parentRefs[0].(map[string]any) g.Expect(ref["name"]).To(Equal("sei-gateway")) g.Expect(ref["namespace"]).To(Equal("istio-system")) - - hostnames := spec["hostnames"].([]any) - g.Expect(hostnames).To(ConsistOf("rpc.pacific-1.sei.io")) } func TestGenerateHTTPRoute_ManagedByAnnotation(t *testing.T) { @@ -134,12 +247,10 @@ func TestGenerateHTTPRoute_ManagedByAnnotation(t *testing.T) { group := newTestGroup("archive-rpc", "sei") group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{ - Hostnames: []string{"rpc.sei.io"}, - }, + Gateway: &seiv1alpha1.GatewayRouteConfig{}, } - routes := resolveEffectiveRoutes(group) + routes := resolveEffectiveRoutes(group, "prod.platform.sei.io") route := generateHTTPRoute(group, routes[0], "sei-gateway", "istio-system") g.Expect(route.GetAnnotations()).To(HaveKeyWithValue("sei.io/managed-by", "seinodedeployment")) } @@ -149,12 +260,10 @@ func TestGenerateHTTPRoute_BackendRef(t *testing.T) { group := newTestGroup("archive-rpc", "sei") group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{ - Hostnames: []string{"rpc.sei.io"}, - }, + Gateway: &seiv1alpha1.GatewayRouteConfig{}, } - routes := resolveEffectiveRoutes(group) + routes := resolveEffectiveRoutes(group, "prod.platform.sei.io") route := generateHTTPRoute(group, routes[0], "sei-gateway", "istio-system") spec := route.Object["spec"].(map[string]any) @@ -169,59 +278,29 @@ func TestGenerateHTTPRoute_BackendRef(t *testing.T) { g.Expect(backend["name"]).To(Equal("archive-rpc-external")) } -// --- BaseDomain HTTPRoutes --- - -func TestResolveEffectiveRoutes_BaseDomain_FiveRoutes(t *testing.T) { +func TestGenerateHTTPRoute_GRPCRoutePort(t *testing.T) { g := NewWithT(t) group := newTestGroup("pacific-1-rpc", "sei") group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{ - BaseDomain: "pacific-1.sei.io", - }, + Gateway: &seiv1alpha1.GatewayRouteConfig{}, } - routes := resolveEffectiveRoutes(group) - g.Expect(routes).To(HaveLen(5)) + routes := resolveEffectiveRoutes(group, "prod.platform.sei.io") - type routeExpectation struct { - Name string - Hostname string - Port int32 - } - expected := []routeExpectation{ - {"pacific-1-rpc-rpc", "rpc.pacific-1.sei.io", 26657}, - {"pacific-1-rpc-rest", "rest.pacific-1.sei.io", 1317}, - {"pacific-1-rpc-grpc", "grpc.pacific-1.sei.io", 9090}, - {"pacific-1-rpc-evm-rpc", "evm-rpc.pacific-1.sei.io", 8545}, - {"pacific-1-rpc-evm-ws", "evm-ws.pacific-1.sei.io", 8546}, - } - for i, exp := range expected { - g.Expect(routes[i].Name).To(Equal(exp.Name)) - g.Expect(routes[i].Hostnames).To(Equal([]string{exp.Hostname})) - g.Expect(routes[i].Port).To(Equal(exp.Port)) + var grpcRoute effectiveRoute + for _, r := range routes { + if r.Name == "pacific-1-rpc-grpc" { + grpcRoute = r + break + } } -} + g.Expect(grpcRoute.Name).NotTo(BeEmpty(), "grpc route should exist") -func TestGenerateHTTPRoute_BaseDomain_CorrectHostnamesAndPorts(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("pacific-1-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{ - BaseDomain: "pacific-1.sei.io", - }, - } - - routes := resolveEffectiveRoutes(group) - g.Expect(routes).To(HaveLen(5)) - - grpcRoute := generateHTTPRoute(group, routes[2], "sei-gateway", "istio-system") - g.Expect(grpcRoute.GetName()).To(Equal("pacific-1-rpc-grpc")) - - spec := grpcRoute.Object["spec"].(map[string]any) + httpRoute := generateHTTPRoute(group, grpcRoute, "sei-gateway", "istio-system") + spec := httpRoute.Object["spec"].(map[string]any) hostnames := spec["hostnames"].([]any) - g.Expect(hostnames).To(ConsistOf("grpc.pacific-1.sei.io")) + g.Expect(hostnames).To(ConsistOf("pacific-1-rpc.grpc.prod.platform.sei.io")) rules := spec["rules"].([]any) backend := rules[0].(map[string]any)["backendRefs"].([]any)[0].(map[string]any) @@ -229,52 +308,15 @@ func TestGenerateHTTPRoute_BaseDomain_CorrectHostnamesAndPorts(t *testing.T) { g.Expect(backend["name"]).To(Equal("pacific-1-rpc-external")) } -func TestResolveEffectiveRoutes_LegacyHostnames_SingleRoute(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{ - Hostnames: []string{"rpc.sei.io"}, - }, - } - - routes := resolveEffectiveRoutes(group) - g.Expect(routes).To(HaveLen(1)) - g.Expect(routes[0].Name).To(Equal("archive-rpc")) - g.Expect(routes[0].Hostnames).To(Equal([]string{"rpc.sei.io"})) - g.Expect(routes[0].Port).To(Equal(int32(26657))) -} - -func TestResolveEffectiveRoutes_BaseDomainTakesPrecedence(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{ - Hostnames: []string{"rpc.sei.io"}, - BaseDomain: "pacific-1.sei.io", - }, - } - - routes := resolveEffectiveRoutes(group) - g.Expect(routes).To(HaveLen(5)) - g.Expect(routes[0].Hostnames).To(Equal([]string{"rpc.pacific-1.sei.io"})) -} +// --- isProtocolActiveForMode --- -func TestGenerateExternalService_FullModeIncludesAllPorts(t *testing.T) { +func TestIsProtocolActiveForMode_EVMMapping(t *testing.T) { g := NewWithT(t) - group := newTestGroup("pacific-1-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - } + activePorts := map[string]bool{"evm-rpc": true, "evm-ws": true, "rpc": true} - svc := generateExternalService(group) - portNames := make([]string, len(svc.Spec.Ports)) - for i, p := range svc.Spec.Ports { - portNames[i] = p.Name - } - g.Expect(portNames).To(ConsistOf("evm-rpc", "evm-ws", "grpc", "rest", "p2p", "rpc", "metrics")) + g.Expect(isProtocolActiveForMode("evm", activePorts)).To(BeTrue()) + g.Expect(isProtocolActiveForMode("rpc", activePorts)).To(BeTrue()) + g.Expect(isProtocolActiveForMode("grpc", activePorts)).To(BeFalse()) } // --- AuthorizationPolicy --- diff --git a/internal/platform/platform.go b/internal/platform/platform.go index 5b3f476..41bc9a5 100644 --- a/internal/platform/platform.go +++ b/internal/platform/platform.go @@ -39,6 +39,7 @@ type Config struct { GatewayName string GatewayNamespace string + GatewayDomain string } // Validate returns an error if required fields are missing. @@ -65,6 +66,7 @@ func (c Config) Validate() error { "SEI_GENESIS_REGION": c.GenesisRegion, "SEI_GATEWAY_NAME": c.GatewayName, "SEI_GATEWAY_NAMESPACE": c.GatewayNamespace, + "SEI_GATEWAY_DOMAIN": c.GatewayDomain, } for name, val := range required { if val == "" { diff --git a/internal/platform/platformtest/config.go b/internal/platform/platformtest/config.go index a91c9b6..9d90b27 100644 --- a/internal/platform/platformtest/config.go +++ b/internal/platform/platformtest/config.go @@ -28,5 +28,6 @@ func Config() platform.Config { GenesisRegion: "us-east-2", GatewayName: "sei-gateway", GatewayNamespace: "istio-system", + GatewayDomain: "test.platform.sei.io", } } diff --git a/manifests/samples/seinodedeployment/pacific-1-rpc-group.yaml b/manifests/samples/seinodedeployment/pacific-1-rpc-group.yaml index 3ec1875..2dfee95 100644 --- a/manifests/samples/seinodedeployment/pacific-1-rpc-group.yaml +++ b/manifests/samples/seinodedeployment/pacific-1-rpc-group.yaml @@ -45,8 +45,7 @@ spec: service: type: ClusterIP - gateway: - baseDomain: pacific-1.sei.io + gateway: {} isolation: authorizationPolicy: diff --git a/manifests/sei.io_seinodedeployments.yaml b/manifests/sei.io_seinodedeployments.yaml index c2abbb2..8777cbb 100644 --- a/manifests/sei.io_seinodedeployments.yaml +++ b/manifests/sei.io_seinodedeployments.yaml @@ -30,10 +30,6 @@ spec: name: Revision priority: 1 type: string - - jsonPath: .spec.networking.gateway.hostnames[0] - name: Host - priority: 1 - type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -198,26 +194,9 @@ spec: annotations: additionalProperties: type: string - description: Annotations are merged onto the HTTPRoute metadata. + description: Annotations are merged onto HTTPRoute metadata. type: object - baseDomain: - description: |- - BaseDomain generates HTTPRoutes for all standard Sei protocols - using conventional subdomain prefixes (rpc.*, rest.*, grpc.*, - evm-rpc.*, evm-ws.*), each routing to the correct backend port. - type: string - hostnames: - description: |- - Hostnames routes all listed hostnames to the RPC port (26657). - For multi-protocol routing, use BaseDomain instead. - items: - type: string - type: array type: object - x-kubernetes-validations: - - message: at least one of hostnames or baseDomain must be set - rule: '(has(self.hostnames) && size(self.hostnames) > 0) || - (has(self.baseDomain) && size(self.baseDomain) > 0)' isolation: description: Isolation configures network-level access control for node pods. From 49de51f20ee951e5954f80e60c2c249ae92752be Mon Sep 17 00:00:00 2001 From: bdchatham Date: Thu, 9 Apr 2026 11:59:52 -0700 Subject: [PATCH 2/2] 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) --- api/v1alpha1/networking_types.go | 7 ++++--- config/crd/sei.io_seinodedeployments.yaml | 9 ++++----- internal/controller/nodedeployment/networking.go | 9 ++++----- .../samples/seinodedeployment/pacific-1-rpc-group.yaml | 2 -- manifests/sei.io_seinodedeployments.yaml | 9 ++++----- 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/api/v1alpha1/networking_types.go b/api/v1alpha1/networking_types.go index cd8a67c..d950de7 100644 --- a/api/v1alpha1/networking_types.go +++ b/api/v1alpha1/networking_types.go @@ -19,15 +19,16 @@ const ( // Routing uses the Kubernetes Gateway API exclusively; the platform must // install the Gateway API CRDs (v1+) and a Gateway implementation such // as Istio before HTTPRoute resources will take effect. -// +kubebuilder:validation:XValidation:rule="!has(self.gateway) || has(self.service)",message="gateway requires service to be configured" type NetworkingConfig struct { // Service creates a non-headless Service shared across all replicas. // Each SeiNode still gets its own headless Service for pod DNS. // +optional Service *ExternalServiceConfig `json:"service,omitempty"` - // Gateway creates a gateway.networking.k8s.io/v1 HTTPRoute - // targeting a shared Gateway (e.g. Istio ingress gateway). + // Gateway provides optional annotations for generated HTTPRoute resources. + // HTTPRoutes are generated automatically when the node mode has public + // ports and the platform Gateway env vars are configured. This field is + // only needed to add custom annotations to the HTTPRoute metadata. // +optional Gateway *GatewayRouteConfig `json:"gateway,omitempty"` diff --git a/config/crd/sei.io_seinodedeployments.yaml b/config/crd/sei.io_seinodedeployments.yaml index 8777cbb..44e73db 100644 --- a/config/crd/sei.io_seinodedeployments.yaml +++ b/config/crd/sei.io_seinodedeployments.yaml @@ -188,8 +188,10 @@ spec: properties: gateway: description: |- - Gateway creates a gateway.networking.k8s.io/v1 HTTPRoute - targeting a shared Gateway (e.g. Istio ingress gateway). + Gateway provides optional annotations for generated HTTPRoute resources. + HTTPRoutes are generated automatically when the node mode has public + ports and the platform Gateway env vars are configured. This field is + only needed to add custom annotations to the HTTPRoute metadata. properties: annotations: additionalProperties: @@ -259,9 +261,6 @@ spec: type: string type: object type: object - x-kubernetes-validations: - - message: gateway requires service to be configured - rule: '!has(self.gateway) || has(self.service)' replicas: default: 1 description: Replicas is the number of SeiNode instances to create. diff --git a/internal/controller/nodedeployment/networking.go b/internal/controller/nodedeployment/networking.go index 5d695bc..d9c90d5 100644 --- a/internal/controller/nodedeployment/networking.go +++ b/internal/controller/nodedeployment/networking.go @@ -203,11 +203,12 @@ func portsForMode(mode seiconfig.NodeMode) []corev1.ServicePort { } func (r *SeiNodeDeploymentReconciler) reconcileRoute(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) error { - if group.Spec.Networking.Gateway == nil { + routes := resolveEffectiveRoutes(group, r.GatewayDomain) + if len(routes) == 0 { removeCondition(group, seiv1alpha1.ConditionRouteReady) return r.deleteHTTPRoutesByLabel(ctx, group) } - return r.reconcileHTTPRoute(ctx, group) + return r.reconcileHTTPRoutes(ctx, group, routes) } func (r *SeiNodeDeploymentReconciler) deleteHTTPRoutesByLabel(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) error { @@ -257,9 +258,7 @@ func isProtocolActiveForMode(prefix string, activePorts map[string]bool) bool { return activePorts[prefix] } -func (r *SeiNodeDeploymentReconciler) reconcileHTTPRoute(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) error { - routes := resolveEffectiveRoutes(group, r.GatewayDomain) - +func (r *SeiNodeDeploymentReconciler) reconcileHTTPRoutes(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment, routes []effectiveRoute) error { desiredNames := make(map[string]bool, len(routes)) for _, er := range routes { desiredNames[er.Name] = true diff --git a/manifests/samples/seinodedeployment/pacific-1-rpc-group.yaml b/manifests/samples/seinodedeployment/pacific-1-rpc-group.yaml index 2dfee95..302273a 100644 --- a/manifests/samples/seinodedeployment/pacific-1-rpc-group.yaml +++ b/manifests/samples/seinodedeployment/pacific-1-rpc-group.yaml @@ -45,8 +45,6 @@ spec: service: type: ClusterIP - gateway: {} - isolation: authorizationPolicy: allowedSources: diff --git a/manifests/sei.io_seinodedeployments.yaml b/manifests/sei.io_seinodedeployments.yaml index 8777cbb..44e73db 100644 --- a/manifests/sei.io_seinodedeployments.yaml +++ b/manifests/sei.io_seinodedeployments.yaml @@ -188,8 +188,10 @@ spec: properties: gateway: description: |- - Gateway creates a gateway.networking.k8s.io/v1 HTTPRoute - targeting a shared Gateway (e.g. Istio ingress gateway). + Gateway provides optional annotations for generated HTTPRoute resources. + HTTPRoutes are generated automatically when the node mode has public + ports and the platform Gateway env vars are configured. This field is + only needed to add custom annotations to the HTTPRoute metadata. properties: annotations: additionalProperties: @@ -259,9 +261,6 @@ spec: type: string type: object type: object - x-kubernetes-validations: - - message: gateway requires service to be configured - rule: '!has(self.gateway) || has(self.service)' replicas: default: 1 description: Replicas is the number of SeiNode instances to create.