Skip to content
Merged
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
10 changes: 6 additions & 4 deletions internal/cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ func newListCmd(opts *Options) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List dev sessions",
Example: ` # List your sessions in the current namespace
Example: ` # List your sessions across all namespaces
okdev list

# List across all namespaces
okdev list --all-namespaces
# Narrow to a specific namespace
okdev list --namespace proj-tango

# List all users' sessions
okdev list --all-users
Expand All @@ -31,6 +31,8 @@ func newListCmd(opts *Options) *cobra.Command {
okdev list --output json`,
RunE: func(cmd *cobra.Command, args []string) error {
cc := &commandContext{opts: opts}
explicitNamespace := strings.TrimSpace(opts.Namespace) != ""
effectiveAllNamespaces := allNamespaces || !explicitNamespace
ns := opts.Namespace
activeSession, activeErr := session.LoadActiveSession()
if activeErr != nil {
Expand All @@ -57,7 +59,7 @@ func newListCmd(opts *Options) *cobra.Command {
if !allUsers {
label = label + "," + ownerLabelSelector(opts)
}
pods, err := cc.kube.ListPods(ctx, cc.namespace, allNamespaces, label)
pods, err := cc.kube.ListPods(ctx, cc.namespace, effectiveAllNamespaces, label)
if err != nil {
return err
}
Expand Down
67 changes: 66 additions & 1 deletion internal/cli/mesh.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,11 @@ func configureSyncthingMeshHub(ctx context.Context, base, key, hubDeviceID strin
fm["path"] = folderPath
fm["type"] = "sendreceive"
fm["markerName"] = "."
fm["devices"] = folderDevices
mergedDevices, err := syncthingMergeMeshHubFolderDevices(fm["devices"], devices, hubDeviceID, receiverIDs)
if err != nil {
return err
}
fm["devices"] = mergedDevices
applyManagedSyncthingFolderDefaults(fm, 60, 1, false)
filteredFolders = append(filteredFolders, fm)
foundFolder = true
Expand All @@ -315,6 +319,67 @@ func configureSyncthingMeshHub(ctx context.Context, base, key, hubDeviceID strin
return syncthingSetConfig(ctx, base, key, cfg)
}

func syncthingMergeMeshHubFolderDevices(existingFolderDevices, devices any, hubDeviceID string, receiverIDs []string) ([]any, error) {
deviceEntries, err := syncthingObjectArray(map[string]any{"devices": devices}, "devices")
if err != nil {
return nil, err
}
deviceNames := make(map[string]string, len(deviceEntries))
for _, d := range deviceEntries {
m, err := syncthingObjectMap(d, "devices")
if err != nil {
return nil, err
}
deviceNames[asString(m["deviceID"])] = asString(m["name"])
}

merged := make([]any, 0, 1+len(receiverIDs)+1)
merged = append(merged, map[string]any{"deviceID": hubDeviceID})
receiverSet := make(map[string]struct{}, len(receiverIDs))
for _, id := range receiverIDs {
receiverSet[id] = struct{}{}
}

folderEntries, ok := existingFolderDevices.([]any)
if ok {
seen := map[string]struct{}{hubDeviceID: {}}
for _, d := range folderEntries {
m, err := syncthingObjectMap(d, "folder devices")
if err != nil {
return nil, err
}
id := asString(m["deviceID"])
if id == "" {
continue
}
if _, exists := seen[id]; exists {
continue
}
if _, isReceiver := receiverSet[id]; isReceiver {
continue
}
if deviceNames[id] == "okdev-mesh-receiver" {
continue
}
seen[id] = struct{}{}
merged = append(merged, map[string]any{"deviceID": id})
}
for _, id := range receiverIDs {
if _, exists := seen[id]; exists {
continue
}
seen[id] = struct{}{}
merged = append(merged, map[string]any{"deviceID": id})
}
return merged, nil
}

for _, id := range receiverIDs {
merged = append(merged, map[string]any{"deviceID": id})
}
return merged, nil
}

// configureAndWaitMeshReceiver configures a single receiver pod's syncthing
// to peer with the hub and waits for initial sync to complete.
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 {
Expand Down
69 changes: 69 additions & 0 deletions internal/cli/status_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,75 @@ func TestNewListCmdOutputsJSON(t *testing.T) {
}
}

func TestNewListCmdDefaultsToAllNamespacesForCurrentOwner(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/api/v1/pods":
_, _ = 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}]}}]}`)
case "/api/v1/namespaces/default/pods":
_, _ = io.WriteString(w, `{"kind":"PodList","apiVersion":"v1","items":[]}`)
default:
http.NotFound(w, r)
}
}))
defer server.Close()

t.Setenv("KUBECONFIG", writeCLITLSTestKubeconfig(t, server))
cfgPath := writeCLIConfig(t, "default")
opts := &Options{ConfigPath: cfgPath, Context: "dev", Output: "json", Owner: "alice"}
cmd := newListCmd(opts)
var out bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(io.Discard)

if err := cmd.Execute(); err != nil {
t.Fatalf("list execute: %v", err)
}

var rows []map[string]any
if err := json.Unmarshal(out.Bytes(), &rows); err != nil {
t.Fatalf("json unmarshal: %v\n%s", err, out.String())
}
if len(rows) != 1 || rows[0]["session"] != "sess-a" || rows[0]["namespace"] != "proj-tango" {
t.Fatalf("unexpected list rows: %#v", rows)
}
}

func TestNewListCmdNamespaceOverrideNarrowsResults(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/api/v1/namespaces/demo/pods":
_, _ = 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}]}}]}`)
case "/api/v1/pods":
_, _ = 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}]}}]}`)
default:
http.NotFound(w, r)
}
}))
defer server.Close()

t.Setenv("KUBECONFIG", writeCLITLSTestKubeconfig(t, server))
opts := &Options{Namespace: "demo", Context: "dev", Output: "json", Owner: "alice"}
cmd := newListCmd(opts)
var out bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(io.Discard)

if err := cmd.Execute(); err != nil {
t.Fatalf("list execute: %v", err)
}

var rows []map[string]any
if err := json.Unmarshal(out.Bytes(), &rows); err != nil {
t.Fatalf("json unmarshal: %v\n%s", err, out.String())
}
if len(rows) != 1 || rows[0]["session"] != "sess-a" || rows[0]["namespace"] != "demo" {
t.Fatalf("unexpected list rows: %#v", rows)
}
}

func TestNewStatusCmdDetailsRequiresSingleSession(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
Expand Down
Loading
Loading