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 <[email protected]>
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
})