From 98ad40fc02bec9509631e153014409bd40662ca3 Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Thu, 2 Apr 2026 15:40:56 +0200 Subject: [PATCH 01/14] projectListCmd: add --format flag --- cmd/management.project.go | 78 ++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/cmd/management.project.go b/cmd/management.project.go index 5b0c796..df12de6 100644 --- a/cmd/management.project.go +++ b/cmd/management.project.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "encoding/json" "fmt" "strings" @@ -36,6 +37,9 @@ func init() { // Add flags for project update command projectUpdateCmd.Flags().String("description", "", "New description for the project") projectUpdateCmd.Flags().StringSlice("tags", []string{}, "Tags for the project (comma-separated)") + + // Add flags for project list command (if needed, e.g., filtering) + projectListCmd.Flags().String("format", "table", "Output format (table, json, yaml)") } // completeProjectID provides completion for project IDs @@ -457,6 +461,12 @@ var projectListCmd = &cobra.Command{ Use: "list", Short: "List all projects", Run: func(cmd *cobra.Command, args []string) { + // Get format flag (defaults to "table" if not provided) + format, _ := cmd.Flags().GetString("format") + if format == "" { + format = "table" + } + // Get SDK client client, err := GetArubaClient() if err != nil { @@ -484,37 +494,55 @@ var projectListCmd = &cobra.Command{ } if response != nil && response.Data != nil && len(response.Data.Values) > 0 { - // Define table columns - headers := []TableColumn{ - {Header: "NAME", Width: 40}, - {Header: "ID", Width: 30}, - {Header: "CREATION DATE", Width: 15}, - } - - // Build rows - var rows [][]string - for _, project := range response.Data.Values { - name := "" - if project.Metadata.Name != nil && *project.Metadata.Name != "" { - name = *project.Metadata.Name + switch format { + case "json": + // Output as JSON + jsonData, err := json.MarshalIndent(response.Data.Values, "", " ") + if err != nil { + fmt.Printf("Error marshaling to JSON: %v\n", err) + return } - - id := "" - if project.Metadata.ID != nil && *project.Metadata.ID != "" { - id = *project.Metadata.ID + fmt.Println(string(jsonData)) + + case "table": + // Define table columns + headers := []TableColumn{ + {Header: "NAME", Width: 40}, + {Header: "ID", Width: 30}, + {Header: "CREATION DATE", Width: 15}, } - // Format creation date as dd-mm-yyyy - creationDate := "N/A" - if !project.Metadata.CreationDate.IsZero() { - creationDate = project.Metadata.CreationDate.Format("02-01-2006") + // Build rows + var rows [][]string + for _, project := range response.Data.Values { + name := "" + if project.Metadata.Name != nil && *project.Metadata.Name != "" { + name = *project.Metadata.Name + } + + id := "" + if project.Metadata.ID != nil && *project.Metadata.ID != "" { + id = *project.Metadata.ID + } + + // Format creation date as dd-mm-yyyy + creationDate := "N/A" + if !project.Metadata.CreationDate.IsZero() { + creationDate = project.Metadata.CreationDate.Format("02-01-2006") + } + + rows = append(rows, []string{name, id, creationDate}) } - rows = append(rows, []string{name, id, creationDate}) - } + // Print the table + PrintTable(headers, rows) - // Print the table - PrintTable(headers, rows) + case "yaml": + fmt.Println("YAML format not yet supported. Please use 'table' or 'json' format.") + + default: + fmt.Printf("Unknown format: %s. Supported formats: table, json, yaml\n", format) + } } else { fmt.Println("No projects found") } From aec2aae3950cb7e32c2557d03330df66c74913f6 Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Fri, 10 Apr 2026 16:27:29 +0200 Subject: [PATCH 02/14] Doc Update: project.md added --format parameter explanation --- docs/website/docs/resources/management/project.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/website/docs/resources/management/project.md b/docs/website/docs/resources/management/project.md index 4ea55ba..7240840 100644 --- a/docs/website/docs/resources/management/project.md +++ b/docs/website/docs/resources/management/project.md @@ -37,6 +37,9 @@ Display all projects in a table format. acloud management project list ``` +**Optional Flags:** +- `--format `: Desired output format for the list (table/json) + **Output:** ``` ID NAME CREATION DATE From 9b15a9c4ab7c0ace668ed3ce3852d6c05adc9485 Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Mon, 13 Apr 2026 11:13:33 +0200 Subject: [PATCH 03/14] Add module: pkg/formaatter/output.go --- pkg/formatter/output.go | 69 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 pkg/formatter/output.go diff --git a/pkg/formatter/output.go b/pkg/formatter/output.go new file mode 100644 index 0000000..b74f6fe --- /dev/null +++ b/pkg/formatter/output.go @@ -0,0 +1,69 @@ +package formatter + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +const ( + FormatTable = "table" + FormatJSON = "json" + FormatYAML = "yaml" +) + +// AddFormatFlag attaches a reusable --format flag to a command. +func AddFormatFlag(cmd *cobra.Command, defaultFormat string) { + if strings.TrimSpace(defaultFormat) == "" { + defaultFormat = FormatTable + } + cmd.Flags().String("format", defaultFormat, "Output format (table, json, yaml)") +} + +// GetOutputFormat reads, normalizes, and validates the --format flag. +func GetOutputFormat(cmd *cobra.Command) (string, error) { + format, _ := cmd.Flags().GetString("format") + format = strings.ToLower(strings.TrimSpace(format)) + if format == "" { + format = FormatTable + } + + switch format { + case FormatTable, FormatJSON, FormatYAML: + return format, nil + default: + return "", fmt.Errorf("unknown format: %s. Supported formats: table, json, yaml", format) + } +} + +// RenderOutput prints the provided data according to the selected format. +// tablePrinter is called only when format=table. +func RenderOutput(format string, data any, tablePrinter func()) error { + switch format { + case FormatJSON: + jsonData, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("error marshaling to JSON: %v", err) + } + fmt.Println(string(jsonData)) + return nil + case FormatYAML: + yamlData, err := yaml.Marshal(data) + if err != nil { + return fmt.Errorf("error marshaling to YAML: %v", err) + } + fmt.Print(string(yamlData)) + return nil + case FormatTable: + if tablePrinter == nil { + return fmt.Errorf("table output requires a table printer") + } + tablePrinter() + return nil + default: + return fmt.Errorf("unknown format: %s. Supported formats: table, json, yaml", format) + } +} From ad64093a6d0c21485949e7b9d44b0a46587c4346 Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Mon, 13 Apr 2026 11:14:42 +0200 Subject: [PATCH 04/14] management.project: reference formatter module --- cmd/management.project.go | 88 +++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 50 deletions(-) diff --git a/cmd/management.project.go b/cmd/management.project.go index df12de6..11fb9b7 100644 --- a/cmd/management.project.go +++ b/cmd/management.project.go @@ -2,10 +2,10 @@ package cmd import ( "context" - "encoding/json" "fmt" "strings" + "acloud/pkg/formatter" "github.com/Arubacloud/sdk-go/pkg/types" "github.com/spf13/cobra" ) @@ -39,7 +39,7 @@ func init() { projectUpdateCmd.Flags().StringSlice("tags", []string{}, "Tags for the project (comma-separated)") // Add flags for project list command (if needed, e.g., filtering) - projectListCmd.Flags().String("format", "table", "Output format (table, json, yaml)") + formatter.AddFormatFlag(projectListCmd, formatter.FormatTable) } // completeProjectID provides completion for project IDs @@ -461,10 +461,10 @@ var projectListCmd = &cobra.Command{ Use: "list", Short: "List all projects", Run: func(cmd *cobra.Command, args []string) { - // Get format flag (defaults to "table" if not provided) - format, _ := cmd.Flags().GetString("format") - if format == "" { - format = "table" + format, err := formatter.GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return } // Get SDK client @@ -493,58 +493,46 @@ var projectListCmd = &cobra.Command{ return } - if response != nil && response.Data != nil && len(response.Data.Values) > 0 { - switch format { - case "json": - // Output as JSON - jsonData, err := json.MarshalIndent(response.Data.Values, "", " ") - if err != nil { - fmt.Printf("Error marshaling to JSON: %v\n", err) - return - } - fmt.Println(string(jsonData)) - - case "table": - // Define table columns - headers := []TableColumn{ - {Header: "NAME", Width: 40}, - {Header: "ID", Width: 30}, - {Header: "CREATION DATE", Width: 15}, - } - - // Build rows - var rows [][]string - for _, project := range response.Data.Values { - name := "" - if project.Metadata.Name != nil && *project.Metadata.Name != "" { - name = *project.Metadata.Name - } + var projects []types.ProjectResponse + if response != nil && response.Data != nil { + projects = response.Data.Values + } - id := "" - if project.Metadata.ID != nil && *project.Metadata.ID != "" { - id = *project.Metadata.ID - } + if err := formatter.RenderOutput(format, projects, func() { + if len(projects) == 0 { + fmt.Println("No projects found") + return + } - // Format creation date as dd-mm-yyyy - creationDate := "N/A" - if !project.Metadata.CreationDate.IsZero() { - creationDate = project.Metadata.CreationDate.Format("02-01-2006") - } + headers := []TableColumn{ + {Header: "NAME", Width: 40}, + {Header: "ID", Width: 30}, + {Header: "CREATION DATE", Width: 15}, + } - rows = append(rows, []string{name, id, creationDate}) + var rows [][]string + for _, project := range projects { + name := "" + if project.Metadata.Name != nil && *project.Metadata.Name != "" { + name = *project.Metadata.Name } - // Print the table - PrintTable(headers, rows) + id := "" + if project.Metadata.ID != nil && *project.Metadata.ID != "" { + id = *project.Metadata.ID + } - case "yaml": - fmt.Println("YAML format not yet supported. Please use 'table' or 'json' format.") + creationDate := "N/A" + if !project.Metadata.CreationDate.IsZero() { + creationDate = project.Metadata.CreationDate.Format("02-01-2006") + } - default: - fmt.Printf("Unknown format: %s. Supported formats: table, json, yaml\n", format) + rows = append(rows, []string{name, id, creationDate}) } - } else { - fmt.Println("No projects found") + + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) } }, } From 16aef91ff4075b13bdfbeedf19bcb17679396654 Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Mon, 13 Apr 2026 17:53:02 +0200 Subject: [PATCH 05/14] Add gout and cobra-gout modules --- cmd/root.go | 131 ++++++- go.mod | 1 + go.sum | 2 + .../drewstinnett/gout/v2/.gitignore | 3 + .../drewstinnett/gout/v2/.golangci.yml | 9 + .../github.com/drewstinnett/gout/v2/LICENSE | 363 ++++++++++++++++++ .../github.com/drewstinnett/gout/v2/README.md | 117 ++++++ .../drewstinnett/gout/v2/config/config.go | 11 + .../gout/v2/formats/builtin/builtin.go | 12 + .../gout/v2/formats/gotemplate/gotemplate.go | 43 +++ .../drewstinnett/gout/v2/formats/json/json.go | 40 ++ .../gout/v2/formats/plain/plain.go | 19 + .../drewstinnett/gout/v2/formats/registry.go | 29 ++ .../drewstinnett/gout/v2/formats/xml/xml.go | 19 + .../drewstinnett/gout/v2/formats/yaml/yaml.go | 18 + .../drewstinnett/gout/v2/formatters.go | 15 + .../github.com/drewstinnett/gout/v2/gout.go | 155 ++++++++ vendor/modules.txt | 11 + 18 files changed, 982 insertions(+), 16 deletions(-) create mode 100644 vendor/github.com/drewstinnett/gout/v2/.gitignore create mode 100644 vendor/github.com/drewstinnett/gout/v2/.golangci.yml create mode 100644 vendor/github.com/drewstinnett/gout/v2/LICENSE create mode 100644 vendor/github.com/drewstinnett/gout/v2/README.md create mode 100644 vendor/github.com/drewstinnett/gout/v2/config/config.go create mode 100644 vendor/github.com/drewstinnett/gout/v2/formats/builtin/builtin.go create mode 100644 vendor/github.com/drewstinnett/gout/v2/formats/gotemplate/gotemplate.go create mode 100644 vendor/github.com/drewstinnett/gout/v2/formats/json/json.go create mode 100644 vendor/github.com/drewstinnett/gout/v2/formats/plain/plain.go create mode 100644 vendor/github.com/drewstinnett/gout/v2/formats/registry.go create mode 100644 vendor/github.com/drewstinnett/gout/v2/formats/xml/xml.go create mode 100644 vendor/github.com/drewstinnett/gout/v2/formats/yaml/yaml.go create mode 100644 vendor/github.com/drewstinnett/gout/v2/formatters.go create mode 100644 vendor/github.com/drewstinnett/gout/v2/gout.go diff --git a/cmd/root.go b/cmd/root.go index 10e31f6..c785988 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,12 +1,16 @@ package cmd import ( + "bytes" "fmt" "log" "os" + "strings" "sync" "github.com/Arubacloud/sdk-go/pkg/aruba" + "github.com/drewstinnett/gout/v2" + "github.com/drewstinnett/gout/v2/formats" "github.com/spf13/cobra" ) @@ -19,6 +23,13 @@ var ( cachedDebug bool cachedBaseURL string cachedTokenIssuer string + outputFormat string +) + +const ( + FormatTable = "table" + FormatJSON = "json" + FormatYAML = "yaml" ) // rootCmd represents the base command when called without any subcommands @@ -51,6 +62,72 @@ func init() { // Add global debug flag rootCmd.PersistentFlags().BoolP("debug", "d", false, "Enable debug logging (shows HTTP requests/responses)") + AddPersistentFormatFlag(rootCmd, FormatTable) +} + +// AddPersistentFormatFlag attaches a reusable persistent --format flag to a command. +func AddPersistentFormatFlag(cmd *cobra.Command, defaultFormat string) { + if strings.TrimSpace(defaultFormat) == "" { + defaultFormat = FormatTable + } + cmd.PersistentFlags().StringVar(&outputFormat, "format", defaultFormat, "Output format (table, json, yaml)") +} + +// GetOutputFormat reads, normalizes, and validates the --format flag. +func GetOutputFormat(cmd *cobra.Command) (string, error) { + format, _ := cmd.Flags().GetString("format") + format = strings.ToLower(strings.TrimSpace(format)) + if format == "" { + format = FormatTable + } + + switch format { + case FormatTable, FormatJSON, FormatYAML: + return format, nil + default: + return "", fmt.Errorf("unknown format: %s. Supported formats: table, json, yaml", format) + } +} + +// RenderOutput prints the provided data according to the selected format. +// tablePrinter is called only when format=table. +func RenderOutput(format string, data any, tablePrinter func()) error { + switch format { + case FormatJSON: + creator, ok := formats.Formats[FormatJSON] + if !ok { + return fmt.Errorf("json formatter not registered in gout") + } + var out bytes.Buffer + g := gout.New(gout.WithWriter(&out), gout.WithFormatter(creator())) + err := g.Print(data) + if err != nil { + return fmt.Errorf("error formatting JSON output: %v", err) + } + fmt.Print(out.String()) + return nil + case FormatYAML: + creator, ok := formats.Formats[FormatYAML] + if !ok { + return fmt.Errorf("yaml formatter not registered in gout") + } + var out bytes.Buffer + g := gout.New(gout.WithWriter(&out), gout.WithFormatter(creator())) + err := g.Print(data) + if err != nil { + return fmt.Errorf("error formatting YAML output: %v", err) + } + fmt.Print(out.String()) + return nil + case FormatTable: + if tablePrinter == nil { + return fmt.Errorf("table output requires a table printer") + } + tablePrinter() + return nil + default: + return fmt.Errorf("unknown format: %s. Supported formats: table, json, yaml", format) + } } // GetArubaClient creates and returns an Aruba Cloud SDK client using stored credentials @@ -164,26 +241,48 @@ type TableColumn struct { // headers: slice of TableColumn defining each column // rows: slice of string slices, each inner slice represents a row func PrintTable(headers []TableColumn, rows [][]string) { - // Print header row - formatStr := "" - headerValues := make([]interface{}, len(headers)) - for i, col := range headers { - formatStr += fmt.Sprintf("%%-%ds ", col.Width) - headerValues[i] = col.Header + format, err := GetOutputFormat(rootCmd) + if err != nil { + fmt.Println(err.Error()) + return } - formatStr += "\n" - fmt.Printf(formatStr, headerValues...) - // Print data rows + data := make([]map[string]string, 0, len(rows)) for _, row := range rows { - rowValues := make([]interface{}, len(row)) - for i, val := range row { - // Truncate if value is too long - if len(headers) > i && len(val) > headers[i].Width { - val = val[:headers[i].Width-3] + "..." + entry := map[string]string{} + for i, col := range headers { + key := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(col.Header), " ", "_")) + value := "" + if i < len(row) { + value = row[i] } - rowValues[i] = val + entry[key] = value } - fmt.Printf(formatStr, rowValues...) + data = append(data, entry) + } + + err = RenderOutput(format, data, func() { + formatStr := "" + headerValues := make([]interface{}, len(headers)) + for i, col := range headers { + formatStr += fmt.Sprintf("%%-%ds ", col.Width) + headerValues[i] = col.Header + } + formatStr += "\n" + fmt.Printf(formatStr, headerValues...) + + for _, row := range rows { + rowValues := make([]interface{}, len(row)) + for i, val := range row { + if len(headers) > i && len(val) > headers[i].Width { + val = val[:headers[i].Width-3] + "..." + } + rowValues[i] = val + } + fmt.Printf(formatStr, rowValues...) + } + }) + if err != nil { + fmt.Println(err.Error()) } } diff --git a/go.mod b/go.mod index 6e48e4a..917e5e1 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.24.2 require ( github.com/Arubacloud/sdk-go v0.1.21 + github.com/drewstinnett/gout/v2 v2.3.0 github.com/spf13/cobra v1.10.2 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 30f9b09..c6b412d 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/drewstinnett/gout/v2 v2.3.0 h1:rX2UM5tvhM75TXIc9KXQfuxOnLP3qV873T4MW2TUes4= +github.com/drewstinnett/gout/v2 v2.3.0/go.mod h1:ZxTVGKOv9mxNxR3TULFD1C/8zV6E6EyIrDT2dahNPzQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= diff --git a/vendor/github.com/drewstinnett/gout/v2/.gitignore b/vendor/github.com/drewstinnett/gout/v2/.gitignore new file mode 100644 index 0000000..a463d40 --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/.gitignore @@ -0,0 +1,3 @@ +*.swp +*.out +coverage.txt diff --git a/vendor/github.com/drewstinnett/gout/v2/.golangci.yml b/vendor/github.com/drewstinnett/gout/v2/.golangci.yml new file mode 100644 index 0000000..8cb08f7 --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/.golangci.yml @@ -0,0 +1,9 @@ +linters: + enable: + - thelper + - gofumpt + - tparallel + - unconvert + - unparam + - wastedassign + - revive diff --git a/vendor/github.com/drewstinnett/gout/v2/LICENSE b/vendor/github.com/drewstinnett/gout/v2/LICENSE new file mode 100644 index 0000000..e87a115 --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/LICENSE @@ -0,0 +1,363 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms of + a Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a + separate file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether + at the time of the initial grant or subsequently, any and all of the + rights conveyed by this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing + basis, if such Contributor fails to notify You of the non-compliance by + some reasonable means prior to 60 days after You have come back into + compliance. Moreover, Your grants from a particular Contributor are + reinstated on an ongoing basis if such Contributor notifies You of the + non-compliance by some reasonable means, this is the first time You have + received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from + such party's negligence to the extent applicable law prohibits such + limitation. Some jurisdictions do not allow the exclusion or limitation of + incidental or consequential damages, so this exclusion and limitation may + not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts + of a jurisdiction where the defendant maintains its principal place of + business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses If You choose to distribute Source Code Form that is + Incompatible With Secondary Licenses under the terms of this version of + the License, the notice described in Exhibit B of this License must be + attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, +then You may include the notice in a location (such as a LICENSE file in a +relevant directory) where a recipient would be likely to look for such a +notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. + diff --git a/vendor/github.com/drewstinnett/gout/v2/README.md b/vendor/github.com/drewstinnett/gout/v2/README.md new file mode 100644 index 0000000..40a7a94 --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/README.md @@ -0,0 +1,117 @@ +# GOUT (Previously go-output-format) + +[![Test](https://github.com/drewstinnett/gout/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/drewstinnett/gout/actions/workflows/test.yml) +[![codecov](https://codecov.io/gh/drewstinnett/gout/branch/main/graph/badge.svg?token=KBITDOWZLQ)](https://codecov.io/gh/drewstinnett/gout) +[![Go Report Card](https://goreportcard.com/badge/github.com/drewstinnett/gout)](https://goreportcard.com/report/github.com/drewstinnett/gout) +[![Go Reference](https://pkg.go.dev/badge/github.com/drewstinnett/gout.svg)](https://pkg.go.dev/github.com/drewstinnett/gout/v2) +[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) + +`gout` is the Go OUTput Formatter for serializing data in a standard way. + +Helper utility to output data structures in to standardized formats, much like +what is built in to [vault](https://www.vaultproject.io/), +[az](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) and +[kubectl](https://kubernetes.io/docs/tasks/tools/) + +I really like how these apps provide for flexible output, but wanted a way to +do it without needing to re-write or copy it for every new tool. + +Need to parse some output with `jq`? JSON is your format. Want to put +it out in an easy to read yet still standardized format? YAML is for you! + +This tool is intended to provide all that in a single reusable package. + +## Usage + +```go +import "github.com/drewstinnett/gout/v2" +``` + +### Builtin + +Gout now comes with a builtin instance, similar to the way Viper does things. + +Example Usage: + +```go +gout.MustPrint(struct { + FirstName string + LastName string +}{ + FirstName: "Bob", + LastName: "Ross", +}) +``` + +Full example code [here](./_examples/builtin/main.go) + +### Custom + +Example Usage: + +```go +// Create a new instance +w, err := gout.New() +if err != nil { + panic(err) +} + +// Use a custom writer +w.SetWriter(os.Stderr) + +// Print something! +w.MustPrint(struct { + FirstName string + LastName string +}{ + FirstName: "Bob", + LastName: "Ross", +}) +// {"FirstName":"Bob","LastName":"Ross"} +``` + +## Built-in Formatters + +### YAML + +Uses the standard `gopkg.in/yaml.v3` library. + +### JSON + +Uses the standard `encoding/json` library. + +### TOML + +Uses `github.com/pelletier/go-toml/v2` library + +### CSV + +Uses `github.com/jszwec/csvutil` library. NOTE: You must pass an iterable +interface in when using this format. It won't do a single struct. + +### XML + +Uses `encoding/xml` library. NOTE: This plugin only works with structs supported +by the base library + +### Plain + +This is just vanilla old Golang output, using the `%+v` format. + +### GoTemplate + +Use this format to parse the data in to a golang template. Useful for spitting +data out in a more arbitrary format. This uses the `text/template` package to +parse each item in the return slice. See [the example +here](_examples/gotemplate/main.go) for full details. + +## Related Projects + +* [Gout-Cobra](https://github.com/drewstinnett/gout-cobra) - Configure a cobra.Command to output using Gout + +## Coming Soon? + +### TSV (Tab Separated Values) + +Intention here is to have a simple way to print out a data structure in a way +that grep and the like can parse it. diff --git a/vendor/github.com/drewstinnett/gout/v2/config/config.go b/vendor/github.com/drewstinnett/gout/v2/config/config.go new file mode 100644 index 0000000..f6ec8c1 --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/config/config.go @@ -0,0 +1,11 @@ +package config + +// type FormatterOpts map[string]interface{} +type FormatterOpts map[string]interface{} + +/* +type FormatterOptsV2 interface { + Set(string, interface{}) error + Get(string) (interface{}, error) +} +*/ diff --git a/vendor/github.com/drewstinnett/gout/v2/formats/builtin/builtin.go b/vendor/github.com/drewstinnett/gout/v2/formats/builtin/builtin.go new file mode 100644 index 0000000..b3bc3bf --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/formats/builtin/builtin.go @@ -0,0 +1,12 @@ +/* +Package builtin is a helper to register all the built in formatters +*/ +package builtin + +import ( + _ "github.com/drewstinnett/gout/v2/formats/gotemplate" + _ "github.com/drewstinnett/gout/v2/formats/json" + _ "github.com/drewstinnett/gout/v2/formats/plain" + _ "github.com/drewstinnett/gout/v2/formats/xml" + _ "github.com/drewstinnett/gout/v2/formats/yaml" +) diff --git a/vendor/github.com/drewstinnett/gout/v2/formats/gotemplate/gotemplate.go b/vendor/github.com/drewstinnett/gout/v2/formats/gotemplate/gotemplate.go new file mode 100644 index 0000000..0e1a400 --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/formats/gotemplate/gotemplate.go @@ -0,0 +1,43 @@ +package gotemplate + +import ( + "bytes" + "errors" + "text/template" + + "github.com/drewstinnett/gout/v2/config" + "github.com/drewstinnett/gout/v2/formats" +) + +type Formatter struct { + // Template string + Opts config.FormatterOpts +} + +// type TemplateField struct{} +func (w Formatter) Format(v interface{}) ([]byte, error) { + var tp string + if t, ok := w.Opts["template"]; !ok { + tp = `{{ printf "%+v" . }}` + } else { + if tp, ok = t.(string); !ok { + return nil, errors.New("Found a template option, but it's not a string") + } + } + var doc bytes.Buffer + tmpl, err := template.New("item").Parse(tp) + if err != nil { + return nil, err + } + err = tmpl.Execute(&doc, v) + if err != nil { + return nil, err + } + return doc.Bytes(), nil +} + +func init() { + formats.Add("gotemplate", func() formats.Formatter { + return &Formatter{} + }) +} diff --git a/vendor/github.com/drewstinnett/gout/v2/formats/json/json.go b/vendor/github.com/drewstinnett/gout/v2/formats/json/json.go new file mode 100644 index 0000000..2218a07 --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/formats/json/json.go @@ -0,0 +1,40 @@ +package json + +import ( + "context" + ujson "encoding/json" + + "github.com/drewstinnett/gout/v2/formats" +) + +type Formatter struct { + ctx context.Context +} + +type IndentField struct{} + +func (w Formatter) Format(v interface{}) ([]byte, error) { + var i any + if w.ctx != nil { + i = w.ctx.Value(IndentField{}) + } + if i == nil { + return ujson.Marshal(v) + } + return ujson.MarshalIndent(v, "", " ") +} + +func (w Formatter) FormatWithContext(ctx context.Context, v interface{}) ([]byte, error) { + return w.withContext(ctx).Format(v) +} + +func (w *Formatter) withContext(ctx context.Context) *Formatter { + w.ctx = ctx + return w +} + +func init() { + formats.Add("json", func() formats.Formatter { + return &Formatter{} + }) +} diff --git a/vendor/github.com/drewstinnett/gout/v2/formats/plain/plain.go b/vendor/github.com/drewstinnett/gout/v2/formats/plain/plain.go new file mode 100644 index 0000000..23d62fb --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/formats/plain/plain.go @@ -0,0 +1,19 @@ +package plain + +import ( + "fmt" + + "github.com/drewstinnett/gout/v2/formats" +) + +type Formatter struct{} + +func (w Formatter) Format(v interface{}) ([]byte, error) { + return []byte(fmt.Sprintf("%+v\n", v)), nil +} + +func init() { + formats.Add("plain", func() formats.Formatter { + return &Formatter{} + }) +} diff --git a/vendor/github.com/drewstinnett/gout/v2/formats/registry.go b/vendor/github.com/drewstinnett/gout/v2/formats/registry.go new file mode 100644 index 0000000..654e1f3 --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/formats/registry.go @@ -0,0 +1,29 @@ +package formats + +import ( + "sort" +) + +type Creator func() Formatter + +var Formats = map[string]Creator{} + +// Add a new format creator +func Add(name string, creator Creator) { + Formats[name] = creator +} + +// Names returns a slice of the names of the formatters +func Names() []string { + ret := []string{} + for k := range Formats { + ret = append(ret, k) + } + sort.Strings(ret) + return ret +} + +// Formatter interface that defines how a thing can be formatted for output +type Formatter interface { + Format(interface{}) ([]byte, error) +} diff --git a/vendor/github.com/drewstinnett/gout/v2/formats/xml/xml.go b/vendor/github.com/drewstinnett/gout/v2/formats/xml/xml.go new file mode 100644 index 0000000..b26f5af --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/formats/xml/xml.go @@ -0,0 +1,19 @@ +package xml + +import ( + uxml "encoding/xml" + + "github.com/drewstinnett/gout/v2/formats" +) + +type Formatter struct{} + +func (w Formatter) Format(v interface{}) ([]byte, error) { + return uxml.Marshal(v) +} + +func init() { + formats.Add("xml", func() formats.Formatter { + return &Formatter{} + }) +} diff --git a/vendor/github.com/drewstinnett/gout/v2/formats/yaml/yaml.go b/vendor/github.com/drewstinnett/gout/v2/formats/yaml/yaml.go new file mode 100644 index 0000000..8099519 --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/formats/yaml/yaml.go @@ -0,0 +1,18 @@ +package yaml + +import ( + "github.com/drewstinnett/gout/v2/formats" + uyaml "gopkg.in/yaml.v3" +) + +type Formatter struct{} + +func (w Formatter) Format(v interface{}) ([]byte, error) { + return uyaml.Marshal(v) +} + +func init() { + formats.Add("yaml", func() formats.Formatter { + return &Formatter{} + }) +} diff --git a/vendor/github.com/drewstinnett/gout/v2/formatters.go b/vendor/github.com/drewstinnett/gout/v2/formatters.go new file mode 100644 index 0000000..c8518d7 --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/formatters.go @@ -0,0 +1,15 @@ +package gout + +// BuiltInFormatters is a map of all formatters that we ship +/* +var BuiltInFormatters = map[string]formats.Formatter{ + "json": json.Formatter{}, + "yaml": yaml.Formatter{}, + "plain": plain.Formatter{}, + "toml": toml.Formatter{}, + "csv": csv.Formatter{}, + "xml": xml.Formatter{}, + "gotemplate": gotemplate.Formatter{}, +} + +*/ diff --git a/vendor/github.com/drewstinnett/gout/v2/gout.go b/vendor/github.com/drewstinnett/gout/v2/gout.go new file mode 100644 index 0000000..fbdba68 --- /dev/null +++ b/vendor/github.com/drewstinnett/gout/v2/gout.go @@ -0,0 +1,155 @@ +package gout + +import ( + "bytes" + "fmt" + "io" + "os" + + "github.com/drewstinnett/gout/v2/formats" + // Include all the builtin formats by default + _ "github.com/drewstinnett/gout/v2/formats/builtin" + "github.com/drewstinnett/gout/v2/formats/yaml" +) + +// Gout is a structure you can use that contains a formatter, and a target +// io.Writer +type Gout struct { + formatter formats.Formatter + writer io.Writer +} + +// Use this for doing things without explicitely creating a new gout, similar to +// viper.Viper +var gi *Gout + +func init() { + gi = New() +} + +// Get gets the default Gout instance +func Get() *Gout { + return gi +} + +// SetWriter set the io.Writer that will be used for printing. By default, this +// will be os.Stdout +func SetWriter(i io.Writer) { gi.SetWriter(i) } + +func (g *Gout) SetWriter(i io.Writer) { + g.writer = i +} + +// SetFormatter sets the Formatter to use for the text. +func SetFormatter(f formats.Formatter) { gi.SetFormatter(f) } + +func (g *Gout) SetFormatter(f formats.Formatter) { + g.formatter = f +} + +// Print print an interface using the given Formatter and io.Writer +func Print(v interface{}) (err error) { return gi.Print(v) } + +func (g *Gout) Print(v interface{}) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("Panic while attempting to format: %v", r) + } + }() + var b []byte + b, err = g.itemizedFormatter(v) + if err != nil { + return err + } + fmt.Fprint(g.writer, string(b)) + return err +} + +// PrintMulti useful when wanting to print multiple interfaces to a single +// serialized item +func PrintMulti(v ...interface{}) (err error) { return gi.PrintMulti(v) } + +func (g *Gout) PrintMulti(v ...interface{}) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("Panic while attempting to format: %v", r) + } + }() + var b []byte + // b, err = c.condensedFormatter(v) + b, err = g.itemizedFormatter(v) + if err != nil { + return err + } + fmt.Fprint(g.writer, string(b)) + return err +} + +// MustPrint print an interface and panic if there is any sort of error +func MustPrint(v interface{}) { gi.MustPrint(v) } + +func (g *Gout) MustPrint(v interface{}) { + err := g.Print(v) + if err != nil { + panic(err) + } +} + +// MustPrintMulti print an multiple interfaces and panic if there is any sort of +// error +func MustPrintMulti(v ...interface{}) { gi.MustPrintMulti(v) } + +func (g *Gout) MustPrintMulti(v ...interface{}) { + err := g.PrintMulti(v) + if err != nil { + panic(err) + } +} + +// Option is an option that can be passed in to help configure a Gout instance +type Option func(*Gout) + +// WithWriter can be passed to New(), specifying which writer should be used for +// output +func WithWriter(w io.Writer) Option { + return func(g *Gout) { + g.writer = w + } +} + +// WithFormatter can be passed to New(), specifying which Formatter should be +// used for output +func WithFormatter(f formats.Formatter) Option { + return func(g *Gout) { + g.formatter = f + } +} + +// New creates a pointer to a new Gout, with some sensible defaults +func New(opts ...Option) *Gout { + g := &Gout{ + formatter: yaml.Formatter{}, + writer: os.Stdout, + } + + for _, opt := range opts { + opt(g) + } + return g +} + +func (g *Gout) itemizedFormatter(v ...interface{}) ([]byte, error) { + var buf bytes.Buffer + for _, item := range v { + bi, err := g.formatter.Format(item) + if err != nil { + return nil, err + } + buf.Write(bi) + } + b := buf.Bytes() + if !bytes.HasSuffix(b, []byte("\n")) { + b = append(b, "\n"...) + } + return b, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 175398a..73182c6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -35,6 +35,17 @@ github.com/cespare/xxhash/v2 # github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f ## explicit github.com/dgryski/go-rendezvous +# github.com/drewstinnett/gout/v2 v2.3.0 +## explicit; go 1.19 +github.com/drewstinnett/gout/v2 +github.com/drewstinnett/gout/v2/config +github.com/drewstinnett/gout/v2/formats +github.com/drewstinnett/gout/v2/formats/builtin +github.com/drewstinnett/gout/v2/formats/gotemplate +github.com/drewstinnett/gout/v2/formats/json +github.com/drewstinnett/gout/v2/formats/plain +github.com/drewstinnett/gout/v2/formats/xml +github.com/drewstinnett/gout/v2/formats/yaml # github.com/go-jose/go-jose/v4 v4.1.3 ## explicit; go 1.24.0 github.com/go-jose/go-jose/v4 From f2f17d723c96635f60408a8220a36612bd02fb4c Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Mon, 13 Apr 2026 17:53:23 +0200 Subject: [PATCH 06/14] Remove custom module formetter --- cmd/management.project.go | 8 ++--- pkg/formatter/output.go | 69 --------------------------------------- 2 files changed, 3 insertions(+), 74 deletions(-) delete mode 100644 pkg/formatter/output.go diff --git a/cmd/management.project.go b/cmd/management.project.go index 11fb9b7..0a1fcd4 100644 --- a/cmd/management.project.go +++ b/cmd/management.project.go @@ -5,7 +5,6 @@ import ( "fmt" "strings" - "acloud/pkg/formatter" "github.com/Arubacloud/sdk-go/pkg/types" "github.com/spf13/cobra" ) @@ -38,8 +37,7 @@ func init() { projectUpdateCmd.Flags().String("description", "", "New description for the project") projectUpdateCmd.Flags().StringSlice("tags", []string{}, "Tags for the project (comma-separated)") - // Add flags for project list command (if needed, e.g., filtering) - formatter.AddFormatFlag(projectListCmd, formatter.FormatTable) + // Output format is provided globally via root persistent --format flag. } // completeProjectID provides completion for project IDs @@ -461,7 +459,7 @@ var projectListCmd = &cobra.Command{ Use: "list", Short: "List all projects", Run: func(cmd *cobra.Command, args []string) { - format, err := formatter.GetOutputFormat(cmd) + format, err := GetOutputFormat(cmd) if err != nil { fmt.Println(err.Error()) return @@ -498,7 +496,7 @@ var projectListCmd = &cobra.Command{ projects = response.Data.Values } - if err := formatter.RenderOutput(format, projects, func() { + if err := RenderOutput(format, projects, func() { if len(projects) == 0 { fmt.Println("No projects found") return diff --git a/pkg/formatter/output.go b/pkg/formatter/output.go deleted file mode 100644 index b74f6fe..0000000 --- a/pkg/formatter/output.go +++ /dev/null @@ -1,69 +0,0 @@ -package formatter - -import ( - "encoding/json" - "fmt" - "strings" - - "github.com/spf13/cobra" - "gopkg.in/yaml.v3" -) - -const ( - FormatTable = "table" - FormatJSON = "json" - FormatYAML = "yaml" -) - -// AddFormatFlag attaches a reusable --format flag to a command. -func AddFormatFlag(cmd *cobra.Command, defaultFormat string) { - if strings.TrimSpace(defaultFormat) == "" { - defaultFormat = FormatTable - } - cmd.Flags().String("format", defaultFormat, "Output format (table, json, yaml)") -} - -// GetOutputFormat reads, normalizes, and validates the --format flag. -func GetOutputFormat(cmd *cobra.Command) (string, error) { - format, _ := cmd.Flags().GetString("format") - format = strings.ToLower(strings.TrimSpace(format)) - if format == "" { - format = FormatTable - } - - switch format { - case FormatTable, FormatJSON, FormatYAML: - return format, nil - default: - return "", fmt.Errorf("unknown format: %s. Supported formats: table, json, yaml", format) - } -} - -// RenderOutput prints the provided data according to the selected format. -// tablePrinter is called only when format=table. -func RenderOutput(format string, data any, tablePrinter func()) error { - switch format { - case FormatJSON: - jsonData, err := json.MarshalIndent(data, "", " ") - if err != nil { - return fmt.Errorf("error marshaling to JSON: %v", err) - } - fmt.Println(string(jsonData)) - return nil - case FormatYAML: - yamlData, err := yaml.Marshal(data) - if err != nil { - return fmt.Errorf("error marshaling to YAML: %v", err) - } - fmt.Print(string(yamlData)) - return nil - case FormatTable: - if tablePrinter == nil { - return fmt.Errorf("table output requires a table printer") - } - tablePrinter() - return nil - default: - return fmt.Errorf("unknown format: %s. Supported formats: table, json, yaml", format) - } -} From 837dd1e404592813f4e2c24fecfacb27f599dd7d Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Tue, 14 Apr 2026 09:54:03 +0200 Subject: [PATCH 07/14] e2e tests: add project_list output examples json and yaml --- e2e/management/project_list.json | 60 ++++++++++++++++++++ e2e/management/project_list.yaml | 95 ++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 e2e/management/project_list.json create mode 100644 e2e/management/project_list.yaml diff --git a/e2e/management/project_list.json b/e2e/management/project_list.json new file mode 100644 index 0000000..0eb2868 --- /dev/null +++ b/e2e/management/project_list.json @@ -0,0 +1,60 @@ +[ + { + "metadata": { + "id": "xxxxxxxxxxxxxxxxxxxxx", + "name": "default", + "creationDate": "2026-03-26T10:57:16.047Z", + "createdBy": "aru-123456" + }, + "properties": { + "description": "default project", + "default": true + } + }, + { + "metadata": { + "id": "xxxxxxxxxxxxxxxxxxxxx", + "name": "default-baremetal", + "creationDate": "2026-03-26T10:57:16.09Z", + "createdBy": "aru-123456" + }, + "properties": { + "description": "default-baremetal default project", + "default": false + } + }, + { + "metadata": { + "id": "xxxxxxxxxxxxxxxxxxxxx", + "name": "cloud-v1", + "creationDate": "2026-03-26T10:57:16.13Z", + "createdBy": "aru-123456" + }, + "properties": { + "description": "cloud-v1 default project", + "default": false + } + }, + { + "metadata": { + "id": "xxxxxxxxxxxxxxxxxxxxx", + "name": "project-1-dev", + "creationDate": "2026-04-09T13:23:08.378Z", + "createdBy": "aru-123456" + }, + "properties": { + "default": false + } + }, + { + "metadata": { + "id": "xxxxxxxxxxxxxxxxxxxxx", + "name": "project-2-dev", + "creationDate": "2026-04-09T13:56:58.434Z", + "createdBy": "aru-123456" + }, + "properties": { + "default": false + } + } +] \ No newline at end of file diff --git a/e2e/management/project_list.yaml b/e2e/management/project_list.yaml new file mode 100644 index 0000000..5fa1b13 --- /dev/null +++ b/e2e/management/project_list.yaml @@ -0,0 +1,95 @@ +- metadata: + id: xxxxxxxxxxxxxxxxxx + uri: null + name: default + locationresponse: null + projectresponsemetadata: null + tags: [] + category: null + creationdate: 2026-03-26T10:57:16.047Z + createdby: aru-123456 + updatedate: null + updatedby: null + version: null + createduser: null + updateduser: null + properties: + description: default project + default: true + resourcesnumber: 0 +- metadata: + id: xxxxxxxxxxxxxxxxxx + uri: null + name: default-baremetal + locationresponse: null + projectresponsemetadata: null + tags: [] + category: null + creationdate: 2026-03-26T10:57:16.09Z + createdby: aru-123456 + updatedate: null + updatedby: null + version: null + createduser: null + updateduser: null + properties: + description: default-baremetal default project + default: false + resourcesnumber: 0 +- metadata: + id: xxxxxxxxxxxxxxxxxx + uri: null + name: cloud-v1 + locationresponse: null + projectresponsemetadata: null + tags: [] + category: null + creationdate: 2026-03-26T10:57:16.13Z + createdby: aru-123456 + updatedate: null + updatedby: null + version: null + createduser: null + updateduser: null + properties: + description: cloud-v1 default project + default: false + resourcesnumber: 0 +- metadata: + id: xxxxxxxxxxxxxxxxxx + uri: null + name: project-1-dev + locationresponse: null + projectresponsemetadata: null + tags: [] + category: null + creationdate: 2026-04-09T13:23:08.378Z + createdby: aru-123456 + updatedate: null + updatedby: null + version: null + createduser: null + updateduser: null + properties: + description: null + default: false + resourcesnumber: 0 +- metadata: + id: xxxxxxxxxxxxxxxxxx + uri: null + name: project-2-dev + locationresponse: null + projectresponsemetadata: null + tags: [] + category: null + creationdate: 2026-04-09T13:56:58.434Z + createdby: aru-123456 + updatedate: null + updatedby: null + version: null + createduser: null + updateduser: null + properties: + description: null + default: false + resourcesnumber: 0 From c9f4985245c9f3d07dc1d0a6361fa547cff7a165 Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Tue, 14 Apr 2026 17:01:55 +0200 Subject: [PATCH 08/14] cmd context: add --format flag --- cmd/context.go | 82 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/cmd/context.go b/cmd/context.go index d244671..2f7becf 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -11,14 +11,20 @@ import ( // Context represents the CLI context configuration type Context struct { - CurrentContext string `yaml:"current-context"` - Contexts map[string]CtxInfo `yaml:"contexts"` + CurrentContext string `yaml:"current-context" json:"current_context"` + Contexts map[string]CtxInfo `yaml:"contexts" json:"contexts"` } // CtxInfo represents a single context type CtxInfo struct { - ProjectID string `yaml:"project-id"` - Name string `yaml:"name,omitempty"` + ProjectID string `yaml:"project-id" json:"project_id"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` +} + +// contextCurrentOutput is the structured output for the current context command +type contextCurrentOutput struct { + Name string `yaml:"name" json:"name"` + ProjectID string `yaml:"project_id" json:"project_id"` } var contextCmd = &cobra.Command{ @@ -71,6 +77,12 @@ var contextUseCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { contextName := args[0] + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + // Load context ctx, err := LoadContext() if err != nil { @@ -97,8 +109,17 @@ var contextUseCmd = &cobra.Command{ return } - fmt.Printf("Switched to context '%s'\n", contextName) - fmt.Printf("Project ID: %s\n", ctx.Contexts[contextName].ProjectID) + output := contextCurrentOutput{ + Name: contextName, + ProjectID: ctx.Contexts[contextName].ProjectID, + } + + if err := RenderOutput(format, output, func() { + fmt.Printf("Switched to context '%s'\n", contextName) + fmt.Printf("Project ID: %s\n", ctx.Contexts[contextName].ProjectID) + }); err != nil { + fmt.Println(err.Error()) + } }, } @@ -106,6 +127,12 @@ var contextListCmd = &cobra.Command{ Use: "list", Short: "List all contexts", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + // Load context ctx, err := LoadContext() if err != nil { @@ -118,17 +145,21 @@ var contextListCmd = &cobra.Command{ return } - fmt.Println("\nContexts:") - fmt.Println("=========") - for name, info := range ctx.Contexts { - current := "" - if name == ctx.CurrentContext { - current = " *" + if err := RenderOutput(format, ctx, func() { + fmt.Println("\nContexts:") + fmt.Println("=========") + for name, info := range ctx.Contexts { + current := "" + if name == ctx.CurrentContext { + current = " *" + } + fmt.Printf("%-20s Project ID: %s%s\n", name, info.ProjectID, current) } - fmt.Printf("%-20s Project ID: %s%s\n", name, info.ProjectID, current) - } - if ctx.CurrentContext != "" { - fmt.Printf("\n* = current context\n") + if ctx.CurrentContext != "" { + fmt.Printf("\n* = current context\n") + } + }); err != nil { + fmt.Println(err.Error()) } }, } @@ -137,6 +168,12 @@ var contextCurrentCmd = &cobra.Command{ Use: "current", Short: "Show current context", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + // Load context ctx, err := LoadContext() if err != nil || ctx.CurrentContext == "" { @@ -150,8 +187,17 @@ var contextCurrentCmd = &cobra.Command{ return } - fmt.Printf("Current context: %s\n", ctx.CurrentContext) - fmt.Printf("Project ID: %s\n", info.ProjectID) + output := contextCurrentOutput{ + Name: ctx.CurrentContext, + ProjectID: info.ProjectID, + } + + if err := RenderOutput(format, output, func() { + fmt.Printf("Current context: %s\n", ctx.CurrentContext) + fmt.Printf("Project ID: %s\n", info.ProjectID) + }); err != nil { + fmt.Println(err.Error()) + } }, } From ed41f5f06452437a645f643f1f31e1c407caca3a Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Tue, 14 Apr 2026 17:03:06 +0200 Subject: [PATCH 09/14] test: context cmd e2e tests --- e2e/context/list.json | 1 + e2e/context/list.yaml | 4 + e2e/context/test.sh | 279 ++++++++++++++++++++++++++++++++++++++++++ e2e/context/use.json | 4 + e2e/context/use.yaml | 2 + 5 files changed, 290 insertions(+) create mode 100644 e2e/context/list.json create mode 100644 e2e/context/list.yaml create mode 100755 e2e/context/test.sh create mode 100644 e2e/context/use.json create mode 100644 e2e/context/use.yaml diff --git a/e2e/context/list.json b/e2e/context/list.json new file mode 100644 index 0000000..ab9ff5f --- /dev/null +++ b/e2e/context/list.json @@ -0,0 +1 @@ +{"current_context":"dev","contexts":{"dev":{"project_id":"xxxxxxxxxxxxxxxxxxxxxxxx"}}} diff --git a/e2e/context/list.yaml b/e2e/context/list.yaml new file mode 100644 index 0000000..e384852 --- /dev/null +++ b/e2e/context/list.yaml @@ -0,0 +1,4 @@ +current-context: dev +contexts: + dev: + project-id: xxxxxxxxxxxxxxxxxxxxxxxx diff --git a/e2e/context/test.sh b/e2e/context/test.sh new file mode 100755 index 0000000..e15538d --- /dev/null +++ b/e2e/context/test.sh @@ -0,0 +1,279 @@ +#!/bin/bash + +# E2E Test Script for context commands +# Validates --format json and --format yaml output structure +# against the fixture files list.json, list.yaml, use.json, use.yaml + +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../../acloud" ]; then + ACLOUD_CMD="$SCRIPT_DIR/../../acloud" +elif [ -f "./acloud" ]; then + ACLOUD_CMD="./acloud" +elif command -v acloud >/dev/null 2>&1; then + ACLOUD_CMD="acloud" +else + ACLOUD_CMD="${ACLOUD_CMD:-./acloud}" +fi + +echo -e "${BLUE}=== Context Command E2E Test ===${NC}\n" +echo "ACLOUD command: $ACLOUD_CMD" +echo "Fixtures dir: $SCRIPT_DIR" +echo "" + +PASS=0 +FAIL=0 + +pass() { echo -e "${GREEN}✓ $1${NC}"; PASS=$((PASS + 1)); } +fail() { echo -e "${RED}✗ $1${NC}"; FAIL=$((FAIL + 1)); } + +# --------------------------------------------------------------------------- +# JSON key extraction (no external deps beyond python3/python/jq) +# --------------------------------------------------------------------------- +json_keys() { + local input="$1" + if command -v jq >/dev/null 2>&1; then + echo "$input" | jq -r 'keys[]' 2>/dev/null + elif command -v python3 >/dev/null 2>&1; then + echo "$input" | python3 -c "import sys,json; [print(k) for k in json.load(sys.stdin).keys()]" 2>/dev/null + elif command -v python >/dev/null 2>&1; then + echo "$input" | python -c "import sys,json; [print(k) for k in json.load(sys.stdin).keys()]" 2>/dev/null + fi +} + +is_valid_json() { + local input="$1" + if command -v python3 >/dev/null 2>&1; then + echo "$input" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null + elif command -v python >/dev/null 2>&1; then + echo "$input" | python -c "import sys,json; json.load(sys.stdin)" 2>/dev/null + else + echo "$input" | grep -qE '^\{|^\[' 2>/dev/null + fi +} + +# Returns the top-level keys of the fixture JSON file +fixture_json_keys() { + local fixture="$1" + json_keys "$(cat "$fixture")" +} + +# --------------------------------------------------------------------------- +# test_context_list_json +# --------------------------------------------------------------------------- +test_context_list_json() { + echo -e "${YELLOW}--- context list --format json ---${NC}" + local fixture="$SCRIPT_DIR/list.json" + + OUTPUT=$($ACLOUD_CMD context list --format json 2>&1) + EXIT_CODE=$? + if [ $EXIT_CODE -ne 0 ]; then + fail "context list --format json: command failed (exit $EXIT_CODE)" + echo " Output: $OUTPUT" + echo "" + return + fi + + # Must be valid JSON + if ! is_valid_json "$OUTPUT"; then + fail "context list --format json: output is not valid JSON" + echo " Output: $OUTPUT" + echo "" + return + fi + pass "context list --format json: output is valid JSON" + + # Validate that all top-level keys from the fixture are present in the output + local missing=0 + while IFS= read -r key; do + [ -z "$key" ] && continue + if echo "$OUTPUT" | grep -q "\"$key\""; then + pass "context list --format json: key '$key' present" + else + fail "context list --format json: key '$key' missing (expected from fixture)" + missing=$((missing + 1)) + fi + done < <(fixture_json_keys "$fixture") + + echo "" +} + +# --------------------------------------------------------------------------- +# test_context_list_yaml +# --------------------------------------------------------------------------- +test_context_list_yaml() { + echo -e "${YELLOW}--- context list --format yaml ---${NC}" + local fixture="$SCRIPT_DIR/list.yaml" + + OUTPUT=$($ACLOUD_CMD context list --format yaml 2>&1) + EXIT_CODE=$? + if [ $EXIT_CODE -ne 0 ]; then + fail "context list --format yaml: command failed (exit $EXIT_CODE)" + echo " Output: $OUTPUT" + echo "" + return + fi + + if [ -z "$OUTPUT" ]; then + fail "context list --format yaml: output is empty" + echo "" + return + fi + pass "context list --format yaml: output is non-empty" + + # Must NOT look like JSON + if echo "$OUTPUT" | head -1 | grep -qE '^\{|^\['; then + fail "context list --format yaml: output looks like JSON, not YAML" + echo "" + return + fi + pass "context list --format yaml: output does not start with JSON braces" + + # Validate that all top-level keys from the fixture are present as YAML keys + while IFS= read -r line; do + # Extract "key:" lines from the fixture (top-level, no leading spaces) + key=$(echo "$line" | grep -E '^[a-zA-Z_-][a-zA-Z0-9_-]*:' | sed 's/:.*//') + [ -z "$key" ] && continue + if echo "$OUTPUT" | grep -qE "^${key}:"; then + pass "context list --format yaml: key '$key' present" + else + fail "context list --format yaml: key '$key' missing (expected from fixture)" + fi + done < "$fixture" + + echo "" +} + +# --------------------------------------------------------------------------- +# test_context_use_json +# --------------------------------------------------------------------------- +test_context_use_json() { + echo -e "${YELLOW}--- context use --format json ---${NC}" + local fixture="$SCRIPT_DIR/use.json" + + # Determine a context name to use from list output + CONTEXT_NAME=$($ACLOUD_CMD context list --format json 2>/dev/null \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(list(d.get('contexts',{}).keys())[0])" 2>/dev/null \ + || $ACLOUD_CMD context list --format json 2>/dev/null \ + | python -c "import sys,json; d=json.load(sys.stdin); print(list(d.get('contexts',{}).keys())[0])" 2>/dev/null) + + if [ -z "$CONTEXT_NAME" ]; then + echo -e " ${YELLOW}Skipping: could not determine a context name to use${NC}" + echo "" + return + fi + + OUTPUT=$($ACLOUD_CMD context use "$CONTEXT_NAME" --format json 2>&1) + EXIT_CODE=$? + if [ $EXIT_CODE -ne 0 ]; then + fail "context use --format json: command failed (exit $EXIT_CODE)" + echo " Output: $OUTPUT" + echo "" + return + fi + + if ! is_valid_json "$OUTPUT"; then + fail "context use --format json: output is not valid JSON" + echo " Output: $OUTPUT" + echo "" + return + fi + pass "context use --format json: output is valid JSON" + + # Validate that all top-level keys from the fixture are present + while IFS= read -r key; do + [ -z "$key" ] && continue + if echo "$OUTPUT" | grep -q "\"$key\""; then + pass "context use --format json: key '$key' present" + else + fail "context use --format json: key '$key' missing (expected from fixture)" + fi + done < <(fixture_json_keys "$fixture") + + echo "" +} + +# --------------------------------------------------------------------------- +# test_context_use_yaml +# --------------------------------------------------------------------------- +test_context_use_yaml() { + echo -e "${YELLOW}--- context use --format yaml ---${NC}" + local fixture="$SCRIPT_DIR/use.yaml" + + CONTEXT_NAME=$($ACLOUD_CMD context list --format json 2>/dev/null \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(list(d.get('contexts',{}).keys())[0])" 2>/dev/null \ + || $ACLOUD_CMD context list --format json 2>/dev/null \ + | python -c "import sys,json; d=json.load(sys.stdin); print(list(d.get('contexts',{}).keys())[0])" 2>/dev/null) + + if [ -z "$CONTEXT_NAME" ]; then + echo -e " ${YELLOW}Skipping: could not determine a context name to use${NC}" + echo "" + return + fi + + OUTPUT=$($ACLOUD_CMD context use "$CONTEXT_NAME" --format yaml 2>&1) + EXIT_CODE=$? + if [ $EXIT_CODE -ne 0 ]; then + fail "context use --format yaml: command failed (exit $EXIT_CODE)" + echo " Output: $OUTPUT" + echo "" + return + fi + + if [ -z "$OUTPUT" ]; then + fail "context use --format yaml: output is empty" + echo "" + return + fi + pass "context use --format yaml: output is non-empty" + + if echo "$OUTPUT" | head -1 | grep -qE '^\{|^\['; then + fail "context use --format yaml: output looks like JSON, not YAML" + echo "" + return + fi + pass "context use --format yaml: output does not start with JSON braces" + + # Validate top-level keys from fixture are present + while IFS= read -r line; do + key=$(echo "$line" | grep -E '^[a-zA-Z_-][a-zA-Z0-9_-]*:' | sed 's/:.*//') + [ -z "$key" ] && continue + if echo "$OUTPUT" | grep -qE "^${key}:"; then + pass "context use --format yaml: key '$key' present" + else + fail "context use --format yaml: key '$key' missing (expected from fixture)" + fi + done < "$fixture" + + echo "" +} + +# --------------------------------------------------------------------------- +# Run +# --------------------------------------------------------------------------- +echo -e "${BLUE}Starting Context E2E Tests...${NC}\n" + +test_context_list_json +test_context_list_yaml +test_context_use_json +test_context_use_yaml + +echo -e "${BLUE}=== Test Summary ===${NC}" +echo -e "${GREEN}Passed: $PASS${NC}" +if [ $FAIL -gt 0 ]; then + echo -e "${RED}Failed: $FAIL${NC}" +else + echo -e "${GREEN}Failed: $FAIL${NC}" +fi +echo "" + +if [ $FAIL -gt 0 ]; then + exit 1 +else + exit 0 +fi diff --git a/e2e/context/use.json b/e2e/context/use.json new file mode 100644 index 0000000..393004c --- /dev/null +++ b/e2e/context/use.json @@ -0,0 +1,4 @@ +{ + "name": "dev", + "project_id": "xxxxxxxxxxxxxxxxxxxxxxxx" +} \ No newline at end of file diff --git a/e2e/context/use.yaml b/e2e/context/use.yaml new file mode 100644 index 0000000..49eb3cc --- /dev/null +++ b/e2e/context/use.yaml @@ -0,0 +1,2 @@ +name: dev +project_id: xxxxxxxxxxxxxxxxxxxxxxxx From cb99f80ba2aae24c343743d935c5430991f2d980 Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Tue, 14 Apr 2026 17:04:56 +0200 Subject: [PATCH 10/14] test: context list cmd e2e json structure update --- e2e/context/list.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/e2e/context/list.json b/e2e/context/list.json index ab9ff5f..e676ff9 100644 --- a/e2e/context/list.json +++ b/e2e/context/list.json @@ -1 +1,8 @@ -{"current_context":"dev","contexts":{"dev":{"project_id":"xxxxxxxxxxxxxxxxxxxxxxxx"}}} +{ + "current_context": "dev", + "contexts": { + "dev": { + "project_id": "xxxxxxxxxxxxxxxxxxxxxxxx" + } + } +} \ No newline at end of file From b2fc5f96fe64c38e8cc3382dff95389a20caabde Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Tue, 14 Apr 2026 19:11:09 +0200 Subject: [PATCH 11/14] CMD Compute: add --format support to cloudserver and keypair funct --- cmd/compute.cloudserver.go | 24 ++++++++++++++++++++++-- cmd/compute.keypair.go | 23 +++++++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/cmd/compute.cloudserver.go b/cmd/compute.cloudserver.go index 62ca8df..a7c19af 100644 --- a/cmd/compute.cloudserver.go +++ b/cmd/compute.cloudserver.go @@ -264,6 +264,12 @@ var cloudserverCreateCmd = &cobra.Command{ return } + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + if response != nil && response.Data != nil { headers := []TableColumn{ {Header: "ID", Width: 30}, @@ -298,7 +304,11 @@ var cloudserverCreateCmd = &cobra.Command{ fmt.Sprintf("%d", hd), regionValue, } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, response.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("Cloud server created, but no data returned.") } @@ -572,6 +582,12 @@ var cloudserverListCmd = &cobra.Command{ Use: "list", Short: "List all cloud servers", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -651,7 +667,11 @@ var cloudserverListCmd = &cobra.Command{ }) } - PrintTable(headers, rows) + if err := RenderOutput(format, response.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No cloud servers found") } diff --git a/cmd/compute.keypair.go b/cmd/compute.keypair.go index fd29f56..8f5b071 100644 --- a/cmd/compute.keypair.go +++ b/cmd/compute.keypair.go @@ -140,6 +140,11 @@ var keypairCreateCmd = &cobra.Command{ } if response != nil && response.Data != nil { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "NAME", Width: 40}, {Header: "PUBLIC_KEY", Width: 60}, @@ -157,7 +162,11 @@ var keypairCreateCmd = &cobra.Command{ }(), publicKeyValue, } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, response.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("Keypair created, but no data returned.") } @@ -311,6 +320,12 @@ var keypairListCmd = &cobra.Command{ Use: "list", Short: "List all keypairs", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -384,7 +399,11 @@ var keypairListCmd = &cobra.Command{ rows = append(rows, []string{name, id, publicKey, status}) } - PrintTable(headers, rows) + if err := RenderOutput(format, response.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No keypairs found") } From fd604944129736ea3b3622e136a2a6b81b1fa698 Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Tue, 14 Apr 2026 19:12:28 +0200 Subject: [PATCH 12/14] Test: Compute e2e test + examplke outputs --- e2e/compute/cloudserver_list.json | 25 ++++++ e2e/compute/cloudserver_list.yaml | 16 ++++ e2e/compute/keypair_list.json | 11 +++ e2e/compute/keypair_list.yaml | 5 ++ e2e/compute/test.sh | 133 +++++++++++++++++++++++++++++- 5 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 e2e/compute/cloudserver_list.json create mode 100644 e2e/compute/cloudserver_list.yaml create mode 100644 e2e/compute/keypair_list.json create mode 100644 e2e/compute/keypair_list.yaml mode change 100644 => 100755 e2e/compute/test.sh diff --git a/e2e/compute/cloudserver_list.json b/e2e/compute/cloudserver_list.json new file mode 100644 index 0000000..00b3bb8 --- /dev/null +++ b/e2e/compute/cloudserver_list.json @@ -0,0 +1,25 @@ +[ + { + "metadata": { + "id": "xxxxxxxxxxxxxxxxxxxxxxxx", + "name": "test-server", + "locationResponse": { + "value": "ITBG-Bergamo" + } + }, + "properties": { + "dataCenter": "itbg1-a", + "flavor": { + "id": "xxxxxxxxxxxxxxxxxxxxxxxx", + "name": "small", + "category": "compute", + "cpu": 2, + "ram": 4096, + "hd": 50 + } + }, + "status": { + "state": "Running" + } + } +] diff --git a/e2e/compute/cloudserver_list.yaml b/e2e/compute/cloudserver_list.yaml new file mode 100644 index 0000000..31dfac1 --- /dev/null +++ b/e2e/compute/cloudserver_list.yaml @@ -0,0 +1,16 @@ +- metadata: + id: xxxxxxxxxxxxxxxxxxxxxxxx + name: test-server + locationresponse: + value: ITBG-Bergamo + properties: + datacenter: itbg1-a + flavor: + id: xxxxxxxxxxxxxxxxxxxxxxxx + name: small + category: compute + cpu: 2 + ram: 4096 + hd: 50 + status: + state: Running diff --git a/e2e/compute/keypair_list.json b/e2e/compute/keypair_list.json new file mode 100644 index 0000000..1a693a8 --- /dev/null +++ b/e2e/compute/keypair_list.json @@ -0,0 +1,11 @@ +[ + { + "metadata": { + "id": "xxxxxxxxxxxxxxxxxxxxxxxx", + "name": "test-keypair" + }, + "properties": { + "value": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... test@example.com" + } + } +] diff --git a/e2e/compute/keypair_list.yaml b/e2e/compute/keypair_list.yaml new file mode 100644 index 0000000..077cad3 --- /dev/null +++ b/e2e/compute/keypair_list.yaml @@ -0,0 +1,5 @@ +- metadata: + id: xxxxxxxxxxxxxxxxxxxxxxxx + name: test-keypair + properties: + value: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... test@example.com diff --git a/e2e/compute/test.sh b/e2e/compute/test.sh old mode 100644 new mode 100755 index 8f633cb..7bfa021 --- a/e2e/compute/test.sh +++ b/e2e/compute/test.sh @@ -48,11 +48,22 @@ extract_id() { echo "$output" | grep -oE '[a-f0-9]{24}' | tail -1 || echo "" } +# Function to check if a string is valid JSON +is_valid_json() { + local input="$1" + if command -v python3 >/dev/null 2>&1; then + echo "$input" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null && return 0 + elif command -v python >/dev/null 2>&1; then + echo "$input" | python -c "import sys,json; json.load(sys.stdin)" 2>/dev/null && return 0 + fi + return 1 +} + # Function to test Cloud Server operations test_cloudserver() { local server_name="${RESOURCE_PREFIX}-server" local flavor="${ACLOUD_FLAVOR:-small}" - local image="${ACLOUD_IMAGE:-your-image-id}" + local image="${ACLOUD_IMAGE:-LU24-DK01}" echo -e "${BLUE}--- Testing Cloud Server Operations ---${NC}" @@ -222,6 +233,124 @@ test_keypair() { echo "" } +# Function to test Cloud Server --format flag +test_cloudserver_format() { + echo -e "${BLUE}--- Testing Cloud Server --format flag ---${NC}" + + echo -e "${YELLOW}Testing cloudserver list --format json...${NC}" + JSON_OUTPUT=$($ACLOUD_CMD compute cloudserver list --project-id "$PROJECT_ID" --format json 2>&1) + JSON_EXIT=$? + + if [ $JSON_EXIT -ne 0 ]; then + echo -e "${RED}✗ cloudserver list --format json: command failed (exit $JSON_EXIT)${NC}" + echo "$JSON_OUTPUT" + elif echo "$JSON_OUTPUT" | grep -qF "No cloud servers found"; then + echo -e "${YELLOW}⚠ cloudserver list --format json: no resources — format validation skipped${NC}" + elif is_valid_json "$JSON_OUTPUT"; then + echo -e "${GREEN}✓ cloudserver list --format json: valid JSON${NC}" + if echo "$JSON_OUTPUT" | grep -q '"metadata"'; then + echo -e "${GREEN}✓ cloudserver list --format json: 'metadata' key present${NC}" + else + echo -e "${RED}✗ cloudserver list --format json: 'metadata' key missing${NC}" + fi + if echo "$JSON_OUTPUT" | grep -q '"properties"'; then + echo -e "${GREEN}✓ cloudserver list --format json: 'properties' key present${NC}" + else + echo -e "${RED}✗ cloudserver list --format json: 'properties' key missing${NC}" + fi + else + echo -e "${RED}✗ cloudserver list --format json: output is not valid JSON${NC}" + echo "$JSON_OUTPUT" + fi + + echo -e "${YELLOW}Testing cloudserver list --format yaml...${NC}" + YAML_OUTPUT=$($ACLOUD_CMD compute cloudserver list --project-id "$PROJECT_ID" --format yaml 2>&1) + YAML_EXIT=$? + + if [ $YAML_EXIT -ne 0 ]; then + echo -e "${RED}✗ cloudserver list --format yaml: command failed (exit $YAML_EXIT)${NC}" + echo "$YAML_OUTPUT" + elif echo "$YAML_OUTPUT" | grep -qF "No cloud servers found"; then + echo -e "${YELLOW}⚠ cloudserver list --format yaml: no resources — format validation skipped${NC}" + elif echo "$YAML_OUTPUT" | grep -qE '^[a-zA-Z].*:|^- '; then + echo -e "${GREEN}✓ cloudserver list --format yaml: output looks like YAML${NC}" + if echo "$YAML_OUTPUT" | grep -q 'metadata:'; then + echo -e "${GREEN}✓ cloudserver list --format yaml: 'metadata' key present${NC}" + else + echo -e "${RED}✗ cloudserver list --format yaml: 'metadata' key missing${NC}" + fi + if echo "$YAML_OUTPUT" | grep -q 'properties:'; then + echo -e "${GREEN}✓ cloudserver list --format yaml: 'properties' key present${NC}" + else + echo -e "${RED}✗ cloudserver list --format yaml: 'properties' key missing${NC}" + fi + else + echo -e "${RED}✗ cloudserver list --format yaml: output does not look like YAML${NC}" + echo "$YAML_OUTPUT" + fi + + echo "" +} + +# Function to test Key Pair --format flag +test_keypair_format() { + echo -e "${BLUE}--- Testing Key Pair --format flag ---${NC}" + + echo -e "${YELLOW}Testing keypair list --format json...${NC}" + JSON_OUTPUT=$($ACLOUD_CMD compute keypair list --project-id "$PROJECT_ID" --format json 2>&1) + JSON_EXIT=$? + + if [ $JSON_EXIT -ne 0 ]; then + echo -e "${RED}✗ keypair list --format json: command failed (exit $JSON_EXIT)${NC}" + echo "$JSON_OUTPUT" + elif echo "$JSON_OUTPUT" | grep -qF "No keypairs found"; then + echo -e "${YELLOW}⚠ keypair list --format json: no resources — format validation skipped${NC}" + elif is_valid_json "$JSON_OUTPUT"; then + echo -e "${GREEN}✓ keypair list --format json: valid JSON${NC}" + if echo "$JSON_OUTPUT" | grep -q '"metadata"'; then + echo -e "${GREEN}✓ keypair list --format json: 'metadata' key present${NC}" + else + echo -e "${RED}✗ keypair list --format json: 'metadata' key missing${NC}" + fi + if echo "$JSON_OUTPUT" | grep -q '"properties"'; then + echo -e "${GREEN}✓ keypair list --format json: 'properties' key present${NC}" + else + echo -e "${RED}✗ keypair list --format json: 'properties' key missing${NC}" + fi + else + echo -e "${RED}✗ keypair list --format json: output is not valid JSON${NC}" + echo "$JSON_OUTPUT" + fi + + echo -e "${YELLOW}Testing keypair list --format yaml...${NC}" + YAML_OUTPUT=$($ACLOUD_CMD compute keypair list --project-id "$PROJECT_ID" --format yaml 2>&1) + YAML_EXIT=$? + + if [ $YAML_EXIT -ne 0 ]; then + echo -e "${RED}✗ keypair list --format yaml: command failed (exit $YAML_EXIT)${NC}" + echo "$YAML_OUTPUT" + elif echo "$YAML_OUTPUT" | grep -qF "No keypairs found"; then + echo -e "${YELLOW}⚠ keypair list --format yaml: no resources — format validation skipped${NC}" + elif echo "$YAML_OUTPUT" | grep -qE '^[a-zA-Z].*:|^- '; then + echo -e "${GREEN}✓ keypair list --format yaml: output looks like YAML${NC}" + if echo "$YAML_OUTPUT" | grep -q 'metadata:'; then + echo -e "${GREEN}✓ keypair list --format yaml: 'metadata' key present${NC}" + else + echo -e "${RED}✗ keypair list --format yaml: 'metadata' key missing${NC}" + fi + if echo "$YAML_OUTPUT" | grep -q 'properties:'; then + echo -e "${GREEN}✓ keypair list --format yaml: 'properties' key present${NC}" + else + echo -e "${RED}✗ keypair list --format yaml: 'properties' key missing${NC}" + fi + else + echo -e "${RED}✗ keypair list --format yaml: output does not look like YAML${NC}" + echo "$YAML_OUTPUT" + fi + + echo "" +} + # Cleanup function cleanup() { echo -e "${BLUE}--- Cleanup ---${NC}" @@ -257,6 +386,8 @@ trap cleanup EXIT # Run tests test_cloudserver test_keypair +test_cloudserver_format +test_keypair_format # Summary echo -e "${BLUE}=== Test Summary ===${NC}" From 5bfd4c0b224f2fda4ad0498441700fcd358e11d0 Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Wed, 15 Apr 2026 11:10:30 +0200 Subject: [PATCH 13/14] CMD: add --format flag support to all commands --- cmd/container.containerregistry.go | 11 +++++++++- cmd/container.kaas.go | 23 ++++++++++++++++++-- cmd/database.backup.go | 23 ++++++++++++++++++-- cmd/database.dbaas.database.go | 11 +++++++++- cmd/database.dbaas.go | 23 ++++++++++++++++++-- cmd/database.dbaas.user.go | 11 +++++++++- cmd/network.elasticip.go | 12 +++++++++- cmd/network.loadbalancer.go | 12 +++++++++- cmd/network.securitygroup.go | 33 +++++++++++++++++++++++++--- cmd/network.securityrule.go | 35 +++++++++++++++++++++++++++--- cmd/network.subnet.go | 33 +++++++++++++++++++++++++--- cmd/network.vpc.go | 12 +++++++++- cmd/network.vpcpeering.go | 33 +++++++++++++++++++++++++--- cmd/network.vpcpeeringroute.go | 35 +++++++++++++++++++++++++++--- cmd/network.vpnroute.go | 35 +++++++++++++++++++++++++++--- cmd/network.vpntunnel.go | 12 +++++++++- cmd/schedule.job.go | 23 ++++++++++++++++++-- cmd/security.kms.go | 23 ++++++++++++++++++-- cmd/storage.backup.go | 12 +++++++++- cmd/storage.blockstorage.go | 13 +++++++++-- cmd/storage.restore.go | 12 +++++++++- cmd/storage.snapshot.go | 11 +++++++++- 22 files changed, 408 insertions(+), 40 deletions(-) diff --git a/cmd/container.containerregistry.go b/cmd/container.containerregistry.go index 1d93246..041ddc0 100644 --- a/cmd/container.containerregistry.go +++ b/cmd/container.containerregistry.go @@ -612,6 +612,11 @@ var containerregistryListCmd = &cobra.Command{ } if response.Data != nil && len(response.Data.Values) > 0 { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "NAME", Width: 40}, {Header: "ID", Width: 30}, @@ -649,7 +654,11 @@ var containerregistryListCmd = &cobra.Command{ }) } - PrintTable(headers, rows) + if err := RenderOutput(format, response.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No container registries found") } diff --git a/cmd/container.kaas.go b/cmd/container.kaas.go index 52240d9..2c13178 100644 --- a/cmd/container.kaas.go +++ b/cmd/container.kaas.go @@ -284,6 +284,11 @@ var kaasCreateCmd = &cobra.Command{ } if response != nil && response.Data != nil { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "ID", Width: 30}, {Header: "NAME", Width: 40}, @@ -315,7 +320,11 @@ var kaasCreateCmd = &cobra.Command{ return "" }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, response.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("KaaS cluster created, but no data returned.") } @@ -693,6 +702,12 @@ var kaasListCmd = &cobra.Command{ Use: "list", Short: "List all KaaS clusters", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -753,7 +768,11 @@ var kaasListCmd = &cobra.Command{ }) } - PrintTable(headers, rows) + if err := RenderOutput(format, response.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No KaaS clusters found") } diff --git a/cmd/database.backup.go b/cmd/database.backup.go index c7b3291..9c13854 100644 --- a/cmd/database.backup.go +++ b/cmd/database.backup.go @@ -189,6 +189,11 @@ var backupCreateCmd = &cobra.Command{ } if response != nil && response.Data != nil { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "ID", Width: 30}, {Header: "NAME", Width: 40}, @@ -221,7 +226,11 @@ var backupCreateCmd = &cobra.Command{ return "" }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, response.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("Backup created, but no data returned.") } @@ -308,6 +317,12 @@ var backupListCmd = &cobra.Command{ Use: "list", Short: "List all database backups", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -376,7 +391,11 @@ var backupListCmd = &cobra.Command{ } rows = append(rows, row) } - PrintTable(headers, rows) + if err := RenderOutput(format, resp.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No backups found") } diff --git a/cmd/database.dbaas.database.go b/cmd/database.dbaas.database.go index 4c6017b..328d3fd 100644 --- a/cmd/database.dbaas.database.go +++ b/cmd/database.dbaas.database.go @@ -240,6 +240,11 @@ var dbaasDatabaseListCmd = &cobra.Command{ } if resp != nil && resp.Data != nil && len(resp.Data.Values) > 0 { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "NAME", Width: 40}, {Header: "CREATION DATE", Width: 25}, @@ -265,7 +270,11 @@ var dbaasDatabaseListCmd = &cobra.Command{ } rows = append(rows, row) } - PrintTable(headers, rows) + if err := RenderOutput(format, resp.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No databases found") } diff --git a/cmd/database.dbaas.go b/cmd/database.dbaas.go index a88279c..3a9a79b 100644 --- a/cmd/database.dbaas.go +++ b/cmd/database.dbaas.go @@ -156,6 +156,11 @@ var dbaasCreateCmd = &cobra.Command{ } if response != nil && response.Data != nil { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "ID", Width: 30}, {Header: "NAME", Width: 40}, @@ -202,7 +207,11 @@ var dbaasCreateCmd = &cobra.Command{ return "" }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, response.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("DBaaS instance created, but no data returned.") } @@ -303,6 +312,12 @@ var dbaasListCmd = &cobra.Command{ Use: "list", Short: "List all DBaaS instances", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -392,7 +407,11 @@ var dbaasListCmd = &cobra.Command{ } rows = append(rows, row) } - PrintTable(headers, rows) + if err := RenderOutput(format, resp.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No DBaaS instances found") } diff --git a/cmd/database.dbaas.user.go b/cmd/database.dbaas.user.go index 1809350..3937305 100644 --- a/cmd/database.dbaas.user.go +++ b/cmd/database.dbaas.user.go @@ -244,6 +244,11 @@ var dbaasUserListCmd = &cobra.Command{ } if resp != nil && resp.Data != nil && len(resp.Data.Values) > 0 { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "USERNAME", Width: 40}, {Header: "CREATION DATE", Width: 25}, @@ -269,7 +274,11 @@ var dbaasUserListCmd = &cobra.Command{ } rows = append(rows, row) } - PrintTable(headers, rows) + if err := RenderOutput(format, resp.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No users found") } diff --git a/cmd/network.elasticip.go b/cmd/network.elasticip.go index ff9a6b1..47b70f3 100644 --- a/cmd/network.elasticip.go +++ b/cmd/network.elasticip.go @@ -179,6 +179,12 @@ var elasticipListCmd = &cobra.Command{ Use: "list", Short: "List all Elastic IPs", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + // Get project ID from flag or context projectID, err := GetProjectID(cmd) if err != nil { @@ -240,7 +246,11 @@ var elasticipListCmd = &cobra.Command{ } // Print the table - PrintTable(headers, rows) + if err := RenderOutput(format, response.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No Elastic IPs found") } diff --git a/cmd/network.loadbalancer.go b/cmd/network.loadbalancer.go index 51a8d15..65be68a 100644 --- a/cmd/network.loadbalancer.go +++ b/cmd/network.loadbalancer.go @@ -71,6 +71,12 @@ var loadbalancerListCmd = &cobra.Command{ Use: "list", Short: "List all Load Balancers", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + // Get project ID from flag or context projectID, err := GetProjectID(cmd) if err != nil { @@ -132,7 +138,11 @@ var loadbalancerListCmd = &cobra.Command{ } // Print the table - PrintTable(headers, rows) + if err := RenderOutput(format, response.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No Load Balancers found") } diff --git a/cmd/network.securitygroup.go b/cmd/network.securitygroup.go index 9edbdf4..03ad5cf 100644 --- a/cmd/network.securitygroup.go +++ b/cmd/network.securitygroup.go @@ -44,6 +44,11 @@ var securitygroupCreateCmd = &cobra.Command{ fmt.Println("Error: --name and --region are required") return } + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -95,7 +100,11 @@ var securitygroupCreateCmd = &cobra.Command{ } }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, resp.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("Security group created, but no ID returned.") } @@ -177,6 +186,11 @@ var securitygroupListCmd = &cobra.Command{ Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { vpcID := args[0] + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -230,7 +244,11 @@ var securitygroupListCmd = &cobra.Command{ } rows = append(rows, []string{name, id, region, status}) } - PrintTable(headers, rows) + if err := RenderOutput(format, resp.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No security groups found.") } @@ -321,6 +339,11 @@ var securitygroupUpdateCmd = &cobra.Command{ return } if resp != nil && resp.Data != nil { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "NAME", Width: 30}, {Header: "ID", Width: 26}, @@ -348,7 +371,11 @@ var securitygroupUpdateCmd = &cobra.Command{ return "" }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, resp.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Printf("Security group '%s' updated.\n", sgID) } diff --git a/cmd/network.securityrule.go b/cmd/network.securityrule.go index b5366ee..37965d4 100644 --- a/cmd/network.securityrule.go +++ b/cmd/network.securityrule.go @@ -116,6 +116,12 @@ var securityruleCreateCmd = &cobra.Command{ targetValue, _ := cmd.Flags().GetString("target-value") verbose, _ := cmd.Flags().GetBool("verbose") + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + // Validate required fields if name == "" { fmt.Println("Error: --name is required") @@ -235,7 +241,11 @@ var securityruleCreateCmd = &cobra.Command{ return "" }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, resp.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("Security rule created, but no ID returned.") } @@ -332,6 +342,12 @@ var securityruleListCmd = &cobra.Command{ vpcID := args[0] securityGroupID := args[1] + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -395,7 +411,11 @@ var securityruleListCmd = &cobra.Command{ } rows = append(rows, []string{name, id, direction, protocol, port, target, status}) } - PrintTable(headers, rows) + if err := RenderOutput(format, resp.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No security rules found.") } @@ -569,6 +589,11 @@ var securityruleUpdateCmd = &cobra.Command{ } if resp != nil && resp.Data != nil { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "NAME", Width: 30}, {Header: "ID", Width: 26}, @@ -600,7 +625,11 @@ var securityruleUpdateCmd = &cobra.Command{ return "" }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, resp.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Printf("Security rule '%s' updated.\n", securityRuleID) } diff --git a/cmd/network.subnet.go b/cmd/network.subnet.go index 758d5f9..25bdfde 100644 --- a/cmd/network.subnet.go +++ b/cmd/network.subnet.go @@ -161,6 +161,11 @@ var subnetCreateCmd = &cobra.Command{ return } if resp != nil && resp.Data != nil && resp.Data.Metadata.ID != nil { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "NAME", Width: 30}, {Header: "ID", Width: 26}, @@ -190,7 +195,11 @@ var subnetCreateCmd = &cobra.Command{ } }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, resp.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("Subnet created, but no ID returned.") } @@ -318,6 +327,11 @@ var subnetListCmd = &cobra.Command{ return } if resp != nil && resp.Data != nil && len(resp.Data.Values) > 0 { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "NAME", Width: 30}, {Header: "ID", Width: 26}, @@ -346,7 +360,11 @@ var subnetListCmd = &cobra.Command{ } rows = append(rows, []string{name, id, region, cidr, status}) } - PrintTable(headers, rows) + if err := RenderOutput(format, resp.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No subnets found.") } @@ -515,6 +533,11 @@ var subnetUpdateCmd = &cobra.Command{ return } if resp != nil && resp.Data != nil { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "NAME", Width: 30}, {Header: "ID", Width: 26}, @@ -537,7 +560,11 @@ var subnetUpdateCmd = &cobra.Command{ if resp.Data.Status.State != nil { status = *resp.Data.Status.State } - PrintTable(headers, [][]string{{name, id, cidr, status}}) + if err := RenderOutput(format, resp.Data, func() { + PrintTable(headers, [][]string{{name, id, cidr, status}}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Printf("Subnet '%s' updated.\n", subnetID) } diff --git a/cmd/network.vpc.go b/cmd/network.vpc.go index 3db3d32..56c0c03 100644 --- a/cmd/network.vpc.go +++ b/cmd/network.vpc.go @@ -420,6 +420,12 @@ var vpcListCmd = &cobra.Command{ Use: "list", Short: "List all VPCs", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + // Get SDK client client, err := GetArubaClient() if err != nil { @@ -478,7 +484,11 @@ var vpcListCmd = &cobra.Command{ } // Print the table - PrintTable(headers, rows) + if err := RenderOutput(format, response.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No VPCs found") } diff --git a/cmd/network.vpcpeering.go b/cmd/network.vpcpeering.go index 7af6f81..02f0536 100644 --- a/cmd/network.vpcpeering.go +++ b/cmd/network.vpcpeering.go @@ -58,6 +58,11 @@ var vpcpeeringCreateCmd = &cobra.Command{ fmt.Println("Error: --name, --peer-vpc-id, and --region are required") return } + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -127,7 +132,11 @@ var vpcpeeringCreateCmd = &cobra.Command{ } }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, resp.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("VPC peering created, but no ID returned.") } @@ -209,6 +218,11 @@ var vpcpeeringListCmd = &cobra.Command{ Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { vpcID := args[0] + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -267,7 +281,11 @@ var vpcpeeringListCmd = &cobra.Command{ } rows = append(rows, []string{name, id, peerVPC, region, status}) } - PrintTable(headers, rows) + if err := RenderOutput(format, resp.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No VPC peerings found.") } @@ -362,6 +380,11 @@ var vpcpeeringUpdateCmd = &cobra.Command{ return } if resp != nil && resp.Data != nil { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "NAME", Width: 30}, {Header: "ID", Width: 26}, @@ -401,7 +424,11 @@ var vpcpeeringUpdateCmd = &cobra.Command{ return "" }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, resp.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Printf("VPC peering '%s' updated.\n", peeringID) } diff --git a/cmd/network.vpcpeeringroute.go b/cmd/network.vpcpeeringroute.go index a18c222..7bb3c79 100644 --- a/cmd/network.vpcpeeringroute.go +++ b/cmd/network.vpcpeeringroute.go @@ -119,6 +119,12 @@ var vpcpeeringrouteCreateCmd = &cobra.Command{ return } + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -195,7 +201,11 @@ var vpcpeeringrouteCreateCmd = &cobra.Command{ return "" }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, resp.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("VPC peering route created, but no data returned.") } @@ -274,6 +284,12 @@ var vpcpeeringrouteListCmd = &cobra.Command{ vpcID := args[0] peeringID := args[1] + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -322,7 +338,11 @@ var vpcpeeringrouteListCmd = &cobra.Command{ } rows = append(rows, []string{name, localNetwork, remoteNetwork, status}) } - PrintTable(headers, rows) + if err := RenderOutput(format, resp.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No VPC peering routes found.") } @@ -440,6 +460,11 @@ var vpcpeeringrouteUpdateCmd = &cobra.Command{ } if resp != nil && resp.Data != nil { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "NAME", Width: 30}, {Header: "LOCAL NETWORK", Width: 18}, @@ -457,7 +482,11 @@ var vpcpeeringrouteUpdateCmd = &cobra.Command{ return "" }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, resp.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Printf("VPC peering route '%s' updated.\n", routeID) } diff --git a/cmd/network.vpnroute.go b/cmd/network.vpnroute.go index ea36505..840fc25 100644 --- a/cmd/network.vpnroute.go +++ b/cmd/network.vpnroute.go @@ -107,6 +107,12 @@ var vpnrouteCreateCmd = &cobra.Command{ tags, _ := cmd.Flags().GetStringSlice("tags") verbose, _ := cmd.Flags().GetBool("verbose") + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + // Validate required fields if name == "" { fmt.Println("Error: --name is required") @@ -205,7 +211,11 @@ var vpnrouteCreateCmd = &cobra.Command{ return "" }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, resp.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("VPN route created, but no ID returned.") } @@ -295,6 +305,12 @@ var vpnrouteListCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { vpnTunnelID := args[0] + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -351,7 +367,11 @@ var vpnrouteListCmd = &cobra.Command{ } rows = append(rows, []string{name, id, cloudSubnet, onPremSubnet, status}) } - PrintTable(headers, rows) + if err := RenderOutput(format, resp.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No VPN routes found.") } @@ -477,6 +497,11 @@ var vpnrouteUpdateCmd = &cobra.Command{ } if resp != nil && resp.Data != nil { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "NAME", Width: 30}, {Header: "ID", Width: 26}, @@ -506,7 +531,11 @@ var vpnrouteUpdateCmd = &cobra.Command{ return "" }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, resp.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Printf("VPN route '%s' updated.\n", routeID) } diff --git a/cmd/network.vpntunnel.go b/cmd/network.vpntunnel.go index ee13e9d..84a04dc 100644 --- a/cmd/network.vpntunnel.go +++ b/cmd/network.vpntunnel.go @@ -111,6 +111,12 @@ var vpntunnelListCmd = &cobra.Command{ Use: "list", Short: "List all VPN tunnels", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + // Get SDK client client, err := GetArubaClient() if err != nil { @@ -171,7 +177,11 @@ var vpntunnelListCmd = &cobra.Command{ } // Print the table - PrintTable(headers, rows) + if err := RenderOutput(format, response.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No VPN tunnels found") } diff --git a/cmd/schedule.job.go b/cmd/schedule.job.go index 016a51b..e4d52fc 100644 --- a/cmd/schedule.job.go +++ b/cmd/schedule.job.go @@ -187,6 +187,11 @@ var jobCreateCmd = &cobra.Command{ } if response != nil && response.Data != nil { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "ID", Width: 30}, {Header: "NAME", Width: 40}, @@ -226,7 +231,11 @@ var jobCreateCmd = &cobra.Command{ return "" }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, response.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("Job created, but no data returned.") } @@ -324,6 +333,12 @@ var jobListCmd = &cobra.Command{ Use: "list", Short: "List all scheduled jobs", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -406,7 +421,11 @@ var jobListCmd = &cobra.Command{ } rows = append(rows, row) } - PrintTable(headers, rows) + if err := RenderOutput(format, resp.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No jobs found") } diff --git a/cmd/security.kms.go b/cmd/security.kms.go index 0c5352c..84cf483 100644 --- a/cmd/security.kms.go +++ b/cmd/security.kms.go @@ -144,6 +144,11 @@ var kmsCreateCmd = &cobra.Command{ } if response != nil && response.Data != nil { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } headers := []TableColumn{ {Header: "ID", Width: 30}, {Header: "NAME", Width: 40}, @@ -176,7 +181,11 @@ var kmsCreateCmd = &cobra.Command{ return "" }(), } - PrintTable(headers, [][]string{row}) + if err := RenderOutput(format, response.Data, func() { + PrintTable(headers, [][]string{row}) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("KMS created, but no data returned.") } @@ -263,6 +272,12 @@ var kmsListCmd = &cobra.Command{ Use: "list", Short: "List all KMS resources", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -331,7 +346,11 @@ var kmsListCmd = &cobra.Command{ } rows = append(rows, row) } - PrintTable(headers, rows) + if err := RenderOutput(format, resp.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No KMS resources found") } diff --git a/cmd/storage.backup.go b/cmd/storage.backup.go index d80ab30..f9c5cc9 100644 --- a/cmd/storage.backup.go +++ b/cmd/storage.backup.go @@ -229,6 +229,12 @@ var storageBackupListCmd = &cobra.Command{ Use: "list", Short: "List storage backups", Run: func(cmd *cobra.Command, args []string) { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -278,7 +284,11 @@ var storageBackupListCmd = &cobra.Command{ rows = append(rows, []string{name, id, backupType, status}) } - PrintTable(headers, rows) + if err := RenderOutput(format, response.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No backups found") } diff --git a/cmd/storage.blockstorage.go b/cmd/storage.blockstorage.go index 90d97f4..4267a53 100644 --- a/cmd/storage.blockstorage.go +++ b/cmd/storage.blockstorage.go @@ -528,6 +528,12 @@ var blockstorageListCmd = &cobra.Command{ // Get flags verbose, _ := cmd.Flags().GetBool("verbose") + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + // Get SDK client client, err := GetArubaClient() if err != nil { @@ -623,8 +629,11 @@ var blockstorageListCmd = &cobra.Command{ rows = append(rows, []string{name, id, size, region, zone, volumeType, status}) } - // Print the table - PrintTable(headers, rows) + if err := RenderOutput(format, response.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No block storage found") } diff --git a/cmd/storage.restore.go b/cmd/storage.restore.go index 7ac61ae..a1788f6 100644 --- a/cmd/storage.restore.go +++ b/cmd/storage.restore.go @@ -239,6 +239,12 @@ var storageRestoreListCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { backupID := args[0] + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } + projectID, err := GetProjectID(cmd) if err != nil { fmt.Printf("Error: %v\n", err) @@ -285,7 +291,11 @@ var storageRestoreListCmd = &cobra.Command{ rows = append(rows, []string{name, id, status}) } - PrintTable(headers, rows) + if err := RenderOutput(format, response.Data.Values, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No restores found for this backup") } diff --git a/cmd/storage.snapshot.go b/cmd/storage.snapshot.go index 4ea3e6d..ba80066 100644 --- a/cmd/storage.snapshot.go +++ b/cmd/storage.snapshot.go @@ -476,6 +476,11 @@ var snapshotListCmd = &cobra.Command{ } if response != nil && response.Data != nil && len(response.Data.Values) > 0 { + format, err := GetOutputFormat(cmd) + if err != nil { + fmt.Println(err.Error()) + return + } // Filter snapshots by volume URI var filteredSnapshots []types.SnapshotResponse for _, snapshot := range response.Data.Values { @@ -521,7 +526,11 @@ var snapshotListCmd = &cobra.Command{ } // Print the table - PrintTable(headers, rows) + if err := RenderOutput(format, filteredSnapshots, func() { + PrintTable(headers, rows) + }); err != nil { + fmt.Println(err.Error()) + } } else { fmt.Println("No snapshots found") } From 0b85c8e364795c2806bd5de5b06a20486fbcb4e9 Mon Sep 17 00:00:00 2001 From: Teodoro PICCINNI Date: Wed, 15 Apr 2026 11:11:33 +0200 Subject: [PATCH 14/14] Test: Extended e2e tests to test --format flag --- e2e/compute/test.sh | 208 +++-- e2e/container/test.sh | 99 +++ e2e/context/test.sh | 40 +- e2e/database/test.sh | 99 +++ e2e/management/test.sh | 93 +++ e2e/network/test.sh | 1635 +++++++++++++++++++++------------------- e2e/schedule/test.sh | 99 +++ e2e/security/test.sh | 99 +++ e2e/storage/test.sh | 99 +++ 9 files changed, 1582 insertions(+), 889 deletions(-) mode change 100644 => 100755 e2e/container/test.sh mode change 100644 => 100755 e2e/database/test.sh mode change 100644 => 100755 e2e/management/test.sh mode change 100644 => 100755 e2e/network/test.sh mode change 100644 => 100755 e2e/schedule/test.sh mode change 100644 => 100755 e2e/security/test.sh mode change 100644 => 100755 e2e/storage/test.sh diff --git a/e2e/compute/test.sh b/e2e/compute/test.sh index 7bfa021..ebae501 100755 --- a/e2e/compute/test.sh +++ b/e2e/compute/test.sh @@ -59,6 +59,92 @@ is_valid_json() { return 1 } +# Generic --format flag test helper +# Usage: test_format_flags "Label" "no resources msg" cmd arg1 arg2... +test_format_flags() { + local label="$1" + local no_resources_msg="$2" + shift 2 + local cmd_args=("$@") + + echo -e "${BLUE}--- Testing $label --format flag ---${NC}" + + for fmt in "" table; do + local lbl="--format \"$fmt\"" + [ -z "$fmt" ] && lbl='--format "" (default)' + echo -e "${YELLOW}Testing $lbl...${NC}" + if [ -z "$fmt" ]; then + OUT=$($ACLOUD_CMD "${cmd_args[@]}" 2>&1) + else + OUT=$($ACLOUD_CMD "${cmd_args[@]}" --format "$fmt" 2>&1) + fi + EXIT=$? + if [ $EXIT -eq 0 ]; then + echo -e "${GREEN}✓ $lbl: command succeeded${NC}" + if ! is_valid_json "$OUT" || echo "$OUT" | grep -qF "$no_resources_msg"; then + echo -e "${GREEN}✓ $lbl: output is table/plain (not JSON)${NC}" + else + echo -e "${RED}✗ $lbl: output unexpectedly looks like JSON${NC}" + fi + else + echo -e "${RED}✗ $lbl: command failed (exit $EXIT)${NC}" + echo "$OUT" + fi + done + + echo -e "${YELLOW}Testing --format json...${NC}" + JSON_OUTPUT=$($ACLOUD_CMD "${cmd_args[@]}" --format json 2>&1) + JSON_EXIT=$? + if [ $JSON_EXIT -ne 0 ]; then + echo -e "${RED}✗ --format json: command failed (exit $JSON_EXIT)${NC}" + echo "$JSON_OUTPUT" + elif echo "$JSON_OUTPUT" | grep -qF "$no_resources_msg"; then + echo -e "${YELLOW}⚠ --format json: no resources — format validation skipped${NC}" + elif is_valid_json "$JSON_OUTPUT"; then + echo -e "${GREEN}✓ --format json: valid JSON${NC}" + if echo "$JSON_OUTPUT" | grep -q '"metadata"'; then + echo -e "${GREEN}✓ --format json: 'metadata' key present${NC}" + else + echo -e "${RED}✗ --format json: 'metadata' key missing${NC}" + fi + if echo "$JSON_OUTPUT" | grep -q '"properties"'; then + echo -e "${GREEN}✓ --format json: 'properties' key present${NC}" + else + echo -e "${RED}✗ --format json: 'properties' key missing${NC}" + fi + else + echo -e "${RED}✗ --format json: output is not valid JSON${NC}" + echo "$JSON_OUTPUT" + fi + + echo -e "${YELLOW}Testing --format yaml...${NC}" + YAML_OUTPUT=$($ACLOUD_CMD "${cmd_args[@]}" --format yaml 2>&1) + YAML_EXIT=$? + if [ $YAML_EXIT -ne 0 ]; then + echo -e "${RED}✗ --format yaml: command failed (exit $YAML_EXIT)${NC}" + echo "$YAML_OUTPUT" + elif echo "$YAML_OUTPUT" | grep -qF "$no_resources_msg"; then + echo -e "${YELLOW}⚠ --format yaml: no resources — format validation skipped${NC}" + elif echo "$YAML_OUTPUT" | grep -qE '^[a-zA-Z].*:|^- '; then + echo -e "${GREEN}✓ --format yaml: output looks like YAML${NC}" + if echo "$YAML_OUTPUT" | grep -q 'metadata:'; then + echo -e "${GREEN}✓ --format yaml: 'metadata' key present${NC}" + else + echo -e "${RED}✗ --format yaml: 'metadata' key missing${NC}" + fi + if echo "$YAML_OUTPUT" | grep -q 'properties:'; then + echo -e "${GREEN}✓ --format yaml: 'properties' key present${NC}" + else + echo -e "${RED}✗ --format yaml: 'properties' key missing${NC}" + fi + else + echo -e "${RED}✗ --format yaml: output does not look like YAML${NC}" + echo "$YAML_OUTPUT" + fi + + echo "" +} + # Function to test Cloud Server operations test_cloudserver() { local server_name="${RESOURCE_PREFIX}-server" @@ -233,124 +319,6 @@ test_keypair() { echo "" } -# Function to test Cloud Server --format flag -test_cloudserver_format() { - echo -e "${BLUE}--- Testing Cloud Server --format flag ---${NC}" - - echo -e "${YELLOW}Testing cloudserver list --format json...${NC}" - JSON_OUTPUT=$($ACLOUD_CMD compute cloudserver list --project-id "$PROJECT_ID" --format json 2>&1) - JSON_EXIT=$? - - if [ $JSON_EXIT -ne 0 ]; then - echo -e "${RED}✗ cloudserver list --format json: command failed (exit $JSON_EXIT)${NC}" - echo "$JSON_OUTPUT" - elif echo "$JSON_OUTPUT" | grep -qF "No cloud servers found"; then - echo -e "${YELLOW}⚠ cloudserver list --format json: no resources — format validation skipped${NC}" - elif is_valid_json "$JSON_OUTPUT"; then - echo -e "${GREEN}✓ cloudserver list --format json: valid JSON${NC}" - if echo "$JSON_OUTPUT" | grep -q '"metadata"'; then - echo -e "${GREEN}✓ cloudserver list --format json: 'metadata' key present${NC}" - else - echo -e "${RED}✗ cloudserver list --format json: 'metadata' key missing${NC}" - fi - if echo "$JSON_OUTPUT" | grep -q '"properties"'; then - echo -e "${GREEN}✓ cloudserver list --format json: 'properties' key present${NC}" - else - echo -e "${RED}✗ cloudserver list --format json: 'properties' key missing${NC}" - fi - else - echo -e "${RED}✗ cloudserver list --format json: output is not valid JSON${NC}" - echo "$JSON_OUTPUT" - fi - - echo -e "${YELLOW}Testing cloudserver list --format yaml...${NC}" - YAML_OUTPUT=$($ACLOUD_CMD compute cloudserver list --project-id "$PROJECT_ID" --format yaml 2>&1) - YAML_EXIT=$? - - if [ $YAML_EXIT -ne 0 ]; then - echo -e "${RED}✗ cloudserver list --format yaml: command failed (exit $YAML_EXIT)${NC}" - echo "$YAML_OUTPUT" - elif echo "$YAML_OUTPUT" | grep -qF "No cloud servers found"; then - echo -e "${YELLOW}⚠ cloudserver list --format yaml: no resources — format validation skipped${NC}" - elif echo "$YAML_OUTPUT" | grep -qE '^[a-zA-Z].*:|^- '; then - echo -e "${GREEN}✓ cloudserver list --format yaml: output looks like YAML${NC}" - if echo "$YAML_OUTPUT" | grep -q 'metadata:'; then - echo -e "${GREEN}✓ cloudserver list --format yaml: 'metadata' key present${NC}" - else - echo -e "${RED}✗ cloudserver list --format yaml: 'metadata' key missing${NC}" - fi - if echo "$YAML_OUTPUT" | grep -q 'properties:'; then - echo -e "${GREEN}✓ cloudserver list --format yaml: 'properties' key present${NC}" - else - echo -e "${RED}✗ cloudserver list --format yaml: 'properties' key missing${NC}" - fi - else - echo -e "${RED}✗ cloudserver list --format yaml: output does not look like YAML${NC}" - echo "$YAML_OUTPUT" - fi - - echo "" -} - -# Function to test Key Pair --format flag -test_keypair_format() { - echo -e "${BLUE}--- Testing Key Pair --format flag ---${NC}" - - echo -e "${YELLOW}Testing keypair list --format json...${NC}" - JSON_OUTPUT=$($ACLOUD_CMD compute keypair list --project-id "$PROJECT_ID" --format json 2>&1) - JSON_EXIT=$? - - if [ $JSON_EXIT -ne 0 ]; then - echo -e "${RED}✗ keypair list --format json: command failed (exit $JSON_EXIT)${NC}" - echo "$JSON_OUTPUT" - elif echo "$JSON_OUTPUT" | grep -qF "No keypairs found"; then - echo -e "${YELLOW}⚠ keypair list --format json: no resources — format validation skipped${NC}" - elif is_valid_json "$JSON_OUTPUT"; then - echo -e "${GREEN}✓ keypair list --format json: valid JSON${NC}" - if echo "$JSON_OUTPUT" | grep -q '"metadata"'; then - echo -e "${GREEN}✓ keypair list --format json: 'metadata' key present${NC}" - else - echo -e "${RED}✗ keypair list --format json: 'metadata' key missing${NC}" - fi - if echo "$JSON_OUTPUT" | grep -q '"properties"'; then - echo -e "${GREEN}✓ keypair list --format json: 'properties' key present${NC}" - else - echo -e "${RED}✗ keypair list --format json: 'properties' key missing${NC}" - fi - else - echo -e "${RED}✗ keypair list --format json: output is not valid JSON${NC}" - echo "$JSON_OUTPUT" - fi - - echo -e "${YELLOW}Testing keypair list --format yaml...${NC}" - YAML_OUTPUT=$($ACLOUD_CMD compute keypair list --project-id "$PROJECT_ID" --format yaml 2>&1) - YAML_EXIT=$? - - if [ $YAML_EXIT -ne 0 ]; then - echo -e "${RED}✗ keypair list --format yaml: command failed (exit $YAML_EXIT)${NC}" - echo "$YAML_OUTPUT" - elif echo "$YAML_OUTPUT" | grep -qF "No keypairs found"; then - echo -e "${YELLOW}⚠ keypair list --format yaml: no resources — format validation skipped${NC}" - elif echo "$YAML_OUTPUT" | grep -qE '^[a-zA-Z].*:|^- '; then - echo -e "${GREEN}✓ keypair list --format yaml: output looks like YAML${NC}" - if echo "$YAML_OUTPUT" | grep -q 'metadata:'; then - echo -e "${GREEN}✓ keypair list --format yaml: 'metadata' key present${NC}" - else - echo -e "${RED}✗ keypair list --format yaml: 'metadata' key missing${NC}" - fi - if echo "$YAML_OUTPUT" | grep -q 'properties:'; then - echo -e "${GREEN}✓ keypair list --format yaml: 'properties' key present${NC}" - else - echo -e "${RED}✗ keypair list --format yaml: 'properties' key missing${NC}" - fi - else - echo -e "${RED}✗ keypair list --format yaml: output does not look like YAML${NC}" - echo "$YAML_OUTPUT" - fi - - echo "" -} - # Cleanup function cleanup() { echo -e "${BLUE}--- Cleanup ---${NC}" @@ -386,8 +354,8 @@ trap cleanup EXIT # Run tests test_cloudserver test_keypair -test_cloudserver_format -test_keypair_format +test_format_flags "Cloud Server list" "No cloud servers found" compute cloudserver list --project-id "$PROJECT_ID" +test_format_flags "Key Pair list" "No keypairs found" compute keypair list --project-id "$PROJECT_ID" # Summary echo -e "${BLUE}=== Test Summary ===${NC}" diff --git a/e2e/container/test.sh b/e2e/container/test.sh old mode 100644 new mode 100755 index ed83b2c..fa3a63c --- a/e2e/container/test.sh +++ b/e2e/container/test.sh @@ -67,6 +67,102 @@ extract_id() { echo "$output" | grep -oE '[a-f0-9]{24}' | tail -1 || echo "" } +# Check if string is valid JSON +is_valid_json() { + local input="$1" + if command -v python3 >/dev/null 2>&1; then + echo "$input" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null && return 0 + elif command -v python >/dev/null 2>&1; then + echo "$input" | python -c "import sys,json; json.load(sys.stdin)" 2>/dev/null && return 0 + fi + return 1 +} + +# Generic format test helper: test_format_flags