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