Skip to content

Commit 8622b5f

Browse files
authored
refactor(cli): pass parameter struct to cmd factories (#735)
relates to STACKITCLI-180
1 parent de4712f commit 8622b5f

File tree

793 files changed

+4407
-3681
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

793 files changed

+4407
-3681
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package client
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/spf13/viper"
6+
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
7+
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
8+
"github.com/stackitcloud/stackit-sdk-go/services/foo"
9+
// (...)
10+
)
11+
12+
func ConfigureClient(cmd *cobra.Command) (*foo.APIClient, error) {
13+
var err error
14+
var apiClient foo.APIClient
15+
var cfgOptions []sdkConfig.ConfigurationOption
16+
17+
authCfgOption, err := auth.AuthenticationConfig(cmd, auth.AuthorizeUser)
18+
if err != nil {
19+
return nil, &errors.AuthError{}
20+
}
21+
cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion("eu01")) // Configuring region is needed if "foo" is a regional API
22+
23+
customEndpoint := viper.GetString(config.fooCustomEndpointKey)
24+
25+
if customEndpoint != "" {
26+
cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint))
27+
}
28+
29+
apiClient, err = foo.NewAPIClient(cfgOptions...)
30+
if err != nil {
31+
return nil, &errors.AuthError{}
32+
}
33+
34+
return apiClient, nil
35+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package bar
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
18+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
19+
"gopkg.in/yaml.v2"
20+
// (...)
21+
)
22+
23+
// Define consts for command flags
24+
const (
25+
someArg = "MY_ARG"
26+
someFlag = "my-flag"
27+
)
28+
29+
// Struct to model user input (arguments and/or flags)
30+
type inputModel struct {
31+
*globalflags.GlobalFlagModel
32+
MyArg string
33+
MyFlag *string
34+
}
35+
36+
// "bar" command constructor
37+
func NewCmd(params *params.CmdParams) *cobra.Command {
38+
cmd := &cobra.Command{
39+
Use: "bar",
40+
Short: "Short description of the command (is shown in the help of parent command)",
41+
Long: "Long description of the command. Can contain some more information about the command usage. It is shown in the help of the current command.",
42+
Args: args.SingleArg(someArg, utils.ValidateUUID), // Validate argument, with an optional validation function
43+
Example: examples.Build(
44+
examples.NewExample(
45+
`Do something with command "bar"`,
46+
"$ stackit foo bar arg-value --my-flag flag-value"),
47+
//...
48+
),
49+
RunE: func(cmd *cobra.Command, args []string) error {
50+
ctx := context.Background()
51+
model, err := parseInput(params.Printer, cmd, args)
52+
if err != nil {
53+
return err
54+
}
55+
56+
// Configure API client
57+
apiClient, err := client.ConfigureClient(params.Printer, cmd)
58+
if err != nil {
59+
return err
60+
}
61+
62+
// Call API
63+
req := buildRequest(ctx, model, apiClient)
64+
resp, err := req.Execute()
65+
if err != nil {
66+
return fmt.Errorf("(...): %w", err)
67+
}
68+
69+
projectLabel, err := projectname.GetProjectName(ctx, params.Printer, cmd)
70+
if err != nil {
71+
projectLabel = model.ProjectId
72+
}
73+
74+
// Check API response "resp" and output accordingly
75+
if resp.Item == nil {
76+
params.Printer.Info("(...)", projectLabel)
77+
return nil
78+
}
79+
return outputResult(params.Printer, cmd, model.OutputFormat, instances)
80+
},
81+
}
82+
83+
configureFlags(cmd)
84+
return cmd
85+
}
86+
87+
// Configure command flags (type, default value, and description)
88+
func configureFlags(cmd *cobra.Command) {
89+
cmd.Flags().StringP(myFlag, "defaultValue", "My flag description")
90+
}
91+
92+
// Parse user input (arguments and/or flags)
93+
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
94+
myArg := inputArgs[0]
95+
96+
globalFlags := globalflags.Parse(cmd)
97+
if globalFlags.ProjectId == "" {
98+
return nil, &errors.ProjectIdError{}
99+
}
100+
101+
model := inputModel{
102+
GlobalFlagModel: globalFlags,
103+
MyArg: myArg,
104+
MyFlag: flags.FlagToStringPointer(cmd, myFlag),
105+
}
106+
107+
// Write the input model to the debug logs
108+
if p.IsVerbosityDebug() {
109+
modelStr, err := print.BuildDebugStrFromInputModel(model)
110+
if err != nil {
111+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
112+
} else {
113+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
114+
}
115+
}
116+
117+
return &model, nil
118+
}
119+
120+
// Build request to the API
121+
func buildRequest(ctx context.Context, model *inputModel, apiClient *foo.APIClient) foo.ApiListInstancesRequest {
122+
req := apiClient.GetBar(ctx, model.ProjectId, model.MyArg, someParam)
123+
return req
124+
}
125+
126+
// Output result based on the configured output format
127+
func outputResult(p *print.Printer, cmd *cobra.Command, outputFormat string, resources []foo.Resource) error {
128+
switch outputFormat {
129+
case print.JSONOutputFormat:
130+
details, err := json.MarshalIndent(resources, "", " ")
131+
if err != nil {
132+
return fmt.Errorf("marshal resource list: %w", err)
133+
}
134+
p.Outputln(string(details))
135+
return nil
136+
case print.YAMLOutputFormat:
137+
details, err := yaml.Marshal(resources)
138+
if err != nil {
139+
return fmt.Errorf("marshal resource list: %w", err)
140+
}
141+
p.Outputln(string(details))
142+
return nil
143+
default:
144+
table := tables.NewTable()
145+
table.SetHeader("ID", "NAME", "STATE")
146+
for i := range resources {
147+
resource := resources[i]
148+
table.AddRow(*resource.ResourceId, *resource.Name, *resource.State)
149+
}
150+
err := table.Display(cmd)
151+
if err != nil {
152+
return fmt.Errorf("render table: %w", err)
153+
}
154+
return nil
155+
}
156+
}

CONTRIBUTION.md

Lines changed: 2 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -53,148 +53,7 @@ Please remember to run `make generate-docs` after your changes to keep the comma
5353

5454
Below is a typical structure of a CLI command:
5555

56-
```go
57-
package bar
58-
59-
import (
60-
(...)
61-
)
62-
63-
// Define consts for command flags
64-
const (
65-
someArg = "MY_ARG"
66-
someFlag = "my-flag"
67-
)
68-
69-
// Struct to model user input (arguments and/or flags)
70-
type inputModel struct {
71-
*globalflags.GlobalFlagModel
72-
MyArg string
73-
MyFlag *string
74-
}
75-
76-
// "bar" command constructor
77-
func NewCmd(p *print.Printer) *cobra.Command {
78-
cmd := &cobra.Command{
79-
Use: "bar",
80-
Short: "Short description of the command (is shown in the help of parent command)",
81-
Long: "Long description of the command. Can contain some more information about the command usage. It is shown in the help of the current command.",
82-
Args: args.SingleArg(someArg, utils.ValidateUUID), // Validate argument, with an optional validation function
83-
Example: examples.Build(
84-
examples.NewExample(
85-
`Do something with command "bar"`,
86-
"$ stackit foo bar arg-value --my-flag flag-value"),
87-
...
88-
),
89-
RunE: func(cmd *cobra.Command, args []string) error {
90-
ctx := context.Background()
91-
model, err := parseInput(p, cmd, args)
92-
if err != nil {
93-
return err
94-
}
95-
96-
// Configure API client
97-
apiClient, err := client.ConfigureClient(p, cmd)
98-
if err != nil {
99-
return err
100-
}
101-
102-
// Call API
103-
req := buildRequest(ctx, model, apiClient)
104-
resp, err := req.Execute()
105-
if err != nil {
106-
return fmt.Errorf("(...): %w", err)
107-
}
108-
109-
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
110-
if err != nil {
111-
projectLabel = model.ProjectId
112-
}
113-
114-
// Check API response "resp" and output accordingly
115-
if resp.Item == nil {
116-
p.Info("(...)", projectLabel)
117-
return nil
118-
}
119-
return outputResult(p, cmd, model.OutputFormat, instances)
120-
},
121-
}
122-
123-
configureFlags(cmd)
124-
return cmd
125-
}
126-
127-
// Configure command flags (type, default value, and description)
128-
func configureFlags(cmd *cobra.Command) {
129-
cmd.Flags().StringP(myFlag, "defaultValue", "My flag description")
130-
}
131-
132-
// Parse user input (arguments and/or flags)
133-
func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
134-
myArg := inputArgs[0]
135-
136-
globalFlags := globalflags.Parse(cmd)
137-
if globalFlags.ProjectId == "" {
138-
return nil, &errors.ProjectIdError{}
139-
}
140-
141-
model := inputModel{
142-
GlobalFlagModel: globalFlags,
143-
MyArg myArg,
144-
MyFlag: flags.FlagToStringPointer(cmd, myFlag),
145-
}, nil
146-
147-
// Write the input model to the debug logs
148-
if p.IsVerbosityDebug() {
149-
modelStr, err := print.BuildDebugStrFromInputModel(model)
150-
if err != nil {
151-
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
152-
} else {
153-
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
154-
}
155-
}
156-
157-
return &model, nil
158-
}
159-
160-
// Build request to the API
161-
func buildRequest(ctx context.Context, model *inputModel, apiClient *foo.APIClient) foo.ApiListInstancesRequest {
162-
req := apiClient.GetBar(ctx, model.ProjectId, model.MyArg, someParam)
163-
return req
164-
}
165-
166-
// Output result based on the configured output format
167-
func outputResult(p *print.Printer, cmd *cobra.Command, outputFormat string, resources []foo.Resource) error {
168-
switch outputFormat {
169-
case print.JSONOutputFormat:
170-
details, err := json.MarshalIndent(resources, "", " ")
171-
if err != nil {
172-
return fmt.Errorf("marshal resource list: %w", err)
173-
}
174-
p.Outputln(string(details))
175-
return nil
176-
case print.YAMLOutputFormat:
177-
details, err := yaml.Marshal(resources)
178-
if err != nil {
179-
return fmt.Errorf("marshal resource list: %w", err)
180-
}
181-
p.Outputln(string(details))
182-
return nil
183-
default:
184-
table := tables.NewTable()
185-
table.SetHeader("ID", "NAME", "STATE")
186-
for i := range resources {
187-
resource := resources[i]
188-
table.AddRow(*resource.ResourceId, *resource.Name, *resource.State)
189-
}
190-
err := table.Display(cmd)
191-
if err != nil {
192-
return fmt.Errorf("render table: %w", err)
193-
}
194-
return nil
195-
}
196-
}
197-
```
56+
https://github.com/stackitcloud/stackit-cli/blob/6f762bd56407ed232080efabc4d2bf87f260b71d/.github/docs/contribution-guide/cmd.go#L23-L156
19857

19958
Please remember to always add unit tests for `parseInput`, `buildRequest` (in `bar_test.go`), and any other util functions used.
20059

@@ -224,39 +83,7 @@ If you want to add a command that uses a STACKIT service `foo` that was not yet
22483
1. This is done in `internal/pkg/services/foo/client/client.go`
22584
2. Below is an example of a typical `client.go` file structure:
22685

227-
```go
228-
package client
229-
230-
import (
231-
(...)
232-
"github.com/stackitcloud/stackit-sdk-go/services/foo"
233-
)
234-
235-
func ConfigureClient(cmd *cobra.Command) (*foo.APIClient, error) {
236-
var err error
237-
var apiClient foo.APIClient
238-
var cfgOptions []sdkConfig.ConfigurationOption
239-
240-
authCfgOption, err := auth.AuthenticationConfig(cmd, auth.AuthorizeUser)
241-
if err != nil {
242-
return nil, &errors.AuthError{}
243-
}
244-
cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion("eu01")) // Configuring region is needed if "foo" is a regional API
245-
246-
customEndpoint := viper.GetString(config.fooCustomEndpointKey)
247-
248-
if customEndpoint != "" {
249-
cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint))
250-
}
251-
252-
apiClient, err = foo.NewAPIClient(cfgOptions...)
253-
if err != nil {
254-
return nil, &errors.AuthError{}
255-
}
256-
257-
return apiClient, nil
258-
}
259-
```
86+
https://github.com/stackitcloud/stackit-cli/blob/6f762bd56407ed232080efabc4d2bf87f260b71d/.github/docs/contribution-guide/client.go#L12-L35
26087

26188
### Local development
26289

0 commit comments

Comments
 (0)