From 63d2ede6c963caba6c87d9267a8f58760175ebd0 Mon Sep 17 00:00:00 2001 From: NucleoFusion Date: Sat, 7 Mar 2026 10:26:22 +0530 Subject: [PATCH 1/3] enhancement: Support and / or in query flags and doc gen for query Signed-off-by: NucleoFusion --- cmd/harbor/root/project/list.go | 27 +++++++++++------ pkg/utils/query.go | 51 +++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/cmd/harbor/root/project/list.go b/cmd/harbor/root/project/list.go index 825d4e9a6..5b55804bb 100644 --- a/cmd/harbor/root/project/list.go +++ b/cmd/harbor/root/project/list.go @@ -34,10 +34,14 @@ func ListProjectCommand() *cobra.Command { allProjects []*models.Project err error // For querying, opts.Q - fuzzy []string - match []string - ranges []string + fuzzy []string + match []string + ranges []string + and []string + or []string + validKeys = []string{"name", "project_id", "public", "creation_time", "owner_id"} ) + cmd := &cobra.Command{ Use: "list", Short: "List projects", @@ -72,9 +76,7 @@ func ListProjectCommand() *cobra.Command { } if len(fuzzy) != 0 || len(match) != 0 || len(ranges) != 0 { // Only Building Query if a param exists - q, qErr := utils.BuildQueryParam(fuzzy, match, ranges, - []string{"name", "project_id", "public", "creation_time", "owner_id"}, - ) + q, qErr := utils.BuildQueryParam(fuzzy, match, ranges, validKeys) if qErr != nil { return qErr } @@ -108,6 +110,15 @@ func ListProjectCommand() *cobra.Command { }, } + // Adding Query Description + var qDesc string + if cmd.Long != "" { + qDesc = "\n\n" + utils.GenerateQueryDocs(validKeys) + } else { + qDesc = utils.GenerateQueryDocs(validKeys) + } + cmd.Long += qDesc + flags := cmd.Flags() flags.StringVarP(&opts.Name, "name", "", "", "Name of the project") flags.Int64VarP(&opts.Page, "page", "", 1, "Page number") @@ -115,9 +126,7 @@ func ListProjectCommand() *cobra.Command { flags.BoolVarP(&private, "private", "", false, "Show only private projects") flags.BoolVarP(&public, "public", "", false, "Show only public projects") flags.StringVarP(&opts.Sort, "sort", "", "", "Sort the resource list in ascending or descending order") - flags.StringSliceVar(&fuzzy, "fuzzy", nil, "Fuzzy match filter (key=value)") - flags.StringSliceVar(&match, "match", nil, "exact match filter (key=value)") - flags.StringSliceVar(&ranges, "range", nil, "range filter (key=min~max)") + utils.SetQueryFlags(flags, &match, &fuzzy, &ranges, &and, &or) // Adds the 5 query flags return cmd } diff --git a/pkg/utils/query.go b/pkg/utils/query.go index 5dd57d454..32ff0e354 100644 --- a/pkg/utils/query.go +++ b/pkg/utils/query.go @@ -16,6 +16,8 @@ package utils import ( "fmt" "strings" + + "github.com/spf13/pflag" ) // Builds the `q` param for List API's @@ -73,6 +75,55 @@ func BuildQueryParam(fuzzy, match, ranges []string, validKeys []string) (string, return strings.Join(parts, ","), nil } +func GenerateQueryDocs(validKeys []string) string { + keys := strings.Join(validKeys, ", ") + + doc := fmt.Sprintf(` +Query Filters + +The following flags can be used to filter results. + +Supported query types: + + --exact key=value + Match an exact value. + + --fuzzy key=value + Perform a fuzzy match (partial match). + + --range key=min:max + Match values within a range. + + --all key=v1,v2 + Match resources that contain ALL specified values. + + --any key=v1,v2 + Match resources that contain ANY of the specified values. + +Examples: + + --exact project_id=12 + --fuzzy name=test + --range update_time=2024-01-01:2024-02-01 + --any tag=v1,v2 + --all label=prod,stable + +Valid keys for this command: + + %s +`, keys) + + return strings.TrimSpace(doc) +} + +func SetQueryFlags(f *pflag.FlagSet, match, fuzzy, ranges, and, or *[]string) { + f.StringSliceVar(fuzzy, "fuzzy", nil, "Fuzzy match filter (key=value)") + f.StringSliceVar(match, "match", nil, "exact match filter (key=value)") + f.StringSliceVar(ranges, "range", nil, "range filter (key=min~max)") + f.StringSliceVar(and, "all", nil, "match-all filter (key=v1,v2,v3)") + f.StringSliceVar(or, "any", nil, "match-any filter (key=v1,v2,v3)") +} + // Validates Key provided by user for ListFlags.Q func validateKey(key string, validKeys []string) error { found := false From 2e02fc8be844b210297d1401322135c5a341afda Mon Sep 17 00:00:00 2001 From: NucleoFusion Date: Sat, 7 Mar 2026 10:45:28 +0530 Subject: [PATCH 2/3] fix: spelling Signed-off-by: NucleoFusion --- pkg/utils/query.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/utils/query.go b/pkg/utils/query.go index 32ff0e354..a6c776362 100644 --- a/pkg/utils/query.go +++ b/pkg/utils/query.go @@ -85,7 +85,7 @@ The following flags can be used to filter results. Supported query types: - --exact key=value + --match key=value Match an exact value. --fuzzy key=value @@ -102,7 +102,7 @@ Supported query types: Examples: - --exact project_id=12 + --match project_id=12 --fuzzy name=test --range update_time=2024-01-01:2024-02-01 --any tag=v1,v2 From 56fd227987d266c65220a36a06c864824e565541 Mon Sep 17 00:00:00 2001 From: NucleoFusion Date: Wed, 1 Apr 2026 18:41:08 +0530 Subject: [PATCH 3/3] feat: adding doc-gen and query flags Signed-off-by: NucleoFusion --- cmd/harbor/root/labels/list.go | 22 ++++++--- cmd/harbor/root/project/list.go | 8 ++-- pkg/utils/query.go | 80 ++++++++++++++++++++++++++++----- 3 files changed, 90 insertions(+), 20 deletions(-) diff --git a/cmd/harbor/root/labels/list.go b/cmd/harbor/root/labels/list.go index 61cb3edbe..e8686c250 100644 --- a/cmd/harbor/root/labels/list.go +++ b/cmd/harbor/root/labels/list.go @@ -33,7 +33,12 @@ func ListLabelCommand() *cobra.Command { fuzzy []string match []string ranges []string + all []string + any []string + + validKeys = []string{"name", "id", "label_id", "creation_time", "owner_id", "color", "description"} ) + cmd := &cobra.Command{ Use: "list", Short: "list labels", @@ -65,9 +70,7 @@ func ListLabelCommand() *cobra.Command { } if len(fuzzy) != 0 || len(match) != 0 || len(ranges) != 0 { // Only Building Query if a param exists - q, qErr := utils.BuildQueryParam(fuzzy, match, ranges, - []string{"name", "id", "label_id", "creation_time", "owner_id", "color", "description"}, - ) + q, qErr := utils.BuildQueryParam(fuzzy, match, ranges, all, any, validKeys) if qErr != nil { return qErr } @@ -94,6 +97,15 @@ func ListLabelCommand() *cobra.Command { }, } + // Adding Query Description + var qDesc string + if cmd.Long != "" { + qDesc = "\n\n" + utils.GenerateQueryDocs(validKeys) + } else { + qDesc = utils.GenerateQueryDocs(validKeys) + } + cmd.Long += qDesc + flags := cmd.Flags() flags.Int64VarP(&opts.Page, "page", "", 1, "Page number") flags.Int64VarP(&opts.PageSize, "page-size", "", 20, "Size of per page") @@ -102,9 +114,7 @@ func ListLabelCommand() *cobra.Command { flags.Int64VarP(&opts.ProjectID, "project-id", "i", 0, "project ID when query project labels") flags.BoolVarP(&isGlobal, "global", "", false, "whether to list global or project scope labels. (default scope is global)") flags.StringVarP(&opts.Sort, "sort", "", "", "Sort the label list in ascending or descending order") - flags.StringSliceVar(&fuzzy, "fuzzy", nil, "Fuzzy match filter (key=value)") - flags.StringSliceVar(&match, "match", nil, "exact match filter (key=value)") - flags.StringSliceVar(&ranges, "range", nil, "range filter (key=min~max)") + utils.SetQueryFlags(flags, &match, &fuzzy, &ranges, &all, &any) return cmd } diff --git a/cmd/harbor/root/project/list.go b/cmd/harbor/root/project/list.go index 5b55804bb..103287e3f 100644 --- a/cmd/harbor/root/project/list.go +++ b/cmd/harbor/root/project/list.go @@ -37,8 +37,8 @@ func ListProjectCommand() *cobra.Command { fuzzy []string match []string ranges []string - and []string - or []string + all []string + any []string validKeys = []string{"name", "project_id", "public", "creation_time", "owner_id"} ) @@ -76,7 +76,7 @@ func ListProjectCommand() *cobra.Command { } if len(fuzzy) != 0 || len(match) != 0 || len(ranges) != 0 { // Only Building Query if a param exists - q, qErr := utils.BuildQueryParam(fuzzy, match, ranges, validKeys) + q, qErr := utils.BuildQueryParam(fuzzy, match, ranges, all, any, validKeys) if qErr != nil { return qErr } @@ -126,7 +126,7 @@ func ListProjectCommand() *cobra.Command { flags.BoolVarP(&private, "private", "", false, "Show only private projects") flags.BoolVarP(&public, "public", "", false, "Show only public projects") flags.StringVarP(&opts.Sort, "sort", "", "", "Sort the resource list in ascending or descending order") - utils.SetQueryFlags(flags, &match, &fuzzy, &ranges, &and, &or) // Adds the 5 query flags + utils.SetQueryFlags(flags, &match, &fuzzy, &ranges, &all, &any) // Adds the 5 query flags return cmd } diff --git a/pkg/utils/query.go b/pkg/utils/query.go index a6c776362..6c278929c 100644 --- a/pkg/utils/query.go +++ b/pkg/utils/query.go @@ -20,58 +20,118 @@ import ( "github.com/spf13/pflag" ) -// Builds the `q` param for List API's -func BuildQueryParam(fuzzy, match, ranges []string, validKeys []string) (string, error) { +// BuildQueryParam builds the `q` param for List API's +func BuildQueryParam(fuzzy, match, ranges, all, any []string, validKeys []string) (string, error) { var parts []string + m := map[string]bool{} // existence map for key mapping // Fuzzy for _, v := range fuzzy { kv := strings.Split(v, "=") if len(kv) != 2 { - return "", fmt.Errorf("invalid fuzzy arg: %s ", v) + return "", fmt.Errorf("invalid fuzzy arg: %s", v) } if err := validateKey(kv[0], validKeys); err != nil { return "", err } + // Checking if key already exists + if m[kv[0]] { + return "", fmt.Errorf("found duplicate key: %s", kv[0]) + } + + m[kv[0]] = true parts = append(parts, fmt.Sprintf("%s=~%s", kv[0], kv[1])) } - // Exact Match's + // Exact match for _, v := range match { kv := strings.Split(v, "=") if len(kv) != 2 { - return "", fmt.Errorf("invalid match arg: %s ", v) + return "", fmt.Errorf("invalid match arg: %s", v) } if err := validateKey(kv[0], validKeys); err != nil { return "", err } + // Checking if key already exists + if m[kv[0]] { + return "", fmt.Errorf("found duplicate key: %s", kv[0]) + } + + m[kv[0]] = true parts = append(parts, fmt.Sprintf("%s=%s", kv[0], kv[1])) } - // Ranges + // Range (min~max) for _, v := range ranges { kv := strings.Split(v, "=") if len(kv) != 2 { - return "", fmt.Errorf("invalid range arg: %s ", v) + return "", fmt.Errorf("invalid range arg: %s", v) } if err := validateKey(kv[0], validKeys); err != nil { return "", err } - // Validating that range is in format min~max + // Checking if key already exists + if m[kv[0]] { + return "", fmt.Errorf("found duplicate key: %s", kv[0]) + } + rng := strings.Split(kv[1], "~") if len(rng) != 2 { - return "", fmt.Errorf("invalid range arg: %s ", v) + return "", fmt.Errorf("invalid range arg: %s", v) } + m[kv[0]] = true parts = append(parts, fmt.Sprintf("%s=[%s~%s]", kv[0], rng[0], rng[1])) } + // All + for _, v := range all { + kv := strings.Split(v, "=") + if len(kv) != 2 { + return "", fmt.Errorf("invalid all arg: %s", v) + } + + if err := validateKey(kv[0], validKeys); err != nil { + return "", err + } + + // Checking if key already exists + if m[kv[0]] { + return "", fmt.Errorf("found duplicate key: %s", kv[0]) + } + + m[kv[0]] = true + vals := strings.Split(kv[1], ",") // Splitting and replacing "," with " ", Harbor syntax is {v1 v2 v3} + parts = append(parts, fmt.Sprintf("%s={%s}", kv[0], strings.Join(vals, " "))) + } + + // Any + for _, v := range any { + kv := strings.Split(v, "=") + if len(kv) != 2 { + return "", fmt.Errorf("invalid any arg: %s", v) + } + + if err := validateKey(kv[0], validKeys); err != nil { + return "", err + } + + // Checking if key already exists + if m[kv[0]] { + return "", fmt.Errorf("found duplicate key: %s", kv[0]) + } + + m[kv[0]] = true + vals := strings.Split(kv[1], ",") // Splitting and replacing "," with " ", Harbor syntax is {v1 v2 v3} + parts = append(parts, fmt.Sprintf("%s=(%s)", kv[0], strings.Join(vals, " "))) + } + return strings.Join(parts, ","), nil } @@ -104,7 +164,7 @@ Examples: --match project_id=12 --fuzzy name=test - --range update_time=2024-01-01:2024-02-01 + --range update_time=2024-01-01~2024-02-01 --any tag=v1,v2 --all label=prod,stable