Skip to content
Open
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ require (
sigs.k8s.io/yaml v1.6.0
unikraft.com/cloud/sdk v0.3.1-0.20260730110715-92b5bae5afbe
unikraft.com/x/colors v0.0.0-20260710212606-b68ba398853d
unikraft.com/x/filters v0.0.0-20260416164455-ec39ae908f3f
unikraft.com/x/filters v0.0.0-20260804153219-d1b47a40e047
unikraft.com/x/fingerprint v0.0.0-20260126094137-ab6e717e5679
unikraft.com/x/guesstermwidth v0.0.0-20260708141520-0cbb0a92056b
unikraft.com/x/iata v0.0.0-20260713183529-fd34645687a0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -488,8 +488,8 @@ unikraft.com/cloud/sdk v0.3.1-0.20260730110715-92b5bae5afbe h1:oupb6urtot/mVqphO
unikraft.com/cloud/sdk v0.3.1-0.20260730110715-92b5bae5afbe/go.mod h1:m/TX4opCAcR4OFFP/X8TWAjtzzW0Z6FxuVYznw9UW2Q=
unikraft.com/x/colors v0.0.0-20260710212606-b68ba398853d h1:xNtt0WCZRm/WIr3F48AnYc7m41W6TqgvsqprDbXi4/A=
unikraft.com/x/colors v0.0.0-20260710212606-b68ba398853d/go.mod h1:4jHfTebx2ff1Gd5MbS/xMrGFKRQTuwvTdzmAzaJWlSM=
unikraft.com/x/filters v0.0.0-20260416164455-ec39ae908f3f h1:v6pitpzsBnOjyzDIW0/YAEHHSI6cNOw4QOwQV0uD+dc=
unikraft.com/x/filters v0.0.0-20260416164455-ec39ae908f3f/go.mod h1:m4Qdsw8FQThJcu8g+XTEdcmpN+blf/jBuNRI81juz1M=
unikraft.com/x/filters v0.0.0-20260804153219-d1b47a40e047 h1:ZeMtVwVhGtiEIwnHXbzp4NAtiZp2pCAhEro0qPN+Vgs=
unikraft.com/x/filters v0.0.0-20260804153219-d1b47a40e047/go.mod h1:deba9Bu2u3ibYXGbsO1QRxFPuXHQXoiuY6eFmwTbuLU=
unikraft.com/x/fingerprint v0.0.0-20260126094137-ab6e717e5679 h1:zdvJjNkjsriS8RM46FcdgcRoCh4EYM66PGjqVgi/ups=
unikraft.com/x/fingerprint v0.0.0-20260126094137-ab6e717e5679/go.mod h1:FP7uOxux/W5PKqSRQsR4tyjNuLq4Cfio7mc5QVH1kW8=
unikraft.com/x/guesstermwidth v0.0.0-20260708141520-0cbb0a92056b h1:/VC/nXdu6k0G+a9Lkd9P/sz1r1mrlh0rocNtr1M84V8=
Expand Down
82 changes: 0 additions & 82 deletions internal/resource/cmd/cmd.go
Comment thread
Omkar-Ugal marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,10 @@ import (
"fmt"
"io"
"os"
"reflect"
"slices"
"strconv"
"strings"
"time"

"github.com/charmbracelet/x/ansi"
"unikraft.com/x/filters"
"unikraft.com/x/kingkong"
"unikraft.com/x/log"
Expand All @@ -29,7 +26,6 @@ import (
"unikraft.com/cli/internal/multimetro"
"unikraft.com/cli/internal/resource"
"unikraft.com/cli/internal/resource/patch"
"unikraft.com/cli/internal/resource/value"
"unikraft.com/cli/internal/tui/watcher"
xkong "unikraft.com/cli/internal/x/kong"
"unikraft.com/cloud/sdk/platform/group"
Expand Down Expand Up @@ -412,84 +408,6 @@ func filterResources(ctx context.Context, resources []resource.Resource, filter
return filtered, rerr
}

// newFieldAdaptor creates a filters.Adaptor that can traverse resource fields.
// It handles both structured fields (with subfields) and slice values (like []string tags).
func newFieldAdaptor(fields []resource.Field) filters.AdapterFunc {
return func(key []string) (string, []string, bool) {
matched := resource.GetFieldByPath(fields, key)
if len(matched) == 0 {
// GetFieldByPath may not find a match if we're looking up an index
// in a slice value (e.g., ["tags", "0"]). Try to handle this case.
if len(key) >= 2 {
parentMatched := resource.GetFieldByPath(fields, key[:len(key)-1])
if len(parentMatched) == 1 {
if slice, ok := getSliceValue(parentMatched[0].Value); ok {
idx, err := strconv.Atoi(key[len(key)-1])
if err == nil && idx >= 0 && idx < len(slice) {
return slice[idx], nil, true
}
}
}
}
return "", nil, false
}
if len(matched) == 1 {
field := matched[0]
// If the field has subfields, return their names as entries
// This enables wildcard filtering (e.g., nested.*.value)
if len(field.Subfields) > 0 {
entries := make([]string, len(field.Subfields))
for i, sub := range field.Subfields {
entries[i] = sub.Name
}
return "", entries, true
}
// Check if the field's value is a slice (e.g., []string tags)
// If so, return indices as entries for wildcard support
if slice, ok := getSliceValue(field.Value); ok {
entries := make([]string, len(slice))
for i := range slice {
entries[i] = strconv.Itoa(i)
}
return "", entries, true
}
// HACK: strip escape sequences from rendered output
out, _ := field.Render(value.RenderOpts{})
return ansi.Strip(out), nil, true
}
// >1 fields = ambiguous match, return entries for wildcard support
entries := make([]string, len(matched))
for i, field := range matched {
entries[i] = field.Name
}
return "", entries, true
}
}

// getSliceValue extracts a []string from a field value if possible.
// It handles both []string and other slice types by converting to strings.
func getSliceValue(value any) ([]string, bool) {
if value == nil {
return nil, false
}
rv := reflect.ValueOf(value)
if rv.Kind() != reflect.Slice {
return nil, false
}
result := make([]string, rv.Len())
for i := range rv.Len() {
elem := rv.Index(i)
if s, ok := elem.Interface().(string); ok {
result[i] = s
} else if s, ok := elem.Interface().(fmt.Stringer); ok {
result[i] = s.String()
} else {
result[i] = fmt.Sprintf("%v", elem.Interface())
}
}
return result, true
}

type ResourceRemoveCmd[R resource.DeletableResource] struct {
Targets []string `arg:"" name:"target" completion-predictor:"resource-key-${name}" help:"Target ${names} to remove."`

Expand Down
157 changes: 153 additions & 4 deletions internal/resource/cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,16 @@ func setupTestEnv() *resourcet.TestEnv {
Hidden: "hidden-test1",
Invisible: "invisible-test1",
Settings: resourcet.TestSettings{
Foo: 42,
Bar: "hello",
Foo: 42,
Bar: "hello",
Score: 10.0,
Flag: true,
},
Authors: []resourcet.TestAuthor{
{Name: "Alice", Email: "alice@example.com"},
{Name: "Bob", Email: "bob@example.com"},
},
Tags: []string{"prod", "web"},
})
env.Add(resourcet.TestResource{
ID: "id-test2",
Expand All @@ -58,13 +61,15 @@ func setupTestEnv() *resourcet.TestEnv {
Hidden: "hidden-test2",
Invisible: "invisible-test2",
Settings: resourcet.TestSettings{
Foo: 7,
Bar: "world",
Foo: 7,
Bar: "world",
Score: 1.2,
},
Authors: []resourcet.TestAuthor{
{Name: "Charlie", Email: "charlie@example.com"},
{Name: "Dana", Email: "dana@example.com"},
},
Tags: []string{"staging"},
})
return env
}
Expand Down Expand Up @@ -2066,3 +2071,147 @@ func TestDeleteBulk(t *testing.T) {
assert.Contains(t, env.Store, "test2")
})
}

func TestFilterComparisonOperators(t *testing.T) {
env := setupTestEnv()
ctx := resourcet.WithTestEnv(context.Background(), env)
sandbox := &resource.Sandbox{}

runFilter := func(t *testing.T, filter string) (string, error) {
t.Helper()
var out bytes.Buffer
cmd := &ResourceListCmd[resourcet.TestResource]{
Filter: []string{filter},
FormatOpts: FormatOpts{
Output: Printer{Type: PrinterTypeQuiet},
},
}
err := cmd.Run(ctx, testStdio(&out), sandbox)
return out.String(), err
}

t.Run("equal", func(t *testing.T) {
output, err := runFilter(t, "settings.foo==7")
require.NoError(t, err)
assert.Contains(t, output, "test2")
assert.NotContains(t, output, "test1")
})

t.Run("greater", func(t *testing.T) {
output, err := runFilter(t, "settings.foo>7")
require.NoError(t, err)
assert.Contains(t, output, "test1")
assert.NotContains(t, output, "test2")
})
Comment thread
Omkar-Ugal marked this conversation as resolved.

t.Run("greater_equal", func(t *testing.T) {
output, err := runFilter(t, "settings.foo>=7")
require.NoError(t, err)
assert.Contains(t, output, "test1")
assert.Contains(t, output, "test2")
})

t.Run("less", func(t *testing.T) {
output, err := runFilter(t, "settings.foo<42")
require.NoError(t, err)
assert.NotContains(t, output, "test1")
assert.Contains(t, output, "test2")
})

t.Run("less_equal", func(t *testing.T) {
output, err := runFilter(t, "settings.foo<=7")
require.NoError(t, err)
assert.NotContains(t, output, "test1")
assert.Contains(t, output, "test2")
})

t.Run("float_field_vs_int_literal", func(t *testing.T) {
output, err := runFilter(t, "settings.score<5")
require.NoError(t, err)
assert.Contains(t, output, "test2")
assert.NotContains(t, output, "test1")
})

t.Run("float_field_greater_equal", func(t *testing.T) {
output, err := runFilter(t, "settings.score>=10")
require.NoError(t, err)
assert.Contains(t, output, "test1")
assert.NotContains(t, output, "test2")
})

t.Run("invalid_literal_no_match_no_error", func(t *testing.T) {
output, err := runFilter(t, "settings.foo>notanumber")
require.NoError(t, err)
assert.Empty(t, strings.TrimSpace(output))
})

t.Run("string_field_ordering_no_op", func(t *testing.T) {
output, err := runFilter(t, "state>pending")
require.NoError(t, err)
assert.Empty(t, strings.TrimSpace(output))
})

t.Run("string_field_ordering_disallowed_even_with_differing_values", func(t *testing.T) {
for _, filter := range []string{
"settings.bar>hello",
"settings.bar>=hello",
"settings.bar<world",
"settings.bar<=world",
} {
output, err := runFilter(t, filter)
require.NoError(t, err)
assert.Empty(t, strings.TrimSpace(output), "filter %q should not match any resource", filter)
}

output, err := runFilter(t, "settings.bar==hello")
require.NoError(t, err)
assert.Contains(t, output, "test1")
assert.NotContains(t, output, "test2")
})

t.Run("scalar_slice_indexed_equal", func(t *testing.T) {
output, err := runFilter(t, "tags.0==prod")
require.NoError(t, err)
assert.Contains(t, output, "test1")
assert.NotContains(t, output, "test2")
})

t.Run("scalar_slice_wildcard_equal", func(t *testing.T) {
output, err := runFilter(t, "tags.*==staging")
require.NoError(t, err)
assert.Contains(t, output, "test2")
assert.NotContains(t, output, "test1")
})

t.Run("scalar_slice_wildcard_not_equal", func(t *testing.T) {
output, err := runFilter(t, "tags.*!=prod")
require.NoError(t, err)
assert.Contains(t, output, "test2")
assert.NotContains(t, output, "test1")
})

t.Run("bool_field_ordering_disallowed", func(t *testing.T) {
for _, filter := range []string{
"settings.flag>false",
"settings.flag>=false",
"settings.flag<true",
"settings.flag<=true",
} {
output, err := runFilter(t, filter)
require.NoError(t, err)
assert.Empty(t, strings.TrimSpace(output), "filter %q should not match any resource", filter)
}

output, err := runFilter(t, "settings.flag==true")
require.NoError(t, err)
assert.Contains(t, output, "test1")
assert.NotContains(t, output, "test2")
})

t.Run("combined_with_equality", func(t *testing.T) {
output, err := runFilter(t, "state==pending,settings.foo>7")
require.NoError(t, err)
assert.Contains(t, output, "test1")
assert.NotContains(t, output, "test2")
})
}
Loading