Feat: Implementing mgrctl get system command.#789
Conversation
Signed-off-by: katara-Jayprakash <katarajayprakash@icloud.com>
Signed-off-by: katara-Jayprakash <katarajayprakash@icloud.com>
Signed-off-by: katara-Jayprakash <katarajayprakash@icloud.com>
Signed-off-by: katara-Jayprakash <katarajayprakash@icloud.com>
4f67d97 to
7584b1e
Compare
|
Signed-off-by: katara-Jayprakash <katarajayprakash@icloud.com>
6f0a0dd to
9b288c7
Compare
Signed-off-by: katara-Jayprakash <katarajayprakash@icloud.com>
| filterKey := parts[0] | ||
| filterValue := parts[1] | ||
| // The API strictly requires filterKey, filterValue, page, and pageSize | ||
| url := fmt.Sprintf("system/listSystemsFiltered?filterKey=%s&filterValue=%s&page=0&pageSize=50", filterKey, filterValue) |
There was a problem hiding this comment.
There are several issues:
- you only get the first 50 items
- Passing the user-provided values as is is probably going to fail
9b288c7 to
1a71128
Compare
| if exp != act { | ||
| if i < len(expectedLines) { | ||
| diff.WriteString(fmt.Sprintf("-%d: %s\n", i+1, exp)) | ||
| fmt.Fprintf(&diff, "-%d: %s\n", i+1, exp) |
| @@ -0,0 +1,85 @@ | |||
| // SPDX-FileCopyrightText: 2026 SUSE LLC | |||
There was a problem hiding this comment.
Copyright is not for SUSE here, it's yours
| func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { | ||
| var flags getFlags | ||
|
|
||
| // 1. Parse the first word ("system") as a positional argument |
There was a problem hiding this comment.
| // 1. Parse the first word ("system") as a positional argument | |
| // Parse the first word ("system") as a positional argument |
Please don't add the steps numbers: it's a pain to maintain
| query.Set("filterKey", filterKey) | ||
| query.Set("filterValue", filterValue) | ||
| query.Set("page", fmt.Sprintf("%d", page)) | ||
| query.Set("pageSize", fmt.Sprintf("%d", pageSize)) |
There was a problem hiding this comment.
These are specific to the system: move them to the systemFetcher
| // Set up the URL parameters (like adding ?page=1&pageSize=50 to a URL) | ||
| query := url.Values{} | ||
| query.Set("filterKey", filterKey) | ||
| query.Set("filterValue", filterValue) |
There was a problem hiding this comment.
the filterKey and filterValue probably need to be sanitized to prevent the user from breaking the HTTP query
| // struct that has these two exact functions: | ||
| type ResourceFetcher interface { | ||
| // A function that knows how to fetch the data from the API | ||
| List(client *api.APIClient, filter string, page, pageSize int) ([]map[string]any, error) |
There was a problem hiding this comment.
List also needs to return the total number of items as it may not match the length of the returned slice if paging is performed.
| // 3. This is the "dictionary" that powers the whole architecture. | ||
| // When the user types 'mgrctl get system', we look up the word "system" here. | ||
| // To add a new command in the future, you just add ONE line to this map. | ||
| var resourceTypes = map[string]ResourceFetcher{ |
There was a problem hiding this comment.
You probably want to also have short names or aliases for the resources. Of course a unit test should check that there is no duplicate too.
so system could be shortened to sys and systemgroup to grp.
This variable needs to be looped through to show all the possible names and aliases in the help.
| . "github.com/uyuni-project/uyuni-tools/shared/l10n" | ||
| ) | ||
|
|
||
| // 1. This tells the system how to map a column header (like "ID") |
There was a problem hiding this comment.
Numbered comments are a pain to maintain…
| fetcher, ok := resourceTypes[name] | ||
| if !ok { | ||
| // If the user typed something wrong (like 'mgrctl get fake'), it throws an error. | ||
| return nil, fmt.Errorf(L("unknown resource type %q; available: %s"), name, registeredTypesText()) |
There was a problem hiding this comment.
The list of resource names and aliases can grow big over time. Better list them in a nice way in the help and point to the help from the errors.
| return nil, err | ||
| } | ||
| // Return the raw JSON data | ||
| return res.Result.Data, nil |
There was a problem hiding this comment.
Return the total items count for paging too.
| } | ||
|
|
||
| // 2. Parse the output and filter flags and save them to the getFlags struct | ||
| cmd.Flags().StringVarP(&flags.OutputFormat, "output", "o", "table", |
There was a problem hiding this comment.
The output format flag definition should be in a function in the shared/utils module in order to be shared too.
|
|
||
| // 2. Parse the output and filter flags and save them to the getFlags struct | ||
| cmd.Flags().StringVarP(&flags.OutputFormat, "output", "o", "table", | ||
| L("Output format: table|json|yaml|custom-columns=SPEC|custom-columns-file=PATH")) |
There was a problem hiding this comment.
In the help, document what the columns spec needs to be: users can't guess
| cmd.Flags().StringVarP(&flags.OutputFormat, "output", "o", "table", | ||
| L("Output format: table|json|yaml|custom-columns=SPEC|custom-columns-file=PATH")) | ||
| cmd.Flags().StringVar(&flags.Filter, "filter", "", | ||
| L("Filter expression (e.g. extra_pkg_count=0)")) |
There was a problem hiding this comment.
You need to at least document what are the possible operators. For the keys, it's a bit more complex as it is tied to the API and each object, but it would be nice to list them exhaustively too. I only fear it could be hard to maintain.
| // A function that knows how to fetch the data from the API | ||
| List(client *api.APIClient, filter string, page, pageSize int) ([]map[string]any, error) | ||
| // A function that returns the default table columns | ||
| Columns() []ColumnDef |
There was a problem hiding this comment.
Add a function to return the help dedicated to this resource. This way you can loop over them in the command help in a special section about the supported resources.
| // When the user types 'mgrctl get system', we look up the word "system" here. | ||
| // To add a new command in the future, you just add ONE line to this map. | ||
| var resourceTypes = map[string]ResourceFetcher{ | ||
| "system": systemFetcher{}, |
There was a problem hiding this comment.
With a Resource type, you could extend this to aliases.
| "system": systemFetcher{}, | |
| "system": Resource{ []string{"sys"}, systemFetcher{} }, |
7e3f8f5 to
ce72c46
Compare
Signed-off-by: katara-Jayprakash <katarajayprakash@icloud.com>
ce72c46 to
701705d
Compare
|
| return nil, fmt.Errorf(L("unknown resource type %[1]q; available: %[2]s"), name, registeredTypesText()) | ||
| } | ||
|
|
||
| func listFiltered(client *api.APIClient, endpoint, query string) ([]map[string]any, int, error) { |
There was a problem hiding this comment.
listFiltered doesn't make sense to exist if you're passing the endpoint and query parameters as function arguments, only to assemble it there.
| } | ||
|
|
||
| func listFiltered(client *api.APIClient, endpoint, query string) ([]map[string]any, int, error) { | ||
| res, err := api.Get[apitypes.FilteredResponse[map[string]any]]( |
There was a problem hiding this comment.
| res, err := api.Get[apitypes.FilteredResponse[map[string]any]]( | |
| res, err := api.GetChecked[apitypes.FilteredResponse[map[string]any]]( |
You should check if the endpoint exists instead of assuming it does.
| package types | ||
|
|
||
| // System describes an Uyuni registered system/minion in the API. | ||
| type System struct { |
There was a problem hiding this comment.
This struct seems to be never used? Is this intended?
| } | ||
|
|
||
| // SystemGroup describes an Uyuni system group in the API. | ||
| type SystemGroup struct { |
| __pycache__ | ||
| **/tags | ||
| *.mo | ||
| .DS_Store |
There was a problem hiding this comment.
Maybe this should go in a separate commit?
There was a problem hiding this comment.
There needs to be more unit tests, also ones for parseFilter, runGet and printTable.
| cmd.Flags().IntVar(&flags.Page, "page", 0, | ||
| L("Page number for paginated results (0-indexed)")) | ||
| cmd.Flags().IntVar(&flags.PageSize, "page-size", 50, | ||
| L("Number of items per page")) |
There was a problem hiding this comment.
not sure if we want these page flags
There was a problem hiding this comment.
At least it delegates the complex part of getting all the results to the user and his scripts. Looping is nice but you can get incoherent results if a change happens in the middle. I would keep them and state in their help that they may not be used, depending on the resource type and its corresponding API.
| cmd.Flags().IntVar(&flags.Page, "page", 0, | ||
| L("Page number for paginated results (0-indexed)")) | ||
| cmd.Flags().IntVar(&flags.PageSize, "page-size", 50, | ||
| L("Number of items per page")) |
There was a problem hiding this comment.
At least it delegates the complex part of getting all the results to the user and his scripts. Looping is nice but you can get incoherent results if a change happens in the middle. I would keep them and state in their help that they may not be used, depending on the resource type and its corresponding API.
| Description string | ||
| } | ||
|
|
||
| var resourceTypes = map[string]Resource{ |
There was a problem hiding this comment.
It would be better to keep that variable empty here and add a registerResource() funtion to be called in each resource file.
| "system": { | ||
| Fetcher: systemFetcher{}, | ||
| Aliases: []string{"sys"}, | ||
| Description: "List systems", | ||
| }, |
There was a problem hiding this comment.
This should be something like this in the system.go file:
registerResource("system", systemFetcher{}, []string{"sys"}, "List systems")
| ) | ||
|
|
||
| func TestResourceTypesNoDuplicates(t *testing.T) { | ||
| seen := make(map[string]bool) |
There was a problem hiding this comment.
No need for a map here, a slice is enough
|
|
||
| type systemGroupFetcher struct{} | ||
|
|
||
| func (systemGroupFetcher) List(client *api.APIClient, _ string, _, _ int) ([]map[string]any, int, error) { |
There was a problem hiding this comment.
| func (systemGroupFetcher) List(client *api.APIClient, _ string, _, _ int) ([]map[string]any, int, error) { | |
| func (systemGroupFetcher) List(client *api.APIClient, _ string, _, _ int) ([]SystemGroup, int, error) { |
|
|
||
| type systemFetcher struct{} | ||
|
|
||
| func (systemFetcher) List(client *api.APIClient, filter string, page, pageSize int) ([]map[string]any, int, error) { |
There was a problem hiding this comment.
| func (systemFetcher) List(client *api.APIClient, filter string, page, pageSize int) ([]map[string]any, int, error) { | |
| func (systemFetcher) List(client *api.APIClient, filter string, page, pageSize int) ([]System, int, error) { |
|
|
||
| func AddOutputFlag(cmd *cobra.Command, outputFormat *string) { | ||
| cmd.Flags().StringVarP(outputFormat, "output", "o", "table", | ||
| L("Output format: table|json|yaml|custom-columns=SPEC|custom-columns-file=PATH")) |
There was a problem hiding this comment.
I am pestering often enough against kubectl get help linking to web pages for the format description: you need to add at least a rough description of what the SPEC is for that format.
| return printTable(items, parsed, out) | ||
| } | ||
|
|
||
| if strings.HasPrefix(format, "custom-columns-file=") { |
There was a problem hiding this comment.
This should just be a wrapper around the custom-columns implementation rather than a separate implementation. Why are those two cases also not in the switch below? This made me think that you forgot them…
I would rather see them as printColumns() and printColumnsFromFile() function where the code is shared there.
| return cols | ||
| } | ||
|
|
||
| func parseCustomColumnsFile(path string) ([]ColumnDef, error) { |
There was a problem hiding this comment.
This function should only parse the file into the format for the custom-columns. Then you would have the logic to create the ColumnDef in one place only.



What does this PR change?
This PR adds an initial
getcommand group tomgrctland introducesmgrctl get systemfor listing registered systems fromthe Uyuni API.
Included in this PR
mgrctl getas a new top-level command groupmgrctl get system(alias:systems) usingsystem/listSystemstable(default)jsonyamlmgrctl get systemwith mocked API login + listSystems flowCodespace
Check if you already have a running container clicking on
Test coverage
Links
Issue(s): #
Changelogs
Make sure the changelogs entries you are adding are compliant with https://github.com/uyuni-project/uyuni/wiki/Contributing#changelogs and https://github.com/uyuni-project/uyuni/wiki/Contributing#uyuni-projectuyuni-repository
If you don't need a changelog check, please mark this checkbox:
If you uncheck the checkbox after the PR is created, you will need to re-run
changelog_test(see below)Before you merge
Check How to branch and merge properly!