Skip to content

Commit

Permalink
Extend contribution guidelines (new commands, services, verbosity) (#202
Browse files Browse the repository at this point in the history
)

* Extend contribution guidelines (new commands, services, verbosity)

* Remove leftover

* Fix make command call

* Update CONTRIBUTION.md

Co-authored-by: Vicente Pinto <[email protected]>

* Update CONTRIBUTION.md

Co-authored-by: Vicente Pinto <[email protected]>

* Update CONTRIBUTION.md

Co-authored-by: Vicente Pinto <[email protected]>

* Update CONTRIBUTION.md

Co-authored-by: Vicente Pinto <[email protected]>

* Update CONTRIBUTION.md

Co-authored-by: Vicente Pinto <[email protected]>

* Adjustments after review

---------

Co-authored-by: Vicente Pinto <[email protected]>
  • Loading branch information
joaopalet and vicentepinto98 authored Apr 9, 2024
1 parent d2e8240 commit 329b4be
Showing 1 changed file with 216 additions and 10 deletions.
226 changes: 216 additions & 10 deletions CONTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ Your contribution is welcome! Thank you for your interest in contributing to the
## Table of contents

- [Developer Guide](#developer-guide)
- [Useful Make commands](#useful-make-commands)
- [Repository structure](#repository-structure)
- [Implementing a new command](#implementing-a-new-command)
- [Command file structure](#command-file-structure)
- [Outputs, prints and debug logs](#outputs-prints-and-debug-logs)
- [Onboarding a new STACKIT service](#onboarding-a-new-stackit-service)
- [Local development](#local-development)
- [Code Contributions](#code-contributions)
- [Bug Reports](#bug-reports)

Expand All @@ -15,15 +22,7 @@ Prerequisites:
- [`Go`](https://go.dev/doc/install) 1.22+
- [`yamllint`](https://yamllint.readthedocs.io/en/stable/quickstart.html)

### Repository structure

The CLI commands are located under `internal/cmd`, where each folder includes the source code for a `group` of commands. Inside `pkg` you can find several useful packages that are shared by the commands and provide additional functionality such as `flags`, `globalflags`, `tables`, etc.

### Getting started

Check the [Authentication](README.md#authentication) section on the README.

#### Useful Make commands
### Useful Make commands

These commands can be executed from the project root:

Expand All @@ -33,7 +32,214 @@ These commands can be executed from the project root:
- `make generate-docs`: generate Markdown documentation for every command
- `make test`: run unit tests

#### Local development
### Repository structure

The CLI commands are located under `internal/cmd`, where each folder includes the source code for each subcommand (including their own subcommands). Inside `pkg` you can find several useful packages that are shared by the commands and provide additional functionality such as `flags`, `globalflags`, `tables`, etc.

### Implementing a new command

Let's suppose you want to want to implement a new command `bar`, that would be the direct child of an existing command `stackit foo` (meaning it would be invoked as `stackit foo bar`):

1. You would start by creating a new folder `bar/` inside `internal/cmd/foo/`
2. Following with the creation of a file `bar.go` inside your new folder `internal/cmd/foo/bar/`
1. The Go package should be similar to the command usage, in this case `package bar` would be an adequate name
2. Please refer to the [Command file structure](./CONTRIBUTION.md/#command-file-structure) section for details on the strcutre of the file itself
3. To register the command `bar` as a child of the existing command `foo`, add `cmd.AddCommand(bar.NewCmd(p))` to the `addSubcommands` method of the constructor of the `foo` command
1. In this case, `p` is the `printer` that is passed from the root command to all subcommands of the tree (refer to the [Outputs, prints and debug logs](./CONTRIBUTION.md/#outputs-prints-and-debug-logs) section for more details regarding the `printer`)

Please remeber to run `make generate-docs` after your changes to keep the commands' documentation updated.

#### Command file structure

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() *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(cmd, args)
if err != nil {
return err
}

// Configure API client
apiClient, err := client.ConfigureClient(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, 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(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{}
}

return &inputModel{
GlobalFlagModel: globalFlags,
MyArg myArg,
MyFlag: flags.FlagToStringPointer(cmd, myFlag),
}, 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(cmd *cobra.Command, outputFormat string, resources []foo.Resource) error {
switch outputFormat {
case globalflags.JSONOutputFormat:
details, err := json.MarshalIndent(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
}
}
```

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

If the new command `bar` is the first command in the CLI using a STACKIT service `foo`, please refer to [Onboarding a new STACKIT service](./CONTRIBUTION.md/#onboarding-a-new-stackit-service).

#### Outputs, prints and debug logs

The CLI has 4 different verbosity levels:

- `error`: For only displaying errors
- `warning`: For displaying user facing warnings _(and all of the above)_
- `info` (default): For displaying user facing info, such as operation success messages and spinners _(and all of the above)_
- `debug`: For displaying structured logs with different levels, including errors _(and all of the above)_

For prints that are specific to a certain log level, you can use the methods defined in the `print` package: `Error`, `Warn`, `Info`, and `Debug`.

For command outputs that should always be displayed, no matter the defined verbosity, you should use the `print` methods `Outputf` and `Outputln`. These should only be used for the actual output of the commands, which can usually be described by "I ran the command to see _this_".

### Onboarding a new STACKIT service

If you want to add a command that uses a STACKIT service `foo` that was not yet used by the CLI, you will first need to implement a few extra steps to configure the new service:

1. Add a `FooCustomEndpointKey` key in `internal/pkg/config/config.go` (and add it to `ConfigKeys` and set the to default to `""` using `viper.SetDefault`)
2. Update the `stackit config unset` and `stackit config unset` commands by adding flags to set and unset a custom endpoint for the `foo` service API, respectively, and update their unit tests
3. Setup the SDK client configuration, using the authentication method configured in the CLI

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
}
```

### Local development

To test your changes, you can either:

Expand Down

0 comments on commit 329b4be

Please sign in to comment.