From 800f41fdb3acf6bb0f2b4a0e245d2a1c3fa1436d Mon Sep 17 00:00:00 2001 From: "Patrick W. Healy" Date: Wed, 16 Sep 2026 23:39:49 +0000 Subject: [PATCH] net: enable summary-only routine status publication by default Activate summary in runtime and rendered ConfigMap defaults after controller retention and client migration. Keep explicit full publication rollback. Exercise default-mode HTTP/WebSocket publishers and document startup settings, controller-first rollout, TTL and fallback semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d2243398-6c36-4c3d-969e-7ed7bfb5b459 --- .../status_detail_config_test.go | 2 +- .../status_publication_test.go | 5 ++- deploy/net/01-configmap.yaml.tmpl | 4 +- docs/net/configuration.md | 41 ++++++++++++++----- internal/net/config/runtime_config.go | 8 ++-- internal/net/config/status_detail_test.go | 4 +- 6 files changed, 42 insertions(+), 22 deletions(-) diff --git a/cmd/unbounded-net-node/status_detail_config_test.go b/cmd/unbounded-net-node/status_detail_config_test.go index 31e5ba0b9..186782820 100644 --- a/cmd/unbounded-net-node/status_detail_config_test.go +++ b/cmd/unbounded-net-node/status_detail_config_test.go @@ -14,7 +14,7 @@ func TestNodeStatusDetailConfig(t *testing.T) { name, yaml, flag, want string invalid bool }{ - {name: "default", yaml: "node: {}", want: "full"}, + {name: "default", yaml: "node: {}", want: "summary"}, {name: "summary", yaml: "node:\n statusDetailMode: summary", want: "summary"}, {name: "full", yaml: "node:\n statusDetailMode: full", want: "full"}, {name: "invalid YAML value", yaml: "node:\n statusDetailMode: invalid", invalid: true}, diff --git a/cmd/unbounded-net-node/status_publication_test.go b/cmd/unbounded-net-node/status_publication_test.go index 4747d1a8c..5065c7832 100644 --- a/cmd/unbounded-net-node/status_publication_test.go +++ b/cmd/unbounded-net-node/status_publication_test.go @@ -17,6 +17,7 @@ import ( "github.com/coder/websocket" "google.golang.org/protobuf/proto" + configpkg "github.com/Azure/unbounded/internal/net/config" netstatus "github.com/Azure/unbounded/internal/net/status" statusproto "github.com/Azure/unbounded/internal/net/status/proto" statusv1alpha1 "github.com/Azure/unbounded/internal/net/status/v1alpha1" @@ -28,7 +29,7 @@ func TestSummaryPublicationNeverCollectsDetails(t *testing.T) { h := blockedBootstrapHealthState() h.setStatusServer(s) - cfg := &config{StatusDetailMode: "summary", StatusPushDelta: true} + cfg := &config{StatusDetailMode: configpkg.DefaultStatusDetailMode, StatusPushDelta: true} for _, force := range []bool{true, false} { msg, base := collectPublication(h, cfg, &NodeStatusResponse{Peers: make([]WireGuardPeerStatus, 100)}, force, 42) if base != nil || msg.Status != nil || msg.Delta != nil || msg.Type != statusv1alpha1.NodeStatusSummaryType || msg.Summary == nil { @@ -131,7 +132,7 @@ func TestRoutineSummaryPublishers(t *testing.T) { defer server.Close() cfg := &config{ - NodeName: "node-a", StatusDetailMode: "summary", StatusPushEnabled: transport == "HTTP", + NodeName: "node-a", StatusDetailMode: configpkg.DefaultStatusDetailMode, StatusPushEnabled: transport == "HTTP", StatusWSEnabled: transport == "WS", StatusPushURL: server.URL, StatusWSURL: "ws" + strings.TrimPrefix(server.URL, "http"), StatusPushInterval: 5 * time.Millisecond, StatusPushDelta: true, StatusWSAPIServerMode: statusWSAPIServerModeNever, CriticalDeltaEvery: 5 * time.Millisecond, StatsDeltaEvery: 7 * time.Millisecond, FullSyncEvery: 9 * time.Millisecond, diff --git a/deploy/net/01-configmap.yaml.tmpl b/deploy/net/01-configmap.yaml.tmpl index e742250ad..c630365fb 100644 --- a/deploy/net/01-configmap.yaml.tmpl +++ b/deploy/net/01-configmap.yaml.tmpl @@ -70,8 +70,8 @@ data: statusPushEnabled: {{ default "true" .NodeStatusPushEnabled }} statusPushURL: "{{ default "" .NodeStatusPushURL }}" statusPushDelta: {{ default "true" .NodeStatusPushDelta }} - # Preparatory, startup-only; keep full until summary rollout is activated. - statusDetailMode: "{{ default "full" .NodeStatusDetailMode }}" + # Startup-only; full restores legacy node publication, not bulk exports. + statusDetailMode: "{{ default "summary" .NodeStatusDetailMode }}" statusPushInterval: "{{ default "60s" .NodeStatusPushInterval }}" statusPushApiserverInterval: "{{ default "60s" .NodeStatusPushApiserverInterval }}" healthCheckPort: "{{ default "9997" .NodeHealthCheckPort }}" diff --git a/docs/net/configuration.md b/docs/net/configuration.md index 0574e27da..1649351ba 100644 --- a/docs/net/configuration.md +++ b/docs/net/configuration.md @@ -13,22 +13,40 @@ Both binaries now load runtime settings from a shared YAML file mounted from the - Startup behavior: fail-fast if the config file is missing or invalid - CLI flags still work as explicit overrides when set -### Preparatory detail status settings +### Summary publication and explicit details -These startup-only settings prepare the lightweight-status rollout. They are -parsed and validated now; collection, caching, and request delivery are wired in -subsequent layers. Publication behavior remains unchanged, with `full` as the -default until final activation. Changing these settings requires a pod restart. +Routine node publications contain overview metadata and aggregate counts, not +peer lists, route entries, or BPF maps. Detailed data is collected for an explicit +single-node request and kept temporarily. Changing these settings requires a +restart of the affected controller or node-agent pods. -| Runtime setting | CLI override | Current default | Allowed values | +| Runtime setting | CLI override | Default | Allowed values | |-----------------|--------------|-----------------|----------------| -| `node.statusDetailMode` | `--status-detail-mode` | `full` | `summary`, `full` | +| `node.statusDetailMode` | `--status-detail-mode` | `summary` | `summary`, `full` | | `controller.statusDetailCacheTTL` | `--status-detail-cache-ttl` | `300s` | Strictly positive duration | | `controller.statusDetailRequestTimeout` | `--status-detail-request-timeout` | `120s` | Strictly positive duration | -The intended cache lifetime starts when actual details arrive, not on summary -updates or reads. The request timeout covers all delivery attempts together. -Upgrade controllers before enabling summary publication in the completed rollout. +The cache lifetime starts when actual details arrive, not on summary updates or +reads. Continuous legacy full publications refresh it; on-demand duplicate or +late replies do not. The request timeout covers all delivery attempts together. + +Upgrade controllers and their dashboard/CLI consumers before restarting agents +in summary mode. An incompatible controller is reported explicitly; agents do +not silently resume full streaming. For node-publishing rollback, set +`node.statusDetailMode: full` and restart agents. This restores full/delta +publication, not detailed bulk exports or the retired connectivity views. + +Bulk APIs and global broadcasts remain summary-only in either mode. With no +active capable node WebSocket, an explicit request tries HTTP pull first, +regardless of the background-pull toggle. Failed pulls leave a command for the +next authenticated status POST response. Both publishers may remain disabled: +local HTTP diagnostics still work, but a failed pull has no polling fallback +until outbound POST publication resumes. + +Requests and details are leader-local and in memory. Restart or leadership +change may require an explicit retry. TTL limits retention duration, not peak +memory during a burst of requests; releasing references permits garbage +collection but does not guarantee an immediate RSS decrease. ### Runtime config structure @@ -53,6 +71,8 @@ controller: statusStaleThreshold: 40s statusWebsocketKeepaliveInterval: 10s statusWsKeepaliveFailureCount: 2 + statusDetailCacheTTL: 300s + statusDetailRequestTimeout: 120s registerAggregatedAPIServer: true # Optional. Selects local OIDC validation for node service account JWTs. # When unset, discover from the controller's mounted token and fall back to @@ -97,6 +117,7 @@ node: statusPushEnabled: true statusPushURL: "" statusPushDelta: true + statusDetailMode: summary statusPushInterval: 10s statusPushApiserverInterval: 30s healthCheckPort: 9997 diff --git a/internal/net/config/runtime_config.go b/internal/net/config/runtime_config.go index da6cb7396..992de96f7 100644 --- a/internal/net/config/runtime_config.go +++ b/internal/net/config/runtime_config.go @@ -159,11 +159,9 @@ func ParsePositiveDurationField(raw, fieldName string) (time.Duration, error) { } const ( - StatusDetailModeSummary = "summary" - StatusDetailModeFull = "full" - // DefaultStatusDetailMode preserves legacy publication during preparatory rollout. - // Summary becomes the default only after collectors and consumers are wired. - DefaultStatusDetailMode = StatusDetailModeFull + StatusDetailModeSummary = "summary" + StatusDetailModeFull = "full" + DefaultStatusDetailMode = StatusDetailModeSummary DefaultStatusDetailCacheTTL = 300 * time.Second DefaultStatusDetailRequestTimeout = 120 * time.Second ) diff --git a/internal/net/config/status_detail_test.go b/internal/net/config/status_detail_test.go index a6fc06aee..6fec81a72 100644 --- a/internal/net/config/status_detail_test.go +++ b/internal/net/config/status_detail_test.go @@ -12,8 +12,8 @@ import ( ) func TestStatusDetailMode(t *testing.T) { - if DefaultStatusDetailMode != "full" { - t.Fatal("preparatory default must preserve full publication") + if DefaultStatusDetailMode != "summary" { + t.Fatal("routine publication must default to summaries") } for _, mode := range []string{"summary", "full", "", "SUMMARY", "other", " full "} {