Skip to content

Commit e8e7f99

Browse files
authored
fix(sync,list): preserve folder membership and list across namespaces (#75)
* fix(sync): preserve peer and mesh folder membership * fix(list): default to all namespaces for owner * test: cover pytorchjob pvc subpath workspace sync * test: handle syncthing ignores in two-phase tests * test: stabilize pytorchjob sync verification * fix(workload): mirror workspace subpath on sidecar
1 parent 69413a2 commit e8e7f99

8 files changed

Lines changed: 848 additions & 52 deletions

File tree

internal/cli/list.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ func newListCmd(opts *Options) *cobra.Command {
1818
cmd := &cobra.Command{
1919
Use: "list",
2020
Short: "List dev sessions",
21-
Example: ` # List your sessions in the current namespace
21+
Example: ` # List your sessions across all namespaces
2222
okdev list
2323
24-
# List across all namespaces
25-
okdev list --all-namespaces
24+
# Narrow to a specific namespace
25+
okdev list --namespace proj-tango
2626
2727
# List all users' sessions
2828
okdev list --all-users
@@ -31,6 +31,8 @@ func newListCmd(opts *Options) *cobra.Command {
3131
okdev list --output json`,
3232
RunE: func(cmd *cobra.Command, args []string) error {
3333
cc := &commandContext{opts: opts}
34+
explicitNamespace := strings.TrimSpace(opts.Namespace) != ""
35+
effectiveAllNamespaces := allNamespaces || !explicitNamespace
3436
ns := opts.Namespace
3537
activeSession, activeErr := session.LoadActiveSession()
3638
if activeErr != nil {
@@ -57,7 +59,7 @@ func newListCmd(opts *Options) *cobra.Command {
5759
if !allUsers {
5860
label = label + "," + ownerLabelSelector(opts)
5961
}
60-
pods, err := cc.kube.ListPods(ctx, cc.namespace, allNamespaces, label)
62+
pods, err := cc.kube.ListPods(ctx, cc.namespace, effectiveAllNamespaces, label)
6163
if err != nil {
6264
return err
6365
}

internal/cli/mesh.go

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,11 @@ func configureSyncthingMeshHub(ctx context.Context, base, key, hubDeviceID strin
290290
fm["path"] = folderPath
291291
fm["type"] = "sendreceive"
292292
fm["markerName"] = "."
293-
fm["devices"] = folderDevices
293+
mergedDevices, err := syncthingMergeMeshHubFolderDevices(fm["devices"], devices, hubDeviceID, receiverIDs)
294+
if err != nil {
295+
return err
296+
}
297+
fm["devices"] = mergedDevices
294298
applyManagedSyncthingFolderDefaults(fm, 60, 1, false)
295299
filteredFolders = append(filteredFolders, fm)
296300
foundFolder = true
@@ -315,6 +319,67 @@ func configureSyncthingMeshHub(ctx context.Context, base, key, hubDeviceID strin
315319
return syncthingSetConfig(ctx, base, key, cfg)
316320
}
317321

322+
func syncthingMergeMeshHubFolderDevices(existingFolderDevices, devices any, hubDeviceID string, receiverIDs []string) ([]any, error) {
323+
deviceEntries, err := syncthingObjectArray(map[string]any{"devices": devices}, "devices")
324+
if err != nil {
325+
return nil, err
326+
}
327+
deviceNames := make(map[string]string, len(deviceEntries))
328+
for _, d := range deviceEntries {
329+
m, err := syncthingObjectMap(d, "devices")
330+
if err != nil {
331+
return nil, err
332+
}
333+
deviceNames[asString(m["deviceID"])] = asString(m["name"])
334+
}
335+
336+
merged := make([]any, 0, 1+len(receiverIDs)+1)
337+
merged = append(merged, map[string]any{"deviceID": hubDeviceID})
338+
receiverSet := make(map[string]struct{}, len(receiverIDs))
339+
for _, id := range receiverIDs {
340+
receiverSet[id] = struct{}{}
341+
}
342+
343+
folderEntries, ok := existingFolderDevices.([]any)
344+
if ok {
345+
seen := map[string]struct{}{hubDeviceID: {}}
346+
for _, d := range folderEntries {
347+
m, err := syncthingObjectMap(d, "folder devices")
348+
if err != nil {
349+
return nil, err
350+
}
351+
id := asString(m["deviceID"])
352+
if id == "" {
353+
continue
354+
}
355+
if _, exists := seen[id]; exists {
356+
continue
357+
}
358+
if _, isReceiver := receiverSet[id]; isReceiver {
359+
continue
360+
}
361+
if deviceNames[id] == "okdev-mesh-receiver" {
362+
continue
363+
}
364+
seen[id] = struct{}{}
365+
merged = append(merged, map[string]any{"deviceID": id})
366+
}
367+
for _, id := range receiverIDs {
368+
if _, exists := seen[id]; exists {
369+
continue
370+
}
371+
seen[id] = struct{}{}
372+
merged = append(merged, map[string]any{"deviceID": id})
373+
}
374+
return merged, nil
375+
}
376+
377+
for _, id := range receiverIDs {
378+
merged = append(merged, map[string]any{"deviceID": id})
379+
}
380+
return merged, nil
381+
}
382+
318383
// configureAndWaitMeshReceiver configures a single receiver pod's syncthing
319384
// to peer with the hub and waits for initial sync to complete.
320385
func configureAndWaitMeshReceiver(ctx context.Context, opts *Options, k *kube.Client, namespace string, pod kube.PodSummary, recvKey, recvDeviceID, hubBase, hubKey, hubDeviceID, hubAddr, folderID, workspaceMountPath string, timeout time.Duration) meshReceiverStatus {

internal/cli/status_list_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,75 @@ func TestNewListCmdOutputsJSON(t *testing.T) {
8080
}
8181
}
8282

83+
func TestNewListCmdDefaultsToAllNamespacesForCurrentOwner(t *testing.T) {
84+
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
85+
w.Header().Set("Content-Type", "application/json")
86+
switch r.URL.Path {
87+
case "/api/v1/pods":
88+
_, _ = io.WriteString(w, `{"kind":"PodList","apiVersion":"v1","items":[{"metadata":{"namespace":"proj-tango","name":"okdev-sess-a","creationTimestamp":"2026-03-29T00:00:00Z","labels":{"okdev.io/session":"sess-a","okdev.io/owner":"alice","okdev.io/workload-type":"pod"}},"status":{"phase":"Running","containerStatuses":[{"name":"dev","ready":true,"restartCount":1}]}}]}`)
89+
case "/api/v1/namespaces/default/pods":
90+
_, _ = io.WriteString(w, `{"kind":"PodList","apiVersion":"v1","items":[]}`)
91+
default:
92+
http.NotFound(w, r)
93+
}
94+
}))
95+
defer server.Close()
96+
97+
t.Setenv("KUBECONFIG", writeCLITLSTestKubeconfig(t, server))
98+
cfgPath := writeCLIConfig(t, "default")
99+
opts := &Options{ConfigPath: cfgPath, Context: "dev", Output: "json", Owner: "alice"}
100+
cmd := newListCmd(opts)
101+
var out bytes.Buffer
102+
cmd.SetOut(&out)
103+
cmd.SetErr(io.Discard)
104+
105+
if err := cmd.Execute(); err != nil {
106+
t.Fatalf("list execute: %v", err)
107+
}
108+
109+
var rows []map[string]any
110+
if err := json.Unmarshal(out.Bytes(), &rows); err != nil {
111+
t.Fatalf("json unmarshal: %v\n%s", err, out.String())
112+
}
113+
if len(rows) != 1 || rows[0]["session"] != "sess-a" || rows[0]["namespace"] != "proj-tango" {
114+
t.Fatalf("unexpected list rows: %#v", rows)
115+
}
116+
}
117+
118+
func TestNewListCmdNamespaceOverrideNarrowsResults(t *testing.T) {
119+
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
120+
w.Header().Set("Content-Type", "application/json")
121+
switch r.URL.Path {
122+
case "/api/v1/namespaces/demo/pods":
123+
_, _ = io.WriteString(w, `{"kind":"PodList","apiVersion":"v1","items":[{"metadata":{"namespace":"demo","name":"okdev-sess-a","creationTimestamp":"2026-03-29T00:00:00Z","labels":{"okdev.io/session":"sess-a","okdev.io/owner":"alice","okdev.io/workload-type":"pod"}},"status":{"phase":"Running","containerStatuses":[{"name":"dev","ready":true,"restartCount":1}]}}]}`)
124+
case "/api/v1/pods":
125+
_, _ = io.WriteString(w, `{"kind":"PodList","apiVersion":"v1","items":[{"metadata":{"namespace":"proj-tango","name":"okdev-sess-b","creationTimestamp":"2026-03-29T00:00:00Z","labels":{"okdev.io/session":"sess-b","okdev.io/owner":"alice","okdev.io/workload-type":"pod"}},"status":{"phase":"Running","containerStatuses":[{"name":"dev","ready":true,"restartCount":1}]}}]}`)
126+
default:
127+
http.NotFound(w, r)
128+
}
129+
}))
130+
defer server.Close()
131+
132+
t.Setenv("KUBECONFIG", writeCLITLSTestKubeconfig(t, server))
133+
opts := &Options{Namespace: "demo", Context: "dev", Output: "json", Owner: "alice"}
134+
cmd := newListCmd(opts)
135+
var out bytes.Buffer
136+
cmd.SetOut(&out)
137+
cmd.SetErr(io.Discard)
138+
139+
if err := cmd.Execute(); err != nil {
140+
t.Fatalf("list execute: %v", err)
141+
}
142+
143+
var rows []map[string]any
144+
if err := json.Unmarshal(out.Bytes(), &rows); err != nil {
145+
t.Fatalf("json unmarshal: %v\n%s", err, out.String())
146+
}
147+
if len(rows) != 1 || rows[0]["session"] != "sess-a" || rows[0]["namespace"] != "demo" {
148+
t.Fatalf("unexpected list rows: %#v", rows)
149+
}
150+
}
151+
83152
func TestNewStatusCmdDetailsRequiresSingleSession(t *testing.T) {
84153
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
85154
w.Header().Set("Content-Type", "application/json")

0 commit comments

Comments
 (0)