Skip to content

Feat: Implementing mgrctl get system command.#789

Draft
katara-Jayprakash wants to merge 8 commits into
uyuni-project:mainfrom
katara-Jayprakash:feat/mgrctl-get-system-command
Draft

Feat: Implementing mgrctl get system command.#789
katara-Jayprakash wants to merge 8 commits into
uyuni-project:mainfrom
katara-Jayprakash:feat/mgrctl-get-system-command

Conversation

@katara-Jayprakash

@katara-Jayprakash katara-Jayprakash commented May 11, 2026

Copy link
Copy Markdown

What does this PR change?

This PR adds an initial get command group to mgrctl and introduces mgrctl get system for listing registered systems from
the Uyuni API.

Included in this PR

  • Adds mgrctl get as a new top-level command group
  • Adds mgrctl get system (alias: systems) using system/listSystems
  • Supports output formats:
    • table (default)
    • json
    • yaml
  • Adds API type mapping for system responses
  • Adds unit test coverage for mgrctl get system with mocked API login + listSystems flow

Codespace

Check if you already have a running container clicking on Running CodeSpace

Create CodeSpace About billing for Github Codespaces CodeSpace Billing Summary CodeSpace Limit

Test coverage

  • Unit tests were added
  • DONE

Links

Issue(s): #

  • DONE

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:

  • No changelog needed

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!

@katara-Jayprakash

Copy link
Copy Markdown
Author

/cc @cbosdo @marv7000 @mbussolotto

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>
@katara-Jayprakash katara-Jayprakash force-pushed the feat/mgrctl-get-system-command branch from 4f67d97 to 7584b1e Compare June 9, 2026 14:04
@sonarqubecloud

sonarqubecloud Bot commented Jun 9, 2026

Copy link
Copy Markdown

Signed-off-by: katara-Jayprakash <katarajayprakash@icloud.com>
@katara-Jayprakash katara-Jayprakash force-pushed the feat/mgrctl-get-system-command branch from 6f0a0dd to 9b288c7 Compare June 23, 2026 18:20
Signed-off-by: katara-Jayprakash <katarajayprakash@icloud.com>
Comment thread mgrctl/cmd/get/system_desc.go Outdated
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several issues:

  1. you only get the first 50 items
  2. Passing the user-provided values as is is probably going to fail

@katara-Jayprakash katara-Jayprakash force-pushed the feat/mgrctl-get-system-command branch from 9b288c7 to 1a71128 Compare June 30, 2026 13:34
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you changing this?

Comment thread mgrctl/cmd/get/get.go Outdated
@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2026 SUSE LLC

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copyright is not for SUSE here, it's yours

Comment thread mgrctl/cmd/get/get.go Outdated
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
var flags getFlags

// 1. Parse the first word ("system") as a positional argument

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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

Comment thread mgrctl/cmd/get/resource.go Outdated
Comment on lines +82 to +85
query.Set("filterKey", filterKey)
query.Set("filterValue", filterValue)
query.Set("page", fmt.Sprintf("%d", page))
query.Set("pageSize", fmt.Sprintf("%d", pageSize))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are specific to the system: move them to the systemFetcher

@mbussolotto mbussolotto self-requested a review June 30, 2026 14:49
Comment thread mgrctl/cmd/get/resource.go Outdated
// 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the filterKey and filterValue probably need to be sanitized to prevent the user from breaking the HTTP query

Comment thread mgrctl/cmd/get/resource.go Outdated
// 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread mgrctl/cmd/get/resource.go Outdated
// 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{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread mgrctl/cmd/get/resource.go Outdated
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
)

// 1. This tells the system how to map a column header (like "ID")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Numbered comments are a pain to maintain…

Comment thread mgrctl/cmd/get/resource.go Outdated
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())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread mgrctl/cmd/get/resource.go Outdated
return nil, err
}
// Return the raw JSON data
return res.Result.Data, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return the total items count for paging too.

Comment thread mgrctl/cmd/get/get.go Outdated
}

// 2. Parse the output and filter flags and save them to the getFlags struct
cmd.Flags().StringVarP(&flags.OutputFormat, "output", "o", "table",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output format flag definition should be in a function in the shared/utils module in order to be shared too.

Comment thread mgrctl/cmd/get/get.go Outdated

// 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"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the help, document what the columns spec needs to be: users can't guess

Comment thread mgrctl/cmd/get/get.go
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)"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread mgrctl/cmd/get/resource.go Outdated
// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread mgrctl/cmd/get/resource.go Outdated
// 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{},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a Resource type, you could extend this to aliases.

Suggested change
"system": systemFetcher{},
"system": Resource{ []string{"sys"}, systemFetcher{} },

@katara-Jayprakash katara-Jayprakash force-pushed the feat/mgrctl-get-system-command branch 3 times, most recently from 7e3f8f5 to ce72c46 Compare July 4, 2026 13:02
Signed-off-by: katara-Jayprakash <katarajayprakash@icloud.com>
@katara-Jayprakash katara-Jayprakash force-pushed the feat/mgrctl-get-system-command branch from ce72c46 to 701705d Compare July 4, 2026 13:04
@sonarqubecloud

sonarqubecloud Bot commented Jul 4, 2026

Copy link
Copy Markdown

Comment thread mgrctl/cmd/get/system.go
Comment thread mgrctl/cmd/get/system.go
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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]](

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This struct seems to be never used? Is this intended?

}

// SystemGroup describes an Uyuni system group in the API.
type SystemGroup struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This struct seems unused?

Comment thread .gitignore
__pycache__
**/tags
*.mo
.DS_Store

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should go in a separate commit?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There needs to be more unit tests, also ones for parseFilter, runGet and printTable.

Comment thread mgrctl/cmd/get/get.go
Comment on lines +60 to +63
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"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if we want these page flags

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread mgrctl/cmd/get/get.go
Comment on lines +60 to +63
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"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread mgrctl/cmd/get/system.go
Description string
}

var resourceTypes = map[string]Resource{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to keep that variable empty here and add a registerResource() funtion to be called in each resource file.

Comment on lines +30 to +34
"system": {
Fetcher: systemFetcher{},
Aliases: []string{"sys"},
Description: "List systems",
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (systemGroupFetcher) List(client *api.APIClient, _ string, _, _ int) ([]map[string]any, int, error) {
func (systemGroupFetcher) List(client *api.APIClient, _ string, _, _ int) ([]SystemGroup, int, error) {

Comment thread mgrctl/cmd/get/system.go

type systemFetcher struct{}

func (systemFetcher) List(client *api.APIClient, filter string, page, pageSize int) ([]map[string]any, int, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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) {

Comment thread shared/utils/printer.go

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"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread shared/utils/printer.go
return printTable(items, parsed, out)
}

if strings.HasPrefix(format, "custom-columns-file=") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread shared/utils/printer.go
return cols
}

func parseCustomColumnsFile(path string) ([]ColumnDef, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants