Skip to content

Commit 79f09fb

Browse files
authored
Add support for excluding a list of parameters from an API response (#163)
This PR address ##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 } ```
1 parent ee1a001 commit 79f09fb

File tree

4 files changed

+77
-26
lines changed

4 files changed

+77
-26
lines changed

cli/completer.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int)
357357
}
358358
return
359359
}
360+
360361
if arg.Type == config.FAKE && arg.Name == "filter=" {
361362
offset = 0
362363
filterInputs := strings.Split(strings.Replace(argInput, ",", ",|", -1), "|")
@@ -373,6 +374,22 @@ func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int)
373374
return
374375
}
375376

377+
if arg.Type == config.FAKE && arg.Name == "exclude=" {
378+
offset = 0
379+
excludeFilterInputs := strings.Split(strings.Replace(argInput, ",", ",|", -1), "|")
380+
lastExcludeFilterInput := lastString(excludeFilterInputs)
381+
for _, key := range apiFound.ResponseKeys {
382+
if inArray(key, excludeFilterInputs) {
383+
continue
384+
}
385+
if strings.HasPrefix(key, lastExcludeFilterInput) {
386+
options = append(options, []rune(key[len(lastExcludeFilterInput):]))
387+
offset = len(lastExcludeFilterInput)
388+
}
389+
}
390+
return
391+
}
392+
376393
autocompleteAPI := findAutocompleteAPI(arg, apiFound, apiMap)
377394
if autocompleteAPI == nil {
378395
return nil, 0

cmd/api.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func init() {
8282
if strings.HasSuffix(err.Error(), "context canceled") {
8383
return nil
8484
} else if response != nil {
85-
printResult(r.Config.Core.Output, response, nil)
85+
printResult(r.Config.Core.Output, response, nil, nil)
8686
}
8787
return err
8888
}
@@ -98,8 +98,19 @@ func init() {
9898
}
9999
}
100100

101+
var excludeKeys []string
102+
for _, arg := range apiArgs {
103+
if strings.HasPrefix(arg, "exclude=") {
104+
for _, excludeKey := range strings.Split(strings.Split(arg, "=")[1], ",") {
105+
if len(strings.TrimSpace(excludeKey)) > 0 {
106+
excludeKeys = append(excludeKeys, strings.TrimSpace(excludeKey))
107+
}
108+
}
109+
}
110+
}
111+
101112
if len(response) > 0 {
102-
printResult(r.Config.Core.Output, response, filterKeys)
113+
printResult(r.Config.Core.Output, response, filterKeys, excludeKeys)
103114
}
104115

105116
return nil

cmd/output.go

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -206,51 +206,67 @@ func printCsv(response map[string]interface{}, filter []string) {
206206
enc.Flush()
207207
}
208208

209-
func filterResponse(response map[string]interface{}, filter []string, outputType string) map[string]interface{} {
210-
if filter == nil || len(filter) == 0 {
209+
func filterResponse(response map[string]interface{}, filter []string, excludeFilter []string, outputType string) map[string]interface{} {
210+
if (filter == nil || len(filter) == 0) && (excludeFilter == nil || len(excludeFilter) == 0) {
211211
return response
212212
}
213+
214+
excludeSet := make(map[string]struct{}, len(excludeFilter))
215+
for _, key := range excludeFilter {
216+
excludeSet[key] = struct{}{}
217+
}
218+
219+
filterSet := make(map[string]struct{}, len(filter))
220+
for _, key := range filter {
221+
filterSet[key] = struct{}{}
222+
}
223+
213224
filteredResponse := make(map[string]interface{})
214-
for k, v := range response {
215-
valueType := reflect.TypeOf(v)
216-
if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map {
217-
items, ok := v.([]interface{})
218-
if !ok {
219-
continue
220-
}
225+
226+
for key, value := range response {
227+
switch items := value.(type) {
228+
case []interface{}:
221229
var filteredRows []interface{}
222230
for _, item := range items {
223231
row, ok := item.(map[string]interface{})
224-
if !ok || len(row) < 1 {
232+
if !ok || len(row) == 0 {
225233
continue
226234
}
235+
227236
filteredRow := make(map[string]interface{})
228-
for _, filterKey := range filter {
229-
for field := range row {
230-
if filterKey == field {
231-
filteredRow[field] = row[field]
237+
238+
if len(filter) > 0 {
239+
// Include only keys that exist in filterSet
240+
for filterKey := range filterSet {
241+
if val, exists := row[filterKey]; exists {
242+
filteredRow[filterKey] = val
243+
} else if outputType == config.COLUMN || outputType == config.CSV || outputType == config.TABLE {
244+
filteredRow[filterKey] = "" // Ensure all filter keys exist in row
232245
}
233246
}
234-
if outputType == config.COLUMN || outputType == config.CSV || outputType == config.TABLE {
235-
if _, ok := filteredRow[filterKey]; !ok {
236-
filteredRow[filterKey] = ""
247+
} else {
248+
// Exclude keys from excludeFilter
249+
for field, val := range row {
250+
if _, excluded := excludeSet[field]; !excluded {
251+
filteredRow[field] = val
237252
}
238253
}
239254
}
255+
240256
filteredRows = append(filteredRows, filteredRow)
241257
}
242-
filteredResponse[k] = filteredRows
243-
} else {
244-
filteredResponse[k] = v
245-
continue
246-
}
258+
filteredResponse[key] = filteredRows
247259

260+
default:
261+
filteredResponse[key] = value
262+
}
248263
}
264+
249265
return filteredResponse
250266
}
251267

252-
func printResult(outputType string, response map[string]interface{}, filter []string) {
253-
response = filterResponse(response, filter, outputType)
268+
func printResult(outputType string, response map[string]interface{}, filter []string, excludeFilter []string) {
269+
response = filterResponse(response, filter, excludeFilter, outputType)
254270
switch outputType {
255271
case config.JSON:
256272
printJSON(response)

config/cache.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@ func (c *Config) UpdateCache(response map[string]interface{}) interface{} {
151151
Description: "cloudmonkey specific response key filtering",
152152
})
153153

154+
// Add exclude arg
155+
apiArgs = append(apiArgs, &APIArg{
156+
Name: "exclude=",
157+
Type: FAKE,
158+
Description: "cloudmonkey specific response key to exlude when filtering",
159+
})
160+
154161
sort.Slice(apiArgs, func(i, j int) bool {
155162
return apiArgs[i].Name < apiArgs[j].Name
156163
})

0 commit comments

Comments
 (0)