Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
35 changes: 35 additions & 0 deletions .github/docs/contribution-guide/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package client

import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-sdk-go/services/foo"
// (...)
)

func ConfigureClient(cmd *cobra.Command) (*foo.APIClient, error) {
var err error
var apiClient foo.APIClient
var cfgOptions []sdkConfig.ConfigurationOption

authCfgOption, err := auth.AuthenticationConfig(cmd, auth.AuthorizeUser)
if err != nil {
return nil, &errors.AuthError{}
}
cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion("eu01")) // Configuring region is needed if "foo" is a regional API

customEndpoint := viper.GetString(config.fooCustomEndpointKey)

if customEndpoint != "" {
cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint))
}

apiClient, err = foo.NewAPIClient(cfgOptions...)
if err != nil {
return nil, &errors.AuthError{}
}

return apiClient, nil
}
156 changes: 156 additions & 0 deletions .github/docs/contribution-guide/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package bar

import (
"context"
"encoding/json"
"fmt"

"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"gopkg.in/yaml.v2"
// (...)
)

// Define consts for command flags
const (
someArg = "MY_ARG"
someFlag = "my-flag"
)

// Struct to model user input (arguments and/or flags)
type inputModel struct {
*globalflags.GlobalFlagModel
MyArg string
MyFlag *string
}

// "bar" command constructor
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "bar",
Short: "Short description of the command (is shown in the help of parent command)",
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.",
Args: args.SingleArg(someArg, utils.ValidateUUID), // Validate argument, with an optional validation function
Example: examples.Build(
examples.NewExample(
`Do something with command "bar"`,
"$ stackit foo bar arg-value --my-flag flag-value"),
//...
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}

// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, cmd)
if err != nil {
return err
}

// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("(...): %w", err)
}

projectLabel, err := projectname.GetProjectName(ctx, params.Printer, cmd)
if err != nil {
projectLabel = model.ProjectId
}

// Check API response "resp" and output accordingly
if resp.Item == nil {
params.Printer.Info("(...)", projectLabel)
return nil
}
return outputResult(params.Printer, cmd, model.OutputFormat, instances)
},
}

configureFlags(cmd)
return cmd
}

// Configure command flags (type, default value, and description)
func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(myFlag, "defaultValue", "My flag description")
}

// Parse user input (arguments and/or flags)
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
myArg := inputArgs[0]

globalFlags := globalflags.Parse(cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}

model := inputModel{
GlobalFlagModel: globalFlags,
MyArg: myArg,
MyFlag: flags.FlagToStringPointer(cmd, myFlag),
}

// Write the input model to the debug logs
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}

return &model, nil
}

// Build request to the API
func buildRequest(ctx context.Context, model *inputModel, apiClient *foo.APIClient) foo.ApiListInstancesRequest {
req := apiClient.GetBar(ctx, model.ProjectId, model.MyArg, someParam)
return req
}

// Output result based on the configured output format
func outputResult(p *print.Printer, cmd *cobra.Command, outputFormat string, resources []foo.Resource) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resources, "", " ")
if err != nil {
return fmt.Errorf("marshal resource list: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.Marshal(resources)
if err != nil {
return fmt.Errorf("marshal resource list: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("ID", "NAME", "STATE")
for i := range resources {
resource := resources[i]
table.AddRow(*resource.ResourceId, *resource.Name, *resource.State)
}
err := table.Display(cmd)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
177 changes: 2 additions & 175 deletions CONTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,148 +53,7 @@ Please remember to run `make generate-docs` after your changes to keep the comma

Below is a typical structure of a CLI command:

```go
package bar

import (
(...)
)

// Define consts for command flags
const (
someArg = "MY_ARG"
someFlag = "my-flag"
)

// Struct to model user input (arguments and/or flags)
type inputModel struct {
*globalflags.GlobalFlagModel
MyArg string
MyFlag *string
}

// "bar" command constructor
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "bar",
Short: "Short description of the command (is shown in the help of parent command)",
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.",
Args: args.SingleArg(someArg, utils.ValidateUUID), // Validate argument, with an optional validation function
Example: examples.Build(
examples.NewExample(
`Do something with command "bar"`,
"$ stackit foo bar arg-value --my-flag flag-value"),
...
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}

// Configure API client
apiClient, err := client.ConfigureClient(p, cmd)
if err != nil {
return err
}

// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("(...): %w", err)
}

projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
if err != nil {
projectLabel = model.ProjectId
}

// Check API response "resp" and output accordingly
if resp.Item == nil {
p.Info("(...)", projectLabel)
return nil
}
return outputResult(p, cmd, model.OutputFormat, instances)
},
}

configureFlags(cmd)
return cmd
}

// Configure command flags (type, default value, and description)
func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(myFlag, "defaultValue", "My flag description")
}

// Parse user input (arguments and/or flags)
func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
myArg := inputArgs[0]

globalFlags := globalflags.Parse(cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}

model := inputModel{
GlobalFlagModel: globalFlags,
MyArg myArg,
MyFlag: flags.FlagToStringPointer(cmd, myFlag),
}, nil

// Write the input model to the debug logs
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}

return &model, nil
}

// Build request to the API
func buildRequest(ctx context.Context, model *inputModel, apiClient *foo.APIClient) foo.ApiListInstancesRequest {
req := apiClient.GetBar(ctx, model.ProjectId, model.MyArg, someParam)
return req
}

// Output result based on the configured output format
func outputResult(p *print.Printer, cmd *cobra.Command, outputFormat string, resources []foo.Resource) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resources, "", " ")
if err != nil {
return fmt.Errorf("marshal resource list: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.Marshal(resources)
if err != nil {
return fmt.Errorf("marshal resource list: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("ID", "NAME", "STATE")
for i := range resources {
resource := resources[i]
table.AddRow(*resource.ResourceId, *resource.Name, *resource.State)
}
err := table.Display(cmd)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
```
https://github.com/stackitcloud/stackit-cli/blob/6f762bd56407ed232080efabc4d2bf87f260b71d/.github/docs/contribution-guide/cmd.go#L23-L156

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

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

```go
package client

import (
(...)
"github.com/stackitcloud/stackit-sdk-go/services/foo"
)

func ConfigureClient(cmd *cobra.Command) (*foo.APIClient, error) {
var err error
var apiClient foo.APIClient
var cfgOptions []sdkConfig.ConfigurationOption

authCfgOption, err := auth.AuthenticationConfig(cmd, auth.AuthorizeUser)
if err != nil {
return nil, &errors.AuthError{}
}
cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion("eu01")) // Configuring region is needed if "foo" is a regional API

customEndpoint := viper.GetString(config.fooCustomEndpointKey)

if customEndpoint != "" {
cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint))
}

apiClient, err = foo.NewAPIClient(cfgOptions...)
if err != nil {
return nil, &errors.AuthError{}
}

return apiClient, nil
}
```
https://github.com/stackitcloud/stackit-cli/blob/6f762bd56407ed232080efabc4d2bf87f260b71d/.github/docs/contribution-guide/client.go#L12-L35

### Local development

Expand Down
Loading