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
12 changes: 3 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.26.2
require (
charm.land/bubbles/v2 v2.1.0
charm.land/bubbletea/v2 v2.0.6
charm.land/lipgloss/v2 v2.0.2
charm.land/lipgloss/v2 v2.0.4
github.com/MakeNowJust/heredoc v1.0.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/alecthomas/kong v1.15.0
Expand Down Expand Up @@ -49,7 +49,7 @@ require (
gotest.tools/v3 v3.5.2
mvdan.cc/sh/v3 v3.13.1
sigs.k8s.io/yaml v1.6.0
unikraft.com/cloud/sdk v0.0.0-20260610162742-d9f288f10c67
unikraft.com/cloud/sdk v0.0.0-20260703085859-98d4365c023c
unikraft.com/x/colors v0.0.0-20260313145522-d793c36d706e
unikraft.com/x/filters v0.0.0-20260416164455-ec39ae908f3f
unikraft.com/x/fingerprint v0.0.0-20260126094137-ab6e717e5679
Expand Down Expand Up @@ -182,7 +182,7 @@ require (
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
Expand All @@ -193,9 +193,3 @@ require (
)

tool github.com/caarlos0/svu/v3

// HACK: hot-fix for https://github.com/charmbracelet/lipgloss/pull/636
replace charm.land/lipgloss/v2 => github.com/jedevc/lipgloss/v2 v2.0.0-20260322175907-46dd1f11f91e

// HACK: pin to rc.2 due to bug in rc.3, see https://github.com/moby/buildkit/pull/6616
replace github.com/containerd/platforms => github.com/containerd/platforms v1.0.0-rc.2
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ charm.land/bubbletea/v2 v2.0.6 h1:UHN/91OyuhaOFGSrBXQ/hMZD8IO1Uc4BvHlgHXL2WJo=
charm.land/bubbletea/v2 v2.0.6/go.mod h1:MH/D8ZLlN3op37vQvijKuU29g3rqTp+aQapURFonF9g=
charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY=
charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII=
charm.land/lipgloss/v2 v2.0.4 h1:lcPeVtcp23SNra7lHy8iYE4UC2aIipVQ47sbGyyxR5Q=
charm.land/lipgloss/v2 v2.0.4/go.mod h1:0653x8epbZSzdDfO/XPS1a/uYPOBeSsCssOpJOqDzik=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
Expand Down Expand Up @@ -90,8 +92,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/nydus-snapshotter v0.15.13 h1:z9yCiTPMxVBIZlHxOPinZXhly2MdcIqxk9VXPlHIOJY=
github.com/containerd/nydus-snapshotter v0.15.13/go.mod h1:t95dwCb4I0RE4n1iOk0sJCWosNoACA8daOXmU5A2VHI=
github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4=
github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4=
github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U=
github.com/containerd/plugin v1.1.0/go.mod h1:qBTum+A8lJ6lO44A19Eo7y1OlcLj4OWFH1DA/vnHmcc=
github.com/containerd/stargz-snapshotter v0.18.2 h1:Ev/sxfQUjwzJQ9eqy3XzttcQ3osMIqkQgMYlcET+10M=
Expand Down Expand Up @@ -189,8 +191,6 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jedevc/lipgloss/v2 v2.0.0-20260322175907-46dd1f11f91e h1:12fN0ZfFtzWaWgJjYPYR3FaFTCpNAzkhKpFMoxRGwEg=
github.com/jedevc/lipgloss/v2 v2.0.0-20260322175907-46dd1f11f91e/go.mod h1:MkdWTl3o51ESV1QkIH6L20davW9X3rp0EWVqM7gzKv8=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jotaen/kong-completion v0.0.14 h1:R/SZOwnr/an8xDwo1T4gLygyeF6yNqCL1i/tqm6FmZc=
github.com/jotaen/kong-completion v0.0.14/go.mod h1:dyIG20e3qq128SUBtF8jzI7YtkfzjWMlgbqkAJd6xHQ=
Expand Down Expand Up @@ -439,8 +439,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down Expand Up @@ -485,8 +485,8 @@ sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
tailscale.com v1.94.1 h1:0dAst/ozTuFkgmxZULc3oNwR9+qPIt5ucvzH7kaM0Jw=
tailscale.com v1.94.1/go.mod h1:gLnVrEOP32GWvroaAHHGhjSGMPJ1i4DvqNwEg+Yuov4=
unikraft.com/cloud/sdk v0.0.0-20260610162742-d9f288f10c67 h1:T/hIAHzgEqQmBLkO0l9YC5GX8Sixpzc5HYklKxbdNBM=
unikraft.com/cloud/sdk v0.0.0-20260610162742-d9f288f10c67/go.mod h1:XM2/ocAKjvRqXN5oXoKQAI/bIdu452d9q9bdtehQcao=
unikraft.com/cloud/sdk v0.0.0-20260703085859-98d4365c023c h1:TLzc2cPuTVCPCb3qoJ/kzNkPRm0WBZJ6MGCHDIbYJYM=
unikraft.com/cloud/sdk v0.0.0-20260703085859-98d4365c023c/go.mod h1:XM2/ocAKjvRqXN5oXoKQAI/bIdu452d9q9bdtehQcao=
unikraft.com/x/colors v0.0.0-20260313145522-d793c36d706e h1:C/V6l4ut5XpcVTN5CvnskRv6NHDbyIeLdgFVLEJ9BIE=
unikraft.com/x/colors v0.0.0-20260313145522-d793c36d706e/go.mod h1:SVlAGfyQ7MwJom7m9M2w83+TrO+nJoiLxeduJAxagEo=
unikraft.com/x/filters v0.0.0-20260416164455-ec39ae908f3f h1:v6pitpzsBnOjyzDIW0/YAEHHSI6cNOw4QOwQV0uD+dc=
Expand Down
173 changes: 131 additions & 42 deletions internal/resource/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ 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 @@ -413,56 +414,144 @@ func filterResources(ctx context.Context, resources []resource.Resource, filter

// 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
}

// fieldAdaptor implements filters.Adaptor over a resource's fields, using
// resource.GetFieldByPath to resolve field paths, and exposing raw typed
// values so comparison operators (>, <, >=, <=) can work against native
// types (e.g. memory>128mib, created_at>2026-01-01).
type fieldAdaptor struct {
fields []resource.Field
prefix []string
}

// newFieldAdaptor creates a filters.Adaptor that can traverse resource fields.
func newFieldAdaptor(fields []resource.Field) filters.Adaptor {
return &fieldAdaptor{fields: fields}
}

func (a *fieldAdaptor) fullPath(key []string) []string {
full := make([]string, 0, len(a.prefix)+len(key))
full = append(full, a.prefix...)
full = append(full, key...)
return full
}

func (a *fieldAdaptor) resolve(path []string) (field *resource.Field, entries []string, sliceVal string, sliceOK bool, ok bool) {
matched := resource.GetFieldByPath(a.fields, path)
if len(matched) == 0 {
if len(path) >= 2 {
parentMatched := resource.GetFieldByPath(a.fields, path[:len(path)-1])
if len(parentMatched) == 1 {
if slice, sok := getSliceValue(parentMatched[0].Value); sok {
idx, err := strconv.Atoi(path[len(path)-1])
if err == nil && idx >= 0 && idx < len(slice) {
return nil, nil, slice[idx], true, 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
}
return nil, nil, "", false, false
}
if len(matched) == 1 {
f := matched[0]
if len(f.Subfields) > 0 {
names := make([]string, len(f.Subfields))
for i, sub := range f.Subfields {
names[i] = sub.Name
}
// 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
return nil, names, "", false, true
}
if slice, sok := getSliceValue(f.Value); sok {
names := make([]string, len(slice))
for i := range slice {
names[i] = strconv.Itoa(i)
}
// HACK: strip escape sequences from rendered output
out, _ := field.Render()
return ansi.Strip(out), nil, true
return nil, names, "", false, 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 &f, nil, "", false, true
}
names := make([]string, len(matched))
for i, f := range matched {
names[i] = f.Name
}
return nil, names, "", false, true
}

func (a *fieldAdaptor) Select(key []string) (filters.Adaptor, bool) {
full := a.fullPath(key)
if _, _, _, _, ok := a.resolve(full); !ok {
return nil, false
}
return &fieldAdaptor{fields: a.fields, prefix: full}, true
}

func (a *fieldAdaptor) String() string {
field, _, sliceVal, sliceOK, ok := a.resolve(a.prefix)
if !ok {
return ""
}
if sliceOK {
return sliceVal
}
if field == nil {
return ""
}
out, _ := field.Render()
return ansi.Strip(out)
}

func (a *fieldAdaptor) Value() any {
field, _, sliceVal, sliceOK, ok := a.resolve(a.prefix)
if !ok {
return nil
}
if sliceOK {
return sliceVal
}
if field == nil {
return nil
}
return field.Value
}

func (a *fieldAdaptor) Entries() []string {
_, entries, _, _, ok := a.resolve(a.prefix)
if !ok {
return nil
}
return entries
}

func (a *fieldAdaptor) Compare(other string) (int, bool) {
field, _, _, sliceOK, ok := a.resolve(a.prefix)
if !ok {
return 0, false
}
if sliceOK {
return 0, false
}
if field == nil || field.Value == nil {
return 0, false
}
if !hasOrdering(field.Value) {
return 0, false
}
parsed, err := value.ParseNew([]string{other}, field.Value)
if err != nil {
return 0, false
}
return value.Compare(field.Value, parsed), true
}

func hasOrdering(v any) bool {
rv := reflect.ValueOf(v)
for rv.Kind() == reflect.Pointer {
if rv.IsNil() {
return false
}
return "", entries, true
rv = rv.Elem()
}
return rv.Kind() != reflect.String
}

// getSliceValue extracts a []string from a field value if possible.
Expand Down