From 14f2bcf3ace33fb46544c302c6e2295aec11e842 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Fri, 10 Apr 2026 16:41:39 -0700 Subject: [PATCH 1/2] refactor: simplify NetworkingConfig to empty presence signal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strip NetworkingConfig down to an empty struct. The presence of networking: {} means public (ClusterIP Service + HTTPRoutes), absence means private (headless Services only). Removed: - ExternalServiceConfig (Type, Annotations) — always ClusterIP now - GatewayRouteConfig — unused annotation passthrough - NetworkIsolationConfig / AuthorizationPolicy — deferred until isolation requirements are defined - LoadBalancer readiness gate — replaced with DNS hostname check - External P2P address propagation — LB-specific, no longer applies - ControllerSA field — only used by removed AuthorizationPolicy - ConditionExternalServiceReady, ConditionIsolationReady conditions - AuthorizationPolicy RBAC markers Co-Authored-By: Claude Opus 4.6 (1M context) --- api/v1alpha1/networking_types.go | 91 +--- api/v1alpha1/seinodedeployment_types.go | 2 - api/v1alpha1/zz_generated.deepcopy.go | 128 +----- cmd/main.go | 2 - config/crd/sei.io_seinodedeployments.yaml | 75 --- config/rbac/role.yaml | 12 - .../controller/nodedeployment/controller.go | 15 +- .../nodedeployment/external_address_test.go | 80 +--- internal/controller/nodedeployment/metrics.go | 2 - .../controller/nodedeployment/networking.go | 286 ++---------- .../nodedeployment/networking_test.go | 434 +++++------------- internal/controller/nodedeployment/status.go | 55 +-- .../controller/nodedeployment/status_test.go | 12 +- manifests/role.yaml | 12 - manifests/sei.io_seinodedeployments.yaml | 75 --- 15 files changed, 171 insertions(+), 1110 deletions(-) diff --git a/api/v1alpha1/networking_types.go b/api/v1alpha1/networking_types.go index d950de7..7b0338a 100644 --- a/api/v1alpha1/networking_types.go +++ b/api/v1alpha1/networking_types.go @@ -1,9 +1,5 @@ package v1alpha1 -import ( - corev1 "k8s.io/api/core/v1" -) - // DeletionPolicy controls what happens to managed networking resources // and child SeiNodes when their parent is deleted. // +kubebuilder:validation:Enum=Delete;Retain @@ -14,83 +10,14 @@ const ( DeletionPolicyRetain DeletionPolicy = "Retain" ) -// NetworkingConfig controls how the group is exposed to traffic. +// NetworkingConfig enables public networking for the deployment. // -// 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. -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 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"` - - // Isolation configures network-level access control for node pods. - // +optional - Isolation *NetworkIsolationConfig `json:"isolation,omitempty"` -} - -// ExternalServiceConfig defines the shared non-headless Service. -// Ports are derived automatically from the node mode via -// seiconfig.NodePortsForMode — no manual port selection needed. -type ExternalServiceConfig struct { - // Type is the Kubernetes Service type. - // +optional - // +kubebuilder:default=ClusterIP - // +kubebuilder:validation:Enum=ClusterIP;LoadBalancer;NodePort - Type corev1.ServiceType `json:"type,omitempty"` - - // Annotations are merged onto the Service metadata. - // +optional - Annotations map[string]string `json:"annotations,omitempty"` -} - -// GatewayRouteConfig creates gateway.networking.k8s.io/v1 HTTPRoute resources -// targeting the platform Gateway (configured via SEI_GATEWAY_NAME and -// SEI_GATEWAY_NAMESPACE environment variables on the controller). +// When present, the controller creates a ClusterIP Service (as the +// HTTPRoute backend) and generates HTTPRoute resources on the platform +// Gateway for each protocol the node mode supports. Hostnames are +// derived automatically from the deployment name, namespace, and the +// platform domain env vars. // -// 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 { - // Annotations are merged onto HTTPRoute metadata. - // +optional - Annotations map[string]string `json:"annotations,omitempty"` -} - -// NetworkIsolationConfig defines network-level access control. -type NetworkIsolationConfig struct { - // AuthorizationPolicy creates an Istio AuthorizationPolicy - // restricting which identities can reach node pods. - // +optional - AuthorizationPolicy *AuthorizationPolicyConfig `json:"authorizationPolicy,omitempty"` -} - -// AuthorizationPolicyConfig defines allowed traffic sources. -type AuthorizationPolicyConfig struct { - // AllowedSources defines who can reach this group's pods. - // The controller generates an ALLOW policy; traffic from - // sources not listed here is denied. - // +kubebuilder:validation:MinItems=1 - AllowedSources []TrafficSource `json:"allowedSources"` -} - -// TrafficSource identifies a set of callers by Istio identity. -// +kubebuilder:validation:XValidation:rule="has(self.principals) || has(self.namespaces)",message="at least one of principals or namespaces must be set" -type TrafficSource struct { - // Principals are SPIFFE identities (e.g. - // "cluster.local/ns/istio-system/sa/istio-ingressgateway"). - // +optional - Principals []string `json:"principals,omitempty"` - - // Namespaces allows all pods in these namespaces. - // +optional - Namespaces []string `json:"namespaces,omitempty"` -} +// When absent (nil), the deployment is private — only per-node headless +// Services exist for in-cluster access. +type NetworkingConfig struct{} diff --git a/api/v1alpha1/seinodedeployment_types.go b/api/v1alpha1/seinodedeployment_types.go index 6ac84bd..845da18 100644 --- a/api/v1alpha1/seinodedeployment_types.go +++ b/api/v1alpha1/seinodedeployment_types.go @@ -303,9 +303,7 @@ type DeploymentStatus struct { // Status condition types for SeiNodeDeployment. const ( ConditionNodesReady = "NodesReady" - ConditionExternalServiceReady = "ExternalServiceReady" ConditionRouteReady = "RouteReady" - ConditionIsolationReady = "IsolationReady" ConditionServiceMonitorReady = "ServiceMonitorReady" ConditionGenesisCeremonyComplete = "GenesisCeremonyComplete" ConditionPlanInProgress = "PlanInProgress" diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index a2fa5d1..5583dc9 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -31,28 +31,6 @@ func (in *ArchiveSpec) DeepCopy() *ArchiveSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AuthorizationPolicyConfig) DeepCopyInto(out *AuthorizationPolicyConfig) { - *out = *in - if in.AllowedSources != nil { - in, out := &in.AllowedSources, &out.AllowedSources - *out = make([]TrafficSource, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthorizationPolicyConfig. -func (in *AuthorizationPolicyConfig) DeepCopy() *AuthorizationPolicyConfig { - if in == nil { - return nil - } - out := new(AuthorizationPolicyConfig) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeploymentStatus) DeepCopyInto(out *DeploymentStatus) { *out = *in @@ -120,28 +98,6 @@ func (in *EntrypointConfig) DeepCopy() *EntrypointConfig { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExternalServiceConfig) DeepCopyInto(out *ExternalServiceConfig) { - *out = *in - if in.Annotations != nil { - in, out := &in.Annotations, &out.Annotations - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalServiceConfig. -func (in *ExternalServiceConfig) DeepCopy() *ExternalServiceConfig { - if in == nil { - return nil - } - out := new(ExternalServiceConfig) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FailedTaskInfo) DeepCopyInto(out *FailedTaskInfo) { *out = *in @@ -197,28 +153,6 @@ func (in *FullNodeSpec) DeepCopy() *FullNodeSpec { return out } -// 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.Annotations != nil { - in, out := &in.Annotations, &out.Annotations - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayRouteConfig. -func (in *GatewayRouteConfig) DeepCopy() *GatewayRouteConfig { - if in == nil { - return nil - } - out := new(GatewayRouteConfig) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GenesisAccount) DeepCopyInto(out *GenesisAccount) { *out = *in @@ -378,44 +312,9 @@ func (in *MonitoringConfig) DeepCopy() *MonitoringConfig { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *NetworkIsolationConfig) DeepCopyInto(out *NetworkIsolationConfig) { - *out = *in - if in.AuthorizationPolicy != nil { - in, out := &in.AuthorizationPolicy, &out.AuthorizationPolicy - *out = new(AuthorizationPolicyConfig) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkIsolationConfig. -func (in *NetworkIsolationConfig) DeepCopy() *NetworkIsolationConfig { - if in == nil { - return nil - } - out := new(NetworkIsolationConfig) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkingConfig) DeepCopyInto(out *NetworkingConfig) { *out = *in - if in.Service != nil { - in, out := &in.Service, &out.Service - *out = new(ExternalServiceConfig) - (*in).DeepCopyInto(*out) - } - if in.Gateway != nil { - in, out := &in.Gateway, &out.Gateway - *out = new(GatewayRouteConfig) - (*in).DeepCopyInto(*out) - } - if in.Isolation != nil { - in, out := &in.Isolation, &out.Isolation - *out = new(NetworkIsolationConfig) - (*in).DeepCopyInto(*out) - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkingConfig. @@ -666,7 +565,7 @@ func (in *SeiNodeDeploymentSpec) DeepCopyInto(out *SeiNodeDeploymentSpec) { if in.Networking != nil { in, out := &in.Networking, &out.Networking *out = new(NetworkingConfig) - (*in).DeepCopyInto(*out) + **out = **in } if in.Monitoring != nil { in, out := &in.Monitoring, &out.Monitoring @@ -1073,31 +972,6 @@ func (in *TaskPlan) DeepCopy() *TaskPlan { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TrafficSource) DeepCopyInto(out *TrafficSource) { - *out = *in - if in.Principals != nil { - in, out := &in.Principals, &out.Principals - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Namespaces != nil { - in, out := &in.Namespaces, &out.Namespaces - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficSource. -func (in *TrafficSource) DeepCopy() *TrafficSource { - if in == nil { - return nil - } - out := new(TrafficSource) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UpdateStrategy) DeepCopyInto(out *UpdateStrategy) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index c0083c9..f308295 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -177,14 +177,12 @@ func main() { os.Exit(1) } - controllerSA := os.Getenv("SEI_CONTROLLER_SA_PRINCIPAL") //nolint:staticcheck // migrating to events.EventRecorder API is a separate effort recorder := mgr.GetEventRecorderFor("seinodedeployment-controller") if err := (&nodedeploymentcontroller.SeiNodeDeploymentReconciler{ Client: kc, Scheme: mgr.GetScheme(), Recorder: recorder, - ControllerSA: controllerSA, GatewayName: platformCfg.GatewayName, GatewayNamespace: platformCfg.GatewayNamespace, GatewayDomain: platformCfg.GatewayDomain, diff --git a/config/crd/sei.io_seinodedeployments.yaml b/config/crd/sei.io_seinodedeployments.yaml index 1a4b222..778b5e6 100644 --- a/config/crd/sei.io_seinodedeployments.yaml +++ b/config/crd/sei.io_seinodedeployments.yaml @@ -185,81 +185,6 @@ spec: description: |- Networking controls how the group is exposed to traffic. Networking resources are shared across all replicas. - properties: - gateway: - description: |- - 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: - type: string - description: Annotations are merged onto HTTPRoute metadata. - type: object - type: object - isolation: - description: Isolation configures network-level access control - for node pods. - properties: - authorizationPolicy: - description: |- - AuthorizationPolicy creates an Istio AuthorizationPolicy - restricting which identities can reach node pods. - properties: - allowedSources: - description: |- - AllowedSources defines who can reach this group's pods. - The controller generates an ALLOW policy; traffic from - sources not listed here is denied. - items: - description: TrafficSource identifies a set of callers - by Istio identity. - properties: - namespaces: - description: Namespaces allows all pods in these - namespaces. - items: - type: string - type: array - principals: - description: |- - Principals are SPIFFE identities (e.g. - "cluster.local/ns/istio-system/sa/istio-ingressgateway"). - items: - type: string - type: array - type: object - x-kubernetes-validations: - - message: at least one of principals or namespaces - must be set - rule: has(self.principals) || has(self.namespaces) - minItems: 1 - type: array - required: - - allowedSources - type: object - type: object - service: - description: |- - Service creates a non-headless Service shared across all replicas. - Each SeiNode still gets its own headless Service for pod DNS. - properties: - annotations: - additionalProperties: - type: string - description: Annotations are merged onto the Service metadata. - type: object - type: - default: ClusterIP - description: Type is the Kubernetes Service type. - enum: - - ClusterIP - - LoadBalancer - - NodePort - type: string - type: object type: object replicas: default: 1 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index ecc6dcf..99209e7 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -80,18 +80,6 @@ rules: - patch - update - watch -- apiGroups: - - security.istio.io - resources: - - authorizationpolicies - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - apiGroups: - sei.io resources: diff --git a/internal/controller/nodedeployment/controller.go b/internal/controller/nodedeployment/controller.go index be7de09..e756d28 100644 --- a/internal/controller/nodedeployment/controller.go +++ b/internal/controller/nodedeployment/controller.go @@ -35,11 +35,6 @@ type SeiNodeDeploymentReconciler struct { Scheme *runtime.Scheme Recorder record.EventRecorder - // ControllerSA is the SPIFFE principal of the controller's ServiceAccount. - // It is auto-injected into every AuthorizationPolicy to ensure the - // controller can always reach the seictl sidecar. - ControllerSA string - // 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. @@ -60,7 +55,6 @@ type SeiNodeDeploymentReconciler struct { // +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch // +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=security.istio.io,resources=authorizationpolicies,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;list;watch;create;update;patch;delete func (r *SeiNodeDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -98,12 +92,9 @@ func (r *SeiNodeDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Re return ctrl.Result{}, fmt.Errorf("reconciling networking: %w", err) } - // Gate: wait for the LoadBalancer to provision an external address - // before creating nodes so the P2P address is baked into the plan. - if r.hasExternalService(group) && r.resolveExternalP2PAddress(ctx, group) == "" { - logger.Info("waiting for LoadBalancer to provision external address") - setCondition(group, seiv1alpha1.ConditionExternalServiceReady, metav1.ConditionFalse, - "LoadBalancerPending", "Waiting for LoadBalancer to provision external P2P address") + if !r.routeHostnameResolvable(ctx, group) { + setCondition(group, seiv1alpha1.ConditionRouteReady, metav1.ConditionFalse, + "DNSPending", "Waiting for route hostname to resolve in DNS") if err := r.updateStatus(ctx, group, statusBase); err != nil { return ctrl.Result{}, fmt.Errorf("updating status: %w", err) } diff --git a/internal/controller/nodedeployment/external_address_test.go b/internal/controller/nodedeployment/external_address_test.go index 34afae3..44f10b1 100644 --- a/internal/controller/nodedeployment/external_address_test.go +++ b/internal/controller/nodedeployment/external_address_test.go @@ -1,81 +1,33 @@ package nodedeployment import ( + "context" "testing" . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" ) -func TestHasExternalService(t *testing.T) { - r := &SeiNodeDeploymentReconciler{} +func TestRouteHostnameResolvable_NoRoutes(t *testing.T) { + g := NewWithT(t) + r := &SeiNodeDeploymentReconciler{GatewayDomain: "prod.platform.sei.io"} - t.Run("nil networking", func(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("test", "ns") - g.Expect(r.hasExternalService(group)).To(BeFalse()) - }) + group := newTestGroup("pacific-1-val", "pacific-1") + group.Spec.Template.Spec.FullNode = nil + group.Spec.Template.Spec.Validator = &seiv1alpha1.ValidatorSpec{} + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} - t.Run("nil service", func(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("test", "ns") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} - g.Expect(r.hasExternalService(group)).To(BeFalse()) - }) - - t.Run("service configured", func(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("test", "ns") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{Type: corev1.ServiceTypeLoadBalancer}, - } - g.Expect(r.hasExternalService(group)).To(BeTrue()) - }) + g.Expect(r.routeHostnameResolvable(context.Background(), group)).To(BeTrue(), + "validator mode has no routes, should be immediately resolvable") } -func TestResolveExternalP2PAddress_FromService(t *testing.T) { - t.Run("prefers hostname over IP", func(t *testing.T) { - g := NewWithT(t) - svc := &corev1.Service{ - Status: corev1.ServiceStatus{ - LoadBalancer: corev1.LoadBalancerStatus{ - Ingress: []corev1.LoadBalancerIngress{ - {Hostname: "p2p.atlantic-2.seinetwork.io", IP: "1.2.3.4"}, - }, - }, - }, - } - addr := externalAddressFromService(svc) - g.Expect(addr).To(Equal("p2p.atlantic-2.seinetwork.io:26656")) - }) - - t.Run("falls back to IP", func(t *testing.T) { - g := NewWithT(t) - svc := &corev1.Service{ - Status: corev1.ServiceStatus{ - LoadBalancer: corev1.LoadBalancerStatus{ - Ingress: []corev1.LoadBalancerIngress{ - {IP: "10.0.0.1"}, - }, - }, - }, - } - addr := externalAddressFromService(svc) - g.Expect(addr).To(Equal("10.0.0.1:26656")) - }) +func TestRouteHostnameResolvable_NilNetworking(t *testing.T) { + g := NewWithT(t) + r := &SeiNodeDeploymentReconciler{GatewayDomain: "prod.platform.sei.io"} - t.Run("empty when no ingress", func(t *testing.T) { - g := NewWithT(t) - svc := &corev1.Service{} - addr := externalAddressFromService(svc) - g.Expect(addr).To(BeEmpty()) - }) + group := newTestGroup("pacific-1-wave", "pacific-1") - t.Run("empty when nil", func(t *testing.T) { - g := NewWithT(t) - addr := externalAddressFromService(nil) - g.Expect(addr).To(BeEmpty()) - }) + g.Expect(r.routeHostnameResolvable(context.Background(), group)).To(BeTrue(), + "no networking means private, should be immediately resolvable") } diff --git a/internal/controller/nodedeployment/metrics.go b/internal/controller/nodedeployment/metrics.go index c2d1ffb..d592124 100644 --- a/internal/controller/nodedeployment/metrics.go +++ b/internal/controller/nodedeployment/metrics.go @@ -24,9 +24,7 @@ var allGroupPhases = []string{ var allConditionTypes = []string{ seiv1alpha1.ConditionNodesReady, - seiv1alpha1.ConditionExternalServiceReady, seiv1alpha1.ConditionRouteReady, - seiv1alpha1.ConditionIsolationReady, seiv1alpha1.ConditionServiceMonitorReady, } diff --git a/internal/controller/nodedeployment/networking.go b/internal/controller/nodedeployment/networking.go index 190592f..660f359 100644 --- a/internal/controller/nodedeployment/networking.go +++ b/internal/controller/nodedeployment/networking.go @@ -3,7 +3,6 @@ package nodedeployment import ( "context" "fmt" - "maps" "net" seiconfig "github.com/sei-protocol/sei-config" @@ -40,114 +39,42 @@ type effectiveRoute struct { WSPort int32 } -// hasExternalService returns true when the deployment has a LoadBalancer -// Service that will produce an external address to gate on. -func (r *SeiNodeDeploymentReconciler) hasExternalService(group *seiv1alpha1.SeiNodeDeployment) bool { - return group.Spec.Networking != nil && - group.Spec.Networking.Service != nil && - group.Spec.Networking.Service.Type == corev1.ServiceTypeLoadBalancer +// routeHostnameResolvable returns true when the deployment's first public +// hostname resolves in DNS, indicating the HTTPRoute + External-DNS pipeline +// is ready. Returns true when no routes are expected (private deployments, +// validator mode). +func (r *SeiNodeDeploymentReconciler) routeHostnameResolvable(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) bool { + if group.Spec.Networking == nil { + return true + } + routes := resolveEffectiveRoutes(group, r.GatewayDomain, r.GatewayPublicDomain) + if len(routes) == 0 { + return true + } + hostname := routes[0].Hostnames[0] + if _, err := net.DefaultResolver.LookupHost(ctx, hostname); err != nil { + log.FromContext(ctx).Info("route hostname not yet resolvable", "hostname", hostname) + return false + } + return true } func (r *SeiNodeDeploymentReconciler) reconcileNetworking(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) error { if group.Spec.Networking == nil { - removeCondition(group, seiv1alpha1.ConditionExternalServiceReady) removeCondition(group, seiv1alpha1.ConditionRouteReady) - removeCondition(group, seiv1alpha1.ConditionIsolationReady) return r.deleteNetworkingResources(ctx, group) } if err := r.reconcileExternalService(ctx, group); err != nil { return fmt.Errorf("reconciling external service: %w", err) } - if err := r.reconcileExternalAddress(ctx, group); err != nil { - return fmt.Errorf("reconciling external P2P address: %w", err) - } if err := r.reconcileRoute(ctx, group); err != nil { return fmt.Errorf("reconciling route: %w", err) } - if err := r.reconcileIsolation(ctx, group); err != nil { - return fmt.Errorf("reconciling isolation: %w", err) - } - return nil -} - -// reconcileExternalAddress propagates the external P2P address from the -// LoadBalancer Service to child SeiNode status so that the planner can -// inject p2p.external_address into CometBFT config at plan build time. -func (r *SeiNodeDeploymentReconciler) reconcileExternalAddress(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) error { - addr := r.resolveExternalP2PAddress(ctx, group) - if addr == "" { - return nil - } - - nodes, err := r.listChildSeiNodes(ctx, group) - if err != nil { - return fmt.Errorf("listing child SeiNodes: %w", err) - } - - for i := range nodes { - node := &nodes[i] - if node.Status.ExternalAddress == addr { - continue - } - patch := client.MergeFrom(node.DeepCopy()) - node.Status.ExternalAddress = addr - if err := r.Status().Patch(ctx, node, patch); err != nil { - return fmt.Errorf("patching external address status on SeiNode %s: %w", node.Name, err) - } - log.FromContext(ctx).Info("set P2P external address on status", "node", node.Name, "address", addr) - } return nil } -// resolveExternalP2PAddress returns the routable P2P address for this -// deployment's nodes, or "" if not yet available. Returns empty if the -// hostname is assigned but not yet resolvable in DNS. -func (r *SeiNodeDeploymentReconciler) resolveExternalP2PAddress(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) string { - svc, err := r.fetchExternalService(ctx, group) - if err != nil { - return "" - } - addr := externalAddressFromService(svc) - if addr == "" { - return "" - } - // Verify the hostname resolves before using it — NLB hostnames are - // assigned immediately by AWS but DNS propagation takes a moment. - host, _, _ := net.SplitHostPort(addr) - if net.ParseIP(host) == nil { - if _, err := net.DefaultResolver.LookupHost(ctx, host); err != nil { - log.FromContext(ctx).Info("external address not yet resolvable", "host", host) - return "" - } - } - return addr -} - -// externalAddressFromService extracts the P2P address from a Service's -// LoadBalancer ingress. Prefers hostname (DNS) over IP. -func externalAddressFromService(svc *corev1.Service) string { - if svc == nil { - return "" - } - for _, ingress := range svc.Status.LoadBalancer.Ingress { - host := ingress.Hostname - if host == "" { - host = ingress.IP - } - if host != "" { - return fmt.Sprintf("%s:%d", host, seiconfig.PortP2P) - } - } - return "" -} - func (r *SeiNodeDeploymentReconciler) reconcileExternalService(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) error { - if group.Spec.Networking.Service == nil { - removeCondition(group, seiv1alpha1.ConditionExternalServiceReady) - return r.deleteExternalService(ctx, group) - } - desired := generateExternalService(group) desired.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service")) if err := ctrl.SetControllerReference(group, desired, r.Scheme); err != nil { @@ -158,25 +85,18 @@ func (r *SeiNodeDeploymentReconciler) reconcileExternalService(ctx context.Conte } func generateExternalService(group *seiv1alpha1.SeiNodeDeployment) *corev1.Service { - svcConfig := group.Spec.Networking.Service - labels := resourceLabels(group) - svc := &corev1.Service{ + return &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: externalServiceName(group), Namespace: group.Namespace, - Labels: labels, + Labels: resourceLabels(group), }, Spec: corev1.ServiceSpec{ - Type: svcConfig.Type, + Type: corev1.ServiceTypeClusterIP, Selector: groupSelector(group), Ports: portsForMode(groupMode(group)), }, } - if len(svcConfig.Annotations) > 0 { - svc.Annotations = make(map[string]string, len(svcConfig.Annotations)) - maps.Copy(svc.Annotations, svcConfig.Annotations) - } - return svc } func groupMode(group *seiv1alpha1.SeiNodeDeployment) seiconfig.NodeMode { @@ -383,7 +303,7 @@ func generateHTTPRoute(group *seiv1alpha1.SeiNodeDeployment, er effectiveRoute, }) } - route := &unstructured.Unstructured{ + return &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "gateway.networking.k8s.io/v1", "kind": "HTTPRoute", @@ -400,16 +320,6 @@ func generateHTTPRoute(group *seiv1alpha1.SeiNodeDeployment, er effectiveRoute, }, }, } - - if gw := group.Spec.Networking.Gateway; gw != nil && len(gw.Annotations) > 0 { - metadata := route.Object["metadata"].(map[string]any) - annotations := metadata["annotations"].(map[string]any) - for k, v := range gw.Annotations { - annotations[k] = v - } - } - - return route } func httpRouteGVK() schema.GroupVersionKind { @@ -420,122 +330,20 @@ func httpRouteGVK() schema.GroupVersionKind { } } -// --- AuthorizationPolicy (unstructured) --- - -func (r *SeiNodeDeploymentReconciler) reconcileIsolation(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) error { - if group.Spec.Networking.Isolation == nil || group.Spec.Networking.Isolation.AuthorizationPolicy == nil { - removeCondition(group, seiv1alpha1.ConditionIsolationReady) - return r.deleteUnstructured(ctx, group, authPolicyGVK()) - } - - if r.ControllerSA == "" { - if !hasConditionReason(group, seiv1alpha1.ConditionIsolationReady, "ControllerSAMissing") { - r.Recorder.Event(group, corev1.EventTypeWarning, "ControllerSAMissing", "SEI_CONTROLLER_SA_PRINCIPAL is not set; AuthorizationPolicy will not include controller SA, sidecar communication may be blocked") - } - setCondition(group, seiv1alpha1.ConditionIsolationReady, metav1.ConditionFalse, - "ControllerSAMissing", "SEI_CONTROLLER_SA_PRINCIPAL env var is not set; controller SA will not be injected into AuthorizationPolicy") - } - - desired := generateAuthorizationPolicy(group, r.ControllerSA) - if err := ctrl.SetControllerReference(group, desired, r.Scheme); err != nil { - return fmt.Errorf("setting owner reference on AuthorizationPolicy: %w", err) - } +// --- Deletion helpers --- - //nolint:staticcheck // migrating unstructured SSA to typed ApplyConfiguration is a separate effort - err := r.Patch(ctx, desired, client.Apply, fieldOwner, client.ForceOwnership) - if meta.IsNoMatchError(err) { - if !hasConditionReason(group, seiv1alpha1.ConditionIsolationReady, "CRDNotInstalled") { - r.Recorder.Event(group, corev1.EventTypeWarning, "CRDNotInstalled", "Istio CRD (AuthorizationPolicy) is not installed; isolation will not be enforced") - } - setCondition(group, seiv1alpha1.ConditionIsolationReady, metav1.ConditionFalse, - "CRDNotInstalled", "Istio CRD (AuthorizationPolicy) is not installed") +func (r *SeiNodeDeploymentReconciler) deleteUnstructured(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment, gvk schema.GroupVersionKind) error { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(gvk) + obj.SetName(group.Name) + obj.SetNamespace(group.Namespace) + err := r.Delete(ctx, obj) + if apierrors.IsNotFound(err) || meta.IsNoMatchError(err) { return nil } - if err != nil { - return err - } - if r.ControllerSA != "" { - if !hasConditionReason(group, seiv1alpha1.ConditionIsolationReady, "AuthorizationPolicyReady") { - r.Recorder.Event(group, corev1.EventTypeNormal, "AuthorizationPolicyReady", "AuthorizationPolicy reconciled successfully") - } - setCondition(group, seiv1alpha1.ConditionIsolationReady, metav1.ConditionTrue, - "AuthorizationPolicyReady", "AuthorizationPolicy reconciled successfully") - } - return nil -} - -func generateAuthorizationPolicy(group *seiv1alpha1.SeiNodeDeployment, controllerSA string) *unstructured.Unstructured { - cfg := group.Spec.Networking.Isolation.AuthorizationPolicy - - var rules []any - for _, src := range cfg.AllowedSources { - rule := map[string]any{} - from := map[string]any{} - source := map[string]any{} - if len(src.Principals) > 0 { - principals := make([]any, len(src.Principals)) - for i, p := range src.Principals { - principals[i] = p - } - source["principals"] = principals - } - if len(src.Namespaces) > 0 { - namespaces := make([]any, len(src.Namespaces)) - for i, n := range src.Namespaces { - namespaces[i] = n - } - source["namespaces"] = namespaces - } - from["source"] = source - rule["from"] = []any{from} - rules = append(rules, rule) - } - - // Auto-inject the controller's SA so sidecar communication is never blocked - if controllerSA != "" { - rules = append(rules, map[string]any{ - "from": []any{ - map[string]any{ - "source": map[string]any{ - "principals": []any{controllerSA}, - }, - }, - }, - }) - } - - policy := &unstructured.Unstructured{ - Object: map[string]any{ - "apiVersion": "security.istio.io/v1", - "kind": "AuthorizationPolicy", - "metadata": map[string]any{ - "name": group.Name, - "namespace": group.Namespace, - "labels": toStringInterfaceMap(resourceLabels(group)), - "annotations": toStringInterfaceMap(managedByAnnotations()), - }, - "spec": map[string]any{ - "selector": map[string]any{ - "matchLabels": toStringInterfaceMap(groupSelector(group)), - }, - "action": "ALLOW", - "rules": rules, - }, - }, - } - return policy -} - -func authPolicyGVK() schema.GroupVersionKind { - return schema.GroupVersionKind{ - Group: "security.istio.io", - Version: "v1", - Kind: "AuthorizationPolicy", - } + return err } -// --- Deletion policy helpers --- - func (r *SeiNodeDeploymentReconciler) deleteExternalService(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) error { svc := &corev1.Service{} err := r.Get(ctx, types.NamespacedName{Name: externalServiceName(group), Namespace: group.Namespace}, svc) @@ -555,30 +363,12 @@ func (r *SeiNodeDeploymentReconciler) deleteNetworkingResources(ctx context.Cont if err := r.deleteExternalService(ctx, group); err != nil { return err } - if err := r.deleteHTTPRoutesByLabel(ctx, group); err != nil { return fmt.Errorf("deleting HTTPRoutes: %w", err) } - for _, gvk := range []schema.GroupVersionKind{authPolicyGVK(), serviceMonitorGVK()} { - if err := r.deleteUnstructured(ctx, group, gvk); err != nil { - return fmt.Errorf("deleting %s: %w", gvk.Kind, err) - } - } return nil } -func (r *SeiNodeDeploymentReconciler) deleteUnstructured(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment, gvk schema.GroupVersionKind) error { - obj := &unstructured.Unstructured{} - obj.SetGroupVersionKind(gvk) - obj.SetName(group.Name) - obj.SetNamespace(group.Namespace) - err := r.Delete(ctx, obj) - if apierrors.IsNotFound(err) || meta.IsNoMatchError(err) { - return nil - } - return err -} - func (r *SeiNodeDeploymentReconciler) orphanNetworkingResources(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) error { svc := &corev1.Service{} err := r.Get(ctx, types.NamespacedName{Name: externalServiceName(group), Namespace: group.Namespace}, svc) @@ -604,20 +394,6 @@ func (r *SeiNodeDeploymentReconciler) orphanNetworkingResources(ctx context.Cont } } - for _, gvk := range []schema.GroupVersionKind{authPolicyGVK(), serviceMonitorGVK()} { - obj := &unstructured.Unstructured{} - obj.SetGroupVersionKind(gvk) - err := r.Get(ctx, types.NamespacedName{Name: group.Name, Namespace: group.Namespace}, obj) - if meta.IsNoMatchError(err) || apierrors.IsNotFound(err) { - continue - } - if err != nil { - return fmt.Errorf("fetching %s for orphan: %w", gvk.Kind, err) - } - if err := r.removeOwnerRef(ctx, obj, group); err != nil { - return fmt.Errorf("orphaning %s: %w", gvk.Kind, err) - } - } return nil } diff --git a/internal/controller/nodedeployment/networking_test.go b/internal/controller/nodedeployment/networking_test.go index fd31bdb..c160d46 100644 --- a/internal/controller/nodedeployment/networking_test.go +++ b/internal/controller/nodedeployment/networking_test.go @@ -13,40 +13,38 @@ import ( func TestGenerateExternalService_BasicFields(t *testing.T) { g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{ - Type: corev1.ServiceTypeClusterIP, - }, - } + group := newTestGroup("pacific-1-wave", "pacific-1") + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} svc := generateExternalService(group) - g.Expect(svc.Name).To(Equal("archive-rpc-external")) - g.Expect(svc.Namespace).To(Equal("sei")) - g.Expect(svc.Labels).To(HaveKeyWithValue(groupLabel, "archive-rpc")) + g.Expect(svc.Name).To(Equal("pacific-1-wave-external")) + g.Expect(svc.Namespace).To(Equal("pacific-1")) + g.Expect(svc.Labels).To(HaveKeyWithValue(groupLabel, "pacific-1-wave")) g.Expect(svc.Spec.Type).To(Equal(corev1.ServiceTypeClusterIP)) - g.Expect(svc.Spec.Selector).To(HaveKeyWithValue(groupLabel, "archive-rpc")) + g.Expect(svc.Spec.Selector).To(HaveKeyWithValue(groupLabel, "pacific-1-wave")) } -func TestGenerateExternalService_AllPortsWhenEmpty(t *testing.T) { +func TestGenerateExternalService_AllPortsForFullMode(t *testing.T) { g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - } + group := newTestGroup("pacific-1-wave", "pacific-1") + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} svc := generateExternalService(group) g.Expect(svc.Spec.Ports).To(HaveLen(7)) + + 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")) } func TestGenerateExternalService_ValidatorModePorts(t *testing.T) { g := NewWithT(t) - group := newTestGroup("pacific-1-val", "sei") + group := newTestGroup("pacific-1-val", "pacific-1") group.Spec.Template.Spec.Validator = &seiv1alpha1.ValidatorSpec{} - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - } + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} svc := generateExternalService(group) g.Expect(svc.Spec.Ports).To(HaveLen(2)) @@ -60,10 +58,8 @@ func TestGenerateExternalService_ValidatorModePorts(t *testing.T) { func TestGenerateExternalService_GRPCAppProtocol(t *testing.T) { g := NewWithT(t) - group := newTestGroup("pacific-1-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - } + group := newTestGroup("pacific-1-wave", "pacific-1") + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} svc := generateExternalService(group) for _, p := range svc.Spec.Ports { @@ -76,59 +72,14 @@ func TestGenerateExternalService_GRPCAppProtocol(t *testing.T) { t.Fatal("grpc port not found") } -func TestGenerateExternalService_Annotations(t *testing.T) { +func TestGenerateExternalService_AlwaysClusterIP(t *testing.T) { g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{ - Annotations: map[string]string{ - "service.beta.kubernetes.io/aws-load-balancer-type": "nlb", - }, - }, - } - - svc := generateExternalService(group) - g.Expect(svc.Annotations).To(HaveKeyWithValue( - "service.beta.kubernetes.io/aws-load-balancer-type", "nlb")) -} - -func TestGenerateExternalService_LoadBalancer(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{ - Type: corev1.ServiceTypeLoadBalancer, - }, - } - - svc := generateExternalService(group) - g.Expect(svc.Spec.Type).To(Equal(corev1.ServiceTypeLoadBalancer)) -} - -func TestGenerateExternalService_NoPublishNotReady(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - } - - svc := generateExternalService(group) - g.Expect(svc.Spec.PublishNotReadyAddresses).To(BeFalse()) -} - -func TestGenerateExternalService_FullModeIncludesAllPorts(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("pacific-1-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - } + group := newTestGroup("pacific-1-wave", "pacific-1") + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} 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(svc.Spec.Type).To(Equal(corev1.ServiceTypeClusterIP)) + g.Expect(svc.Annotations).To(BeEmpty()) } // --- Effective Routes --- @@ -136,10 +87,7 @@ func TestGenerateExternalService_FullModeIncludesAllPorts(t *testing.T) { func TestResolveEffectiveRoutes_FullMode_FourRoutes(t *testing.T) { g := NewWithT(t) group := newTestGroup("pacific-1-wave", "pacific-1") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{}, - } + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") g.Expect(routes).To(HaveLen(4)) @@ -164,13 +112,10 @@ func TestResolveEffectiveRoutes_FullMode_FourRoutes(t *testing.T) { func TestResolveEffectiveRoutes_ArchiveMode_FourRoutes(t *testing.T) { g := NewWithT(t) - group := newTestGroup("pacific-1-archive", "sei") + group := newTestGroup("pacific-1-archive", "pacific-1") group.Spec.Template.Spec.FullNode = nil group.Spec.Template.Spec.Archive = &seiv1alpha1.ArchiveSpec{} - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{}, - } + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") g.Expect(routes).To(HaveLen(4)) @@ -178,25 +123,34 @@ func TestResolveEffectiveRoutes_ArchiveMode_FourRoutes(t *testing.T) { func TestResolveEffectiveRoutes_ValidatorMode_NoRoutes(t *testing.T) { g := NewWithT(t) - group := newTestGroup("pacific-1-val", "sei") + group := newTestGroup("pacific-1-val", "pacific-1") group.Spec.Template.Spec.FullNode = nil group.Spec.Template.Spec.Validator = &seiv1alpha1.ValidatorSpec{} - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{}, - } + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") g.Expect(routes).To(BeEmpty()) } -func TestGenerateHTTPRoute_HostnamePattern(t *testing.T) { +func TestResolveEffectiveRoutes_NoPublicDomain_SingleHostname(t *testing.T) { g := NewWithT(t) group := newTestGroup("pacific-1-wave", "pacific-1") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{}, + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} + + routes := resolveEffectiveRoutes(group, "dev.platform.sei.io", "") + g.Expect(routes).To(HaveLen(4)) + for _, r := range routes { + g.Expect(r.Hostnames).To(HaveLen(1)) } + g.Expect(routes[0].Hostnames[0]).To(Equal("pacific-1-wave-evm.dev.platform.sei.io")) +} + +// --- HTTPRoute Generation --- + +func TestGenerateHTTPRoute_HostnamePattern(t *testing.T) { + g := NewWithT(t) + group := newTestGroup("pacific-1-wave", "pacific-1") + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") g.Expect(routes).NotTo(BeEmpty()) @@ -211,85 +165,16 @@ func TestGenerateHTTPRoute_HostnamePattern(t *testing.T) { } } -func TestGenerateHTTPRoute_EVMMerged(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("pacific-1-wave", "pacific-1") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{}, - } - - routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") - - var evmRoute effectiveRoute - evmCount := 0 - for _, r := range routes { - if r.Name == "pacific-1-wave-evm" { - evmCount++ - 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-wave", "pacific-1") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - } - - routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") - var evmRoute effectiveRoute - for _, r := range routes { - if r.Name == "pacific-1-wave-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) { g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{}, - } + group := newTestGroup("pacific-1-wave", "pacific-1") + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") g.Expect(routes).NotTo(BeEmpty()) - route := generateHTTPRoute(group, routes[0], "sei-gateway", "istio-system") + route := generateHTTPRoute(group, routes[0], "sei-gateway", "gateway") - g.Expect(route.GetNamespace()).To(Equal("sei")) + g.Expect(route.GetNamespace()).To(Equal("pacific-1")) spec := route.Object["spec"].(map[string]any) parentRefs := spec["parentRefs"].([]any) @@ -297,59 +182,46 @@ 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")) + g.Expect(ref["namespace"]).To(Equal("gateway")) } func TestGenerateHTTPRoute_ManagedByAnnotation(t *testing.T) { g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{}, - } + group := newTestGroup("pacific-1-wave", "pacific-1") + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") - route := generateHTTPRoute(group, routes[0], "sei-gateway", "istio-system") + route := generateHTTPRoute(group, routes[0], "sei-gateway", "gateway") g.Expect(route.GetAnnotations()).To(HaveKeyWithValue("sei.io/managed-by", "seinodedeployment")) } func TestGenerateHTTPRoute_BackendRef(t *testing.T) { g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{}, - } + group := newTestGroup("pacific-1-wave", "pacific-1") + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") var rpcRoute effectiveRoute for _, r := range routes { - if r.Name == "archive-rpc-rpc" { + if r.Name == "pacific-1-wave-rpc" { rpcRoute = r break } } - route := generateHTTPRoute(group, rpcRoute, "sei-gateway", "istio-system") + route := generateHTTPRoute(group, rpcRoute, "sei-gateway", "gateway") spec := route.Object["spec"].(map[string]any) rules := spec["rules"].([]any) g.Expect(rules).To(HaveLen(1)) - rule := rules[0].(map[string]any) - backends := rule["backendRefs"].([]any) - g.Expect(backends).To(HaveLen(1)) - - backend := backends[0].(map[string]any) - g.Expect(backend["name"]).To(Equal("archive-rpc-external")) + backend := rules[0].(map[string]any)["backendRefs"].([]any)[0].(map[string]any) + g.Expect(backend["name"]).To(Equal("pacific-1-wave-external")) } func TestGenerateHTTPRoute_GRPCRoutePort(t *testing.T) { g := NewWithT(t) group := newTestGroup("pacific-1-wave", "pacific-1") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - Gateway: &seiv1alpha1.GatewayRouteConfig{}, - } + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") @@ -360,9 +232,9 @@ func TestGenerateHTTPRoute_GRPCRoutePort(t *testing.T) { break } } - g.Expect(grpcRoute.Name).NotTo(BeEmpty(), "grpc route should exist") + g.Expect(grpcRoute.Name).NotTo(BeEmpty()) - httpRoute := generateHTTPRoute(group, grpcRoute, "sei-gateway", "istio-system") + httpRoute := generateHTTPRoute(group, grpcRoute, "sei-gateway", "gateway") spec := httpRoute.Object["spec"].(map[string]any) hostnames := spec["hostnames"].([]any) g.Expect(hostnames).To(ConsistOf("pacific-1-wave-grpc.prod.platform.sei.io", "pacific-1-wave-grpc.pacific-1.platform.sei.io")) @@ -373,65 +245,59 @@ func TestGenerateHTTPRoute_GRPCRoutePort(t *testing.T) { g.Expect(backend["name"]).To(Equal("pacific-1-wave-external")) } -// --- isProtocolActiveForMode --- - -func TestIsProtocolActiveForMode_EVMMapping(t *testing.T) { +func TestGenerateHTTPRoute_EVMMerged(t *testing.T) { g := NewWithT(t) - activePorts := map[string]bool{"evm-rpc": true, "evm-ws": true, "rpc": true} - - g.Expect(isProtocolActiveForMode("evm", activePorts)).To(BeTrue()) - g.Expect(isProtocolActiveForMode("rpc", activePorts)).To(BeTrue()) - g.Expect(isProtocolActiveForMode("grpc", activePorts)).To(BeFalse()) -} + group := newTestGroup("pacific-1-wave", "pacific-1") + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} -// --- Edge Cases --- + routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") -func TestResolveEffectiveRoutes_EmptyDomain_MalformedHostnames(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("pacific-1-wave", "pacific-1") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, + var evmRoute effectiveRoute + evmCount := 0 + for _, r := range routes { + if r.Name == "pacific-1-wave-evm" { + evmCount++ + evmRoute = r + } } - - routes := resolveEffectiveRoutes(group, "", "") - g.Expect(routes).To(HaveLen(4), "routes are still generated even with empty domain") - g.Expect(routes[0].Hostnames).To(HaveLen(1), "no public hostname when public domain is empty") - g.Expect(routes[0].Hostnames[0]).To(Equal("pacific-1-wave-evm."), "empty domain produces trailing dot") + g.Expect(evmCount).To(Equal(1)) + g.Expect(evmRoute.Port).To(Equal(int32(8545))) + g.Expect(evmRoute.WSPort).To(Equal(int32(8546))) } -func TestResolveEffectiveRoutes_NoPublicDomain_SingleHostname(t *testing.T) { +func TestGenerateHTTPRoute_EVMWebSocketRule(t *testing.T) { g := NewWithT(t) group := newTestGroup("pacific-1-wave", "pacific-1") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - } + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} - routes := resolveEffectiveRoutes(group, "dev.platform.sei.io", "") - g.Expect(routes).To(HaveLen(4)) + routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") + var evmRoute effectiveRoute for _, r := range routes { - g.Expect(r.Hostnames).To(HaveLen(1), "only internal hostname when public domain is empty") + if r.Name == "pacific-1-wave-evm" { + evmRoute = r + break + } } - g.Expect(routes[0].Hostnames[0]).To(Equal("pacific-1-wave-evm.dev.platform.sei.io")) -} -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{}, - } + httpRoute := generateHTTPRoute(group, evmRoute, "sei-gateway", "gateway") + spec := httpRoute.Object["spec"].(map[string]any) + rules := spec["rules"].([]any) + g.Expect(rules).To(HaveLen(2)) - routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") - g.Expect(routes).To(BeEmpty(), "validator mode should produce zero routes") + httpBackend := rules[0].(map[string]any)["backendRefs"].([]any)[0].(map[string]any) + g.Expect(httpBackend["port"]).To(Equal(int64(8545))) + + wsRule := rules[1].(map[string]any) + wsHeader := wsRule["matches"].([]any)[0].(map[string]any)["headers"].([]any)[0].(map[string]any) + g.Expect(wsHeader["name"]).To(Equal("Upgrade")) + g.Expect(wsHeader["value"]).To(Equal("websocket")) + g.Expect(wsRule["backendRefs"].([]any)[0].(map[string]any)["port"]).To(Equal(int64(8546))) } func TestGenerateHTTPRoute_NonEVMRoute_SingleRule(t *testing.T) { g := NewWithT(t) group := newTestGroup("pacific-1-wave", "pacific-1") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - } + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} routes := resolveEffectiveRoutes(group, "prod.platform.sei.io", "platform.sei.io") for _, r := range routes { @@ -439,111 +305,21 @@ func TestGenerateHTTPRoute_NonEVMRoute_SingleRule(t *testing.T) { 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") + g.Expect(rules).To(HaveLen(1)) + g.Expect(r.WSPort).To(Equal(int32(0))) return } } t.Fatal("rpc route not found") } -// --- AuthorizationPolicy --- - -func TestGenerateAuthorizationPolicy_BasicStructure(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Isolation: &seiv1alpha1.NetworkIsolationConfig{ - AuthorizationPolicy: &seiv1alpha1.AuthorizationPolicyConfig{ - AllowedSources: []seiv1alpha1.TrafficSource{{ - Principals: []string{"cluster.local/ns/istio-system/sa/istio-ingressgateway"}, - }}, - }, - }, - } - - policy := generateAuthorizationPolicy(group, "") - - g.Expect(policy.GetName()).To(Equal("archive-rpc")) - g.Expect(policy.GetNamespace()).To(Equal("sei")) - - spec := policy.Object["spec"].(map[string]any) - g.Expect(spec["action"]).To(Equal("ALLOW")) - - selector := spec["selector"].(map[string]any) - matchLabels := selector["matchLabels"].(map[string]any) - g.Expect(matchLabels[groupLabel]).To(Equal("archive-rpc")) -} - -func TestGenerateAuthorizationPolicy_InjectsControllerSA(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Isolation: &seiv1alpha1.NetworkIsolationConfig{ - AuthorizationPolicy: &seiv1alpha1.AuthorizationPolicyConfig{ - AllowedSources: []seiv1alpha1.TrafficSource{{ - Principals: []string{"cluster.local/ns/istio-system/sa/istio-ingressgateway"}, - }}, - }, - }, - } - - controllerSA := "cluster.local/ns/sei-system/sa/sei-controller" - policy := generateAuthorizationPolicy(group, controllerSA) - - spec := policy.Object["spec"].(map[string]any) - rules := spec["rules"].([]any) - g.Expect(rules).To(HaveLen(2), "should have user source + injected controller SA") - - lastRule := rules[1].(map[string]any) - from := lastRule["from"].([]any) - source := from[0].(map[string]any)["source"].(map[string]any) - principals := source["principals"].([]any) - g.Expect(principals).To(ConsistOf(controllerSA)) -} - -func TestGenerateAuthorizationPolicy_NoControllerSA(t *testing.T) { - g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Isolation: &seiv1alpha1.NetworkIsolationConfig{ - AuthorizationPolicy: &seiv1alpha1.AuthorizationPolicyConfig{ - AllowedSources: []seiv1alpha1.TrafficSource{{ - Principals: []string{"cluster.local/ns/istio-system/sa/istio-ingressgateway"}, - }}, - }, - }, - } - - policy := generateAuthorizationPolicy(group, "") - - spec := policy.Object["spec"].(map[string]any) - rules := spec["rules"].([]any) - g.Expect(rules).To(HaveLen(1), "only user sources when controller SA is empty") -} +// --- isProtocolActiveForMode --- -func TestGenerateAuthorizationPolicy_NamespaceSource(t *testing.T) { +func TestIsProtocolActiveForMode_EVMMapping(t *testing.T) { g := NewWithT(t) - group := newTestGroup("archive-rpc", "sei") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Isolation: &seiv1alpha1.NetworkIsolationConfig{ - AuthorizationPolicy: &seiv1alpha1.AuthorizationPolicyConfig{ - AllowedSources: []seiv1alpha1.TrafficSource{{ - Namespaces: []string{"monitoring", "sei-system"}, - }}, - }, - }, - } - - policy := generateAuthorizationPolicy(group, "") - - spec := policy.Object["spec"].(map[string]any) - rules := spec["rules"].([]any) - g.Expect(rules).To(HaveLen(1)) + activePorts := map[string]bool{"evm-rpc": true, "evm-ws": true, "rpc": true} - rule := rules[0].(map[string]any) - from := rule["from"].([]any) - source := from[0].(map[string]any)["source"].(map[string]any) - namespaces := source["namespaces"].([]any) - g.Expect(namespaces).To(ConsistOf("monitoring", "sei-system")) + g.Expect(isProtocolActiveForMode("evm", activePorts)).To(BeTrue()) + g.Expect(isProtocolActiveForMode("rpc", activePorts)).To(BeTrue()) + g.Expect(isProtocolActiveForMode("grpc", activePorts)).To(BeFalse()) } diff --git a/internal/controller/nodedeployment/status.go b/internal/controller/nodedeployment/status.go index 1ca1ca2..7ab3c2f 100644 --- a/internal/controller/nodedeployment/status.go +++ b/internal/controller/nodedeployment/status.go @@ -4,13 +4,9 @@ import ( "context" "fmt" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" ) @@ -46,11 +42,9 @@ func (r *SeiNodeDeploymentReconciler) updateStatus(ctx context.Context, group *s group.Status.Phase = computeGroupPhase(group, readyReplicas, group.Spec.Replicas, nodes) - svc, svcErr := r.fetchExternalService(ctx, group) group.Status.NetworkingStatus = r.buildNetworkingStatus(group) setNodesReadyCondition(group, readyReplicas, group.Spec.Replicas, nodes) - setExternalServiceCondition(group, svc, svcErr) return r.Status().Patch(ctx, group, statusBase) } @@ -88,26 +82,10 @@ func computeGroupPhase(group *seiv1alpha1.SeiNodeDeployment, ready, desired int3 return seiv1alpha1.GroupPhaseInitializing } -// fetchExternalService returns the external Service if networking is configured. -// Returns (nil, nil) when not configured or NotFound, and (nil, err) on -// transient API errors so callers can surface an accurate condition. -func (r *SeiNodeDeploymentReconciler) fetchExternalService(ctx context.Context, group *seiv1alpha1.SeiNodeDeployment) (*corev1.Service, error) { - if group.Spec.Networking == nil || group.Spec.Networking.Service == nil { - return nil, nil - } - svc := &corev1.Service{} - err := r.Get(ctx, types.NamespacedName{Name: externalServiceName(group), Namespace: group.Namespace}, svc) - if apierrors.IsNotFound(err) { - return nil, nil - } - if err != nil { - log.FromContext(ctx).Error(err, "fetching external Service for status") - return nil, err - } - return svc, nil -} - func (r *SeiNodeDeploymentReconciler) buildNetworkingStatus(group *seiv1alpha1.SeiNodeDeployment) *seiv1alpha1.NetworkingStatus { + if group.Spec.Networking == nil { + return nil + } routes := resolveEffectiveRoutes(group, r.GatewayDomain, r.GatewayPublicDomain) if len(routes) == 0 { return nil @@ -153,33 +131,6 @@ func setNodesReadyCondition(group *seiv1alpha1.SeiNodeDeployment, ready, desired setCondition(group, seiv1alpha1.ConditionNodesReady, status, reason, message) } -func setExternalServiceCondition(group *seiv1alpha1.SeiNodeDeployment, svc *corev1.Service, fetchErr error) { - if group.Spec.Networking == nil || group.Spec.Networking.Service == nil { - return - } - - if fetchErr != nil { - setCondition(group, seiv1alpha1.ConditionExternalServiceReady, metav1.ConditionFalse, - "FetchError", fmt.Sprintf("Unable to fetch external Service: %v", fetchErr)) - return - } - - if svc == nil { - setCondition(group, seiv1alpha1.ConditionExternalServiceReady, metav1.ConditionFalse, - "ServiceNotFound", "External Service not yet created") - return - } - - if svc.Spec.Type == corev1.ServiceTypeLoadBalancer && len(svc.Status.LoadBalancer.Ingress) == 0 { - setCondition(group, seiv1alpha1.ConditionExternalServiceReady, metav1.ConditionFalse, - "LoadBalancerPending", "Waiting for load balancer provisioning") - return - } - - setCondition(group, seiv1alpha1.ConditionExternalServiceReady, metav1.ConditionTrue, - "ServiceReady", fmt.Sprintf("External Service %s is ready", svc.Name)) -} - func hasConditionTrue(group *seiv1alpha1.SeiNodeDeployment, condType string) bool { //nolint:unparam // general-purpose utility c := apimeta.FindStatusCondition(group.Status.Conditions, condType) return c != nil && c.Status == metav1.ConditionTrue diff --git a/internal/controller/nodedeployment/status_test.go b/internal/controller/nodedeployment/status_test.go index 9d8ce45..03b46ec 100644 --- a/internal/controller/nodedeployment/status_test.go +++ b/internal/controller/nodedeployment/status_test.go @@ -101,9 +101,7 @@ func makeNodes(n int, phase seiv1alpha1.SeiNodePhase) []seiv1alpha1.SeiNode { func TestBuildNetworkingStatus_FullMode_DualDomain(t *testing.T) { g := NewWithT(t) group := newTestGroup("pacific-1-wave", "pacific-1") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - } + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} r := &SeiNodeDeploymentReconciler{ GatewayDomain: "prod.platform.sei.io", @@ -133,9 +131,7 @@ func TestBuildNetworkingStatus_FullMode_DualDomain(t *testing.T) { func TestBuildNetworkingStatus_SingleDomain(t *testing.T) { g := NewWithT(t) group := newTestGroup("pacific-1-wave", "pacific-1") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - } + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} r := &SeiNodeDeploymentReconciler{ GatewayDomain: "dev.platform.sei.io", @@ -166,9 +162,7 @@ func TestBuildNetworkingStatus_ValidatorMode_Nil(t *testing.T) { func TestBuildNetworkingStatus_ProtocolValues(t *testing.T) { g := NewWithT(t) group := newTestGroup("pacific-1-wave", "pacific-1") - group.Spec.Networking = &seiv1alpha1.NetworkingConfig{ - Service: &seiv1alpha1.ExternalServiceConfig{}, - } + group.Spec.Networking = &seiv1alpha1.NetworkingConfig{} r := &SeiNodeDeploymentReconciler{ GatewayDomain: "prod.platform.sei.io", diff --git a/manifests/role.yaml b/manifests/role.yaml index ecc6dcf..99209e7 100644 --- a/manifests/role.yaml +++ b/manifests/role.yaml @@ -80,18 +80,6 @@ rules: - patch - update - watch -- apiGroups: - - security.istio.io - resources: - - authorizationpolicies - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - apiGroups: - sei.io resources: diff --git a/manifests/sei.io_seinodedeployments.yaml b/manifests/sei.io_seinodedeployments.yaml index 1a4b222..778b5e6 100644 --- a/manifests/sei.io_seinodedeployments.yaml +++ b/manifests/sei.io_seinodedeployments.yaml @@ -185,81 +185,6 @@ spec: description: |- Networking controls how the group is exposed to traffic. Networking resources are shared across all replicas. - properties: - gateway: - description: |- - 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: - type: string - description: Annotations are merged onto HTTPRoute metadata. - type: object - type: object - isolation: - description: Isolation configures network-level access control - for node pods. - properties: - authorizationPolicy: - description: |- - AuthorizationPolicy creates an Istio AuthorizationPolicy - restricting which identities can reach node pods. - properties: - allowedSources: - description: |- - AllowedSources defines who can reach this group's pods. - The controller generates an ALLOW policy; traffic from - sources not listed here is denied. - items: - description: TrafficSource identifies a set of callers - by Istio identity. - properties: - namespaces: - description: Namespaces allows all pods in these - namespaces. - items: - type: string - type: array - principals: - description: |- - Principals are SPIFFE identities (e.g. - "cluster.local/ns/istio-system/sa/istio-ingressgateway"). - items: - type: string - type: array - type: object - x-kubernetes-validations: - - message: at least one of principals or namespaces - must be set - rule: has(self.principals) || has(self.namespaces) - minItems: 1 - type: array - required: - - allowedSources - type: object - type: object - service: - description: |- - Service creates a non-headless Service shared across all replicas. - Each SeiNode still gets its own headless Service for pod DNS. - properties: - annotations: - additionalProperties: - type: string - description: Annotations are merged onto the Service metadata. - type: object - type: - default: ClusterIP - description: Type is the Kubernetes Service type. - enum: - - ClusterIP - - LoadBalancer - - NodePort - type: string - type: object type: object replicas: default: 1 From 6b9ef6a945ab3797851177a0cdb241963a510083 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Fri, 10 Apr 2026 16:53:14 -0700 Subject: [PATCH 2/2] fix: address review findings from adversarial review - Remove SEI_CONTROLLER_SA_PRINCIPAL from base manager manifest - Update sample manifest to use networking: {} (removed stale schema) - Restore ServiceMonitor orphaning on Retain deletion policy - Fix stale comments referencing LoadBalancer and AuthorizationPolicy - Update Networking field godoc to reflect public/private semantics - Rename external_address_test.go to route_resolvable_test.go - Regenerate CRDs with updated field descriptions Co-Authored-By: Claude Opus 4.6 (1M context) --- api/v1alpha1/seinodedeployment_types.go | 6 +++-- config/crd/sei.io_seinodedeployments.yaml | 6 +++-- config/manager/manager.yaml | 2 -- .../controller/nodedeployment/controller.go | 4 ++-- internal/controller/nodedeployment/labels.go | 2 +- .../controller/nodedeployment/networking.go | 22 ++++++++++--------- ...dress_test.go => route_resolvable_test.go} | 0 .../pacific-1-rpc-group.yaml | 19 ++++------------ manifests/sei.io_seinodedeployments.yaml | 6 +++-- 9 files changed, 31 insertions(+), 36 deletions(-) rename internal/controller/nodedeployment/{external_address_test.go => route_resolvable_test.go} (100%) diff --git a/api/v1alpha1/seinodedeployment_types.go b/api/v1alpha1/seinodedeployment_types.go index 845da18..fb36542 100644 --- a/api/v1alpha1/seinodedeployment_types.go +++ b/api/v1alpha1/seinodedeployment_types.go @@ -30,8 +30,10 @@ type SeiNodeDeploymentSpec struct { // +optional Genesis *GenesisCeremonyConfig `json:"genesis,omitempty"` - // Networking controls how the group is exposed to traffic. - // Networking resources are shared across all replicas. + // Networking enables public networking for the deployment. + // When present, the controller creates a ClusterIP Service and + // HTTPRoutes on the platform Gateway. When absent, the deployment + // is private with only per-node headless Services. // +optional Networking *NetworkingConfig `json:"networking,omitempty"` diff --git a/config/crd/sei.io_seinodedeployments.yaml b/config/crd/sei.io_seinodedeployments.yaml index 778b5e6..4766576 100644 --- a/config/crd/sei.io_seinodedeployments.yaml +++ b/config/crd/sei.io_seinodedeployments.yaml @@ -183,8 +183,10 @@ spec: type: object networking: description: |- - Networking controls how the group is exposed to traffic. - Networking resources are shared across all replicas. + Networking enables public networking for the deployment. + When present, the controller creates a ClusterIP Service and + HTTPRoutes on the platform Gateway. When absent, the deployment + is private with only per-node headless Services. type: object replicas: default: 1 diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index f60d175..ac0b12e 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -79,8 +79,6 @@ spec: value: gateway - name: SEI_GATEWAY_DOMAIN value: prod.platform.sei.io - - name: SEI_CONTROLLER_SA_PRINCIPAL - value: "cluster.local/ns/sei-k8s-controller-system/sa/sei-k8s-controller-manager" ports: - containerPort: 8080 name: metrics diff --git a/internal/controller/nodedeployment/controller.go b/internal/controller/nodedeployment/controller.go index e756d28..8ae9585 100644 --- a/internal/controller/nodedeployment/controller.go +++ b/internal/controller/nodedeployment/controller.go @@ -82,8 +82,8 @@ func (r *SeiNodeDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Re statusBase := client.MergeFromWithOptions(group.DeepCopy(), client.MergeFromWithOptimisticLock{}) ns, name := group.Namespace, group.Name - // Networking runs before node creation so that the external P2P - // address (from the LoadBalancer ingress) is known at plan build time. + // Networking runs before node creation so the DNS readiness gate + // can block until public routes are resolvable. if err := timeSubstep("reconcileNetworking", func() error { return r.reconcileNetworking(ctx, group) }); err != nil { diff --git a/internal/controller/nodedeployment/labels.go b/internal/controller/nodedeployment/labels.go index 7456f40..69737bf 100644 --- a/internal/controller/nodedeployment/labels.go +++ b/internal/controller/nodedeployment/labels.go @@ -37,7 +37,7 @@ func externalServiceName(group *seiv1alpha1.SeiNodeDeployment) string { } // groupSelector returns the label selector used by the shared external -// Service, AuthorizationPolicy, and ServiceMonitor. During an active +// Service, HTTPRoutes, and ServiceMonitor. During an active // deployment, it includes the revision label to pin traffic to the // active set. At steady state, it selects by group membership only. func groupSelector(group *seiv1alpha1.SeiNodeDeployment) map[string]string { diff --git a/internal/controller/nodedeployment/networking.go b/internal/controller/nodedeployment/networking.go index 660f359..8618d36 100644 --- a/internal/controller/nodedeployment/networking.go +++ b/internal/controller/nodedeployment/networking.go @@ -380,16 +380,18 @@ func (r *SeiNodeDeploymentReconciler) orphanNetworkingResources(ctx context.Cont return fmt.Errorf("fetching external Service for orphan: %w", err) } - httpRoutes := &unstructured.UnstructuredList{} - httpRoutes.SetGroupVersionKind(httpRouteGVK()) - listErr := r.List(ctx, httpRoutes, client.InNamespace(group.Namespace), client.MatchingLabels(resourceLabels(group))) - if listErr != nil && !meta.IsNoMatchError(listErr) { - return fmt.Errorf("listing HTTPRoutes for orphan: %w", listErr) - } - if listErr == nil { - for i := range httpRoutes.Items { - if err := r.removeOwnerRef(ctx, &httpRoutes.Items[i], group); err != nil { - return fmt.Errorf("orphaning HTTPRoute %s: %w", httpRoutes.Items[i].GetName(), err) + for _, gvk := range []schema.GroupVersionKind{httpRouteGVK(), serviceMonitorGVK()} { + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(gvk) + listErr := r.List(ctx, list, client.InNamespace(group.Namespace), client.MatchingLabels(resourceLabels(group))) + if listErr != nil && !meta.IsNoMatchError(listErr) { + return fmt.Errorf("listing %s for orphan: %w", gvk.Kind, listErr) + } + if listErr == nil { + for i := range list.Items { + if err := r.removeOwnerRef(ctx, &list.Items[i], group); err != nil { + return fmt.Errorf("orphaning %s %s: %w", gvk.Kind, list.Items[i].GetName(), err) + } } } } diff --git a/internal/controller/nodedeployment/external_address_test.go b/internal/controller/nodedeployment/route_resolvable_test.go similarity index 100% rename from internal/controller/nodedeployment/external_address_test.go rename to internal/controller/nodedeployment/route_resolvable_test.go diff --git a/manifests/samples/seinodedeployment/pacific-1-rpc-group.yaml b/manifests/samples/seinodedeployment/pacific-1-rpc-group.yaml index 302273a..49ef688 100644 --- a/manifests/samples/seinodedeployment/pacific-1-rpc-group.yaml +++ b/manifests/samples/seinodedeployment/pacific-1-rpc-group.yaml @@ -1,9 +1,8 @@ # SeiNodeDeployment — Pacific-1 RPC Fleet # -# Three full nodes behind a shared Service with Gateway API routing, -# Istio network isolation, and Prometheus monitoring. Each SeiNode -# bootstraps from an S3 snapshot and block-syncs to tip. Genesis is -# resolved automatically by the sidecar. +# Three full nodes with public Gateway API routing and Prometheus +# monitoring. Each SeiNode bootstraps from an S3 snapshot and +# block-syncs to tip. Genesis is resolved automatically by the sidecar. apiVersion: sei.io/v1alpha1 kind: SeiNodeDeployment metadata: @@ -41,17 +40,7 @@ spec: targetHeight: 198740000 trustPeriod: "9999h0m0s" - networking: - service: - type: ClusterIP - - isolation: - authorizationPolicy: - allowedSources: - - principals: - - "cluster.local/ns/istio-system/sa/sei-gateway-istio" - - namespaces: - - default + networking: {} monitoring: serviceMonitor: diff --git a/manifests/sei.io_seinodedeployments.yaml b/manifests/sei.io_seinodedeployments.yaml index 778b5e6..4766576 100644 --- a/manifests/sei.io_seinodedeployments.yaml +++ b/manifests/sei.io_seinodedeployments.yaml @@ -183,8 +183,10 @@ spec: type: object networking: description: |- - Networking controls how the group is exposed to traffic. - Networking resources are shared across all replicas. + Networking enables public networking for the deployment. + When present, the controller creates a ClusterIP Service and + HTTPRoutes on the platform Gateway. When absent, the deployment + is private with only per-node headless Services. type: object replicas: default: 1