From a2256ef6de91cc503daac9fa8a7be890edf9cf15 Mon Sep 17 00:00:00 2001 From: Omkar Ugalmugle Date: Sat, 11 Jul 2026 13:30:58 +0000 Subject: [PATCH 1/3] resource/cmd: support comparison operators --- go.mod | 2 + go.sum | 2 - internal/resource/cmd/cmd.go | 173 ++++++++++++++++++++++++++--------- 3 files changed, 133 insertions(+), 44 deletions(-) diff --git a/go.mod b/go.mod index 78ec1689..ec4d5c47 100644 --- a/go.mod +++ b/go.mod @@ -199,3 +199,5 @@ replace charm.land/lipgloss/v2 => github.com/jedevc/lipgloss/v2 v2.0.0-202603221 // 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 + +replace unikraft.com/x/filters => ../x/filters diff --git a/go.sum b/go.sum index 40b04a65..9809f28b 100644 --- a/go.sum +++ b/go.sum @@ -489,8 +489,6 @@ unikraft.com/cloud/sdk v0.0.0-20260610162742-d9f288f10c67 h1:T/hIAHzgEqQmBLkO0l9 unikraft.com/cloud/sdk v0.0.0-20260610162742-d9f288f10c67/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= -unikraft.com/x/filters v0.0.0-20260416164455-ec39ae908f3f/go.mod h1:m4Qdsw8FQThJcu8g+XTEdcmpN+blf/jBuNRI81juz1M= 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-20260304162956-523940cab1de h1:1xafSiBA1yfMvhnM3q1baUliqkV6wkE1AXxiOSUkJSA= diff --git a/internal/resource/cmd/cmd.go b/internal/resource/cmd/cmd.go index 044e6fb7..0201f8d2 100644 --- a/internal/resource/cmd/cmd.go +++ b/internal/resource/cmd/cmd.go @@ -28,6 +28,7 @@ import ( "unikraft.com/cli/internal/config" "unikraft.com/cli/internal/multimetro" "unikraft.com/cli/internal/resource" + "unikraft.com/cli/internal/resource/value" "unikraft.com/cli/internal/resource/patch" "unikraft.com/cli/internal/tui/watcher" xkong "unikraft.com/cli/internal/x/kong" @@ -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. From a25e455cc868c728c7e6f1c0185b0ae2c5674507 Mon Sep 17 00:00:00 2001 From: Omkar Ugalmugle Date: Sat, 11 Jul 2026 13:38:39 +0000 Subject: [PATCH 2/3] gofmt --- internal/resource/cmd/cmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/resource/cmd/cmd.go b/internal/resource/cmd/cmd.go index 0201f8d2..30e0e420 100644 --- a/internal/resource/cmd/cmd.go +++ b/internal/resource/cmd/cmd.go @@ -28,8 +28,8 @@ import ( "unikraft.com/cli/internal/config" "unikraft.com/cli/internal/multimetro" "unikraft.com/cli/internal/resource" - "unikraft.com/cli/internal/resource/value" "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" From 345a785d2ddb8573f898c60a6e38dd5494ead549 Mon Sep 17 00:00:00 2001 From: Omkar Ugalmugle Date: Sat, 11 Jul 2026 14:23:17 +0000 Subject: [PATCH 3/3] cleanup --- go.mod | 14 +++----------- go.sum | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index ec4d5c47..0834fe6b 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 @@ -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 @@ -193,11 +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 - -replace unikraft.com/x/filters => ../x/filters diff --git a/go.sum b/go.sum index 9809f28b..a8494215 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= @@ -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= @@ -485,10 +485,12 @@ 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= +unikraft.com/x/filters v0.0.0-20260416164455-ec39ae908f3f/go.mod h1:m4Qdsw8FQThJcu8g+XTEdcmpN+blf/jBuNRI81juz1M= 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-20260304162956-523940cab1de h1:1xafSiBA1yfMvhnM3q1baUliqkV6wkE1AXxiOSUkJSA=