This is an automated email from the ASF dual-hosted git repository. shwstppr pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/cloudstack-cloudmonkey.git
The following commit(s) were added to refs/heads/main by this push: new 79f09fb Add support for excluding a list of parameters from an API response (#163) 79f09fb is described below commit 79f09fbcdad6d9af6eac23976010c29f3efc4389 Author: Pearl Dsilva <pearl1...@gmail.com> AuthorDate: Tue Aug 5 23:41:56 2025 -0400 Add support for excluding a list of parameters from an API response (#163) This PR address #https://github.com/apache/cloudstack-cloudmonkey/issues/149, it adds an exclude parameter to remove fields from the response For example, without the exclude parameter, the listAccounts response looks like: ``` $ ./cmk -p pd-env list accounts name=admin { "account": [ { "accounttype": 1, "apikeyaccess": "INHERIT", "backupavailable": "Unlimited", "backuplimit": "Unlimited", "backupstorageavailable": "Unlimited", "backupstoragelimit": "Unlimited", "backupstoragetotal": 0, "backuptotal": 0, "bucketavailable": "Unlimited", "bucketlimit": "Unlimited", "buckettotal": 0, "cpuavailable": "Unlimited", "cpulimit": "Unlimited", "cputotal": 4, "domain": "ROOT", "domainid": "db0704ab-5827-11f0-804a-1e0052000469", "domainpath": "ROOT", "groups": [], "id": "24e85a59-5828-11f0-804a-1e0052000469", "ipavailable": "Unlimited", "iplimit": "Unlimited", "iptotal": 1, "isdefault": true, "memoryavailable": "Unlimited", "memorylimit": "Unlimited", "memorytotal": 4096, "name": "admin", "networkavailable": "Unlimited", "networklimit": "Unlimited", "networktotal": 1, "objectstorageavailable": "Unlimited", "objectstoragelimit": "Unlimited", "objectstoragetotal": 0, "primarystorageavailable": "Unlimited", "primarystoragelimit": "Unlimited", "primarystoragetotal": 16, "projectavailable": "Unlimited", "projectlimit": "Unlimited", "projecttotal": 0, "receivedbytes": 1346690740, "roleid": "fff4ee8f-5827-11f0-804a-1e0052000469", "rolename": "Root Admin", "roletype": "Admin", "secondarystorageavailable": "Unlimited", "secondarystoragelimit": "Unlimited", "secondarystoragetotal": 0, "sentbytes": 35867842, "snapshotavailable": "Unlimited", "snapshotlimit": "Unlimited", "snapshottotal": 0, "state": "enabled", "templateavailable": "Unlimited", "templatelimit": "Unlimited", "templatetotal": 0, "user": [ { "account": "admin", "accountid": "24e85a59-5828-11f0-804a-1e0052000469", "accounttype": 1, "apikey": "LIN6rqXuaJwMPfGYFh13qDwYz5VNNz1J2J6qIOWcd3oLQOq0WtD4CwRundBL6rzXToa3lQOC_vKjI3nkHtiD8Q", "created": "2025-07-03T16:09:48+0000", "domain": "ROOT", "domainid": "db0704ab-5827-11f0-804a-1e0052000469", "firstname": "admin", "id": "24e902f0-5828-11f0-804a-1e0052000469", "is2faenabled": false, "is2famandated": false, "iscallerchilddomain": false, "isdefault": true, "lastname": "cloud", "roleid": "fff4ee8f-5827-11f0-804a-1e0052000469", "rolename": "Root Admin", "roletype": "Admin", "state": "enabled", "username": "admin", "usersource": "native" }, { "account": "admin", "accountid": "24e85a59-5828-11f0-804a-1e0052000469", "accounttype": 1, "apikey": "U7qz8y6CjK1ECQsOuRLT7XaIfaWF3QuB4VJBHvDOBfQjyzsVvhWgDhMkHveJzu1Bb7oFnYKG4CZAfdHLpnim6w", "created": "2025-07-09T17:26:05+0000", "domain": "ROOT", "domainid": "db0704ab-5827-11f0-804a-1e0052000469", "email": "kubeadmin", "firstname": "admin", "id": "8ea65043-7df2-4906-8953-641a7e25a5cf", "is2faenabled": false, "is2famandated": false, "iscallerchilddomain": false, "isdefault": false, "lastname": "kubeadmin", "roleid": "fff4ee8f-5827-11f0-804a-1e0052000469", "rolename": "Root Admin", "roletype": "Admin", "state": "enabled", "username": "admin-kubeadmin", "usersource": "native" } ], "vmavailable": "Unlimited", "vmlimit": "Unlimited", "vmrunning": 2, "vmstopped": 0, "vmtotal": 2, "volumeavailable": "Unlimited", "volumelimit": "Unlimited", "volumetotal": 2, "vpcavailable": "Unlimited", "vpclimit": "Unlimited", "vpctotal": 0 } ], "count": 1 } ``` With this patch, if user wants to remove `user` field from the response, they could do so using the exclude param: ``` $ ./cmk -p pd-env list accounts name=admin exclude=user { "account": [ { "accounttype": 1, "apikeyaccess": "INHERIT", "backupavailable": "Unlimited", "backuplimit": "Unlimited", "backupstorageavailable": "Unlimited", "backupstoragelimit": "Unlimited", "backupstoragetotal": 0, "backuptotal": 0, "bucketavailable": "Unlimited", "bucketlimit": "Unlimited", "buckettotal": 0, "cpuavailable": "Unlimited", "cpulimit": "Unlimited", "cputotal": 4, "domain": "ROOT", "domainid": "db0704ab-5827-11f0-804a-1e0052000469", "domainpath": "ROOT", "groups": [], "id": "24e85a59-5828-11f0-804a-1e0052000469", "ipavailable": "Unlimited", "iplimit": "Unlimited", "iptotal": 1, "isdefault": true, "memoryavailable": "Unlimited", "memorylimit": "Unlimited", "memorytotal": 4096, "name": "admin", "networkavailable": "Unlimited", "networklimit": "Unlimited", "networktotal": 1, "objectstorageavailable": "Unlimited", "objectstoragelimit": "Unlimited", "objectstoragetotal": 0, "primarystorageavailable": "Unlimited", "primarystoragelimit": "Unlimited", "primarystoragetotal": 16, "projectavailable": "Unlimited", "projectlimit": "Unlimited", "projecttotal": 0, "receivedbytes": 1346706148, "roleid": "fff4ee8f-5827-11f0-804a-1e0052000469", "rolename": "Root Admin", "roletype": "Admin", "secondarystorageavailable": "Unlimited", "secondarystoragelimit": "Unlimited", "secondarystoragetotal": 0, "sentbytes": 35872906, "snapshotavailable": "Unlimited", "snapshotlimit": "Unlimited", "snapshottotal": 0, "state": "enabled", "templateavailable": "Unlimited", "templatelimit": "Unlimited", "templatetotal": 0, "vmavailable": "Unlimited", "vmlimit": "Unlimited", "vmrunning": 2, "vmstopped": 0, "vmtotal": 2, "volumeavailable": "Unlimited", "volumelimit": "Unlimited", "volumetotal": 2, "vpcavailable": "Unlimited", "vpclimit": "Unlimited", "vpctotal": 0 } ], "count": 1 } ``` --- cli/completer.go | 17 +++++++++++++++ cmd/api.go | 15 +++++++++++-- cmd/output.go | 64 +++++++++++++++++++++++++++++++++++--------------------- config/cache.go | 7 +++++++ 4 files changed, 77 insertions(+), 26 deletions(-) diff --git a/cli/completer.go b/cli/completer.go index be1ba21..53de5fb 100644 --- a/cli/completer.go +++ b/cli/completer.go @@ -357,6 +357,7 @@ func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int) } return } + if arg.Type == config.FAKE && arg.Name == "filter=" { offset = 0 filterInputs := strings.Split(strings.Replace(argInput, ",", ",|", -1), "|") @@ -373,6 +374,22 @@ func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int) return } + if arg.Type == config.FAKE && arg.Name == "exclude=" { + offset = 0 + excludeFilterInputs := strings.Split(strings.Replace(argInput, ",", ",|", -1), "|") + lastExcludeFilterInput := lastString(excludeFilterInputs) + for _, key := range apiFound.ResponseKeys { + if inArray(key, excludeFilterInputs) { + continue + } + if strings.HasPrefix(key, lastExcludeFilterInput) { + options = append(options, []rune(key[len(lastExcludeFilterInput):])) + offset = len(lastExcludeFilterInput) + } + } + return + } + autocompleteAPI := findAutocompleteAPI(arg, apiFound, apiMap) if autocompleteAPI == nil { return nil, 0 diff --git a/cmd/api.go b/cmd/api.go index 058c6fb..872a328 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -82,7 +82,7 @@ func init() { if strings.HasSuffix(err.Error(), "context canceled") { return nil } else if response != nil { - printResult(r.Config.Core.Output, response, nil) + printResult(r.Config.Core.Output, response, nil, nil) } return err } @@ -98,8 +98,19 @@ func init() { } } + var excludeKeys []string + for _, arg := range apiArgs { + if strings.HasPrefix(arg, "exclude=") { + for _, excludeKey := range strings.Split(strings.Split(arg, "=")[1], ",") { + if len(strings.TrimSpace(excludeKey)) > 0 { + excludeKeys = append(excludeKeys, strings.TrimSpace(excludeKey)) + } + } + } + } + if len(response) > 0 { - printResult(r.Config.Core.Output, response, filterKeys) + printResult(r.Config.Core.Output, response, filterKeys, excludeKeys) } return nil diff --git a/cmd/output.go b/cmd/output.go index 08004cf..17606e6 100644 --- a/cmd/output.go +++ b/cmd/output.go @@ -206,51 +206,67 @@ func printCsv(response map[string]interface{}, filter []string) { enc.Flush() } -func filterResponse(response map[string]interface{}, filter []string, outputType string) map[string]interface{} { - if filter == nil || len(filter) == 0 { +func filterResponse(response map[string]interface{}, filter []string, excludeFilter []string, outputType string) map[string]interface{} { + if (filter == nil || len(filter) == 0) && (excludeFilter == nil || len(excludeFilter) == 0) { return response } + + excludeSet := make(map[string]struct{}, len(excludeFilter)) + for _, key := range excludeFilter { + excludeSet[key] = struct{}{} + } + + filterSet := make(map[string]struct{}, len(filter)) + for _, key := range filter { + filterSet[key] = struct{}{} + } + filteredResponse := make(map[string]interface{}) - for k, v := range response { - valueType := reflect.TypeOf(v) - if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map { - items, ok := v.([]interface{}) - if !ok { - continue - } + + for key, value := range response { + switch items := value.(type) { + case []interface{}: var filteredRows []interface{} for _, item := range items { row, ok := item.(map[string]interface{}) - if !ok || len(row) < 1 { + if !ok || len(row) == 0 { continue } + filteredRow := make(map[string]interface{}) - for _, filterKey := range filter { - for field := range row { - if filterKey == field { - filteredRow[field] = row[field] + + if len(filter) > 0 { + // Include only keys that exist in filterSet + for filterKey := range filterSet { + if val, exists := row[filterKey]; exists { + filteredRow[filterKey] = val + } else if outputType == config.COLUMN || outputType == config.CSV || outputType == config.TABLE { + filteredRow[filterKey] = "" // Ensure all filter keys exist in row } } - if outputType == config.COLUMN || outputType == config.CSV || outputType == config.TABLE { - if _, ok := filteredRow[filterKey]; !ok { - filteredRow[filterKey] = "" + } else { + // Exclude keys from excludeFilter + for field, val := range row { + if _, excluded := excludeSet[field]; !excluded { + filteredRow[field] = val } } } + filteredRows = append(filteredRows, filteredRow) } - filteredResponse[k] = filteredRows - } else { - filteredResponse[k] = v - continue - } + filteredResponse[key] = filteredRows + default: + filteredResponse[key] = value + } } + return filteredResponse } -func printResult(outputType string, response map[string]interface{}, filter []string) { - response = filterResponse(response, filter, outputType) +func printResult(outputType string, response map[string]interface{}, filter []string, excludeFilter []string) { + response = filterResponse(response, filter, excludeFilter, outputType) switch outputType { case config.JSON: printJSON(response) diff --git a/config/cache.go b/config/cache.go index 096582d..13596dd 100644 --- a/config/cache.go +++ b/config/cache.go @@ -151,6 +151,13 @@ func (c *Config) UpdateCache(response map[string]interface{}) interface{} { Description: "cloudmonkey specific response key filtering", }) + // Add exclude arg + apiArgs = append(apiArgs, &APIArg{ + Name: "exclude=", + Type: FAKE, + Description: "cloudmonkey specific response key to exlude when filtering", + }) + sort.Slice(apiArgs, func(i, j int) bool { return apiArgs[i].Name < apiArgs[j].Name })