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
48 changes: 24 additions & 24 deletions _test/nexlet_inmem/inmemagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,34 +263,34 @@ func (a *InMemAgent) QueryWorkloads(namespace string, filter []string) (*models.
a.Workloads.RLock()
defer a.Workloads.RUnlock()

workloads, ok := a.Workloads.State[namespace]
if !ok {
a.Logger.Debug("namespace not found", slog.String("namespace", namespace))
return &models.AgentListWorkloadsResponse{}, nil
}

resp := models.AgentListWorkloadsResponse{}
for _, workload := range workloads {
resp = append(resp, models.WorkloadSummary{
Id: workload.id,
Name: workload.name,
Runtime: time.Since(workload.startTime).String(),
StartTime: workload.startTime.Format(time.RFC3339),
WorkloadType: a.WorkloadType,
WorkloadState: models.WorkloadStateRunning,
WorkloadLifecycle: "service",
Metadata: map[string]string{"extra": "metadata"},
})
}

if len(filter) > 0 {
filteredResp := models.AgentListWorkloadsResponse{}
for _, workload := range resp {
if slices.Contains(filter, workload.Id) {
filteredResp = append(filteredResp, workload)
// The system namespace is administrative: a query for it returns
// workloads across every namespace. Any other namespace only returns
// workloads that actually live under that map key. When filter is
// non-empty, only workloads whose id or name matches one of the
// filter entries are returned.
for mapNS, workloads := range a.Workloads.State {
if namespace != models.SystemNamespace && namespace != mapNS {
continue
}
workloadNS := mapNS
for _, workload := range workloads {
if len(filter) > 0 && !slices.Contains(filter, workload.id) && !slices.Contains(filter, workload.name) {
continue
}
resp = append(resp, models.WorkloadSummary{
Id: workload.id,
Namespace: &workloadNS,
Name: workload.name,
Runtime: time.Since(workload.startTime).String(),
StartTime: workload.startTime.Format(time.RFC3339),
WorkloadType: a.WorkloadType,
WorkloadState: models.WorkloadStateRunning,
WorkloadLifecycle: "service",
Metadata: map[string]string{"extra": "metadata"},
})
}
resp = filteredResp
}

a.Logger.Debug("QueryWorkloads successful", slog.String("namespace", namespace), slog.String("filter", strings.Join(filter, ",")))
Expand Down
2 changes: 1 addition & 1 deletion agents/native/native.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func (a *NativeAgent) GetWorkload(workloadId, targetXkey string) (*models.StartW
}

func (a *NativeAgent) QueryWorkloads(namespace string, filter []string) (*models.AgentListWorkloadsResponse, error) {
return a.state.GetNamespaceWorkloadList(namespace)
return a.state.GetNamespaceWorkloadList(namespace, filter)
}

func (a *NativeAgent) SetLameduck(before time.Duration) error {
Expand Down
47 changes: 30 additions & 17 deletions agents/native/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"log/slog"
"os"
"os/exec"
"slices"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -84,29 +85,41 @@ func (n *nexletState) WorkloadCount() int {
return total
}

func (n *nexletState) GetNamespaceWorkloadList(ns string) (*models.AgentListWorkloadsResponse, error) {
func (n *nexletState) GetNamespaceWorkloadList(ns string, filter []string) (*models.AgentListWorkloadsResponse, error) {
n.Lock()
defer n.Unlock()

ret := new(models.AgentListWorkloadsResponse)
namespace, ok := n.workloads[ns]
if !ok || len(namespace) == 0 {
return ret, nil
}

for id, w := range namespace {
ws := models.WorkloadSummary{
Id: id,
Metadata: map[string]string{},
Name: w.StartRequest.Name,
Runtime: "--",
StartTime: w.StartedAt.Format(time.RFC3339),
WorkloadLifecycle: string(w.StartRequest.WorkloadLifecycle),
WorkloadState: w.GetState(),
WorkloadType: NEXLET_REGISTER_TYPE,
Tags: w.StartRequest.Tags,
// The system namespace is administrative: a query for it returns
// workloads across every namespace. Any other namespace only returns
// workloads that actually live under that map key.
//
// When filter is non-empty, only workloads whose id or name matches
// one of the filter entries are returned.
for mapNS, processes := range n.workloads {
if ns != models.SystemNamespace && ns != mapNS {
continue
}
workloadNS := mapNS
for id, w := range processes {
if len(filter) > 0 && !slices.Contains(filter, id) && !slices.Contains(filter, w.StartRequest.Name) {
continue
}
ws := models.WorkloadSummary{
Id: id,
Namespace: &workloadNS,
Metadata: map[string]string{},
Name: w.StartRequest.Name,
Runtime: "--",
StartTime: w.StartedAt.Format(time.RFC3339),
WorkloadLifecycle: string(w.StartRequest.WorkloadLifecycle),
WorkloadState: w.GetState(),
WorkloadType: NEXLET_REGISTER_TYPE,
Tags: w.StartRequest.Tags,
}
*ret = append(*ret, ws)
}
*ret = append(*ret, ws)
}

return ret, nil
Expand Down
110 changes: 109 additions & 1 deletion agents/native/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func TestNexletState(t *testing.T) {
_, ok = ns.Exists("noexist")
be.False(t, ok)

list, err := ns.GetNamespaceWorkloadList("derp")
list, err := ns.GetNamespaceWorkloadList("derp", nil)
be.NilErr(t, err)
be.Equal(t, 1, len(*list))

Expand Down Expand Up @@ -193,3 +193,111 @@ func TestAddWorkloadWithInvalidUri(t *testing.T) {
_, ok := ns.Exists(workloadID)
be.False(t, ok)
}

// TestGetNamespaceWorkloadListSystemAndFilter exercises GetNamespaceWorkloadList
// without spawning real processes. It verifies three things:
// 1. The system namespace is administrative and returns workloads across
// every stored namespace, while a user namespace returns only its own.
// 2. The Namespace field on each WorkloadSummary is populated with the
// workload's actual owning namespace (sourced from the state map key).
// 3. The filter argument matches against both workload id and workload
// name; an empty filter returns everything.
func TestGetNamespaceWorkloadListSystemAndFilter(t *testing.T) {
ns := nexletState{
Mutex: sync.Mutex{},
ctx: context.Background(),
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
status: models.AgentStateRunning,
workloads: map[string]NativeProcesses{},
}

// Seed three workloads across two user namespaces directly via the
// state map. AddWorkload spawns a real process, which is unnecessary
// for a pure listing test.
seed := func(namespace, id, name string) {
if _, ok := ns.workloads[namespace]; !ok {
ns.workloads[namespace] = make(NativeProcesses)
}
ns.workloads[namespace][id] = &NativeProcess{
Name: name,
StartRequest: models.StartWorkloadRequest{
Name: name,
Namespace: namespace,
WorkloadLifecycle: "service",
},
StartedAt: time.Now(),
State: models.WorkloadStateRunning,
}
}
seed("alpha", "id-a1", "alpha-one")
seed("alpha", "id-a2", "alpha-two")
seed("beta", "id-b1", "beta-one")

namespaceOf := func(ws models.WorkloadSummary) string {
t.Helper()
if ws.Namespace == nil {
t.Fatalf("workload %q is missing Namespace field", ws.Id)
}
return *ws.Namespace
}

t.Run("user namespace returns only its own workloads", func(t *testing.T) {
list, err := ns.GetNamespaceWorkloadList("alpha", nil)
be.NilErr(t, err)
be.Equal(t, 2, len(*list))
for _, ws := range *list {
be.Equal(t, "alpha", namespaceOf(ws))
}
})

t.Run("unknown namespace returns empty", func(t *testing.T) {
list, err := ns.GetNamespaceWorkloadList("gamma", nil)
be.NilErr(t, err)
be.Equal(t, 0, len(*list))
})

t.Run("system namespace returns workloads from every namespace", func(t *testing.T) {
list, err := ns.GetNamespaceWorkloadList(models.SystemNamespace, nil)
be.NilErr(t, err)
be.Equal(t, 3, len(*list))

seen := map[string]string{}
for _, ws := range *list {
seen[ws.Id] = namespaceOf(ws)
}
be.Equal(t, "alpha", seen["id-a1"])
be.Equal(t, "alpha", seen["id-a2"])
be.Equal(t, "beta", seen["id-b1"])
})

t.Run("filter by id matches across system list", func(t *testing.T) {
list, err := ns.GetNamespaceWorkloadList(models.SystemNamespace, []string{"id-a2"})
be.NilErr(t, err)
be.Equal(t, 1, len(*list))
be.Equal(t, "id-a2", (*list)[0].Id)
be.Equal(t, "alpha", namespaceOf((*list)[0]))
})

t.Run("filter by name matches across system list", func(t *testing.T) {
list, err := ns.GetNamespaceWorkloadList(models.SystemNamespace, []string{"beta-one"})
be.NilErr(t, err)
be.Equal(t, 1, len(*list))
be.Equal(t, "id-b1", (*list)[0].Id)
be.Equal(t, "beta", namespaceOf((*list)[0]))
})

t.Run("filter mixes id and name and is scoped by namespace", func(t *testing.T) {
list, err := ns.GetNamespaceWorkloadList("alpha", []string{"id-a1", "alpha-two", "beta-one"})
be.NilErr(t, err)
// beta-one is filtered out because the query is scoped to "alpha",
// not system; id-a1 matches by id, alpha-two matches by name.
be.Equal(t, 2, len(*list))
ids := map[string]bool{}
for _, ws := range *list {
ids[ws.Id] = true
be.Equal(t, "alpha", namespaceOf(ws))
}
be.True(t, ids["id-a1"])
be.True(t, ids["id-a2"])
})
}
28 changes: 24 additions & 4 deletions cmd/nex/workloads.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,15 @@ func (r *ListWorkload) Run(ctx context.Context, globals *Globals) error {
tW.Style().Title.Align = text.AlignCenter
tW.Style().Format.Header = text.FormatDefault
tW.SetTitle("Running Workloads - " + globals.Namespace)
if r.ShowMetadata {
showNamespace := globals.Namespace == models.SystemNamespace
switch {
case showNamespace && r.ShowMetadata:
tW.AppendHeader(table.Row{"Id", "Name", "Namespace", "Start Time", "Execution Time", "Type", "Lifecycle", "State", "Metadata", "Tags"})
case showNamespace:
tW.AppendHeader(table.Row{"Id", "Name", "Namespace", "Start Time", "Execution Time", "Type", "Lifecycle", "State"})
case r.ShowMetadata:
tW.AppendHeader(table.Row{"Id", "Name", "Start Time", "Execution Time", "Type", "Lifecycle", "State", "Metadata", "Tags"})
} else {
default:
tW.AppendHeader(table.Row{"Id", "Name", "Start Time", "Execution Time", "Type", "Lifecycle", "State"})
}
for _, agentResponse := range resp {
Expand All @@ -278,16 +284,30 @@ func (r *ListWorkload) Run(ctx context.Context, globals *Globals) error {
rt = "--"
}

var meta string
if r.ShowMetadata {
meta := "--"
meta = "--"
if workload.Metadata != nil {
metaB, err := json.Marshal(workload.Metadata)
if err == nil {
meta = string(metaB)
}
}
}

wlNS := "--"
if workload.Namespace != nil {
wlNS = *workload.Namespace
}

switch {
case showNamespace && r.ShowMetadata:
tW.AppendRow(table.Row{workload.Id, workload.Name, wlNS, workload.StartTime, rt, workload.WorkloadType, workload.WorkloadLifecycle, workload.WorkloadState, meta, workload.Tags})
case showNamespace:
tW.AppendRow(table.Row{workload.Id, workload.Name, wlNS, workload.StartTime, rt, workload.WorkloadType, workload.WorkloadLifecycle, workload.WorkloadState})
case r.ShowMetadata:
tW.AppendRow(table.Row{workload.Id, workload.Name, workload.StartTime, rt, workload.WorkloadType, workload.WorkloadLifecycle, workload.WorkloadState, meta, workload.Tags})
} else {
default:
tW.AppendRow(table.Row{workload.Id, workload.Name, workload.StartTime, rt, workload.WorkloadType, workload.WorkloadLifecycle, workload.WorkloadState})
}
workloads++
Expand Down
3 changes: 3 additions & 0 deletions models/api_shared.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions models/schema/shared-workload-summary.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
"type": "string",
"description": "The unique identifier of the workload"
},
"namespace": {
"type": "string",
"description": "The namespace the workload belongs to"
},
"name": {
"type": "string",
"description": "The name of the workload"
Expand Down
Loading