diff --git a/cmd/atenet/README.md b/cmd/atenet/README.md
index 43fa5a448..1d1daca19 100644
--- a/cmd/atenet/README.md
+++ b/cmd/atenet/README.md
@@ -44,6 +44,19 @@ likely be split in the future for better scalability.)
RBAC permissions:
* get, list, watch on ate-system EndpointSlices
+The Go router's `/statusz` page displays `ROUTER_SERVICE_IP` as optional
+deployment configuration, independently of `--namespace`. The ingress Deployment
+maps it from Kubernetes' `ATENET_ROUTER_SERVICE_HOST` service-link variable.
+It is a container-start snapshot, not a live Service lookup: recreating the
+Service can leave the address stale until the container restarts. The Service
+is installed before the Deployment to make injection available on a fresh install,
+but kubelet observation races or disabled service links can still leave it absent.
+Deployments with a different Service name can supply their own mapping.
+
+Missing or invalid values display as **Unavailable** in HTML and an empty
+`router_cluster_ip` string in JSON. This diagnostic does not affect readiness or
+require Service API permissions. The egress Deployment leaves it unset.
+
## testing
Run the package tests with `go test ./cmd/atenet/...`. Cluster e2e
diff --git a/cmd/atenet/internal/router/cmd.go b/cmd/atenet/internal/router/cmd.go
index d01dca4c1..acda38533 100644
--- a/cmd/atenet/internal/router/cmd.go
+++ b/cmd/atenet/internal/router/cmd.go
@@ -46,7 +46,7 @@ func NewRouterCmd() *cobra.Command {
cmd.Flags().StringVar(&cfg.LogLevel, "log-level", "info", "Log level: debug, info, warn, error")
cmd.Flags().StringVar(&cfg.MetricsAddr, "metrics-listen-addr", ":9090", "Address and port the prometheus metrics server should listen on.")
cmd.Flags().StringVar(&cfg.AtenetRouter, "atenet-dataplane", string(atenetRouterEnvoy), "Atenet ingress and egress dataplane: envoy or agentgateway")
- cmd.Flags().StringVar(&cfg.Namespace, "namespace", "default", "Target operations namespace")
+ cmd.Flags().StringVar(&cfg.Namespace, "namespace", "default", "Namespace displayed on the status page")
cmd.Flags().StringVar(&cfg.Kubeconfig, "kubeconfig", "", "Absolute path to the kubeconfig configuration file")
cmd.Flags().StringVar(&cfg.AteapiAddr, "ateapi-address", "k8s:///api.ate-system.svc:443", "gRPC dial target for the cluster ateapi Control instance.")
cmd.Flags().IntVar(&cfg.HttpPort, "port-http", 8080, "TCP port for workload traffic entering through the Envoy Router")
diff --git a/cmd/atenet/internal/router/dashboard.html b/cmd/atenet/internal/router/dashboard.html
index 856f2224d..67178a1c6 100644
--- a/cmd/atenet/internal/router/dashboard.html
+++ b/cmd/atenet/internal/router/dashboard.html
@@ -245,11 +245,15 @@
atenet Router Status
Infrastructure details
- Router Service IP
+ Configured Service IP (container-start snapshot)
+ {{ if .RouterClusterIP }}
{{ .RouterClusterIP }}
+ {{ else }}
+ Unavailable
+ {{ end }}
- Namespace Context
+ Configured namespace
{{ .Namespace }}
diff --git a/cmd/atenet/internal/router/status.go b/cmd/atenet/internal/router/status.go
index 687875558..26a856155 100644
--- a/cmd/atenet/internal/router/status.go
+++ b/cmd/atenet/internal/router/status.go
@@ -15,11 +15,11 @@
package router
import (
- "context"
_ "embed"
"encoding/json"
"fmt"
"html/template"
+ "net"
"net/http"
"os"
"runtime/debug"
@@ -28,7 +28,6 @@ import (
"github.com/spf13/pflag"
"go.opentelemetry.io/otel"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc"
"github.com/agent-substrate/substrate/cmd/atenet/internal/router/ingress"
@@ -63,31 +62,16 @@ type FormattedQuery struct {
Duration string `json:"duration"`
}
-func (s *RouterServer) getRouterIP(ctx context.Context) string {
- if s.clientset == nil {
- return "Offline Mode (No Cluster IP)"
- }
-
- svc, err := s.clientset.CoreV1().Services(s.cfg.Namespace).Get(ctx, "atenet-router", metav1.GetOptions{})
- if err != nil {
- return fmt.Sprintf("Lookup Failed: %v", err)
- }
-
- if svc.Spec.ClusterIP == "" || svc.Spec.ClusterIP == "None" {
- return "ClusterIP Unassigned"
- }
-
- return svc.Spec.ClusterIP
-}
-
func (s *RouterServer) handleStatusz(w http.ResponseWriter, req *http.Request) {
- ctx, span := otel.Tracer(extproc.ServiceName).Start(req.Context(), "handleStatusz")
+ _, span := otel.Tracer(extproc.ServiceName).Start(req.Context(), "handleStatusz")
defer span.End()
- ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
- defer cancel()
-
- routerIP := s.getRouterIP(ctx)
+ // Deployment-supplied container-start configuration, independent of --namespace.
+ // Missing service links can leave the manifest's $(...) reference unexpanded.
+ routerIP := os.Getenv("ROUTER_SERVICE_IP")
+ if net.ParseIP(routerIP) == nil {
+ routerIP = ""
+ }
buildInfo := BuildTag
if info, ok := debug.ReadBuildInfo(); ok {
diff --git a/cmd/atenet/internal/router/status_test.go b/cmd/atenet/internal/router/status_test.go
index a1b1280a0..0ab32bf48 100644
--- a/cmd/atenet/internal/router/status_test.go
+++ b/cmd/atenet/internal/router/status_test.go
@@ -28,16 +28,83 @@ import (
"math/big"
"net"
"net/http"
+ "net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
+ kubernetesfake "k8s.io/client-go/kubernetes/fake"
+
"github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc"
"github.com/agent-substrate/substrate/cmd/atenet/internal/router/ingress"
)
+func TestStatuszConfiguredServiceIP(t *testing.T) {
+ // The deployment chooses the address; neither --namespace nor Kubernetes
+ // service links are discovery inputs to the status handler.
+ t.Setenv("POD_NAMESPACE", "deployed-namespace")
+ t.Setenv("ATENET_ROUTER_SERVICE_HOST", "10.96.0.99")
+ for _, tc := range []struct {
+ name, input, want string
+ }{
+ {"IPv4", "10.96.0.42", "10.96.0.42"},
+ {"IPv6", "fd00:10:96::42", "fd00:10:96::42"},
+ {"missing", "", ""},
+ {"headless", "None", ""},
+ {"unexpanded", "$(ATENET_ROUTER_SERVICE_HOST)", ""},
+ {"invalid", "not-an-ip", ""},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Setenv("ROUTER_SERVICE_IP", tc.input)
+ for _, format := range []string{"html", "json"} {
+ t.Run(format, func(t *testing.T) {
+ clientset := kubernetesfake.NewSimpleClientset()
+ srv := &RouterServer{
+ cfg: routerConfig{Namespace: "configured-namespace"},
+ clientset: clientset,
+ }
+ response := httptest.NewRecorder()
+ srv.handleStatusz(response, httptest.NewRequest(http.MethodGet, "/statusz?format="+format, nil))
+ if response.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200", response.Code)
+ }
+ if actions := clientset.Actions(); len(actions) != 0 {
+ t.Errorf("status handler made Kubernetes API calls: %v", actions)
+ }
+ if format == "json" {
+ var data map[string]any
+ if err := json.Unmarshal(response.Body.Bytes(), &data); err != nil {
+ t.Fatal(err)
+ }
+ if got := data["router_cluster_ip"]; got != tc.want {
+ t.Errorf("router_cluster_ip = %#v, want %q", got, tc.want)
+ }
+ if got := data["namespace"]; got != "configured-namespace" {
+ t.Errorf("namespace = %#v, want configured-namespace", got)
+ }
+ return
+ }
+ body := response.Body.String()
+ for _, label := range []string{"Configured Service IP (container-start snapshot)", "Configured namespace", "configured-namespace"} {
+ if !strings.Contains(body, label) {
+ t.Errorf("HTML missing %q", label)
+ }
+ }
+ want := tc.want
+ if want == "" {
+ want = "Unavailable"
+ }
+ if !strings.Contains(body, ">"+want+"") {
+ t.Errorf("HTML missing Service IP value %q", want)
+ }
+ })
+ }
+ })
+ }
+}
+
func TestStatuszEndpoint(t *testing.T) {
dnsAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0")
if err != nil {
diff --git a/manifests/ate-install/atenet-router.yaml b/manifests/ate-install/atenet-router.yaml
index 16e7d903c..587cea21c 100644
--- a/manifests/ate-install/atenet-router.yaml
+++ b/manifests/ate-install/atenet-router.yaml
@@ -112,6 +112,38 @@ data:
address: 127.0.0.1
port_value: 18000
---
+apiVersion: v1
+kind: Service
+metadata:
+ name: atenet-router
+ namespace: ate-system
+spec:
+ type: ClusterIP
+ ipFamilyPolicy: PreferDualStack
+ selector:
+ app: atenet-router
+ ports:
+ - name: http
+ port: 80
+ targetPort: 8080
+ protocol: TCP
+ - name: https
+ port: 443
+ targetPort: 8443
+ protocol: TCP
+ - name: connect
+ port: 8081
+ targetPort: 8081
+ protocol: TCP
+ - name: connect-tls
+ port: 8444
+ targetPort: 8444
+ protocol: TCP
+ - name: status
+ port: 4040
+ targetPort: status
+ protocol: TCP
+---
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -185,6 +217,9 @@ spec:
# If long turns must survive a shutdown, raise --drain-timeout and
# terminationGracePeriodSeconds above together.
env:
+ # Optional /statusz snapshot; service links are resolved at container start.
+ - name: ROUTER_SERVICE_IP
+ value: "$(ATENET_ROUTER_SERVICE_HOST)"
- name: POD_NAME
valueFrom:
fieldRef:
@@ -325,35 +360,3 @@ spec:
matchLabels:
podcert.ate.dev/canarying: live
path: trust-bundle.pem
----
-apiVersion: v1
-kind: Service
-metadata:
- name: atenet-router
- namespace: ate-system
-spec:
- type: ClusterIP
- ipFamilyPolicy: PreferDualStack
- selector:
- app: atenet-router
- ports:
- - name: http
- port: 80
- targetPort: 8080
- protocol: TCP
- - name: https
- port: 443
- targetPort: 8443
- protocol: TCP
- - name: connect
- port: 8081
- targetPort: 8081
- protocol: TCP
- - name: connect-tls
- port: 8444
- targetPort: 8444
- protocol: TCP
- - name: status
- port: 4040
- targetPort: status
- protocol: TCP