You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

mygithub.libinneed.workers.dev/stackitcloud/stackit-cli

Package Overview
Dependencies
Versions
174
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mygithub.libinneed.workers.dev/stackitcloud/stackit-cli - go Package Compare versions

Comparing version
v0.2.0
to
v0.2.1
+216
-10
CONTRIBUTION.md

@@ -8,2 +8,9 @@ # Contribute to the STACKIT CLI

- [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)

@@ -19,12 +26,4 @@ - [Bug Reports](#bug-reports)

### Repository structure
### Useful Make commands
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
These commands can be executed from the project root:

@@ -38,4 +37,211 @@

#### 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:

@@ -42,0 +248,0 @@

+0
-13

@@ -94,3 +94,2 @@ package cmd

cmd.AddCommand(argus.NewCmd(p))
cmd.AddCommand(argus.NewCmd(p))
cmd.AddCommand(auth.NewCmd(p))

@@ -107,9 +106,2 @@ cmd.AddCommand(config.NewCmd(p))

cmd.AddCommand(postgresflex.NewCmd(p))
cmd.AddCommand(logme.NewCmd(p))
cmd.AddCommand(mariadb.NewCmd(p))
cmd.AddCommand(mongodbflex.NewCmd(p))
cmd.AddCommand(objectstorage.NewCmd(p))
cmd.AddCommand(opensearch.NewCmd(p))
cmd.AddCommand(organization.NewCmd(p))
cmd.AddCommand(postgresflex.NewCmd(p))
cmd.AddCommand(project.NewCmd(p))

@@ -121,7 +113,2 @@ cmd.AddCommand(rabbitmq.NewCmd(p))

cmd.AddCommand(ske.NewCmd(p))
cmd.AddCommand(rabbitmq.NewCmd(p))
cmd.AddCommand(redis.NewCmd(p))
cmd.AddCommand(secretsmanager.NewCmd(p))
cmd.AddCommand(serviceaccount.NewCmd(p))
cmd.AddCommand(ske.NewCmd(p))
}

@@ -128,0 +115,0 @@