Skip to content

Commit 8ed8c60

Browse files
Refactor get command
Signed-off-by: katara-Jayprakash <katarajayprakash@icloud.com>
1 parent 6f0a0dd commit 8ed8c60

12 files changed

Lines changed: 288 additions & 514 deletions

File tree

mgrctl/cmd/get/client.go

Lines changed: 0 additions & 25 deletions
This file was deleted.

mgrctl/cmd/get/descriptor.go

Lines changed: 0 additions & 19 deletions
This file was deleted.

mgrctl/cmd/get/get.go

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,71 @@
55
package get
66

77
import (
8+
"os"
9+
10+
"github.com/rs/zerolog/log"
811
"github.com/spf13/cobra"
912
"github.com/uyuni-project/uyuni-tools/shared/api"
1013
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
1114
"github.com/uyuni-project/uyuni-tools/shared/types"
15+
"github.com/uyuni-project/uyuni-tools/shared/utils"
1216
)
1317

14-
type getOptions struct {
18+
type getFlags struct {
1519
api.ConnectionDetails `mapstructure:"api"`
16-
OutputFormat string `mapstructure:"output"` // e.g., "json", "yaml", "table"
20+
OutputFormat string `mapstructure:"output"`
21+
Filter string `mapstructure:"filter"`
22+
Page int `mapstructure:"-"`
23+
PageSize int `mapstructure:"-"`
1724
}
1825

19-
// NewCommand generates the 'mgrctl get' command root.
2026
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
21-
var flags getOptions
27+
var flags getFlags
28+
29+
cmd := &cobra.Command{
30+
Use: "get <resource-type> [flags]",
31+
Short: L("Display one or many resources"),
32+
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
33+
ValidArgs: registeredTypes(),
34+
RunE: func(cmd *cobra.Command, args []string) error {
35+
return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, runGet)
36+
},
37+
}
38+
39+
cmd.Flags().StringVarP(&flags.OutputFormat, "output", "o", "table",
40+
L("Output format: table|json|yaml|custom-columns=SPEC|custom-columns-file=PATH"))
41+
cmd.Flags().StringVar(&flags.Filter, "filter", "",
42+
L("Filter expression (e.g. extra_pkg_count=0)"))
43+
cmd.Flags().IntVar(&flags.Page, "page", 0,
44+
L("Page number for paginated results (0-indexed)"))
45+
cmd.Flags().IntVar(&flags.PageSize, "page-size", 50,
46+
L("Number of items per page"))
47+
48+
api.AddAPIFlags(cmd)
49+
return cmd
50+
}
51+
52+
func runGet(_ *types.GlobalFlags, flags *getFlags, _ *cobra.Command, args []string) error {
53+
resourceType := args[0]
54+
log.Debug().Msgf("Running get %s", resourceType)
2255

23-
getCmd := &cobra.Command{
24-
Use: "get [resource] [options]",
25-
Short: L("Display one or many resources (systems, groups, etc.)"),
26-
Long: L("Prints tables, JSON, or YAML of Uyuni API resources. Modeled after kubectl get."),
56+
fetcher, err := lookupFetcher(resourceType)
57+
if err != nil {
58+
return err
2759
}
2860

29-
getCmd.PersistentFlags().StringVarP(
30-
&flags.OutputFormat,
31-
"output",
32-
"o",
33-
"table",
34-
L("Output format. One of: table|json|yaml"),
35-
)
61+
client, err := api.Init(&flags.ConnectionDetails)
62+
if err == nil && (client.Details.User != "" || client.Details.InSession) {
63+
err = client.Login()
64+
}
65+
if err != nil {
66+
return utils.Errorf(err, L("unable to login to the server"))
67+
}
3668

37-
// passing the -o flag to the subcommands to be able to use it in the same way for all the resources
38-
getCmd.AddCommand(newSystemCommand(globalFlags, &flags))
69+
items, err := fetcher.List(client, flags.Filter, flags.Page, flags.PageSize)
70+
if err != nil {
71+
return err
72+
}
3973

40-
api.AddAPIFlags(getCmd)
41-
return getCmd
74+
return printOutput(flags.OutputFormat, items, fetcher.Columns(), os.Stdout)
4275
}

mgrctl/cmd/get/printer.go

Lines changed: 94 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,62 @@ import (
99
"fmt"
1010
"io"
1111
"os"
12-
"reflect"
1312
"strings"
1413
"text/tabwriter"
1514

1615
"gopkg.in/yaml.v2"
1716
)
1817

19-
func printJSON[T any](items []T, out io.Writer) error {
20-
bytes, err := json.MarshalIndent(items, "", " ")
18+
func printOutput(format string, items []map[string]any, cols []ColumnDef, out io.Writer) error {
19+
if strings.HasPrefix(format, "custom-columns=") {
20+
spec := strings.TrimPrefix(format, "custom-columns=")
21+
parsed := parseCustomColumns(spec)
22+
if len(parsed) == 0 {
23+
return fmt.Errorf("custom-columns format specified but no valid columns given")
24+
}
25+
return printTable(items, parsed, out)
26+
}
27+
28+
if strings.HasPrefix(format, "custom-columns-file=") {
29+
path := strings.TrimPrefix(format, "custom-columns-file=")
30+
fileCols, err := parseCustomColumnsFile(path)
31+
if err != nil {
32+
return err
33+
}
34+
return printTable(items, fileCols, out)
35+
}
36+
37+
switch format {
38+
case "json":
39+
return printJSON(items, out)
40+
case "yaml":
41+
return printYAML(items, out)
42+
default:
43+
return printTable(items, cols, out)
44+
}
45+
}
46+
47+
func printJSON(items []map[string]any, out io.Writer) error {
48+
data, err := json.MarshalIndent(items, "", " ")
2149
if err != nil {
2250
return err
2351
}
24-
_, err = fmt.Fprintln(out, string(bytes))
52+
_, err = fmt.Fprintln(out, string(data))
2553
return err
2654
}
2755

28-
func printYAML[T any](items []T, out io.Writer) error {
29-
bytes, err := yaml.Marshal(items)
56+
func printYAML(items []map[string]any, out io.Writer) error {
57+
data, err := yaml.Marshal(items)
3058
if err != nil {
3159
return err
3260
}
33-
_, err = fmt.Fprint(out, string(bytes))
61+
_, err = fmt.Fprint(out, string(data))
3462
return err
3563
}
3664

37-
func printTable[T any](items []T, cols []ColumnDef, out io.Writer) error {
65+
func printTable(items []map[string]any, cols []ColumnDef, out io.Writer) error {
3866
w := tabwriter.NewWriter(out, 0, 0, 4, ' ', 0)
3967

40-
// Print headers
4168
for i, col := range cols {
4269
if i > 0 {
4370
fmt.Fprint(w, "\t")
@@ -46,16 +73,13 @@ func printTable[T any](items []T, cols []ColumnDef, out io.Writer) error {
4673
}
4774
fmt.Fprintln(w)
4875

49-
// Print rows
5076
for _, item := range items {
51-
v := reflect.ValueOf(item)
5277
for i, col := range cols {
5378
if i > 0 {
5479
fmt.Fprint(w, "\t")
5580
}
56-
field := v.FieldByName(col.Field)
57-
if field.IsValid() {
58-
fmt.Fprintf(w, "%v", field.Interface())
81+
if val, ok := fieldValue(item, col.Field); ok {
82+
fmt.Fprint(w, formatValue(val))
5983
}
6084
}
6185
fmt.Fprintln(w)
@@ -64,99 +88,83 @@ func printTable[T any](items []T, cols []ColumnDef, out io.Writer) error {
6488
return w.Flush()
6589
}
6690

67-
func mapJsonPathToField[T any](path string) string {
68-
path = strings.TrimPrefix(path, ".")
69-
70-
typ := reflect.TypeOf(new(T)).Elem()
71-
if typ.Kind() == reflect.Ptr {
72-
typ = typ.Elem()
91+
func formatValue(v any) string {
92+
if f, ok := v.(float64); ok && f == float64(int64(f)) {
93+
return fmt.Sprintf("%d", int64(f))
7394
}
74-
75-
if typ.Kind() != reflect.Struct {
76-
return path // fallback
77-
}
78-
79-
for i := 0; i < typ.NumField(); i++ {
80-
field := typ.Field(i)
81-
jsonTag := field.Tag.Get("json")
82-
if jsonTag != "" {
83-
name := strings.Split(jsonTag, ",")[0]
84-
if name == path {
85-
return field.Name
86-
}
87-
}
88-
}
89-
90-
return path // fallback if no tag matches
95+
return fmt.Sprintf("%v", v)
9196
}
9297

93-
func parseCustomColumns[T any](spec string) []ColumnDef {
98+
func parseCustomColumns(spec string) []ColumnDef {
9499
var cols []ColumnDef
95-
parts := strings.Split(spec, ",")
96-
for _, part := range parts {
100+
for _, part := range strings.Split(spec, ",") {
97101
kv := strings.SplitN(part, ":", 2)
98102
if len(kv) == 2 {
99103
cols = append(cols, ColumnDef{
100104
Header: strings.TrimSpace(kv[0]),
101-
Field: mapJsonPathToField[T](strings.TrimSpace(kv[1])),
105+
Field: normalizeFieldPath(kv[1]),
102106
})
103107
}
104108
}
105109
return cols
106110
}
107111

108-
func printOutput[T any](format string, items []T, cols []ColumnDef, out io.Writer) error {
109-
if strings.HasPrefix(format, "custom-columns=") {
110-
spec := strings.TrimPrefix(format, "custom-columns=")
111-
newCols := parseCustomColumns[T](spec)
112-
if len(newCols) == 0 {
113-
return fmt.Errorf("custom-columns format specified but no columns given")
114-
}
115-
return printTable(items, newCols, out)
112+
func parseCustomColumnsFile(path string) ([]ColumnDef, error) {
113+
content, err := os.ReadFile(path)
114+
if err != nil {
115+
return nil, err
116116
}
117117

118-
if strings.HasPrefix(format, "custom-columns-file=") {
119-
filename := strings.TrimPrefix(format, "custom-columns-file=")
120-
content, err := os.ReadFile(filename)
121-
if err != nil {
122-
return err
118+
lines := strings.Split(string(content), "\n")
119+
var headers, paths []string
120+
for _, line := range lines {
121+
line = strings.TrimSpace(line)
122+
if line == "" {
123+
continue
123124
}
124-
125-
lines := strings.Split(string(content), "\n")
126-
var headers, paths []string
127-
for _, line := range lines {
128-
line = strings.TrimSpace(line)
129-
if line == "" {
130-
continue
131-
}
132-
fields := strings.Fields(line)
133-
if headers == nil {
134-
headers = fields
135-
} else if paths == nil {
136-
paths = fields
137-
}
125+
if headers == nil {
126+
headers = strings.Fields(line)
127+
} else if paths == nil {
128+
paths = strings.Fields(line)
138129
}
130+
}
139131

140-
if len(headers) != len(paths) || len(headers) == 0 {
141-
return fmt.Errorf("invalid custom-columns file format")
142-
}
132+
if len(headers) != len(paths) || len(headers) == 0 {
133+
return nil, fmt.Errorf("invalid custom-columns file format")
134+
}
143135

144-
var specParts []string
145-
for i := range headers {
146-
specParts = append(specParts, headers[i]+":"+paths[i])
147-
}
148-
spec := strings.Join(specParts, ",")
149-
150-
newCols := parseCustomColumns[T](spec)
151-
return printTable(items, newCols, out)
136+
var cols []ColumnDef
137+
for i := range headers {
138+
cols = append(cols, ColumnDef{
139+
Header: headers[i],
140+
Field: normalizeFieldPath(paths[i]),
141+
})
152142
}
143+
return cols, nil
144+
}
153145

154-
switch format {
155-
case "json":
156-
return printJSON(items, out)
157-
case "yaml":
158-
return printYAML(items, out)
159-
default:
160-
return printTable(items, cols, out)
146+
func normalizeFieldPath(path string) string {
147+
return strings.TrimPrefix(strings.TrimSpace(path), ".")
148+
}
149+
150+
func fieldValue(item map[string]any, path string) (any, bool) {
151+
if path == "" {
152+
return nil, false
153+
}
154+
155+
var current any = item
156+
for _, part := range strings.Split(normalizeFieldPath(path), ".") {
157+
if part == "" {
158+
return nil, false
159+
}
160+
m, ok := current.(map[string]any)
161+
if !ok {
162+
return nil, false
163+
}
164+
current, ok = m[part]
165+
if !ok {
166+
return nil, false
167+
}
161168
}
169+
return current, true
162170
}

0 commit comments

Comments
 (0)