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 825d4e9a6..103287e3f 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 + all []string + any []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, all, any, 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, &all, &any) // Adds the 5 query flags return cmd } diff --git a/pkg/utils/query.go b/pkg/utils/query.go index 5dd57d454..6c278929c 100644 --- a/pkg/utils/query.go +++ b/pkg/utils/query.go @@ -16,63 +16,174 @@ package utils import ( "fmt" "strings" + + "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 } +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: + + --match 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: + + --match 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