Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

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

Package Overview
Dependencies
Versions
172
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 - npm Package Compare versions

Comparing version
v0.48.0
to
v0.49.0
+48
docs/stackit_beta_intake_runner_create.md
## stackit beta intake runner create
Creates a new Intake Runner
### Synopsis
Creates a new Intake Runner.
```
stackit beta intake runner create [flags]
```
### Examples
```
Create a new Intake Runner with a display name and message capacity limits
$ stackit beta intake runner create --display-name my-runner --max-message-size-kib 1000 --max-messages-per-hour 5000
Create a new Intake Runner with a description and labels
$ stackit beta intake runner create --display-name my-runner --max-message-size-kib 1000 --max-messages-per-hour 5000 --description "Main runner for production" --labels="env=prod,team=billing"
```
### Options
```
--description string Description
--display-name string Display name
-h, --help Help for "stackit beta intake runner create"
--labels stringToString Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2" (default [])
--max-message-size-kib int Maximum message size in KiB
--max-messages-per-hour int Maximum number of messages per hour
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta intake runner](./stackit_beta_intake_runner.md) - Provides functionality for Intake Runners
## stackit beta intake runner delete
Deletes an Intake Runner
### Synopsis
Deletes an Intake Runner.
```
stackit beta intake runner delete RUNNER_ID [flags]
```
### Examples
```
Delete an Intake Runner with ID "xxx"
$ stackit beta intake runner delete xxx
```
### Options
```
-h, --help Help for "stackit beta intake runner delete"
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta intake runner](./stackit_beta_intake_runner.md) - Provides functionality for Intake Runners
## stackit beta intake runner describe
Shows details of an Intake Runner
### Synopsis
Shows details of an Intake Runner.
```
stackit beta intake runner describe RUNNER_ID [flags]
```
### Examples
```
Get details of an Intake Runner with ID "xxx"
$ stackit beta intake runner describe xxx
Get details of an Intake Runner with ID "xxx" in JSON format
$ stackit beta intake runner describe xxx --output-format json
```
### Options
```
-h, --help Help for "stackit beta intake runner describe"
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta intake runner](./stackit_beta_intake_runner.md) - Provides functionality for Intake Runners
## stackit beta intake runner list
Lists all Intake Runners
### Synopsis
Lists all Intake Runners for the current project.
```
stackit beta intake runner list [flags]
```
### Examples
```
List all Intake Runners
$ stackit beta intake runner list
List all Intake Runners in JSON format
$ stackit beta intake runner list --output-format json
List up to 5 Intake Runners
$ stackit beta intake runner list --limit 5
```
### Options
```
-h, --help Help for "stackit beta intake runner list"
--limit int Maximum number of entries to list
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta intake runner](./stackit_beta_intake_runner.md) - Provides functionality for Intake Runners
## stackit beta intake runner update
Updates an Intake Runner
### Synopsis
Updates an Intake Runner. Only the specified fields are updated.
```
stackit beta intake runner update RUNNER_ID [flags]
```
### Examples
```
Update the display name of an Intake Runner with ID "xxx"
$ stackit beta intake runner update xxx --display-name "new-runner-name"
Update the message capacity limits for an Intake Runner with ID "xxx"
$ stackit beta intake runner update xxx --max-message-size-kib 1000 --max-messages-per-hour 10000
```
### Options
```
--description string Description
--display-name string Display name
-h, --help Help for "stackit beta intake runner update"
--labels stringToString Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2". (default [])
--max-message-size-kib int Maximum message size in KiB. Note: Overall message capacity cannot be decreased.
--max-messages-per-hour int Maximum number of messages per hour. Note: Overall message capacity cannot be decreased.
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta intake runner](./stackit_beta_intake_runner.md) - Provides functionality for Intake Runners
## stackit beta intake runner
Provides functionality for Intake Runners
### Synopsis
Provides functionality for Intake Runners.
```
stackit beta intake runner [flags]
```
### Options
```
-h, --help Help for "stackit beta intake runner"
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta intake](./stackit_beta_intake.md) - Provides functionality for intake
* [stackit beta intake runner create](./stackit_beta_intake_runner_create.md) - Creates a new Intake Runner
* [stackit beta intake runner delete](./stackit_beta_intake_runner_delete.md) - Deletes an Intake Runner
* [stackit beta intake runner describe](./stackit_beta_intake_runner_describe.md) - Shows details of an Intake Runner
* [stackit beta intake runner list](./stackit_beta_intake_runner_list.md) - Lists all Intake Runners
* [stackit beta intake runner update](./stackit_beta_intake_runner_update.md) - Updates an Intake Runner
## stackit beta intake
Provides functionality for intake
### Synopsis
Provides functionality for intake.
```
stackit beta intake [flags]
```
### Options
```
-h, --help Help for "stackit beta intake"
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands
* [stackit beta intake runner](./stackit_beta_intake_runner.md) - Provides functionality for Intake Runners
## stackit beta kms key describe
Describe a KMS key
### Synopsis
Describe a KMS key
```
stackit beta kms key describe KEY_ID [flags]
```
### Examples
```
Describe a KMS key with ID xxx of keyring yyy
$ stackit beta kms key describe xxx --keyring-id yyy
```
### Options
```
-h, --help Help for "stackit beta kms key describe"
--keyring-id string Key Ring ID
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms key](./stackit_beta_kms_key.md) - Manage KMS keys
## stackit beta kms keyring describe
Describe a KMS key ring
### Synopsis
Describe a KMS key ring
```
stackit beta kms keyring describe KEYRING_ID [flags]
```
### Examples
```
Describe a KMS key ring with ID xxx
$ stackit beta kms keyring describe xxx
```
### Options
```
-h, --help Help for "stackit beta kms keyring describe"
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms keyring](./stackit_beta_kms_keyring.md) - Manage KMS key rings
## stackit beta kms wrapping-key describe
Describe a KMS wrapping key
### Synopsis
Describe a KMS wrapping key
```
stackit beta kms wrapping-key describe WRAPPING_KEY_ID [flags]
```
### Examples
```
Describe a KMS wrapping key with ID xxx of keyring yyy
$ stackit beta kms wrappingkey describe xxx --keyring-id yyy
```
### Options
```
-h, --help Help for "stackit beta kms wrapping-key describe"
--keyring-id string Key Ring ID
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms wrapping-key](./stackit_beta_kms_wrapping-key.md) - Manage KMS wrapping keys
package intake
import (
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/runner"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
)
// NewCmd creates the 'stackit intake' command
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "intake",
Short: "Provides functionality for intake",
Long: "Provides functionality for intake.",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, params)
return cmd
}
func addSubcommands(cmd *cobra.Command, params *params.CmdParams) {
cmd.AddCommand(runner.NewCmd(params))
}
package create
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/intake"
)
// Define a unique key for the context to avoid collisions
type testCtxKey struct{}
const (
testRegion = "eu01"
testDisplayName = "testrunner"
testMaxMessageSizeKiB = int64(1024)
testMaxMessagesPerHour = int64(10000)
testDescription = "This is a test runner"
testLabelsString = "env=test,team=dev"
)
var (
// testCtx dummy context for testing purposes
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
// testClient mock API client
testClient = &intake.APIClient{}
testProjectId = uuid.NewString()
testLabels = map[string]string{"env": "test", "team": "dev"}
)
// fixtureFlagValues generates a map of flag values for tests
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
displayNameFlag: testDisplayName,
maxMessageSizeKiBFlag: "1024",
maxMessagesPerHourFlag: "10000",
descriptionFlag: testDescription,
labelFlag: testLabelsString,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// fixtureInputModel generates an input model for tests
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
DisplayName: utils.Ptr(testDisplayName),
MaxMessageSizeKiB: utils.Ptr(testMaxMessageSizeKiB),
MaxMessagesPerHour: utils.Ptr(testMaxMessagesPerHour),
Description: utils.Ptr(testDescription),
Labels: utils.Ptr(testLabels),
}
for _, mod := range mods {
mod(model)
}
return model
}
// fixtureCreatePayload generates a CreateIntakeRunnerPayload for tests
func fixtureCreatePayload(mods ...func(payload *intake.CreateIntakeRunnerPayload)) intake.CreateIntakeRunnerPayload {
payload := intake.CreateIntakeRunnerPayload{
DisplayName: utils.Ptr(testDisplayName),
MaxMessageSizeKiB: utils.Ptr(testMaxMessageSizeKiB),
MaxMessagesPerHour: utils.Ptr(testMaxMessagesPerHour),
Description: utils.Ptr(testDescription),
Labels: utils.Ptr(testLabels),
}
for _, mod := range mods {
mod(&payload)
}
return payload
}
// fixtureRequest generates an API request for tests
func fixtureRequest(mods ...func(request *intake.ApiCreateIntakeRunnerRequest)) intake.ApiCreateIntakeRunnerRequest {
request := testClient.CreateIntakeRunner(testCtx, testProjectId, testRegion)
request = request.CreateIntakeRunnerPayload(fixtureCreatePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "display name missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, displayNameFlag)
}),
isValid: false,
},
{
description: "max message size missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, maxMessageSizeKiBFlag)
}),
isValid: false,
},
{
description: "max messages per hour missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, maxMessagesPerHourFlag)
}),
isValid: false,
},
{
description: "required fields only",
flagValues: map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
displayNameFlag: testDisplayName,
maxMessageSizeKiBFlag: "1024",
maxMessagesPerHourFlag: "10000",
},
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Description = nil
model.Labels = nil
}),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
parseInputWrapper := func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) {
return parseInput(p, cmd)
}
testutils.TestParseInput(t, NewCmd, parseInputWrapper, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest intake.ApiCreateIntakeRunnerRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
{
description: "no optionals",
model: fixtureInputModel(func(model *inputModel) {
model.Description = nil
model.Labels = nil
}),
expectedRequest: fixtureRequest(func(request *intake.ApiCreateIntakeRunnerRequest) {
*request = (*request).CreateIntakeRunnerPayload(fixtureCreatePayload(func(payload *intake.CreateIntakeRunnerPayload) {
payload.Description = nil
payload.Labels = nil
}))
}),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
type args struct {
model *inputModel
projectLabel string
resp *intake.IntakeRunnerResponse
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "default output",
args: args{
model: fixtureInputModel(),
projectLabel: "my-project",
resp: &intake.IntakeRunnerResponse{Id: utils.Ptr("runner-id-123")},
},
wantErr: false,
},
{
name: "default output - async",
args: args{
model: fixtureInputModel(func(model *inputModel) {
model.Async = true
}),
projectLabel: "my-project",
resp: &intake.IntakeRunnerResponse{Id: utils.Ptr("runner-id-123")},
},
wantErr: false,
},
{
name: "json output",
args: args{
model: fixtureInputModel(func(model *inputModel) {
model.OutputFormat = print.JSONOutputFormat
}),
resp: &intake.IntakeRunnerResponse{Id: utils.Ptr("runner-id-123")},
},
wantErr: false,
},
{
name: "nil response - default output",
args: args{
model: fixtureInputModel(),
resp: nil,
},
wantErr: false,
},
{
name: "nil response - json output",
args: args{
model: fixtureInputModel(func(model *inputModel) {
model.OutputFormat = print.JSONOutputFormat
}),
resp: nil,
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package create
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/intake"
"github.com/stackitcloud/stackit-sdk-go/services/intake/wait"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
cliErr "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/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
)
const (
displayNameFlag = "display-name"
maxMessageSizeKiBFlag = "max-message-size-kib"
maxMessagesPerHourFlag = "max-messages-per-hour"
descriptionFlag = "description"
labelFlag = "labels"
)
// inputModel struct holds all the input parameters for the command
type inputModel struct {
*globalflags.GlobalFlagModel
DisplayName *string
MaxMessageSizeKiB *int64
MaxMessagesPerHour *int64
Description *string
Labels *map[string]string
}
func NewCmd(p *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a new Intake Runner",
Long: "Creates a new Intake Runner.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a new Intake Runner with a display name and message capacity limits`,
`$ stackit beta intake runner create --display-name my-runner --max-message-size-kib 1000 --max-messages-per-hour 5000`),
examples.NewExample(
`Create a new Intake Runner with a description and labels`,
`$ stackit beta intake runner create --display-name my-runner --max-message-size-kib 1000 --max-messages-per-hour 5000 --description "Main runner for production" --labels="env=prod,team=billing"`),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p.Printer, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion)
if err != nil {
return err
}
projectLabel, err := projectname.GetProjectName(ctx, p.Printer, p.CliVersion, cmd)
if err != nil {
p.Printer.Debug(print.ErrorLevel, "get project name: %v", err)
projectLabel = model.ProjectId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to create an Intake Runner for project %q?", projectLabel)
err = p.Printer.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("create Intake Runner: %w", err)
}
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(p.Printer)
s.Start("Creating STACKIT Intake Runner")
_, err = wait.CreateOrUpdateIntakeRunnerWaitHandler(ctx, apiClient, model.ProjectId, model.Region, resp.GetId()).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for STACKIT Intake Runner creation: %w", err)
}
s.Stop()
}
return outputResult(p.Printer, model, projectLabel, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().String(displayNameFlag, "", "Display name")
cmd.Flags().Int64(maxMessageSizeKiBFlag, 0, "Maximum message size in KiB")
cmd.Flags().Int64(maxMessagesPerHourFlag, 0, "Maximum number of messages per hour")
cmd.Flags().String(descriptionFlag, "", "Description")
cmd.Flags().StringToString(labelFlag, nil, "Labels in key=value format, separated by commas. Example: --labels \"key1=value1,key2=value2\"")
err := flags.MarkFlagsRequired(cmd, displayNameFlag, maxMessageSizeKiBFlag, maxMessagesPerHourFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &cliErr.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag),
MaxMessageSizeKiB: flags.FlagToInt64Pointer(p, cmd, maxMessageSizeKiBFlag),
MaxMessagesPerHour: flags.FlagToInt64Pointer(p, cmd, maxMessagesPerHourFlag),
Description: flags.FlagToStringPointer(p, cmd, descriptionFlag),
Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag),
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiCreateIntakeRunnerRequest {
// Start building the request by calling the base method with path parameters
req := apiClient.CreateIntakeRunner(ctx, model.ProjectId, model.Region)
// Create the payload struct with data from the input model
payload := intake.CreateIntakeRunnerPayload{
DisplayName: model.DisplayName,
MaxMessageSizeKiB: model.MaxMessageSizeKiB,
MaxMessagesPerHour: model.MaxMessagesPerHour,
Description: model.Description,
Labels: model.Labels,
}
// Attach the payload to the request builder
req = req.CreateIntakeRunnerPayload(payload)
return req
}
func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *intake.IntakeRunnerResponse) error {
return p.OutputResult(model.OutputFormat, resp, func() error {
if resp == nil {
p.Outputf("Triggered creation of Intake Runner for project %q, but no runner ID was returned.\n", projectLabel)
return nil
}
operationState := "Created"
if model.Async {
operationState = "Triggered creation of"
}
p.Outputf("%s Intake Runner for project %q. Runner ID: %s\n", operationState, projectLabel, utils.PtrString(resp.Id))
return nil
})
}
package delete
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
"github.com/stackitcloud/stackit-sdk-go/services/intake"
)
// Define a unique key for the context to avoid collisions
type testCtxKey struct{}
const (
testRegion = "eu01"
)
var (
// testCtx is a dummy context for testing purposes
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
// testClient is a mock API client
testClient = &intake.APIClient{}
testProjectId = uuid.NewString()
testRunnerId = uuid.NewString()
)
// fixtureArgValues generates a slice of arguments for tests
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testRunnerId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
// fixtureFlagValues generates a map of flag values for tests
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// fixtureInputModel generates an input model for tests
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
RunnerId: testRunnerId,
}
for _, mod := range mods {
mod(model)
}
return model
}
// fixtureRequest generates an API request for tests
func fixtureRequest(mods ...func(request *intake.ApiDeleteIntakeRunnerRequest)) intake.ApiDeleteIntakeRunnerRequest {
request := testClient.DeleteIntakeRunner(testCtx, testProjectId, testRegion, testRunnerId)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "runner id invalid",
argValues: []string{"invalid-uuid"},
flagValues: fixtureFlagValues(),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest intake.ApiDeleteIntakeRunnerRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package delete
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/intake"
"github.com/stackitcloud/stackit-sdk-go/services/intake/wait"
)
const (
runnerIdArg = "RUNNER_ID"
)
// inputModel struct holds all the input parameters for the command
type inputModel struct {
*globalflags.GlobalFlagModel
RunnerId string
}
// NewCmd creates a new cobra command for deleting an Intake Runner
func NewCmd(p *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("delete %s", runnerIdArg),
Short: "Deletes an Intake Runner",
Long: "Deletes an Intake Runner.",
Args: args.SingleArg(runnerIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Delete an Intake Runner with ID "xxx"`,
`$ stackit beta intake runner delete xxx`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p.Printer, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion)
if err != nil {
return err
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete Intake Runner %q?", model.RunnerId)
err = p.Printer.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
if err = req.Execute(); err != nil {
return fmt.Errorf("delete Intake Runner: %w", err)
}
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(p.Printer)
s.Start("Deleting STACKIT Intake Runner")
_, err = wait.DeleteIntakeRunnerWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.RunnerId).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for STACKIT Intake Runner deletion: %w", err)
}
s.Stop()
}
operationState := "Deleted"
if model.Async {
operationState = "Triggered deletion of"
}
p.Printer.Outputf("%s stackit Intake Runner %s \n", operationState, model.RunnerId)
return nil
},
}
return cmd
}
// parseInput parses the command arguments and flags into a standardized model
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
runnerId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &cliErr.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
RunnerId: runnerId,
}
p.DebugInputModel(model)
return &model, nil
}
// buildRequest creates the API request to delete an Intake Runner
func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiDeleteIntakeRunnerRequest {
req := apiClient.DeleteIntakeRunner(ctx, model.ProjectId, model.Region, model.RunnerId)
return req
}
package describe
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
"github.com/stackitcloud/stackit-sdk-go/services/intake"
)
type testCtxKey struct{}
const (
testRegion = "eu01"
)
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &intake.APIClient{}
testProjectId = uuid.NewString()
testRunnerId = uuid.NewString()
)
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testRunnerId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
RunnerId: testRunnerId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *intake.ApiGetIntakeRunnerRequest)) intake.ApiGetIntakeRunnerRequest {
request := testClient.GetIntakeRunner(testCtx, testProjectId, testRegion, testRunnerId)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "runner id invalid",
argValues: []string{"invalid-uuid"},
flagValues: fixtureFlagValues(),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest intake.ApiGetIntakeRunnerRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
type args struct {
outputFormat string
runner *intake.IntakeRunnerResponse
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "default output",
args: args{outputFormat: "default", runner: &intake.IntakeRunnerResponse{}},
wantErr: false,
},
{
name: "json output",
args: args{outputFormat: print.JSONOutputFormat, runner: &intake.IntakeRunnerResponse{}},
wantErr: false,
},
{
name: "yaml output",
args: args{outputFormat: print.YAMLOutputFormat, runner: &intake.IntakeRunnerResponse{}},
wantErr: false,
},
{
name: "nil runner",
args: args{runner: nil},
wantErr: true,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.outputFormat, tt.args.runner); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package describe
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/intake"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
)
const (
runnerIdArg = "RUNNER_ID"
)
type inputModel struct {
*globalflags.GlobalFlagModel
RunnerId string
}
func NewCmd(p *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", runnerIdArg),
Short: "Shows details of an Intake Runner",
Long: "Shows details of an Intake Runner.",
Args: args.SingleArg(runnerIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Get details of an Intake Runner with ID "xxx"`,
`$ stackit beta intake runner describe xxx`),
examples.NewExample(
`Get details of an Intake Runner with ID "xxx" in JSON format`,
`$ stackit beta intake runner describe xxx --output-format json`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p.Printer, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion)
if err != nil {
return err
}
// Call API to get a single runner
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("get Intake Runner: %w", err)
}
return outputResult(p.Printer, model.OutputFormat, resp)
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
runnerId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &cliErr.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
RunnerId: runnerId,
}
p.DebugInputModel(model)
return &model, nil
}
// buildRequest creates the API request to get a single Intake Runner
func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiGetIntakeRunnerRequest {
req := apiClient.GetIntakeRunner(ctx, model.ProjectId, model.Region, model.RunnerId)
return req
}
// outputResult formats the API response and prints it to the console
func outputResult(p *print.Printer, outputFormat string, runner *intake.IntakeRunnerResponse) error {
if runner == nil {
return fmt.Errorf("received nil runner, could not display details")
}
return p.OutputResult(outputFormat, runner, func() error {
table := tables.NewTable()
table.SetHeader("Attribute", "Value")
table.AddRow("ID", runner.GetId())
table.AddRow("Name", runner.GetDisplayName())
table.AddRow("State", runner.GetState())
table.AddRow("Created", runner.GetCreateTime())
table.AddRow("Labels", runner.GetLabels())
table.AddRow("Description", runner.GetDescription())
table.AddRow("Max Message Size (KiB)", runner.GetMaxMessageSizeKiB())
table.AddRow("Max Messages/Hour", runner.GetMaxMessagesPerHour())
table.AddRow("Ingestion URI", runner.GetUri())
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
})
}
package list
import (
"context"
"strconv"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/intake"
)
type testCtxKey struct{}
const (
testRegion = "eu01"
testLimit = int64(5)
)
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &intake.APIClient{}
testProjectId = uuid.NewString()
)
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *intake.ApiListIntakeRunnersRequest)) intake.ApiListIntakeRunnersRequest {
request := testClient.ListIntakeRunners(testCtx, testProjectId, testRegion)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "with limit",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = strconv.FormatInt(testLimit, 10)
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Limit = utils.Ptr(testLimit)
}),
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "limit is zero",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "0"
}),
isValid: false,
},
{
description: "limit is negative",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "-1"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
testutils.TestParseInput(t, NewCmd, func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) {
return parseInput(p, cmd)
}, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest intake.ApiListIntakeRunnersRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
type args struct {
outputFormat string
runners []intake.IntakeRunnerResponse
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "default output",
args: args{outputFormat: "default", runners: []intake.IntakeRunnerResponse{}},
wantErr: false,
},
{
name: "json output",
args: args{outputFormat: print.JSONOutputFormat, runners: []intake.IntakeRunnerResponse{}},
wantErr: false,
},
{
name: "empty slice",
args: args{runners: []intake.IntakeRunnerResponse{}},
wantErr: false,
},
{
name: "nil slice",
args: args{runners: nil},
wantErr: false,
},
{
name: "empty intake runner in slice",
args: args{
runners: []intake.IntakeRunnerResponse{{}},
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.outputFormat, "dummy-projectlabel", tt.args.runners); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package list
import (
"context"
"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/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-sdk-go/services/intake"
)
const (
limitFlag = "limit"
)
// inputModel struct holds all the input parameters for the command
type inputModel struct {
*globalflags.GlobalFlagModel
Limit *int64
}
// NewCmd creates a new cobra command for listing Intake Runners
func NewCmd(p *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all Intake Runners",
Long: "Lists all Intake Runners for the current project.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all Intake Runners`,
`$ stackit beta intake runner list`),
examples.NewExample(
`List all Intake Runners in JSON format`,
`$ stackit beta intake runner list --output-format json`),
examples.NewExample(
`List up to 5 Intake Runners`,
`$ stackit beta intake runner list --limit 5`),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p.Printer, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("list Intake Runners: %w", err)
}
runners := resp.GetIntakeRunners()
// Truncate output
if model.Limit != nil && len(runners) > int(*model.Limit) {
runners = runners[:*model.Limit]
}
projectLabel := model.ProjectId
if len(runners) == 0 {
projectLabel, err = projectname.GetProjectName(ctx, p.Printer, p.CliVersion, cmd)
if err != nil {
p.Printer.Debug(print.ErrorLevel, "get project name: %v", err)
}
}
return outputResult(p.Printer, model.OutputFormat, projectLabel, runners)
},
}
configureFlags(cmd)
return cmd
}
// configureFlags adds the --limit flag to the command
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list")
}
// parseInput parses the command flags into a standardized model
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
limit := flags.FlagToInt64Pointer(p, cmd, limitFlag)
if limit != nil && *limit < 1 {
return nil, &errors.FlagValidationError{
Flag: limitFlag,
Details: "must be greater than 0",
}
}
model := inputModel{
GlobalFlagModel: globalFlags,
Limit: limit,
}
p.DebugInputModel(model)
return &model, nil
}
// buildRequest creates the API request to list Intake Runners
func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiListIntakeRunnersRequest {
req := apiClient.ListIntakeRunners(ctx, model.ProjectId, model.Region)
// Note: we do support API pagination, but for consistency with other services, we fetch all items and apply
// client-side limit.
// A more advanced implementation could use the --limit flag to set the API's PageSize.
return req
}
// outputResult formats the API response and prints it to the console
func outputResult(p *print.Printer, outputFormat, projectLabel string, runners []intake.IntakeRunnerResponse) error {
return p.OutputResult(outputFormat, runners, func() error {
if len(runners) == 0 {
p.Outputf("No intake runners found for project %q\n", projectLabel)
return nil
}
table := tables.NewTable()
table.SetHeader("ID", "NAME", "STATE")
for _, runner := range runners {
table.AddRow(
runner.GetId(),
runner.GetDisplayName(),
runner.GetState(),
)
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
})
}
package runner
import (
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/runner/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/runner/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/runner/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/runner/list"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/runner/update"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
)
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "runner",
Short: "Provides functionality for Intake Runners",
Long: "Provides functionality for Intake Runners.",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
// Pass the params down to each action command
cmd.AddCommand(create.NewCmd(params))
cmd.AddCommand(delete.NewCmd(params))
cmd.AddCommand(describe.NewCmd(params))
cmd.AddCommand(list.NewCmd(params))
cmd.AddCommand(update.NewCmd(params))
return cmd
}
package update
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/intake"
)
type testCtxKey struct{}
const (
testRegion = "eu01"
)
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &intake.APIClient{}
testProjectId = uuid.NewString()
testRunnerId = uuid.NewString()
)
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testRunnerId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
displayNameFlag: "new-runner-name",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
RunnerId: testRunnerId,
DisplayName: utils.Ptr("new-runner-name"),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *intake.ApiUpdateIntakeRunnerRequest)) intake.ApiUpdateIntakeRunnerRequest {
request := testClient.UpdateIntakeRunner(testCtx, testProjectId, testRegion, testRunnerId)
payload := intake.UpdateIntakeRunnerPayload{
DisplayName: utils.Ptr("new-runner-name"),
}
request = request.UpdateIntakeRunnerPayload(payload)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no update flags provided",
argValues: fixtureArgValues(),
flagValues: map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
},
isValid: false,
},
{
description: "update all fields",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[maxMessageSizeKiBFlag] = "2048"
flagValues[maxMessagesPerHourFlag] = "10000"
flagValues[descriptionFlag] = "new description"
flagValues[labelFlag] = "env=prod,team=sre"
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.MaxMessageSizeKiB = utils.Ptr(int64(2048))
model.MaxMessagesPerHour = utils.Ptr(int64(10000))
model.Description = utils.Ptr("new description")
model.Labels = utils.Ptr(map[string]string{"env": "prod", "team": "sre"})
}),
},
{
description: "no args",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest intake.ApiUpdateIntakeRunnerRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
{
description: "update description and labels",
model: fixtureInputModel(func(model *inputModel) {
model.DisplayName = nil
model.Description = utils.Ptr("new-desc")
model.Labels = utils.Ptr(map[string]string{"key": "value"})
}),
expectedRequest: fixtureRequest(func(request *intake.ApiUpdateIntakeRunnerRequest) {
payload := intake.UpdateIntakeRunnerPayload{
Description: utils.Ptr("new-desc"),
Labels: utils.Ptr(map[string]string{"key": "value"}),
}
*request = (*request).UpdateIntakeRunnerPayload(payload)
}),
},
{
description: "update all fields",
model: fixtureInputModel(func(model *inputModel) {
model.DisplayName = utils.Ptr("another-name")
model.MaxMessageSizeKiB = utils.Ptr(int64(4096))
model.MaxMessagesPerHour = utils.Ptr(int64(20000))
model.Description = utils.Ptr("final-desc")
model.Labels = utils.Ptr(map[string]string{"a": "b"})
}),
expectedRequest: fixtureRequest(func(request *intake.ApiUpdateIntakeRunnerRequest) {
payload := intake.UpdateIntakeRunnerPayload{
DisplayName: utils.Ptr("another-name"),
MaxMessageSizeKiB: utils.Ptr(int64(4096)),
MaxMessagesPerHour: utils.Ptr(int64(20000)),
Description: utils.Ptr("final-desc"),
Labels: utils.Ptr(map[string]string{"a": "b"}),
}
*request = (*request).UpdateIntakeRunnerPayload(payload)
}),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
type args struct {
model *inputModel
projectLabel string
resp *intake.IntakeRunnerResponse
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "default output",
args: args{
model: fixtureInputModel(),
projectLabel: "my-project",
resp: &intake.IntakeRunnerResponse{Id: utils.Ptr("runner-id-123")},
},
wantErr: false,
},
{
name: "default output - async",
args: args{
model: fixtureInputModel(func(model *inputModel) {
model.Async = true
}),
projectLabel: "my-project",
resp: &intake.IntakeRunnerResponse{Id: utils.Ptr("runner-id-123")},
},
wantErr: false,
},
{
name: "json output",
args: args{
model: fixtureInputModel(func(model *inputModel) {
model.OutputFormat = print.JSONOutputFormat
}),
resp: &intake.IntakeRunnerResponse{Id: utils.Ptr("runner-id-123")},
},
wantErr: false,
},
{
name: "nil response - default output",
args: args{
model: fixtureInputModel(),
resp: nil,
},
wantErr: false,
},
{
name: "nil response - json output",
args: args{
model: fixtureInputModel(func(model *inputModel) {
model.OutputFormat = print.JSONOutputFormat
}),
resp: nil,
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package update
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
cliErr "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/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/intake"
"github.com/stackitcloud/stackit-sdk-go/services/intake/wait"
)
const (
runnerIdArg = "RUNNER_ID"
)
const (
displayNameFlag = "display-name"
maxMessageSizeKiBFlag = "max-message-size-kib"
maxMessagesPerHourFlag = "max-messages-per-hour"
descriptionFlag = "description"
labelFlag = "labels"
)
type inputModel struct {
*globalflags.GlobalFlagModel
RunnerId string
DisplayName *string
MaxMessageSizeKiB *int64
MaxMessagesPerHour *int64
Description *string
Labels *map[string]string
}
func NewCmd(p *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("update %s", runnerIdArg),
Short: "Updates an Intake Runner",
Long: "Updates an Intake Runner. Only the specified fields are updated.",
Args: args.SingleArg(runnerIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Update the display name of an Intake Runner with ID "xxx"`,
`$ stackit beta intake runner update xxx --display-name "new-runner-name"`),
examples.NewExample(
`Update the message capacity limits for an Intake Runner with ID "xxx"`,
`$ stackit beta intake runner update xxx --max-message-size-kib 1000 --max-messages-per-hour 10000`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p.Printer, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion)
if err != nil {
return err
}
projectLabel, err := projectname.GetProjectName(ctx, p.Printer, p.CliVersion, cmd)
if err != nil {
p.Printer.Debug(print.ErrorLevel, "get project name: %v", err)
projectLabel = model.ProjectId
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("update Intake Runner: %w", err)
}
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(p.Printer)
s.Start("Updating STACKIT Intake Runner")
_, err = wait.CreateOrUpdateIntakeRunnerWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.RunnerId).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for STACKIT Intake Runner update: %w", err)
}
s.Stop()
}
return outputResult(p.Printer, model, projectLabel, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().String(displayNameFlag, "", "Display name")
cmd.Flags().Int64(maxMessageSizeKiBFlag, 0, "Maximum message size in KiB. Note: Overall message capacity cannot be decreased.")
cmd.Flags().Int64(maxMessagesPerHourFlag, 0, "Maximum number of messages per hour. Note: Overall message capacity cannot be decreased.")
cmd.Flags().String(descriptionFlag, "", "Description")
cmd.Flags().StringToString(labelFlag, nil, `Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2".`)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
runnerId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &cliErr.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
RunnerId: runnerId,
DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag),
MaxMessageSizeKiB: flags.FlagToInt64Pointer(p, cmd, maxMessageSizeKiBFlag),
MaxMessagesPerHour: flags.FlagToInt64Pointer(p, cmd, maxMessagesPerHourFlag),
Description: flags.FlagToStringPointer(p, cmd, descriptionFlag),
Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag),
}
if model.DisplayName == nil && model.MaxMessageSizeKiB == nil && model.MaxMessagesPerHour == nil && model.Description == nil && model.Labels == nil {
return nil, &cliErr.EmptyUpdateError{}
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiUpdateIntakeRunnerRequest {
req := apiClient.UpdateIntakeRunner(ctx, model.ProjectId, model.Region, model.RunnerId)
payload := intake.UpdateIntakeRunnerPayload{}
if model.DisplayName != nil {
payload.DisplayName = model.DisplayName
}
if model.MaxMessageSizeKiB != nil {
payload.MaxMessageSizeKiB = model.MaxMessageSizeKiB
}
if model.MaxMessagesPerHour != nil {
payload.MaxMessagesPerHour = model.MaxMessagesPerHour
}
if model.Description != nil {
payload.Description = model.Description
}
if model.Labels != nil {
payload.Labels = model.Labels
}
req = req.UpdateIntakeRunnerPayload(payload)
return req
}
func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *intake.IntakeRunnerResponse) error {
return p.OutputResult(model.OutputFormat, resp, func() error {
if resp == nil {
p.Outputf("Triggered update of Intake Runner for project %q, but no runner ID was returned.\n", projectLabel)
return nil
}
operationState := "Updated"
if model.Async {
operationState = "Triggered update of"
}
p.Outputf("%s Intake Runner for project %q. Runner ID: %s\n", operationState, projectLabel, utils.PtrString(resp.Id))
return nil
})
}
package describe
import (
"bytes"
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &kms.APIClient{}
var testProjectId = uuid.NewString()
var testKeyRingID = uuid.NewString()
var testKeyID = uuid.NewString()
var testTime = time.Time{}
const testRegion = "eu01"
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
flagKeyRingID: testKeyRingID,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyID: testKeyID,
KeyRingID: testKeyRingID,
}
for _, mod := range mods {
mod(model)
}
return model
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argValues: []string{testKeyID},
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argValues: []string{},
flagValues: map[string]string{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: []string{testKeyID},
flagValues: map[string]string{},
isValid: false,
},
{
description: "invalid key id",
argValues: []string{"invalid-uuid"},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "missing key ring id",
argValues: []string{testKeyID},
flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, flagKeyRingID) }),
isValid: false,
},
{
description: "invalid key ring id",
argValues: []string{testKeyID},
flagValues: fixtureFlagValues(func(m map[string]string) {
m[flagKeyRingID] = "invalid-uuid"
}),
isValid: false,
},
{
description: "missing project id",
argValues: []string{testKeyID},
flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, globalflags.ProjectIdFlag) }),
isValid: false,
},
{
description: "invalid project id",
argValues: []string{testKeyID},
flagValues: fixtureFlagValues(func(m map[string]string) { m[globalflags.ProjectIdFlag] = "invalid-uuid" }),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
})
}
}
func TestBuildRequest(t *testing.T) {
got := buildRequest(testCtx, fixtureInputModel(), testClient)
want := testClient.GetKey(testCtx, testProjectId, testRegion, testKeyRingID, testKeyID)
diff := cmp.Diff(got, want,
cmp.AllowUnexported(want),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("buildRequest() mismatch (-want +got):\n%s", diff)
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
outputFmt string
keyRing *kms.Key
wantErr bool
expected string
}{
{
description: "empty",
outputFmt: "table",
wantErr: true,
},
{
description: "table format",
outputFmt: "table",
keyRing: &kms.Key{
AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC),
Algorithm: utils.Ptr(kms.ALGORITHM_AES_256_GCM),
CreatedAt: utils.Ptr(testTime),
DeletionDate: nil,
Description: utils.Ptr("very secure and secret key"),
DisplayName: utils.Ptr("Test Key"),
Id: utils.Ptr(testKeyID),
ImportOnly: utils.Ptr(true),
KeyRingId: utils.Ptr(testKeyRingID),
Protection: utils.Ptr(kms.PROTECTION_SOFTWARE),
Purpose: utils.Ptr(kms.PURPOSE_SYMMETRIC_ENCRYPT_DECRYPT),
State: utils.Ptr(kms.KEYSTATE_ACTIVE),
},
expected: fmt.Sprintf(`
ID │ %-37s
───────────────┼──────────────────────────────────────
DISPLAY NAME │ Test Key
───────────────┼──────────────────────────────────────
CREATED AT │ %-37s
───────────────┼──────────────────────────────────────
STATE │ active
───────────────┼──────────────────────────────────────
DESCRIPTION │ very secure and secret key
───────────────┼──────────────────────────────────────
ACCESS SCOPE │ PUBLIC
───────────────┼──────────────────────────────────────
ALGORITHM │ aes_256_gcm
───────────────┼──────────────────────────────────────
DELETION DATE │
───────────────┼──────────────────────────────────────
IMPORT ONLY │ true
───────────────┼──────────────────────────────────────
KEYRING ID │ %-37s
───────────────┼──────────────────────────────────────
PROTECTION │ software
───────────────┼──────────────────────────────────────
PURPOSE │ symmetric_encrypt_decrypt
`,
testKeyID,
testTime,
testKeyRingID,
),
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
var buf bytes.Buffer
p.Cmd.SetOut(&buf)
if err := outputResult(p, tt.outputFmt, tt.keyRing); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
diff := cmp.Diff(buf.String(), tt.expected)
if diff != "" {
t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff)
}
})
}
}
package describe
import (
"context"
"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/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
argKeyID = "KEY_ID"
flagKeyRingID = "keyring-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyID string
KeyRingID string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", argKeyID),
Short: "Describe a KMS key",
Long: "Describe a KMS key",
Args: args.SingleArg(argKeyID, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Describe a KMS key with ID xxx of keyring yyy`,
`$ stackit beta kms key describe xxx --keyring-id yyy`,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("get key: %w", err)
}
return outputResult(params.Printer, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), flagKeyRingID, "Key Ring ID")
err := flags.MarkFlagsRequired(cmd, flagKeyRingID)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := &inputModel{
GlobalFlagModel: globalFlags,
KeyID: args[0],
KeyRingID: flags.FlagToStringValue(p, cmd, flagKeyRingID),
}
p.DebugInputModel(model)
return model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiGetKeyRequest {
return apiClient.GetKey(ctx, model.ProjectId, model.Region, model.KeyRingID, model.KeyID)
}
func outputResult(p *print.Printer, outputFormat string, key *kms.Key) error {
if key == nil {
return fmt.Errorf("key response is empty")
}
return p.OutputResult(outputFormat, key, func() error {
table := tables.NewTable()
table.AddRow("ID", utils.PtrString(key.Id))
table.AddSeparator()
table.AddRow("DISPLAY NAME", utils.PtrString(key.DisplayName))
table.AddSeparator()
table.AddRow("CREATED AT", utils.PtrString(key.CreatedAt))
table.AddSeparator()
table.AddRow("STATE", utils.PtrString(key.State))
table.AddSeparator()
table.AddRow("DESCRIPTION", utils.PtrString(key.Description))
table.AddSeparator()
table.AddRow("ACCESS SCOPE", utils.PtrString(key.AccessScope))
table.AddSeparator()
table.AddRow("ALGORITHM", utils.PtrString(key.Algorithm))
table.AddSeparator()
table.AddRow("DELETION DATE", utils.PtrString(key.DeletionDate))
table.AddSeparator()
table.AddRow("IMPORT ONLY", utils.PtrString(key.ImportOnly))
table.AddSeparator()
table.AddRow("KEYRING ID", utils.PtrString(key.KeyRingId))
table.AddSeparator()
table.AddRow("PROTECTION", utils.PtrString(key.Protection))
table.AddSeparator()
table.AddRow("PURPOSE", utils.PtrString(key.Purpose))
err := table.Display(p)
if err != nil {
return fmt.Errorf("display table: %w", err)
}
return nil
})
}
package describe
import (
"bytes"
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &kms.APIClient{}
var testProjectId = uuid.NewString()
var testKeyRingID = uuid.NewString()
var testTime = time.Time{}
const testRegion = "eu01"
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingID: testKeyRingID,
}
for _, mod := range mods {
mod(model)
}
return model
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argValues: []string{testKeyRingID},
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argValues: []string{},
flagValues: map[string]string{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: []string{testKeyRingID},
flagValues: map[string]string{},
isValid: false,
},
{
description: "invalid key ring id",
argValues: []string{"!invalid-uuid"},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "missing project id",
argValues: []string{testKeyRingID},
flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, globalflags.ProjectIdFlag) }),
isValid: false,
},
{
description: "invalid project id",
argValues: []string{testKeyRingID},
flagValues: fixtureFlagValues(func(m map[string]string) { m[globalflags.ProjectIdFlag] = "invalid-uuid" }),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
})
}
}
func TestBuildRequest(t *testing.T) {
got := buildRequest(testCtx, fixtureInputModel(), testClient)
want := testClient.GetKeyRing(testCtx, testProjectId, testRegion, testKeyRingID)
diff := cmp.Diff(got, want,
cmp.AllowUnexported(want),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("buildRequest() mismatch (-want +got):\n%s", diff)
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
outputFmt string
keyRing *kms.KeyRing
wantErr bool
expected string
}{
{
description: "empty",
outputFmt: "table",
wantErr: true,
},
{
description: "table format",
outputFmt: "table",
keyRing: &kms.KeyRing{
Id: utils.Ptr(testKeyRingID),
DisplayName: utils.Ptr("Test Key Ring"),
CreatedAt: utils.Ptr(testTime),
Description: utils.Ptr("This is a test key ring."),
State: utils.Ptr(kms.KEYRINGSTATE_ACTIVE),
},
expected: fmt.Sprintf(`
ID │ %-37s
──────────────┼──────────────────────────────────────
DISPLAY NAME │ Test Key Ring
──────────────┼──────────────────────────────────────
CREATED AT │ %-37s
──────────────┼──────────────────────────────────────
STATE │ active
──────────────┼──────────────────────────────────────
DESCRIPTION │ This is a test key ring.
`,
testKeyRingID,
testTime,
),
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
var buf bytes.Buffer
p.Cmd.SetOut(&buf)
if err := outputResult(p, tt.outputFmt, tt.keyRing); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
diff := cmp.Diff(buf.String(), tt.expected)
if diff != "" {
t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff)
}
})
}
}
package describe
import (
"context"
"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/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
argKeyRingID = "KEYRING_ID"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyRingID string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", argKeyRingID),
Short: "Describe a KMS key ring",
Long: "Describe a KMS key ring",
Args: args.SingleArg(argKeyRingID, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Describe a KMS key ring with ID xxx`,
`$ stackit beta kms keyring describe xxx`,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("get key ring: %w", err)
}
return outputResult(params.Printer, model.OutputFormat, resp)
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := &inputModel{
GlobalFlagModel: globalFlags,
KeyRingID: args[0],
}
p.DebugInputModel(model)
return model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiGetKeyRingRequest {
return apiClient.GetKeyRing(ctx, model.ProjectId, model.Region, model.KeyRingID)
}
func outputResult(p *print.Printer, outputFormat string, keyRing *kms.KeyRing) error {
if keyRing == nil {
return fmt.Errorf("key ring response is empty")
}
return p.OutputResult(outputFormat, keyRing, func() error {
table := tables.NewTable()
table.AddRow("ID", utils.PtrString(keyRing.Id))
table.AddSeparator()
table.AddRow("DISPLAY NAME", utils.PtrString(keyRing.DisplayName))
table.AddSeparator()
table.AddRow("CREATED AT", utils.PtrString(keyRing.CreatedAt))
table.AddSeparator()
table.AddRow("STATE", utils.PtrString(keyRing.State))
table.AddSeparator()
table.AddRow("DESCRIPTION", utils.PtrString(keyRing.Description))
err := table.Display(p)
if err != nil {
return fmt.Errorf("display table: %w", err)
}
return nil
})
}
package describe
import (
"bytes"
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &kms.APIClient{}
var testProjectId = uuid.NewString()
var testKeyRingID = uuid.NewString()
var testWrappingKeyID = uuid.NewString()
var testTime = time.Time{}
const testRegion = "eu01"
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
flagKeyRingID: testKeyRingID,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingID: testKeyRingID,
WrappingKeyID: testWrappingKeyID,
}
for _, mod := range mods {
mod(model)
}
return model
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argValues: []string{testWrappingKeyID},
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argValues: []string{},
flagValues: map[string]string{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: []string{testWrappingKeyID},
flagValues: map[string]string{},
isValid: false,
},
{
description: "invalid key ring id",
argValues: []string{testWrappingKeyID},
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[flagKeyRingID] = "invalid-uuid"
}),
isValid: false,
},
{
description: "missing project id",
argValues: []string{testWrappingKeyID},
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "invalid project id",
argValues: []string{testWrappingKeyID},
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
})
}
}
func TestBuildRequest(t *testing.T) {
got := buildRequest(testCtx, fixtureInputModel(), testClient)
want := testClient.GetWrappingKey(testCtx, testProjectId, testRegion, testKeyRingID, testWrappingKeyID)
diff := cmp.Diff(got, want,
cmp.AllowUnexported(want),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("buildRequest() mismatch (-want +got):\n%s", diff)
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
outputFmt string
keyRing *kms.WrappingKey
wantErr bool
expected string
}{
{
description: "empty",
outputFmt: "table",
wantErr: true,
},
{
description: "table format",
outputFmt: "table",
keyRing: &kms.WrappingKey{
Id: utils.Ptr(testWrappingKeyID),
DisplayName: utils.Ptr("Test Key Ring"),
CreatedAt: utils.Ptr(testTime),
Description: utils.Ptr("This is a test key ring."),
State: utils.Ptr(kms.WRAPPINGKEYSTATE_ACTIVE),
AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC),
Algorithm: utils.Ptr(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256),
ExpiresAt: utils.Ptr(testTime),
KeyRingId: utils.Ptr(testKeyRingID),
Protection: utils.Ptr(kms.PROTECTION_SOFTWARE),
PublicKey: utils.Ptr("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQ...\n-----END PUBLIC KEY-----"),
Purpose: utils.Ptr(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY),
},
expected: fmt.Sprintf(`
ID │ %-46s
──────────────┼───────────────────────────────────────────────
DISPLAY NAME │ Test Key Ring
──────────────┼───────────────────────────────────────────────
CREATED AT │ %-46s
──────────────┼───────────────────────────────────────────────
STATE │ active
──────────────┼───────────────────────────────────────────────
DESCRIPTION │ This is a test key ring.
──────────────┼───────────────────────────────────────────────
ACCESS SCOPE │ PUBLIC
──────────────┼───────────────────────────────────────────────
ALGORITHM │ rsa_2048_oaep_sha256
──────────────┼───────────────────────────────────────────────
EXPIRES AT │ %-46s
──────────────┼───────────────────────────────────────────────
KEYRING ID │ %-46s
──────────────┼───────────────────────────────────────────────
PROTECTION │ software
──────────────┼───────────────────────────────────────────────
PUBLIC KEY │ -----BEGIN PUBLIC KEY-----
│ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQ...
│ -----END PUBLIC KEY-----
──────────────┼───────────────────────────────────────────────
PURPOSE │ wrap_asymmetric_key
`,
testWrappingKeyID,
testTime,
testTime,
testKeyRingID),
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
var buf bytes.Buffer
p.Cmd.SetOut(&buf)
if err := outputResult(p, tt.outputFmt, tt.keyRing); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
diff := cmp.Diff(buf.String(), tt.expected)
if diff != "" {
t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff)
}
})
}
}
package describe
import (
"context"
"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/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
argWrappingKeyID = "WRAPPING_KEY_ID"
flagKeyRingID = "keyring-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
WrappingKeyID string
KeyRingID string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", argWrappingKeyID),
Short: "Describe a KMS wrapping key",
Long: "Describe a KMS wrapping key",
Args: args.SingleArg(argWrappingKeyID, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Describe a KMS wrapping key with ID xxx of keyring yyy`,
`$ stackit beta kms wrappingkey describe xxx --keyring-id yyy`,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("get wrapping key: %w", err)
}
return outputResult(params.Printer, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), flagKeyRingID, "Key Ring ID")
err := flags.MarkFlagsRequired(cmd, flagKeyRingID)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := &inputModel{
GlobalFlagModel: globalFlags,
WrappingKeyID: args[0],
KeyRingID: flags.FlagToStringValue(p, cmd, flagKeyRingID),
}
p.DebugInputModel(model)
return model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiGetWrappingKeyRequest {
return apiClient.GetWrappingKey(ctx, model.ProjectId, model.Region, model.KeyRingID, model.WrappingKeyID)
}
func outputResult(p *print.Printer, outputFormat string, wrappingKey *kms.WrappingKey) error {
if wrappingKey == nil {
return fmt.Errorf("wrapping key response is empty")
}
return p.OutputResult(outputFormat, wrappingKey, func() error {
table := tables.NewTable()
table.AddRow("ID", utils.PtrString(wrappingKey.Id))
table.AddSeparator()
table.AddRow("DISPLAY NAME", utils.PtrString(wrappingKey.DisplayName))
table.AddSeparator()
table.AddRow("CREATED AT", utils.PtrString(wrappingKey.CreatedAt))
table.AddSeparator()
table.AddRow("STATE", utils.PtrString(wrappingKey.State))
table.AddSeparator()
table.AddRow("DESCRIPTION", utils.PtrString(wrappingKey.Description))
table.AddSeparator()
table.AddRow("ACCESS SCOPE", utils.PtrString(wrappingKey.AccessScope))
table.AddSeparator()
table.AddRow("ALGORITHM", utils.PtrString(wrappingKey.Algorithm))
table.AddSeparator()
table.AddRow("EXPIRES AT", utils.PtrString(wrappingKey.ExpiresAt))
table.AddSeparator()
table.AddRow("KEYRING ID", utils.PtrString(wrappingKey.KeyRingId))
table.AddSeparator()
table.AddRow("PROTECTION", utils.PtrString(wrappingKey.Protection))
table.AddSeparator()
table.AddRow("PUBLIC KEY", utils.PtrString(wrappingKey.PublicKey))
table.AddSeparator()
table.AddRow("PURPOSE", utils.PtrString(wrappingKey.Purpose))
err := table.Display(p)
if err != nil {
return fmt.Errorf("display table: %w", err)
}
return nil
})
}
package client
import (
"github.com/spf13/viper"
"github.com/stackitcloud/stackit-cli/internal/pkg/config"
genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/intake"
)
// ConfigureClient creates and configures a new Intake API client
func ConfigureClient(p *print.Printer, cliVersion string) (*intake.APIClient, error) {
return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.IntakeCustomEndpointKey), true, genericclient.CreateApiClient[*intake.APIClient](intake.NewAPIClient))
}
+2
-2

@@ -20,3 +20,3 @@ name: CI

- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6

@@ -51,3 +51,3 @@ - name: Install go

- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6

@@ -54,0 +54,0 @@ - name: Check GoReleaser

@@ -26,3 +26,3 @@ # STACKIT CLI release workflow.

- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:

@@ -110,3 +110,3 @@ # Allow goreleaser to access older tag information.

- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6

@@ -149,3 +149,3 @@ # use the artifacts from the "goreleaser" job

- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6

@@ -152,0 +152,0 @@ - name: Download artifacts from workflow

@@ -14,7 +14,7 @@ name: Renovate

- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Self-hosted Renovate
uses: renovatebot/github-action@v44.0.3
uses: renovatebot/github-action@v44.0.4
with:
configurationFile: .github/renovate.json
token: ${{ secrets.RENOVATE_TOKEN }}

@@ -35,2 +35,3 @@ ## stackit beta kms key

* [stackit beta kms key delete](./stackit_beta_kms_key_delete.md) - Deletes a KMS key
* [stackit beta kms key describe](./stackit_beta_kms_key_describe.md) - Describe a KMS key
* [stackit beta kms key import](./stackit_beta_kms_key_import.md) - Import a KMS key

@@ -37,0 +38,0 @@ * [stackit beta kms key list](./stackit_beta_kms_key_list.md) - List all KMS keys

@@ -35,3 +35,4 @@ ## stackit beta kms keyring

* [stackit beta kms keyring delete](./stackit_beta_kms_keyring_delete.md) - Deletes a KMS key ring
* [stackit beta kms keyring describe](./stackit_beta_kms_keyring_describe.md) - Describe a KMS key ring
* [stackit beta kms keyring list](./stackit_beta_kms_keyring_list.md) - Lists all KMS key rings

@@ -35,3 +35,4 @@ ## stackit beta kms wrapping-key

* [stackit beta kms wrapping-key delete](./stackit_beta_kms_wrapping-key_delete.md) - Deletes a KMS wrapping key
* [stackit beta kms wrapping-key describe](./stackit_beta_kms_wrapping-key_describe.md) - Describe a KMS wrapping key
* [stackit beta kms wrapping-key list](./stackit_beta_kms_wrapping-key_list.md) - Lists all KMS wrapping keys

@@ -45,4 +45,5 @@ ## stackit beta

* [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers
* [stackit beta intake](./stackit_beta_intake.md) - Provides functionality for intake
* [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS
* [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex

@@ -39,2 +39,3 @@ ## stackit config set

--identity-provider-custom-well-known-configuration string Identity Provider well-known OpenID configuration URL, used for user authentication
--intake-custom-endpoint string Intake API base URL, used in calls to this API
--kms-custom-endpoint string KMS API base URL, used in calls to this API

@@ -41,0 +42,0 @@ --load-balancer-custom-endpoint string Load Balancer API base URL, used in calls to this API

@@ -37,2 +37,3 @@ ## stackit config unset

--identity-provider-custom-well-known-configuration Identity Provider well-known OpenID configuration URL. If unset, uses the default identity provider
--intake-custom-endpoint Intake API base URL. If unset, uses the default base URL
--kms-custom-endpoint KMS API base URL. If unset, uses the default base URL

@@ -39,0 +40,0 @@ --load-balancer-custom-endpoint Load Balancer API base URL. If unset, uses the default base URL

+28
-27

@@ -7,3 +7,3 @@ module github.com/stackitcloud/stackit-cli

github.com/fatih/color v1.18.0
github.com/goccy/go-yaml v1.18.0
github.com/goccy/go-yaml v1.19.0
github.com/golang-jwt/jwt/v5 v5.3.0

@@ -13,3 +13,3 @@ github.com/google/go-cmp v0.7.0

github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf
github.com/jedib0t/go-pretty/v6 v6.7.2
github.com/jedib0t/go-pretty/v6 v6.7.5
github.com/lmittmann/tint v1.1.2

@@ -20,20 +20,21 @@ github.com/mattn/go-colorable v0.1.14

github.com/spf13/viper v1.21.0
github.com/stackitcloud/stackit-sdk-go/core v0.19.0
github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.1
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0
github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1
github.com/stackitcloud/stackit-sdk-go/services/git v0.9.0
github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.0
github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.2
github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.1
github.com/stackitcloud/stackit-sdk-go/core v0.20.0
github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.2
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.10.0
github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.2
github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1
github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.2
github.com/stackitcloud/stackit-sdk-go/services/intake v0.4.0
github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.3
github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.2
github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.0
github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.1
github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.2
github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1
github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.1
github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2
github.com/stackitcloud/stackit-sdk-go/services/ske v1.4.0
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.1
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.1
github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.2
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2
github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3
github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.2
github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.2
github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.3
github.com/stackitcloud/stackit-sdk-go/services/ske v1.5.0
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.2
github.com/zalando/go-keyring v0.2.6

@@ -248,10 +249,10 @@ golang.org/x/mod v0.30.0

github.com/spf13/cast v1.10.0 // indirect
github.com/stackitcloud/stackit-sdk-go/services/kms v1.1.0
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0
github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.1
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.4.0
github.com/stackitcloud/stackit-sdk-go/services/observability v0.15.0
github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.1
github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1
github.com/stackitcloud/stackit-sdk-go/services/kms v1.1.1
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.1
github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.2
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.2
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.4.1
github.com/stackitcloud/stackit-sdk-go/services/observability v0.15.1
github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.2
github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.2
github.com/subosito/gotenv v1.6.0 // indirect

@@ -258,0 +259,0 @@ go.uber.org/multierr v1.11.0 // indirect

@@ -10,3 +10,3 @@ # This file contains all available configuration options

# timeout for analysis, e.g. 30s, 5m, default is 1m
timeout: 5m
timeout: 10m
linters-settings:

@@ -13,0 +13,0 @@ goimports:

@@ -8,2 +8,4 @@ package list

"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/google/go-cmp/cmp/cmpopts"

@@ -23,9 +25,12 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/params"

var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &alb.APIClient{}
testProjectId = uuid.NewString()
testRegion = "eu01"
testLimit int64 = 10
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &alb.APIClient{}
testProjectId = uuid.NewString()
)
const (
testRegion = "eu01"
testLimit int64 = 10
)
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {

@@ -46,3 +51,3 @@ flagValues := map[string]string{

GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Region: testRegion, Verbosity: globalflags.VerbosityDefault},
Limit: &testLimit,
Limit: utils.Ptr(testLimit),
}

@@ -142,2 +147,3 @@ for _, mod := range mods {

outputFormat string
projectLabel string
items []alb.LoadBalancer

@@ -171,3 +177,3 @@ }

t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr {
if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.items); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)

@@ -174,0 +180,0 @@ }

@@ -28,4 +28,3 @@ package list

const (
labelSelectorFlag = "label-selector"
limitFlag = "limit"
limitFlag = "limit"
)

@@ -77,15 +76,10 @@

}
items := response.GetLoadBalancers()
if items := response.LoadBalancers; items == nil || len(*items) == 0 {
params.Printer.Info("No load balancers found for project %q", projectLabel)
} else {
if model.Limit != nil && len(*items) > int(*model.Limit) {
*items = (*items)[:*model.Limit]
}
if err := outputResult(params.Printer, model.OutputFormat, *items); err != nil {
return fmt.Errorf("output loadbalancers: %w", err)
}
// Truncate output
if model.Limit != nil && len(items) > int(*model.Limit) {
items = items[:*model.Limit]
}
return nil
return outputResult(params.Printer, model.OutputFormat, projectLabel, items)
},

@@ -130,4 +124,9 @@ }

}
func outputResult(p *print.Printer, outputFormat string, items []alb.LoadBalancer) error {
func outputResult(p *print.Printer, outputFormat, projectLabel string, items []alb.LoadBalancer) error {
return p.OutputResult(outputFormat, items, func() error {
if len(items) == 0 {
p.Outputf("No load balancers found for project %q", projectLabel)
return nil
}
table := tables.NewTable()

@@ -134,0 +133,0 @@ table.SetHeader("NAME", "EXTERNAL ADDRESS", "REGION", "STATUS", "VERSION", "ERRORS")

@@ -71,9 +71,5 @@ package list

}
items := resp.GetCredentials()
if resp.Credentials == nil || len(*resp.Credentials) == 0 {
params.Printer.Info("No credentials found\n")
return nil
}
items := *resp.Credentials
// Truncate output
if model.Limit != nil && len(items) > int(*model.Limit) {

@@ -120,8 +116,8 @@ items = items[:*model.Limit]

func outputResult(p *print.Printer, outputFormat string, items []alb.CredentialsResponse) error {
if items == nil {
p.Outputln("no credentials found")
return nil
}
return p.OutputResult(outputFormat, items, func() error {
if len(items) == 0 {
p.Outputf("No credentials found\n")
return nil
}
return p.OutputResult(outputFormat, items, func() error {
table := tables.NewTable()

@@ -128,0 +124,0 @@ table.SetHeader("CREDENTIAL REF", "DISPLAYNAME", "USERNAME", "REGION")

@@ -24,5 +24,6 @@ package plans

testProjectId = uuid.NewString()
testRegion = "eu01"
)
const testRegion = "eu01"
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {

@@ -136,2 +137,3 @@ flagValues := map[string]string{

outputFormat string
projectLabel string
items []alb.PlanDetails

@@ -165,3 +167,3 @@ }

t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr {
if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.items); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)

@@ -168,0 +170,0 @@ }

@@ -65,12 +65,5 @@ package plans

}
items := response.GetValidPlans()
if items := response.ValidPlans; items == nil || len(*items) == 0 {
params.Printer.Info("No plans found for project %q", projectLabel)
} else {
if err := outputResult(params.Printer, model.OutputFormat, *items); err != nil {
return fmt.Errorf("output plans: %w", err)
}
}
return nil
return outputResult(params.Printer, model.OutputFormat, projectLabel, items)
},

@@ -102,4 +95,9 @@ }

func outputResult(p *print.Printer, outputFormat string, items []alb.PlanDetails) error {
func outputResult(p *print.Printer, outputFormat, projectLabel string, items []alb.PlanDetails) error {
return p.OutputResult(outputFormat, items, func() error {
if len(items) == 0 {
p.Outputf("No plans found for project %q", projectLabel)
return nil
}
table := tables.NewTable()

@@ -106,0 +104,0 @@ table.SetHeader("PLAN ID", "NAME", "FLAVOR", "MAX CONNS", "DESCRIPTION")

@@ -7,2 +7,3 @@ package beta

"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms"

@@ -43,3 +44,4 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex"

cmd.AddCommand(alb.NewCmd(params))
cmd.AddCommand(intake.NewCmd(params))
cmd.AddCommand(kms.NewCmd(params))
}

@@ -6,2 +6,3 @@ package key

"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/importKey"

@@ -37,2 +38,3 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/list"

cmd.AddCommand(rotate.NewCmd(params))
cmd.AddCommand(describe.NewCmd(params))
}

@@ -6,2 +6,3 @@ package keyring

"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/list"

@@ -31,2 +32,3 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/params"

cmd.AddCommand(create.NewCmd(params))
cmd.AddCommand(describe.NewCmd(params))
}

@@ -6,2 +6,3 @@ package wrappingkey

"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/list"

@@ -31,2 +32,3 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/params"

cmd.AddCommand(create.NewCmd(params))
cmd.AddCommand(describe.NewCmd(params))
}

@@ -50,2 +50,3 @@ package set

tokenCustomEndpointFlag = "token-custom-endpoint"
intakeCustomEndpointFlag = "intake-custom-endpoint"
)

@@ -165,2 +166,3 @@

cmd.Flags().String(tokenCustomEndpointFlag, "", "Custom token endpoint of the Service Account API, which is used to request access tokens when the service account authentication is activated. Not relevant for user authentication.")
cmd.Flags().String(intakeCustomEndpointFlag, "", "Intake API base URL, used in calls to this API")

@@ -224,2 +226,4 @@ err := viper.BindPFlag(config.SessionTimeLimitKey, cmd.Flags().Lookup(sessionTimeLimitFlag))

cobra.CheckErr(err)
err = viper.BindPFlag(config.IntakeCustomEndpointKey, cmd.Flags().Lookup(intakeCustomEndpointFlag))
cobra.CheckErr(err)
}

@@ -226,0 +230,0 @@

@@ -47,2 +47,3 @@ package unset

tokenCustomEndpointFlag: true,
intakeCustomEndpointFlag: true,
}

@@ -88,2 +89,3 @@ for _, mod := range mods {

TokenCustomEndpoint: true,
IntakeCustomEndpoint: true,
}

@@ -145,2 +147,3 @@ for _, mod := range mods {

model.TokenCustomEndpoint = false
model.IntakeCustomEndpoint = false
}),

@@ -147,0 +150,0 @@ },

@@ -54,2 +54,3 @@ package unset

tokenCustomEndpointFlag = "token-custom-endpoint"
intakeCustomEndpointFlag = "intake-custom-endpoint"
)

@@ -93,2 +94,3 @@

TokenCustomEndpoint bool
IntakeCustomEndpoint bool
}

@@ -217,2 +219,5 @@

}
if model.IntakeCustomEndpoint {
viper.Set(config.IntakeCustomEndpointKey, "")
}

@@ -266,2 +271,3 @@ err := config.Write()

cmd.Flags().Bool(tokenCustomEndpointFlag, false, "Custom token endpoint of the Service Account API, which is used to request access tokens when the service account authentication is activated. Not relevant for user authentication.")
cmd.Flags().Bool(intakeCustomEndpointFlag, false, "Intake API base URL. If unset, uses the default base URL")
}

@@ -306,2 +312,3 @@

TokenCustomEndpoint: flags.FlagToBoolValue(p, cmd, tokenCustomEndpointFlag),
IntakeCustomEndpoint: flags.FlagToBoolValue(p, cmd, intakeCustomEndpointFlag),
}

@@ -308,0 +315,0 @@

@@ -248,4 +248,4 @@ package auth

// Print the link
p.Outputln("Your browser has been opened to visit:\n")
p.Outputf("%s\n\n", authorizationURL)
p.Info("Your browser has been opened to visit:\n\n")
p.Info("%s\n\n", authorizationURL)

@@ -252,0 +252,0 @@ // Start the blocking web server loop

@@ -51,2 +51,3 @@ package config

GitCustomEndpointKey = "git_custom_endpoint"
IntakeCustomEndpointKey = "intake_custom_endpoint"

@@ -112,2 +113,3 @@ ProjectNameKey = "project_name"

GitCustomEndpointKey,
IntakeCustomEndpointKey,
AlbCustomEndpoint,

@@ -200,2 +202,3 @@ }

viper.SetDefault(GitCustomEndpointKey, "")
viper.SetDefault(IntakeCustomEndpointKey, "")
viper.SetDefault(AlbCustomEndpoint, "")

@@ -202,0 +205,0 @@ }

Sorry, the diff of this file is too big to display