Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/controller/node/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,10 @@ func servicePorts() []corev1.ServicePort {
ports := make([]corev1.ServicePort, len(np))
for i, p := range np {
ports[i] = corev1.ServicePort{Name: p.Name, Port: p.Port, TargetPort: intstr.FromInt32(p.Port), Protocol: corev1.ProtocolTCP}
if p.Name == "grpc" {
h2c := "kubernetes.io/h2c"
ports[i].AppProtocol = &h2c
}
}
return ports
}
56 changes: 44 additions & 12 deletions internal/controller/nodedeployment/networking.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type effectiveRoute struct {
Name string
Hostnames []string
Port int32
WSPort int32 // non-zero when WebSocket requires a separate backend port
}

// hasExternalService returns true when the deployment has a LoadBalancer
Expand Down Expand Up @@ -198,6 +199,10 @@ func portsForMode(mode seiconfig.NodeMode) []corev1.ServicePort {
TargetPort: intstr.FromInt32(p.Port),
Protocol: corev1.ProtocolTCP,
}
if p.Name == "grpc" {
h2c := "kubernetes.io/h2c"
ports[i].AppProtocol = &h2c
}
}
return ports
}
Expand Down Expand Up @@ -242,11 +247,15 @@ func resolveEffectiveRoutes(group *seiv1alpha1.SeiNodeDeployment, domain string)
if !isProtocolActiveForMode(proto.Prefix, activePorts) {
continue
}
routes = append(routes, effectiveRoute{
er := 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,
})
}
if proto.Prefix == "evm" && activePorts["evm-ws"] {
er.WSPort = seiconfig.PortEVMWS
}
routes = append(routes, er)
}
return routes
}
Expand Down Expand Up @@ -331,6 +340,38 @@ func generateHTTPRoute(group *seiv1alpha1.SeiNodeDeployment, er effectiveRoute,
"namespace": gatewayNamespace,
}

rules := []any{
map[string]any{
"backendRefs": []any{
map[string]any{
"name": svcName,
"port": int64(er.Port),
},
},
},
}
if er.WSPort != 0 {
rules = append(rules, map[string]any{
"matches": []any{
map[string]any{
"headers": []any{
map[string]any{
"type": "Exact",
"name": "Upgrade",
"value": "websocket",
},
},
},
},
"backendRefs": []any{
map[string]any{
"name": svcName,
"port": int64(er.WSPort),
},
},
})
}

route := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "gateway.networking.k8s.io/v1",
Expand All @@ -344,16 +385,7 @@ func generateHTTPRoute(group *seiv1alpha1.SeiNodeDeployment, er effectiveRoute,
"spec": map[string]any{
"parentRefs": []any{parentRef},
"hostnames": hostnames,
"rules": []any{
map[string]any{
"backendRefs": []any{
map[string]any{
"name": svcName,
"port": int64(er.Port),
},
},
},
},
"rules": rules,
},
},
}
Expand Down
115 changes: 113 additions & 2 deletions internal/controller/nodedeployment/networking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,24 @@ func TestGenerateExternalService_ValidatorModePorts(t *testing.T) {
g.Expect(portNames).To(ConsistOf("p2p", "metrics"))
}

func TestGenerateExternalService_GRPCAppProtocol(t *testing.T) {
g := NewWithT(t)
group := newTestGroup("pacific-1-rpc", "sei")
group.Spec.Networking = &seiv1alpha1.NetworkingConfig{
Service: &seiv1alpha1.ExternalServiceConfig{},
}

svc := generateExternalService(group)
for _, p := range svc.Spec.Ports {
if p.Name == "grpc" {
g.Expect(p.AppProtocol).NotTo(BeNil())
g.Expect(*p.AppProtocol).To(Equal("kubernetes.io/h2c"))
return
}
}
t.Fatal("grpc port not found")
}

func TestGenerateExternalService_Annotations(t *testing.T) {
g := NewWithT(t)
group := newTestGroup("archive-rpc", "sei")
Expand Down Expand Up @@ -202,21 +220,60 @@ func TestGenerateHTTPRoute_EVMMerged(t *testing.T) {

routes := resolveEffectiveRoutes(group, "prod.platform.sei.io")

var evmRoute effectiveRoute
evmCount := 0
for _, r := range routes {
if r.Name == "pacific-1-rpc-evm" {
evmCount++
g.Expect(r.Port).To(Equal(int32(8545)))
evmRoute = r
}
}
g.Expect(evmCount).To(Equal(1), "expected exactly one merged EVM route")
g.Expect(evmRoute.Port).To(Equal(int32(8545)))
g.Expect(evmRoute.WSPort).To(Equal(int32(8546)))

for _, r := range routes {
g.Expect(r.Name).NotTo(ContainSubstring("evm-rpc"))
g.Expect(r.Name).NotTo(ContainSubstring("evm-ws"))
}
}

func TestGenerateHTTPRoute_EVMWebSocketRule(t *testing.T) {
g := NewWithT(t)
group := newTestGroup("pacific-1-rpc", "sei")
group.Spec.Networking = &seiv1alpha1.NetworkingConfig{
Service: &seiv1alpha1.ExternalServiceConfig{},
}

routes := resolveEffectiveRoutes(group, "prod.platform.sei.io")
var evmRoute effectiveRoute
for _, r := range routes {
if r.Name == "pacific-1-rpc-evm" {
evmRoute = r
break
}
}

httpRoute := generateHTTPRoute(group, evmRoute, "sei-gateway", "gateway")
spec := httpRoute.Object["spec"].(map[string]any)
rules := spec["rules"].([]any)
g.Expect(rules).To(HaveLen(2), "EVM route should have HTTP + WebSocket rules")

httpRule := rules[0].(map[string]any)
httpBackend := httpRule["backendRefs"].([]any)[0].(map[string]any)
g.Expect(httpBackend["port"]).To(Equal(int64(8545)))

wsRule := rules[1].(map[string]any)
wsMatches := wsRule["matches"].([]any)
wsHeaders := wsMatches[0].(map[string]any)["headers"].([]any)
wsHeader := wsHeaders[0].(map[string]any)
g.Expect(wsHeader["name"]).To(Equal("Upgrade"))
g.Expect(wsHeader["value"]).To(Equal("websocket"))

wsBackend := wsRule["backendRefs"].([]any)[0].(map[string]any)
g.Expect(wsBackend["port"]).To(Equal(int64(8546)))
}

// --- HTTPRoute Generation ---

func TestGenerateHTTPRoute_BasicFields(t *testing.T) {
Expand Down Expand Up @@ -264,7 +321,14 @@ func TestGenerateHTTPRoute_BackendRef(t *testing.T) {
}

routes := resolveEffectiveRoutes(group, "prod.platform.sei.io")
route := generateHTTPRoute(group, routes[0], "sei-gateway", "istio-system")
var rpcRoute effectiveRoute
for _, r := range routes {
if r.Name == "archive-rpc-rpc" {
rpcRoute = r
break
}
}
route := generateHTTPRoute(group, rpcRoute, "sei-gateway", "istio-system")

spec := route.Object["spec"].(map[string]any)
rules := spec["rules"].([]any)
Expand Down Expand Up @@ -319,6 +383,53 @@ func TestIsProtocolActiveForMode_EVMMapping(t *testing.T) {
g.Expect(isProtocolActiveForMode("grpc", activePorts)).To(BeFalse())
}

// --- Edge Cases ---

func TestResolveEffectiveRoutes_EmptyDomain_MalformedHostnames(t *testing.T) {
g := NewWithT(t)
group := newTestGroup("pacific-1-rpc", "sei")
group.Spec.Networking = &seiv1alpha1.NetworkingConfig{
Service: &seiv1alpha1.ExternalServiceConfig{},
}

routes := resolveEffectiveRoutes(group, "")
g.Expect(routes).To(HaveLen(4), "routes are still generated even with empty domain")
g.Expect(routes[0].Hostnames[0]).To(Equal("pacific-1-rpc.evm."), "empty domain produces trailing dot")
}

func TestReconcileRoute_NoRoutesForValidatorMode(t *testing.T) {
g := NewWithT(t)
group := newTestGroup("pacific-1-val", "sei")
group.Spec.Template.Spec.Validator = &seiv1alpha1.ValidatorSpec{}
group.Spec.Networking = &seiv1alpha1.NetworkingConfig{
Service: &seiv1alpha1.ExternalServiceConfig{},
}

routes := resolveEffectiveRoutes(group, "prod.platform.sei.io")
g.Expect(routes).To(BeEmpty(), "validator mode should produce zero routes")
}

func TestGenerateHTTPRoute_NonEVMRoute_SingleRule(t *testing.T) {
g := NewWithT(t)
group := newTestGroup("pacific-1-rpc", "sei")
group.Spec.Networking = &seiv1alpha1.NetworkingConfig{
Service: &seiv1alpha1.ExternalServiceConfig{},
}

routes := resolveEffectiveRoutes(group, "prod.platform.sei.io")
for _, r := range routes {
if r.Name == "pacific-1-rpc-rpc" {
httpRoute := generateHTTPRoute(group, r, "sei-gateway", "gateway")
spec := httpRoute.Object["spec"].(map[string]any)
rules := spec["rules"].([]any)
g.Expect(rules).To(HaveLen(1), "non-EVM routes should have exactly one rule")
g.Expect(r.WSPort).To(Equal(int32(0)), "non-EVM routes should have zero WSPort")
return
}
}
t.Fatal("rpc route not found")
}

// --- AuthorizationPolicy ---

func TestGenerateAuthorizationPolicy_BasicStructure(t *testing.T) {
Expand Down
Loading