Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6946d6e
ui(net): retire dashboard connectivity graph and matrix views
phealy Sep 16, 2026
9fce058
ui(net): remove retired site topology implementation
phealy Sep 16, 2026
bd2c6e1
net: remove retired connectivity matrix computation and contracts
phealy Sep 16, 2026
4032b99
feat(net): add lightweight node status overview contracts
phealy Sep 16, 2026
feaf0cd
feat(net): add correlated detail request and capability contracts
phealy Sep 16, 2026
71647ce
feat(net): prepare startup configuration for lightweight status
phealy Sep 16, 2026
adfa7cb
net-controller: add standalone expiring node detail cache
phealy Sep 16, 2026
701d3b6
net: share exact overview projection and health semantics
phealy Sep 16, 2026
7fe4956
net-controller: add overview-only routine cache state
phealy Sep 16, 2026
f61e894
net-controller: ingest authenticated summary status on both transports
phealy Sep 16, 2026
bc80e93
refactor(net-node): share streamed peer and kernel route inspection
phealy Sep 16, 2026
ef4bbe1
feat(net-node): count annotated routes without detail snapshots
phealy Sep 16, 2026
7e150d1
feat(net-node): add lightweight overview collection and local endpoint
phealy Sep 16, 2026
190d868
refactor(net-node): reuse shared overview health semantics
phealy Sep 16, 2026
b714aa4
net-controller: add leader-local detail request lifecycle
phealy Sep 16, 2026
ba3d61b
net-controller: serve authorized asynchronous node detail requests
phealy Sep 16, 2026
79e5235
net-controller: preserve existing HTTP detail pull response behavior
phealy Sep 16, 2026
415f621
net-controller: dispatch detail commands through active node WebSockets
phealy Sep 16, 2026
0035abd
net-controller: correlate one-shot details and poll commands through …
phealy Sep 16, 2026
cff4403
frontend: add summary projection and detail response contracts
phealy Sep 16, 2026
7ae03d4
frontend: remove obsolete node table prop during summary migration
phealy Sep 16, 2026
f03e253
frontend: add explicit expiring node detail client lifecycle
phealy Sep 16, 2026
0b32d4c
frontend: prepare explicit detail controls and summary-only JSON
phealy Sep 16, 2026
c1f9fbc
refactor(net-cli): reuse authenticated transport for detail requests
phealy Sep 16, 2026
a26b7ba
feat(net-cli): keep node list and watch overview-only
phealy Sep 16, 2026
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
71 changes: 71 additions & 0 deletions cmd/kubectl-unbounded/app/net/detail_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation.
// SPDX-License-Identifier: Apache-2.0

package net

import (
"context"
"fmt"
"net/url"

"k8s.io/client-go/kubernetes"
)

type statusRequest func(context.Context, string, string, []byte) ([]byte, error)

// newStatusRequest reuses kubectl credentials and authenticated port-forward fallback.
// Each HTTP attempt is bounded independently; the caller owns the overall deadline.
func newStatusRequest(rt *pluginRuntime, opts nodeStatusFetchOptions) (statusRequest, error) {
ns, err := rt.namespace()
if err != nil {
return nil, err
}

client, err := rt.kubeClient()
if err != nil {
return nil, err
}

cfg, err := rt.restConfig()
if err != nil {
return nil, err
}

return func(ctx context.Context, method, path string, body []byte) ([]byte, error) {
attemptCtx, cancel := context.WithTimeout(ctx, opts.timeout)
defer cancel()

raw, err := requestStatusViaAggregatedAPI(attemptCtx, client, method, path, body)
if err == nil || len(raw) > 0 || ctx.Err() != nil {
return raw, err
}

fallbackCtx, fallbackCancel := context.WithTimeout(ctx, opts.timeout)
defer fallbackCancel()

return requestStatusViaPortForward(fallbackCtx, client, cfg, ns,
opts.controllerDeploy, opts.controllerSelector, opts.controllerPort, opts.timeout,
method, path, body)
}, nil
}

func requestStatusViaAggregatedAPI(ctx context.Context, client *kubernetes.Clientset, method, path string, body []byte) ([]byte, error) {
target, err := url.ParseRequestURI(path)
if err != nil || target.IsAbs() || target.Host != "" {
return nil, fmt.Errorf("invalid controller status path %q", path)
}

request := client.CoreV1().RESTClient().Verb(method).
AbsPath("/apis/status.net.unbounded-cloud.io/v1alpha1" + target.Path)
for key, values := range target.Query() {
for _, value := range values {
request.Param(key, value)
}
}

if body != nil {
request.SetHeader("Content-Type", "application/json").Body(body)
}

return request.DoRaw(ctx)
}
143 changes: 143 additions & 0 deletions cmd/kubectl-unbounded/app/net/detail_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright (c) Microsoft Corporation.
// SPDX-License-Identifier: Apache-2.0

package net

import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"

"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

func newDetailTestRuntime(t *testing.T, serverURL string) *pluginRuntime {
t.Helper()

path := filepath.Join(t.TempDir(), "kubeconfig")

data := fmt.Sprintf(`apiVersion: v1
kind: Config
current-context: test
clusters:
- name: test
cluster:
server: %s
contexts:
- name: test
context:
cluster: test
user: test
users:
- name: test
user:
token: test-token
`, serverURL)
if err := os.WriteFile(path, []byte(data), 0o600); err != nil {
t.Fatal(err)
}

rt := newPluginRuntime()
*rt.configFlags.KubeConfig = path
*rt.configFlags.Namespace = "unbounded-system"

return rt
}

func TestStatusRequestAggregatedTransport(t *testing.T) {
for _, method := range []string{http.MethodPost, http.MethodGet} {
t.Run(method, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != method || r.URL.Path != "/apis/status.net.unbounded-cloud.io/v1alpha1/status/node/node-a/details" {
t.Errorf("unexpected request %s %s", r.Method, r.URL)
}

if r.Header.Get("Authorization") != "Bearer test-token" {
t.Error("request did not reuse Kubernetes authentication")
}

if method == http.MethodPost {
body, err := io.ReadAll(r.Body)
if err != nil || string(body) != `{"forceRefresh":true}` || r.Header.Get("Content-Type") != "application/json" {
t.Errorf("unexpected request body %s, %v", body, err)
}
} else if r.URL.Query().Get("requestId") != "id/+ ?" {
t.Errorf("request ID changed: %q", r.URL.Query().Get("requestId"))
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
_, _ = io.WriteString(w, `{"state":"pending"}`)
}))
defer server.Close()

client, err := kubernetes.NewForConfig(&rest.Config{Host: server.URL, BearerToken: "test-token"})
if err != nil {
t.Fatal(err)
}

path := "/status/node/node-a/details"

var body []byte
if method == http.MethodPost {
body = []byte(`{"forceRefresh":true}`)
} else {
path += "?requestId=id%2F%2B+%3F"
}

raw, err := requestStatusViaAggregatedAPI(context.Background(), client, method, path, body)
if err != nil || string(raw) != `{"state":"pending"}` {
t.Fatalf("request = %s, %v", raw, err)
}
})
}
}

func TestStatusRequestPreservesHTTPFailure(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = io.WriteString(w, `{"state":"unavailable","error":"leadership changed"}`)
}))
defer server.Close()

client, err := kubernetes.NewForConfig(&rest.Config{Host: server.URL})
if err != nil {
t.Fatal(err)
}

raw, err := requestStatusViaAggregatedAPI(context.Background(), client, http.MethodGet, "/status/node/node-a/details", nil)
if err == nil || !strings.Contains(string(raw), "leadership changed") {
t.Fatalf("lost explicit controller failure: %s, %v", raw, err)
}

ctx, cancel := context.WithCancel(context.Background())
cancel()

if _, err := requestStatusViaAggregatedAPI(ctx, client, http.MethodGet, "/status/node/node-a/details", nil); err == nil {
t.Fatal("canceled request succeeded")
}

for _, path := range []string{"https://other.invalid/status", "://invalid"} {
if _, err := requestStatusViaAggregatedAPI(context.Background(), client, http.MethodGet, path, nil); err == nil {
t.Errorf("accepted invalid path %q", path)
}
}

request, err := newStatusRequest(newDetailTestRuntime(t, server.URL), defaultNodeStatusFetchOptions())
if err != nil {
t.Fatal(err)
}

raw, err = request(context.Background(), http.MethodGet, "/status/node/node-a/details", nil)
if err == nil || !strings.Contains(string(raw), "leadership changed") {
t.Fatalf("fallback hid controller failure: %s, %v", raw, err)
}
}
Loading