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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cmd/atenet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cmd/atenet/internal/router/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
8 changes: 6 additions & 2 deletions cmd/atenet/internal/router/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,15 @@ <h1>atenet Router Status</h1>
<div class="card">
<div class="card-title">Infrastructure details</div>
<div class="metadata-item">
<span class="label">Router Service IP</span>
<span class="label">Configured Service IP (container-start snapshot)</span>
{{ if .RouterClusterIP }}
<span class="value badge" style="background: rgba(16, 185, 129, 0.1); color: #10b981;">{{ .RouterClusterIP }}</span>
{{ else }}
<span class="value" style="color: var(--text-secondary);">Unavailable</span>
{{ end }}
</div>
<div class="metadata-item">
<span class="label">Namespace Context</span>
<span class="label">Configured namespace</span>
<span class="value">{{ .Namespace }}</span>
</div>
</div>
Expand Down
32 changes: 8 additions & 24 deletions cmd/atenet/internal/router/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
package router

import (
"context"
_ "embed"
"encoding/json"
"fmt"
"html/template"
"net"
"net/http"
"os"
"runtime/debug"
Expand All @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
67 changes: 67 additions & 0 deletions cmd/atenet/internal/router/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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+"</span>") {
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 {
Expand Down
67 changes: 35 additions & 32 deletions manifests/ate-install/atenet-router.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Loading