Skip to content
Closed
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
24 changes: 24 additions & 0 deletions cmd/unbounded-net-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ func main() {
RequireDashboardAuth: true,
StatusWSKeepaliveInterval: 10 * time.Second,
StatusWSKeepaliveFailureCount: 2,
StatusDetailCacheTTL: config.DefaultStatusDetailCacheTTL,
StatusDetailRequestTimeout: config.DefaultStatusDetailRequestTimeout,
ManagedKubeProxyEnabled: true,
NodeTokenLifetime: 4 * time.Hour,
ViewerTokenLifetime: 30 * time.Minute,
Expand Down Expand Up @@ -127,6 +129,8 @@ on site configuration, and maintain SiteNodeSlice and GatewayPool status.`,
flags.IntVar(&cfg.HealthPort, "health-port", 9999, "Port for health check HTTP server (0 to disable)")
flags.IntVar(&cfg.NodeAgentHealthPort, "node-agent-health-port", 9998, "Port where node agents serve their health/status endpoints")
flags.DurationVar(&cfg.StatusStaleThreshold, "status-stale-threshold", 90*time.Second, "Duration after which a node's pushed status is considered stale")
flags.DurationVar(&cfg.StatusDetailCacheTTL, "status-detail-cache-ttl", config.DefaultStatusDetailCacheTTL, "Lifetime of received node details (positive duration; preparatory)")
flags.DurationVar(&cfg.StatusDetailRequestTimeout, "status-detail-request-timeout", config.DefaultStatusDetailRequestTimeout, "End-to-end node detail request timeout (positive duration; preparatory)")
flags.DurationVar(&cfg.StatusWSKeepaliveInterval, "status-ws-keepalive-interval", 10*time.Second, "Interval between websocket keepalive pings on controller node status streams (0 to disable)")
flags.IntVar(&cfg.StatusWSKeepaliveFailureCount, "status-ws-keepalive-failure-count", 2, "Sequential websocket keepalive ping failures before closing node status websocket")
flags.BoolVar(&cfg.RegisterAggregatedAPIServer, "register-aggregated-apiserver", true, "Serve node status push endpoints via aggregated API server paths")
Expand Down Expand Up @@ -164,6 +168,24 @@ func applyControllerRuntimeConfig(cmd *cobra.Command, cfg *config.Config, config

flags := cmd.Flags()

if !flags.Changed("status-detail-cache-ttl") && runtimeCfg.Controller.StatusDetailCacheTTL != "" {
d, parseErr := config.ParsePositiveDurationField(runtimeCfg.Controller.StatusDetailCacheTTL, "controller.statusDetailCacheTTL")
if parseErr != nil {
return parseErr
}

cfg.StatusDetailCacheTTL = d
}

if !flags.Changed("status-detail-request-timeout") && runtimeCfg.Controller.StatusDetailRequestTimeout != "" {
d, parseErr := config.ParsePositiveDurationField(runtimeCfg.Controller.StatusDetailRequestTimeout, "controller.statusDetailRequestTimeout")
if parseErr != nil {
return parseErr
}

cfg.StatusDetailRequestTimeout = d
}

if !flags.Changed("informer-resync-period") {
if d, parseErr := config.ParseDurationField(runtimeCfg.Controller.InformerResyncPeriod, "controller.informerResyncPeriod"); parseErr != nil {
return parseErr
Expand Down Expand Up @@ -321,6 +343,8 @@ General Flags:
--managed-kube-proxy Create kube-proxy DaemonSets for unbounded-managed site nodes not covered by provider kube-proxy (default true)
--managed-kube-proxy-image string kube-proxy image for managed site DaemonSets
--status-stale-threshold duration Duration after which a node's pushed status is considered stale (default 90s)
--status-detail-cache-ttl duration Lifetime of received node details; preparatory (default 5m0s)
--status-detail-request-timeout duration End-to-end node detail request timeout; preparatory (default 2m0s)
--status-ws-keepalive-interval duration Interval between websocket keepalive pings on controller node status streams (0 to disable) (default 10s)
--status-ws-keepalive-failure-count int Sequential websocket keepalive ping failures before closing node status websocket (default 2)

Expand Down
2 changes: 2 additions & 0 deletions cmd/unbounded-net-controller/main_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ func newControllerConfigTestCommand(cfg *config.Config) *cobra.Command {
flags.IntVar(&cfg.HealthPort, "health-port", 9999, "")
flags.IntVar(&cfg.NodeAgentHealthPort, "node-agent-health-port", 9998, "")
flags.DurationVar(&cfg.StatusStaleThreshold, "status-stale-threshold", 40*time.Second, "")
flags.DurationVar(&cfg.StatusDetailCacheTTL, "status-detail-cache-ttl", config.DefaultStatusDetailCacheTTL, "")
flags.DurationVar(&cfg.StatusDetailRequestTimeout, "status-detail-request-timeout", config.DefaultStatusDetailRequestTimeout, "")
flags.DurationVar(&cfg.StatusWSKeepaliveInterval, "status-ws-keepalive-interval", 10*time.Second, "")
flags.IntVar(&cfg.StatusWSKeepaliveFailureCount, "status-ws-keepalive-failure-count", 2, "")
flags.BoolVar(&cfg.RegisterAggregatedAPIServer, "register-aggregated-apiserver", true, "")
Expand Down
63 changes: 63 additions & 0 deletions cmd/unbounded-net-controller/status_detail_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation.
// SPDX-License-Identifier: Apache-2.0

package main

import (
"os"
"path/filepath"
"testing"
"time"

"github.com/Azure/unbounded/internal/net/config"
)

func TestControllerStatusDetailConfig(t *testing.T) {
for _, tc := range []struct {
name, yaml, flagName, flagValue string
ttl, timeout time.Duration
invalid bool
}{
{name: "defaults", yaml: "controller: {}", ttl: 300 * time.Second, timeout: 120 * time.Second},
{name: "configured", yaml: "controller:\n statusDetailCacheTTL: 30s\n statusDetailRequestTimeout: 10s", ttl: 30 * time.Second, timeout: 10 * time.Second},
{name: "TTL zero", yaml: "controller:\n statusDetailCacheTTL: 0s", invalid: true},
{name: "TTL negative", yaml: "controller:\n statusDetailCacheTTL: -1s", invalid: true},
{name: "TTL malformed", yaml: "controller:\n statusDetailCacheTTL: invalid", invalid: true},
{name: "timeout zero", yaml: "controller:\n statusDetailRequestTimeout: 0s", invalid: true},
{name: "timeout negative", yaml: "controller:\n statusDetailRequestTimeout: -1s", invalid: true},
{name: "timeout malformed", yaml: "controller:\n statusDetailRequestTimeout: invalid", invalid: true},
{name: "TTL flag wins", yaml: "controller:\n statusDetailCacheTTL: invalid", flagName: "status-detail-cache-ttl", flagValue: "15s", ttl: 15 * time.Second, timeout: 120 * time.Second},
{name: "timeout flag wins", yaml: "controller:\n statusDetailRequestTimeout: invalid", flagName: "status-detail-request-timeout", flagValue: "15s", ttl: 300 * time.Second, timeout: 15 * time.Second},
{name: "zero flag", yaml: "controller: {}", flagName: "status-detail-cache-ttl", flagValue: "0s", invalid: true},
{name: "negative flag", yaml: "controller: {}", flagName: "status-detail-request-timeout", flagValue: "-1s", invalid: true},
} {
t.Run(tc.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
if err := os.WriteFile(path, []byte(tc.yaml), 0o600); err != nil {
t.Fatal(err)
}

cfg := &config.Config{}

cmd := newControllerConfigTestCommand(cfg)
if tc.flagName != "" {
if err := cmd.Flags().Set(tc.flagName, tc.flagValue); err != nil {
t.Fatal(err)
}
}

err := applyControllerRuntimeConfig(cmd, cfg, path)
if err == nil {
err = cfg.Validate()
}

if (err != nil) != tc.invalid {
t.Fatalf("startup config validation = %v", err)
}

if !tc.invalid && (cfg.StatusDetailCacheTTL != tc.ttl || cfg.StatusDetailRequestTimeout != tc.timeout) {
t.Errorf("lifetimes = %s/%s, want %s/%s", cfg.StatusDetailCacheTTL, cfg.StatusDetailRequestTimeout, tc.ttl, tc.timeout)
}
})
}
}
11 changes: 11 additions & 0 deletions cmd/unbounded-net-node/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ type config struct {
StatusPushInterval time.Duration // Interval between status pushes to controller
StatusPushAPIServerInterval time.Duration // Interval between status pushes via aggregated API server
StatusPushDelta bool // Whether periodic HTTP pushes use deltas
StatusDetailMode string // Startup-loaded; publication wiring follows separately.
StatusWSEnabled bool // Whether websocket push is enabled
StatusWSURL string // Controller websocket URL for status push
StatusWSAPIServerMode string // API server fallback mode: never, fallback, preferred (alias for fallback)
Expand Down Expand Up @@ -230,6 +231,7 @@ func main() {
StatusPushInterval: 10 * time.Second, // Default 10s push interval
StatusPushAPIServerInterval: 30 * time.Second,
StatusPushDelta: true,
StatusDetailMode: configpkg.DefaultStatusDetailMode,
StatusWSEnabled: true,
StatusWSAPIServerMode: statusWSAPIServerModeFallback,
StatusWSAPIServerStartupDelay: 60 * time.Second,
Expand Down Expand Up @@ -328,6 +330,7 @@ then annotates the node with the public key.`,
flags.DurationVar(&cfg.StatusPushInterval, "status-push-interval", 60*time.Second, "Interval between status pushes to controller")
flags.DurationVar(&cfg.StatusPushAPIServerInterval, "status-push-apiserver-interval", 60*time.Second, "Interval between status pushes via aggregated API server")
flags.BoolVar(&cfg.StatusPushDelta, "status-push-delta", true, "Enable delta mode for periodic HTTP status push")
flags.StringVar(&cfg.StatusDetailMode, "status-detail-mode", configpkg.DefaultStatusDetailMode, "Routine status detail mode: summary or full (preparatory; publication behavior unchanged)")
flags.BoolVar(&cfg.StatusWSEnabled, "status-ws-enabled", true, "Enable websocket status push to controller")
flags.StringVar(&cfg.StatusWSURL, "status-ws-url", "", "Controller websocket URL for status push (default: ws://service/status/nodews)")
flags.StringVar(&cfg.StatusWSAPIServerMode, "status-ws-apiserver-mode", statusWSAPIServerModeFallback, "API server fallback mode: never, fallback, preferred (alias for fallback); direct controller endpoints are tried first")
Expand Down Expand Up @@ -365,6 +368,14 @@ func applyNodeRuntimeConfig(cmd *cobra.Command, cfg *config) error {
flags := cmd.Flags()
nodeCfg := runtimeCfg.Node

if !flags.Changed("status-detail-mode") && nodeCfg.StatusDetailMode != "" {
cfg.StatusDetailMode = nodeCfg.StatusDetailMode
}

if err := configpkg.ValidateStatusDetailMode(cfg.StatusDetailMode); err != nil {
return err
}

if !flags.Changed("informer-resync-period") {
if d, parseErr := configpkg.ParseDurationField(nodeCfg.InformerResyncPeriod, "node.informerResyncPeriod"); parseErr != nil {
return parseErr
Expand Down
3 changes: 3 additions & 0 deletions cmd/unbounded-net-node/main_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"time"

"github.com/spf13/cobra"

configpkg "github.com/Azure/unbounded/internal/net/config"
)

func newNodeConfigTestCommand(cfg *config) *cobra.Command {
Expand All @@ -35,6 +37,7 @@ func newNodeConfigTestCommand(cfg *config) *cobra.Command {
flags.DurationVar(&cfg.StatusPushInterval, "status-push-interval", 10*time.Second, "")
flags.DurationVar(&cfg.StatusPushAPIServerInterval, "status-push-apiserver-interval", 30*time.Second, "")
flags.BoolVar(&cfg.StatusPushDelta, "status-push-delta", true, "")
flags.StringVar(&cfg.StatusDetailMode, "status-detail-mode", configpkg.DefaultStatusDetailMode, "")
flags.BoolVar(&cfg.StatusWSEnabled, "status-ws-enabled", true, "")
flags.StringVar(&cfg.StatusWSURL, "status-ws-url", "", "")
flags.StringVar(&cfg.StatusWSAPIServerMode, "status-ws-apiserver-mode", statusWSAPIServerModeFallback, "")
Expand Down
53 changes: 53 additions & 0 deletions cmd/unbounded-net-node/status_detail_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation.
// SPDX-License-Identifier: Apache-2.0

package main

import (
"os"
"path/filepath"
"testing"
)

func TestNodeStatusDetailConfig(t *testing.T) {
for _, tc := range []struct {
name, yaml, flag, want string
invalid bool
}{
{name: "default", yaml: "node: {}", want: "full"},
{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},
{name: "flag wins", yaml: "node:\n statusDetailMode: summary", flag: "full", want: "full"},
{name: "flag overrides invalid YAML", yaml: "node:\n statusDetailMode: invalid", flag: "summary", want: "summary"},
{name: "invalid flag", yaml: "node: {}", flag: "invalid", invalid: true},
} {
t.Run(tc.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
if err := os.WriteFile(path, []byte(tc.yaml), 0o600); err != nil {
t.Fatal(err)
}

cfg := &config{
ConfigFile: path, GeneveInterfaceName: "geneve0", VXLANInterfaceName: "vxlan0",
IPIPInterfaceName: "ipip0", WireGuardInterfacePrefix: "wg",
}

cmd := newNodeConfigTestCommand(cfg)
if tc.flag != "" {
if err := cmd.Flags().Set("status-detail-mode", tc.flag); err != nil {
t.Fatal(err)
}
}

err := applyNodeRuntimeConfig(cmd, cfg)
if (err != nil) != tc.invalid {
t.Fatalf("applyNodeRuntimeConfig() = %v", err)
}

if !tc.invalid && cfg.StatusDetailMode != tc.want {
t.Errorf("mode = %q, want %q", cfg.StatusDetailMode, tc.want)
}
})
}
}
5 changes: 5 additions & 0 deletions deploy/net/01-configmap.yaml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ data:
nodeAgentHealthPort: {{ default "9998" .ControllerNodeAgentHealthPort }}
informerResyncPeriod: "{{ default "300s" .ControllerInformerResyncPeriod }}"
statusStaleThreshold: "{{ default "90s" .ControllerStatusStaleThreshold }}"
# Preparatory, startup-only detail lifetimes; cache/request wiring follows.
statusDetailCacheTTL: "{{ default "300s" .ControllerStatusDetailCacheTTL }}"
statusDetailRequestTimeout: "{{ default "120s" .ControllerStatusDetailRequestTimeout }}"
statusWebsocketKeepaliveInterval: "{{ default "30s" .ControllerStatusWebsocketKeepaliveInterval }}"
statusWsKeepaliveFailureCount: {{ default "3" .ControllerStatusWsKeepaliveFailureCount }}
registerAggregatedAPIServer: {{ default "true" .ControllerRegisterAggregatedAPIServer }}
Expand Down Expand Up @@ -67,6 +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 }}"
statusPushInterval: "{{ default "60s" .NodeStatusPushInterval }}"
statusPushApiserverInterval: "{{ default "60s" .NodeStatusPushApiserverInterval }}"
healthCheckPort: "{{ default "9997" .NodeHealthCheckPort }}"
Expand Down
17 changes: 17 additions & 0 deletions docs/content/reference/networking/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ file mounted from the `unbounded-net-config` ConfigMap.
- 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

The lightweight-status rollout adds startup-only settings. This preparatory
layer parses and validates them without changing publication behavior. `full`
remains the default until the later collector, cache, and consumer activation.
Changes require restarting the affected controller or node pod.

| Runtime setting | CLI override | Current default | Allowed values |
|-----------------|--------------|-----------------|----------------|
| `node.statusDetailMode` | `--status-detail-mode` | `full` | `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 is measured from actual detail receipt; summaries
and reads do not extend it. Request timeout spans all delivery attempts.
Upgrade controllers before enabling summary publication in the completed rollout.

### Config Structure

```yaml
Expand Down
17 changes: 17 additions & 0 deletions docs/net/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@ 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

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.

| Runtime setting | CLI override | Current default | Allowed values |
|-----------------|--------------|-----------------|----------------|
| `node.statusDetailMode` | `--status-detail-mode` | `full` | `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.

### Runtime config structure

```yaml
Expand Down
12 changes: 12 additions & 0 deletions internal/net/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ type Config struct {
// StatusStaleThreshold is the duration after which a node's pushed status is considered stale.
// When stale, the controller falls back to pulling status directly from the node.
StatusStaleThreshold time.Duration
// StatusDetailCacheTTL is the startup-loaded lifetime of received node details.
StatusDetailCacheTTL time.Duration
// StatusDetailRequestTimeout is the startup-loaded end-to-end detail request deadline.
StatusDetailRequestTimeout time.Duration
// RegisterAggregatedAPIServer controls whether the controller serves aggregated API status endpoints.
RegisterAggregatedAPIServer bool
// StatusWSKeepaliveInterval controls websocket ping cadence for node status streams.
Expand Down Expand Up @@ -114,5 +118,13 @@ func (c *Config) Validate() error {
return fmt.Errorf("status websocket keepalive failure count must be >= 1")
}

if c.StatusDetailCacheTTL <= 0 {
return fmt.Errorf("controller.statusDetailCacheTTL must be greater than zero")
}

if c.StatusDetailRequestTimeout <= 0 {
return fmt.Errorf("controller.statusDetailRequestTimeout must be greater than zero")
}

return nil
}
6 changes: 5 additions & 1 deletion internal/net/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ func TestDefaultLeaderElectionConfig(t *testing.T) {

// TestConfigValidate tests ConfigValidate.
func TestConfigValidate(t *testing.T) {
cfg := &Config{StatusWSKeepaliveFailureCount: 2}
cfg := &Config{
StatusWSKeepaliveFailureCount: 2,
StatusDetailCacheTTL: DefaultStatusDetailCacheTTL,
StatusDetailRequestTimeout: DefaultStatusDetailRequestTimeout,
}
if err := cfg.Validate(); err != nil {
t.Fatalf("expected nil validation error, got %v", err)
}
Expand Down
Loading
Loading