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

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

Package Overview
Dependencies
Versions
173
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

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

Comparing version
v0.30.0
to
v0.31.0
+10
.github/dependabot.yml
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
name: "Stale"
on:
schedule:
# every night at 01:30
- cron: "30 1 * * *"
# run this workflow if the workflow definition gets changed within a PR
pull_request:
branches: ["main"]
paths: [".github/workflows/stale.yaml"]
env:
DAYS_BEFORE_PR_STALE: 7
DAYS_BEFORE_PR_CLOSE: 7
permissions:
issues: write
pull-requests: write
jobs:
stale:
name: "Stale"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: "Mark old PRs as stale"
uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-pr-message: "This PR was marked as stale after ${{ env.DAYS_BEFORE_PR_STALE }} days of inactivity and will be closed after another ${{ env.DAYS_BEFORE_PR_CLOSE }} days of further inactivity. If this PR should be kept open, just add a comment, remove the stale label or push new commits to it."
close-pr-message: "This PR was closed automatically because it has been stalled for ${{ env.DAYS_BEFORE_PR_CLOSE }} days with no activity. Feel free to re-open it at any time."
days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }}
days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }}
# never mark issues as stale or close them
days-before-issue-stale: -1
days-before-issue-close: -1
## stackit beta alb create
Creates an application loadbalancer
### Synopsis
Creates an application loadbalancer.
```
stackit beta alb create [flags]
```
### Examples
```
Create an application loadbalancer from a configuration file
$ stackit beta alb create --configuration my-loadbalancer.json
```
### Options
```
-c, --configuration string Filename of the input configuration file
-h, --help Help for "stackit beta alb create"
```
### 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 alb](./stackit_beta_alb.md) - Manages application loadbalancers
## stackit beta alb delete
Deletes an application loadbalancer
### Synopsis
Deletes an application loadbalancer.
```
stackit beta alb delete LOADBALANCER_NAME_ARG [flags]
```
### Examples
```
Delete an application loadbalancer with name "my-load-balancer"
$ stackit beta alb delete my-load-balancer
```
### Options
```
-h, --help Help for "stackit beta alb 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 alb](./stackit_beta_alb.md) - Manages application loadbalancers
## stackit beta alb describe
Describes an application loadbalancer
### Synopsis
Describes an application alb.
```
stackit beta alb describe LOADBALANCER_NAME_ARG [flags]
```
### Examples
```
Get details about an application loadbalancer with name "my-load-balancer"
$ stackit beta alb describe my-load-balancer
```
### Options
```
-h, --help Help for "stackit beta alb 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 alb](./stackit_beta_alb.md) - Manages application loadbalancers
## stackit beta alb list
Lists albs
### Synopsis
Lists application load balancers.
```
stackit beta alb list [flags]
```
### Examples
```
List all load balancers
$ stackit beta alb list
List the first 10 application load balancers
$ stackit beta alb list --limit=10
```
### Options
```
-h, --help Help for "stackit beta alb list"
--limit int Limit the output to the first n elements
```
### 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 alb](./stackit_beta_alb.md) - Manages application loadbalancers
## stackit beta alb observability-credentials add
Adds observability credentials to an application load balancer
### Synopsis
Adds observability credentials (username and password) to an application load balancer. The credentials can be for Observability or another monitoring tool.
```
stackit beta alb observability-credentials add [flags]
```
### Examples
```
Add observability credentials to a load balancer with username "xxx" and display name "yyy", providing the path to a file with the password as flag
$ stackit beta alb observability-credentials add --username xxx --password @./password.txt --display-name yyy
```
### Options
```
-d, --displayname string Displayname for the credentials
-h, --help Help for "stackit beta alb observability-credentials add"
--password string Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).
-u, --username string Username for the credentials
```
### 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 alb observability-credentials](./stackit_beta_alb_observability-credentials.md) - Provides functionality for application loadbalancer credentials
## stackit beta alb observability-credentials delete
Deletes credentials
### Synopsis
Deletes credentials.
```
stackit beta alb observability-credentials delete CREDENTIAL_REF [flags]
```
### Examples
```
Delete credential with name "credential-12345"
$ stackit beta alb observability-credentials delete credential-12345
```
### Options
```
-h, --help Help for "stackit beta alb observability-credentials 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 alb observability-credentials](./stackit_beta_alb_observability-credentials.md) - Provides functionality for application loadbalancer credentials
## stackit beta alb observability-credentials describe
Describes observability credentials for the Application Load Balancer
### Synopsis
Describes observability credentials for the Application Load Balancer.
```
stackit beta alb observability-credentials describe CREDENTIAL_REF [flags]
```
### Examples
```
Get details about credentials with name "credential-12345"
$ stackit beta alb observability-credentials describe credential-12345
```
### Options
```
-h, --help Help for "stackit beta alb observability-credentials 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 alb observability-credentials](./stackit_beta_alb_observability-credentials.md) - Provides functionality for application loadbalancer credentials
## stackit beta alb observability-credentials list
Lists all credentials
### Synopsis
Lists all credentials.
```
stackit beta alb observability-credentials list [flags]
```
### Examples
```
Lists all credentials
$ stackit beta alb observability-credentials list
Lists all credentials in JSON format
$ stackit beta alb observability-credentials list --output-format json
Lists up to 10 credentials
$ stackit beta alb observability-credentials list --limit 10
```
### Options
```
-h, --help Help for "stackit beta alb observability-credentials list"
--limit int Number of credentials 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 alb observability-credentials](./stackit_beta_alb_observability-credentials.md) - Provides functionality for application loadbalancer credentials
## stackit beta alb observability-credentials update
Update credentials
### Synopsis
Update credentials.
```
stackit beta alb observability-credentials update CREDENTIAL_REF_ARG [flags]
```
### Examples
```
Update the password of observability credentials of Application Load Balancer with credentials reference "credentials-xxx", by providing the path to a file with the new password as flag
$ stackit beta alb observability-credentials update credentials-xxx --username user1 --displayname user1 --password @./new-password.txt
```
### Options
```
-d, --displayname string Displayname for the credentials
-h, --help Help for "stackit beta alb observability-credentials update"
--password string Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).
-u, --username string Username for the credentials
```
### 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 alb observability-credentials](./stackit_beta_alb_observability-credentials.md) - Provides functionality for application loadbalancer credentials
## stackit beta alb observability-credentials
Provides functionality for application loadbalancer credentials
### Synopsis
Provides functionality for application loadbalancer credentials
```
stackit beta alb observability-credentials [flags]
```
### Options
```
-h, --help Help for "stackit beta alb observability-credentials"
```
### 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 alb](./stackit_beta_alb.md) - Manages application loadbalancers
* [stackit beta alb observability-credentials add](./stackit_beta_alb_observability-credentials_add.md) - Adds observability credentials to an application load balancer
* [stackit beta alb observability-credentials delete](./stackit_beta_alb_observability-credentials_delete.md) - Deletes credentials
* [stackit beta alb observability-credentials describe](./stackit_beta_alb_observability-credentials_describe.md) - Describes observability credentials for the Application Load Balancer
* [stackit beta alb observability-credentials list](./stackit_beta_alb_observability-credentials_list.md) - Lists all credentials
* [stackit beta alb observability-credentials update](./stackit_beta_alb_observability-credentials_update.md) - Update credentials
## stackit beta alb plans
Lists the application load balancer plans
### Synopsis
Lists the available application load balancer plans.
```
stackit beta alb plans [flags]
```
### Examples
```
List all application load balancer plans
$ stackit beta alb plans
```
### Options
```
-h, --help Help for "stackit beta alb plans"
```
### 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 alb](./stackit_beta_alb.md) - Manages application loadbalancers
## stackit beta alb pool update
Updates an application target pool
### Synopsis
Updates an application target pool.
```
stackit beta alb pool update [flags]
```
### Examples
```
Update an application target pool from a configuration file (the name of the pool is read from the file)
$ stackit beta alb update --configuration my-target-pool.json --name my-load-balancer
```
### Options
```
-c, --configuration string Filename of the input configuration file
-h, --help Help for "stackit beta alb pool update"
-n, --name string Name of the target pool name to update
```
### 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 alb pool](./stackit_beta_alb_pool.md) - Manages target pools for application loadbalancers
## stackit beta alb pool
Manages target pools for application loadbalancers
### Synopsis
Manage the lifecycle of target pools for application loadbalancers.
```
stackit beta alb pool [flags]
```
### Options
```
-h, --help Help for "stackit beta alb pool"
```
### 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 alb](./stackit_beta_alb.md) - Manages application loadbalancers
* [stackit beta alb pool update](./stackit_beta_alb_pool_update.md) - Updates an application target pool
## stackit beta alb quotas
Shows the application load balancer quotas
### Synopsis
Shows the application load balancer quotas for the application load balancers.
```
stackit beta alb quotas [flags]
```
### Examples
```
List all application load balancer quotas
$ stackit beta alb quotas
```
### Options
```
-h, --help Help for "stackit beta alb quotas"
```
### 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 alb](./stackit_beta_alb.md) - Manages application loadbalancers
## stackit beta alb template
creates configuration templates to use for resource creation
### Synopsis
creates a json or yaml template file for creating/updating an application loadbalancer or target pool.
```
stackit beta alb template [flags]
```
### Examples
```
Create a yaml template
$ stackit beta alb template --format=yaml --type alb
Create a json template
$ stackit beta alb template --format=json --type pool
```
### Options
```
-f, --format string Defines the output format ('yaml' or 'json'), default is 'json' (default "json")
-h, --help Help for "stackit beta alb template"
-t, --type string Defines the output type ('alb' or 'pool'), default is 'alb' (default "alb")
```
### 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 alb](./stackit_beta_alb.md) - Manages application loadbalancers
## stackit beta alb update
Updates an application loadbalancer
### Synopsis
Updates an application loadbalancer.
```
stackit beta alb update [flags]
```
### Examples
```
Update an application loadbalancer from a configuration file
$ stackit beta alb update --configuration my-loadbalancer.json
```
### Options
```
-c, --configuration string Filename of the input configuration file
-h, --help Help for "stackit beta alb update"
```
### 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 alb](./stackit_beta_alb.md) - Manages application loadbalancers
## stackit beta alb
Manages application loadbalancers
### Synopsis
Manage the lifecycle of application loadbalancers.
```
stackit beta alb [flags]
```
### Options
```
-h, --help Help for "stackit beta alb"
```
### 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 alb create](./stackit_beta_alb_create.md) - Creates an application loadbalancer
* [stackit beta alb delete](./stackit_beta_alb_delete.md) - Deletes an application loadbalancer
* [stackit beta alb describe](./stackit_beta_alb_describe.md) - Describes an application loadbalancer
* [stackit beta alb list](./stackit_beta_alb_list.md) - Lists albs
* [stackit beta alb observability-credentials](./stackit_beta_alb_observability-credentials.md) - Provides functionality for application loadbalancer credentials
* [stackit beta alb plans](./stackit_beta_alb_plans.md) - Lists the application load balancer plans
* [stackit beta alb pool](./stackit_beta_alb_pool.md) - Manages target pools for application loadbalancers
* [stackit beta alb quotas](./stackit_beta_alb_quotas.md) - Shows the application load balancer quotas
* [stackit beta alb template](./stackit_beta_alb_template.md) - creates configuration templates to use for resource creation
* [stackit beta alb update](./stackit_beta_alb_update.md) - Updates an application loadbalancer
package alb
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/list"
observabilitycredentials "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/plans"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/pool"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/quotas"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/template"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/update"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
)
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "alb",
Short: "Manages application loadbalancers",
Long: "Manage the lifecycle of application loadbalancers.",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, p)
return cmd
}
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(
list.NewCmd(p),
template.NewCmd(p),
create.NewCmd(p),
update.NewCmd(p),
observabilitycredentials.NewCmd(p),
describe.NewCmd(p),
delete.NewCmd(p),
pool.NewCmd(p),
plans.NewCmd(p),
quotas.NewCmd(p),
)
}
package create
import (
"context"
_ "embed"
"encoding/json"
"log"
"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/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
//go:embed testdata/testconfig.json
var testConfiguration []byte
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &alb.APIClient{}
var testProjectId = uuid.NewString()
var testRegion = "eu01"
var testConfig = "testdata/testconfig.json"
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
configurationFlag: testConfig,
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,
Verbosity: globalflags.VerbosityDefault,
Region: testRegion,
},
Configuration: utils.Ptr(testConfig),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixturePayload(mods ...func(payload *alb.CreateLoadBalancerPayload)) (payload alb.CreateLoadBalancerPayload) {
if err := json.Unmarshal(testConfiguration, &payload); err != nil {
log.Panicf("cannot deserialize test configuration: %v", err)
}
for _, f := range mods {
f(&payload)
}
return payload
}
func fixtureRequest(mods ...func(request *alb.ApiCreateLoadBalancerRequest)) alb.ApiCreateLoadBalancerRequest {
request := testClient.CreateLoadBalancer(testCtx, testProjectId, testRegion)
request = request.CreateLoadBalancerPayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description 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: "required fields only",
flagValues: map[string]string{
projectIdFlag: testProjectId,
configurationFlag: testConfig,
},
isValid: true,
expectedModel: &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
Configuration: &testConfig,
},
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest alb.ApiCreateLoadBalancerRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
{
description: "required fields only",
model: &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
Configuration: &testConfig,
},
expectedRequest: testClient.
CreateLoadBalancer(testCtx, testProjectId, testRegion).
CreateLoadBalancerPayload(fixturePayload()),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request, err := buildRequest(testCtx, tt.model, testClient)
if err != nil {
t.Fatalf("canno create request: %v", err)
}
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 *alb.LoadBalancer
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty",
args: args{},
wantErr: true,
},
{
name: "empty response as argument",
args: args{
model: fixtureInputModel(),
resp: &alb.LoadBalancer{},
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(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 (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/goccy/go-yaml"
"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/alb/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
"github.com/stackitcloud/stackit-sdk-go/services/alb/wait"
)
const (
configurationFlag = "configuration"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Configuration *string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates an application loadbalancer",
Long: "Creates an application loadbalancer.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create an application loadbalancer from a configuration file`,
"$ stackit beta alb create --configuration my-loadbalancer.json"),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
if err != nil {
p.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 application loadbalancer for project %q?", projectLabel)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req, err := buildRequest(ctx, model, apiClient)
if err != nil {
return err
}
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("create application loadbalancer: %w", err)
}
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(p)
s.Start("Creating loadbalancer")
_, err = wait.CreateOrUpdateLoadbalancerWaitHandler(ctx, apiClient, model.ProjectId, model.Region, *resp.Name).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for loadbalancer creation: %w", err)
}
s.Stop()
}
return outputResult(p, model, projectLabel, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(configurationFlag, "c", "", "Filename of the input configuration file")
err := flags.MarkFlagsRequired(cmd, configurationFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
Configuration: flags.FlagToStringPointer(p, cmd, configurationFlag),
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) (req alb.ApiCreateLoadBalancerRequest, err error) {
req = apiClient.CreateLoadBalancer(ctx, model.ProjectId, model.Region)
payload, err := readPayload(ctx, model)
if err != nil {
return req, err
}
return req.CreateLoadBalancerPayload(payload), nil
}
func readPayload(_ context.Context, model *inputModel) (payload alb.CreateLoadBalancerPayload, err error) {
if model.Configuration == nil {
return payload, fmt.Errorf("no configuration file defined")
}
file, err := os.Open(*model.Configuration)
if err != nil {
return payload, fmt.Errorf("cannot open configuration file %q: %w", *model.Configuration, err)
}
defer file.Close() // nolint:errcheck // at this point close errors are not relevant anymore
if strings.HasSuffix(*model.Configuration, ".yaml") {
decoder := yaml.NewDecoder(bufio.NewReader(file), yaml.UseJSONUnmarshaler())
if err := decoder.Decode(&payload); err != nil {
return payload, fmt.Errorf("cannot deserialize yaml configuration from %q: %w", *model.Configuration, err)
}
} else if strings.HasSuffix(*model.Configuration, ".json") {
decoder := json.NewDecoder(bufio.NewReader(file))
if err := decoder.Decode(&payload); err != nil {
return payload, fmt.Errorf("cannot deserialize json configuration from %q: %w", *model.Configuration, err)
}
} else {
return payload, fmt.Errorf("cannot determine configuration fileformat of %q by extension. Must be '.json' or '.yaml'", *model.Configuration)
}
return payload, nil
}
func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *alb.LoadBalancer) error {
if resp == nil {
return fmt.Errorf("create loadbalancer response is empty")
}
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal loadbalancer: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal loadbalancer: %w", err)
}
p.Outputln(string(details))
return nil
default:
operationState := "Created"
if model.Async {
operationState = "Triggered creation of"
}
p.Outputf("%s application loadbalancer for %q. Name: %s\n", operationState, projectLabel, utils.PtrString(resp.Name))
return nil
}
}
{
"externalAddress": "10.100.42.1",
"listeners": [
{
"displayName": "listener1",
"http": {},
"https": {
"certificateConfig": {
"certificateIds": [
"cert-1",
"cert-2",
"cert-3"
]
}
},
"port": 443,
"protocol": "PROTOCOL_HTTPS",
"rules": [
{
"host": "front.facing.host",
"http": {
"subRules": [
{
"cookiePersistence": {
"name": "cookie1",
"ttl": "120s"
},
"headers": [
{
"name": "testheader1",
"exactMatch": "X-test-header1"
},
{
"name": "testheader2",
"exactMatch": "X-test-header2"
},
{
"name": "testheader3",
"exactMatch": "X-test-header3"
}
],
"pathPrefix": "/foo",
"queryParameters": [
{
"name": "query-param",
"exactMatch": "q"
},
{
"name": "region",
"exactMatch": "region"
}
],
"targetPool": "my-target-pool",
"webSocket": false
}
]
}
}
]
}
],
"name": "my-load-balancer",
"networks": [
{
"networkId": "00000000-0000-0000-0000-000000000000",
"role": "ROLE_LISTENERS_AND_TARGETS"
},
{
"networkId": "00000000-0000-0000-0000-000000000001",
"role": "ROLE_LISTENERS_AND_TARGETS"
}
],
"options": {
"accessControl": {
"allowedSourceRanges": [
"192.168.42.0-192.168.42.10",
"192.168.54.0-192.168.54.10"
]
},
"ephemeralAddress": true,
"observability": {
"logs": {
"credentialsRef": "my-credentials",
"pushUrl": "https://my.observability.host/<observability-instance-id>/loki/api/v1/push"
},
"metrics": {
"credentialsRef": "my-credentials",
"pushUrl": "https://my.observability.host/<observability-instance-id>/<argus-instance-id>/api/v1/receive"
}
},
"privateNetworkOnly": true
},
"planId": "p10",
"targetPools": [
{
"activeHealthCheck": {
"healthyThreshold": 3,
"httpHealthChecks": {
"okStatuses": [
"200",
"204"
],
"path": "/health"
},
"interval": "10s",
"intervalJitter": "3s",
"timeout": "5s",
"unhealthyThreshold": 1
},
"name": "my-target-pool",
"targetPort": 5732,
"targets": [
{
"displayName": "my-target1",
"ip": "192.11.2.5"
}
],
"tlsConfig": {
"customCa": "my.private.ca",
"enabled": true,
"skipCertificateValidation": false
}
}
]
}
package delete
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "test")
testProjectId = uuid.NewString()
testRegion = "eu01"
testClient = &alb.APIClient{}
testLoadBalancerName = "my-test-loadbalancer"
)
func fixtureArgValues(mods ...func(argVales []string)) []string {
argVales := []string{
testLoadBalancerName,
}
for _, m := range mods {
m(argVales)
}
return argVales
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
}
for _, m := range mods {
m(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
ProjectId: testProjectId,
Region: testRegion,
},
Name: testLoadBalancerName,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *alb.ApiDeleteLoadBalancerRequest)) alb.ApiDeleteLoadBalancerRequest {
request := testClient.DeleteLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argsValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argsValues: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argsValues: []string{},
flagValues: map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
},
isValid: false,
},
{
description: "no arg values",
argsValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argsValues: fixtureArgValues(),
flagValues: map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
},
isValid: true,
expectedModel: fixtureInputModel(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err = cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argsValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %v", err)
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd, tt.argsValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing input: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedResult alb.ApiDeleteLoadBalancerRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedResult: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedResult,
cmp.AllowUnexported(tt.expectedResult),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("data does not match: %s", diff)
}
})
}
}
package delete
import (
"context"
"fmt"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"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/projectname"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
const (
loadbalancerNameArg = "LOADBALANCER_NAME_ARG"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Name string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("delete %s", loadbalancerNameArg),
Short: "Deletes an application loadbalancer",
Long: "Deletes an application loadbalancer.",
Args: args.SingleArg(loadbalancerNameArg, nil),
Example: examples.Build(
examples.NewExample(
`Delete an application loadbalancer with name "my-load-balancer"`,
"$ stackit beta alb delete my-load-balancer",
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
if err != nil {
p.Debug(print.ErrorLevel, "get project name: %v", err)
projectLabel = model.ProjectId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete the application loadbalancer %q for project %q?", model.Name, projectLabel)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
_, err = req.Execute()
if err != nil {
return fmt.Errorf("delete loadbalancer: %w", err)
}
p.Outputf("Load balancer %q deleted.", model.Name)
return nil
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
loadbalancerName := inputArgs[0]
model := inputModel{
GlobalFlagModel: globalFlags,
Name: loadbalancerName,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiDeleteLoadBalancerRequest {
return apiClient.DeleteLoadBalancer(ctx, model.ProjectId, model.Region, model.Name)
}
package describe
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "test")
testProjectId = uuid.NewString()
testRegion = "eu01"
testClient = &alb.APIClient{}
testLoadBalancerName = "my-test-loadbalancer"
)
func fixtureArgValues(mods ...func(argVales []string)) []string {
argVales := []string{
testLoadBalancerName,
}
for _, m := range mods {
m(argVales)
}
return argVales
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
}
for _, m := range mods {
m(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
ProjectId: testProjectId,
Region: testRegion,
},
Name: testLoadBalancerName,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *alb.ApiGetLoadBalancerRequest)) alb.ApiGetLoadBalancerRequest {
request := testClient.GetLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argsValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argsValues: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argsValues: []string{},
flagValues: map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
},
isValid: false,
},
{
description: "no arg values",
argsValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argsValues: fixtureArgValues(),
flagValues: map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
},
isValid: true,
expectedModel: fixtureInputModel(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err = cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argsValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %v", err)
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd, tt.argsValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing input: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedResult alb.ApiGetLoadBalancerRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedResult: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedResult,
cmp.AllowUnexported(tt.expectedResult),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("data does not match: %s", diff)
}
})
}
}
func Test_outputResult(t *testing.T) {
type args struct {
outputFormat string
showOnlyPublicKey bool
response *alb.LoadBalancer
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "base",
args: args{
outputFormat: "",
showOnlyPublicKey: false,
response: &alb.LoadBalancer{},
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(p)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package describe
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"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/alb/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
const (
loadbalancerNameArg = "LOADBALANCER_NAME_ARG"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Name string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", loadbalancerNameArg),
Short: "Describes an application loadbalancer",
Long: "Describes an application alb.",
Args: args.SingleArg(loadbalancerNameArg, nil),
Example: examples.Build(
examples.NewExample(
`Get details about an application loadbalancer with name "my-load-balancer"`,
"$ stackit beta alb describe my-load-balancer",
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("read loadbalancer: %w", err)
}
if loadbalancer := resp; loadbalancer != nil {
return outputResult(p, model.OutputFormat, loadbalancer)
}
p.Outputln("No load balancer found.")
return nil
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
loadbalancerName := inputArgs[0]
model := inputModel{
GlobalFlagModel: globalFlags,
Name: loadbalancerName,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiGetLoadBalancerRequest {
return apiClient.GetLoadBalancer(ctx, model.ProjectId, model.Region, model.Name)
}
func outputResult(p *print.Printer, outputFormat string, response *alb.LoadBalancer) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(response, "", " ")
if err != nil {
return fmt.Errorf("marshal loadbalancer: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(response, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal loadbalancer: %w", err)
}
p.Outputln(string(details))
return nil
default:
if err := outputResultAsTable(p, response); err != nil {
return err
}
}
return nil
}
func outputResultAsTable(p *print.Printer, loadbalancer *alb.LoadBalancer) error {
content := []tables.Table{}
content = append(content, buildLoadBalancerTable(loadbalancer))
if loadbalancer.Listeners != nil {
content = append(content, buildListenersTable(*loadbalancer.Listeners))
}
if loadbalancer.TargetPools != nil {
content = append(content, buildTargetPoolsTable(*loadbalancer.TargetPools))
}
err := tables.DisplayTables(p, content)
if err != nil {
return fmt.Errorf("display output: %w", err)
}
return nil
}
func buildLoadBalancerTable(loadbalancer *alb.LoadBalancer) tables.Table {
acl := []string{}
privateAccessOnly := false
if loadbalancer.Options != nil {
if loadbalancer.Options.AccessControl != nil && loadbalancer.Options.AccessControl.AllowedSourceRanges != nil {
acl = *loadbalancer.Options.AccessControl.AllowedSourceRanges
}
if loadbalancer.Options.PrivateNetworkOnly != nil {
privateAccessOnly = *loadbalancer.Options.PrivateNetworkOnly
}
}
networkId := "-"
if loadbalancer.Networks != nil && len(*loadbalancer.Networks) > 0 {
networks := *loadbalancer.Networks
networkId = *networks[0].NetworkId
}
externalAddress := utils.PtrStringDefault(loadbalancer.ExternalAddress, "-")
errorDescriptions := []string{}
if loadbalancer.Errors != nil && len((*loadbalancer.Errors)) > 0 {
for _, err := range *loadbalancer.Errors {
errorDescriptions = append(errorDescriptions, *err.Description)
}
}
table := tables.NewTable()
table.SetTitle("Load Balancer")
table.AddRow("NAME", utils.PtrString(loadbalancer.Name))
table.AddSeparator()
table.AddRow("STATE", utils.PtrString(loadbalancer.Status))
table.AddSeparator()
if len(errorDescriptions) > 0 {
table.AddRow("ERROR DESCRIPTIONS", strings.Join(errorDescriptions, "\n"))
table.AddSeparator()
}
table.AddRow("PRIVATE ACCESS ONLY", privateAccessOnly)
table.AddSeparator()
table.AddRow("ATTACHED PUBLIC IP", externalAddress)
table.AddSeparator()
table.AddRow("ATTACHED NETWORK ID", networkId)
table.AddSeparator()
table.AddRow("ACL", acl)
return table
}
func buildListenersTable(listeners []alb.Listener) tables.Table {
table := tables.NewTable()
table.SetTitle("Listeners")
table.SetHeader("NAME", "PORT", "PROTOCOL", "TARGET POOL")
for i := range listeners {
listener := listeners[i]
table.AddRow(
utils.PtrString(listener.Name),
utils.PtrString(listener.Port),
utils.PtrString(listener.Protocol),
)
}
return table
}
func buildTargetPoolsTable(targetPools []alb.TargetPool) tables.Table {
table := tables.NewTable()
table.SetTitle("Target Pools")
table.SetHeader("NAME", "PORT", "TARGETS")
for _, targetPool := range targetPools {
table.AddRow(utils.PtrString(targetPool.Name), utils.PtrString(targetPool.TargetPort), len(*targetPool.Targets))
}
return table
}
package list
import (
"context"
"strconv"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &alb.APIClient{}
testProjectId = uuid.NewString()
testRegion = "eu01"
testLimit int64 = 10
)
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
limitFlag: strconv.Itoa(int(testLimit)),
}
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},
Limit: &testLimit,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *alb.ApiListLoadBalancersRequest)) alb.ApiListLoadBalancersRequest {
request := testClient.ListLoadBalancers(context.Background(), testProjectId, testRegion)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description 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 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
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) {
p := print.NewPrinter()
cmd := NewCmd(p)
if err := globalflags.Configure(cmd.Flags()); err != nil {
t.Errorf("cannot configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
if err := cmd.ValidateRequiredFlags(); err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest alb.ApiListLoadBalancersRequest
}{
{
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),
cmpopts.IgnoreFields(alb.ApiListLoadBalancersRequest{}, "ctx"),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func Test_outputResult(t *testing.T) {
type args struct {
outputFormat string
items []alb.LoadBalancer
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty",
args: args{
outputFormat: "",
items: []alb.LoadBalancer{},
},
wantErr: false,
},
{
name: "output format json",
args: args{
outputFormat: print.JSONOutputFormat,
items: []alb.LoadBalancer{},
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(p)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package list
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"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/alb/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Limit *int64
}
const (
labelSelectorFlag = "label-selector"
limitFlag = "limit"
)
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists albs",
Long: "Lists application load balancers.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all load balancers`,
`$ stackit beta alb list`,
),
examples.NewExample(
`List the first 10 application load balancers`,
`$ stackit beta alb list --limit=10`,
),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
if err != nil {
p.Debug(print.ErrorLevel, "get project name: %v", err)
projectLabel = model.ProjectId
} else if projectLabel == "" {
projectLabel = model.ProjectId
}
// Call API
request := buildRequest(ctx, model, apiClient)
response, err := request.Execute()
if err != nil {
return fmt.Errorf("list load balancerse: %w", err)
}
if items := response.LoadBalancers; items == nil || len(*items) == 0 {
p.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(p, model.OutputFormat, *items); err != nil {
return fmt.Errorf("output loadbalancers: %w", err)
}
}
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements")
}
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,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiListLoadBalancersRequest {
request := apiClient.ListLoadBalancers(ctx, model.ProjectId, model.Region)
return request
}
func outputResult(p *print.Printer, outputFormat string, items []alb.LoadBalancer) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(items, "", " ")
if err != nil {
return fmt.Errorf("marshal loadbalancer list: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal loadbalancer list: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("NAME", "EXTERNAL ADDRESS", "REGION", "STATUS", "VERSION", "ERRORS")
for _, item := range items {
var errNo int
if item.Errors != nil {
errNo = len(*item.Errors)
}
table.AddRow(utils.PtrString(item.Name),
utils.PtrString(item.ExternalAddress),
utils.PtrString(item.Region),
utils.PtrString(item.Status),
utils.PtrString(item.Version),
errNo,
)
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package add
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &alb.APIClient{}
var (
testProjectId = uuid.NewString()
testRegion = "eu01"
testDisplayname = "displayname"
testUsername = "testuser"
testPassword = "testpassword"
)
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
usernameFlag: testUsername,
displaynameFlag: testDisplayname,
passwordFlag: testPassword,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
ProjectId: testProjectId,
Region: testRegion,
},
Username: &testUsername,
Displayname: &testDisplayname,
Password: &testPassword,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *alb.ApiCreateCredentialsRequest)) alb.ApiCreateCredentialsRequest {
request := testClient.CreateCredentials(testCtx, testProjectId, testRegion)
request = request.CreateCredentialsPayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func fixturePayload(mods ...func(payload *alb.CreateCredentialsPayload)) alb.CreateCredentialsPayload {
payload := alb.CreateCredentialsPayload{
DisplayName: &testDisplayname,
Password: &testPassword,
Username: &testUsername,
}
for _, mod := range mods {
mod(&payload)
}
return payload
}
func TestParseInput(t *testing.T) {
tests := []struct {
description 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,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err = cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest alb.ApiCreateCredentialsRequest
}{
{
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),
cmp.AllowUnexported(alb.NullableString{}),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func Test_outputResult(t *testing.T) {
type args struct {
item *alb.CreateCredentialsResponse
outputFormat string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty",
args: args{
item: nil,
outputFormat: "",
},
wantErr: true,
},
{
name: "base",
args: args{
item: &alb.CreateCredentialsResponse{},
outputFormat: "",
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(p)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.outputFormat, tt.args.item); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package add
import (
"context"
"encoding/json"
"fmt"
"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/alb/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
const (
usernameFlag = "username"
displaynameFlag = "displayname"
passwordFlag = "password"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Username *string
Displayname *string
Password *string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "add",
Short: "Adds observability credentials to an application load balancer",
Long: "Adds observability credentials (username and password) to an application load balancer. The credentials can be for Observability or another monitoring tool.",
Args: cobra.NoArgs,
Example: examples.Build(
examples.NewExample(
`Add observability credentials to a load balancer with username "xxx" and display name "yyy", providing the path to a file with the password as flag`,
"$ stackit beta alb observability-credentials add --username xxx --password @./password.txt --display-name yyy"),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
if !model.AssumeYes {
prompt := "Are your sure you want to add credentials?"
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("add credential: %w", err)
}
return outputResult(p, model.GlobalFlagModel.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(usernameFlag, "u", "", "Username for the credentials")
cmd.Flags().StringP(displaynameFlag, "d", "", "Displayname for the credentials")
cmd.Flags().Var(flags.ReadFromFileFlag(), passwordFlag, `Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).`)
cobra.CheckErr(flags.MarkFlagsRequired(cmd, usernameFlag, displaynameFlag))
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
model := inputModel{
GlobalFlagModel: globalFlags,
Username: flags.FlagToStringPointer(p, cmd, usernameFlag),
Displayname: flags.FlagToStringPointer(p, cmd, displaynameFlag),
Password: flags.FlagToStringPointer(p, cmd, passwordFlag),
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string fo debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiCreateCredentialsRequest {
req := apiClient.CreateCredentials(ctx, model.ProjectId, model.Region)
payload := alb.CreateCredentialsPayload{
DisplayName: model.Displayname,
Password: model.Password,
Username: model.Username,
}
return req.CreateCredentialsPayload(payload)
}
func outputResult(p *print.Printer, outputFormat string, item *alb.CreateCredentialsResponse) error {
if item == nil {
return fmt.Errorf("no credential found")
}
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(item, "", " ")
if err != nil {
return fmt.Errorf("marshal credential: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(item, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal credential: %w", err)
}
p.Outputln(string(details))
default:
if item.Credential != nil {
p.Outputf("Created credential %s\n",
utils.PtrString(item.Credential.CredentialsRef),
)
}
}
return nil
}
package delete
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "test")
testProjectId = uuid.NewString()
testRegion = "eu01"
testClient = &alb.APIClient{}
testCredentialRef = "credential-12345"
)
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testCredentialRef,
}
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{
Verbosity: globalflags.VerbosityDefault,
ProjectId: testProjectId,
Region: testRegion,
},
CredentialsRef: testCredentialRef,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *alb.ApiDeleteCredentialsRequest)) alb.ApiDeleteCredentialsRequest {
request := testClient.DeleteCredentials(testCtx, testProjectId, testRegion, testCredentialRef)
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 values",
argValues: []string{},
flagValues: map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
},
isValid: false,
},
{
description: "no args",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flags",
argValues: fixtureArgValues(),
flagValues: map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
},
isValid: true,
expectedModel: fixtureInputModel(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err = cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %v", err)
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd, tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing input: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest alb.ApiDeleteCredentialsRequest
}{
{
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/stackitcloud/stackit-cli/internal/pkg/args"
"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/alb/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
const (
credentialRefArg = "CREDENTIAL_REF" // nolint:gosec // false alert, these are not valid credentials
)
type inputModel struct {
*globalflags.GlobalFlagModel
CredentialsRef string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("delete %s", credentialRefArg),
Short: "Deletes credentials",
Long: "Deletes credentials.",
Args: args.SingleArg(credentialRefArg, nil),
Example: examples.Build(
examples.NewExample(
`Delete credential with name "credential-12345"`,
"$ stackit beta alb observability-credentials delete credential-12345",
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete credentials %q?", model.CredentialsRef)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
_, err = req.Execute()
if err != nil {
return fmt.Errorf("delete credential: %w", err)
}
p.Info("Deleted credential %q\n", model.CredentialsRef)
return nil
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
credentialRef := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
model := inputModel{
GlobalFlagModel: globalFlags,
CredentialsRef: credentialRef,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiDeleteCredentialsRequest {
return apiClient.DeleteCredentials(ctx, model.ProjectId, model.Region, model.CredentialsRef)
}
package describe
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "test")
testProjectId = uuid.NewString()
testRegion = "eu01"
testClient = &alb.APIClient{}
testCredentialRef = "credential-12345"
)
func fixtureArgValues(mods ...func(argVales []string)) []string {
argVales := []string{
testCredentialRef,
}
for _, m := range mods {
m(argVales)
}
return argVales
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
}
for _, m := range mods {
m(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
ProjectId: testProjectId,
Region: testRegion,
},
CredentialRef: testCredentialRef,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *alb.ApiGetCredentialsRequest)) alb.ApiGetCredentialsRequest {
request := testClient.GetCredentials(testCtx, testProjectId, testRegion, testCredentialRef)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argsValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argsValues: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argsValues: []string{},
flagValues: map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
},
isValid: false,
},
{
description: "no arg values",
argsValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argsValues: fixtureArgValues(),
flagValues: map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
},
isValid: true,
expectedModel: fixtureInputModel(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err = cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argsValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %v", err)
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd, tt.argsValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing input: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedResult alb.ApiGetCredentialsRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedResult: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedResult,
cmp.AllowUnexported(tt.expectedResult),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("data does not match: %s", diff)
}
})
}
}
func Test_outputResult(t *testing.T) {
type args struct {
outputFormat string
showOnlyPublicKey bool
response alb.CredentialsResponse
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "base",
args: args{
outputFormat: "",
showOnlyPublicKey: false,
response: alb.CredentialsResponse{},
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(p)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package describe
import (
"context"
"encoding/json"
"fmt"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"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/alb/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
const (
credentialRefArg = "CREDENTIAL_REF" // nolint:gosec // false alert, these are not valid credentials
)
type inputModel struct {
*globalflags.GlobalFlagModel
CredentialRef string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", credentialRefArg),
Short: "Describes observability credentials for the Application Load Balancer",
Long: "Describes observability credentials for the Application Load Balancer.",
Args: args.SingleArg(credentialRefArg, nil),
Example: examples.Build(
examples.NewExample(
`Get details about credentials with name "credential-12345"`,
"$ stackit beta alb observability-credentials describe credential-12345",
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("read credentials: %w", err)
}
if credential := resp; credential != nil && credential.Credential != nil {
return outputResult(p, model.OutputFormat, *credential.Credential)
}
p.Outputln("No credentials found.")
return nil
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
credentialRef := inputArgs[0]
model := inputModel{
GlobalFlagModel: globalFlags,
CredentialRef: credentialRef,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiGetCredentialsRequest {
return apiClient.GetCredentials(ctx, model.ProjectId, model.Region, model.CredentialRef)
}
func outputResult(p *print.Printer, outputFormat string, response alb.CredentialsResponse) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(response, "", " ")
if err != nil {
return fmt.Errorf("marshal credentials: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(response, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal credentials: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.AddRow("CREDENTIAL REF", utils.PtrString(response.CredentialsRef))
table.AddSeparator()
table.AddRow("DISPLAYNAME", utils.PtrString(response.DisplayName))
table.AddSeparator()
table.AddRow("UESRNAME", utils.PtrString(response.Username))
table.AddSeparator()
table.AddRow("REGION", utils.PtrString(response.Region))
table.AddSeparator()
p.Outputln(table.Render())
}
return nil
}
package list
import (
"context"
"strconv"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "test")
testClient = &alb.APIClient{}
testProjectId = uuid.NewString()
testRegion = "eu01"
testLimit = int64(64)
)
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
limitFlag: strconv.FormatInt(testLimit, 10),
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(inputModel *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
ProjectId: testProjectId,
Region: testRegion,
},
Limit: utils.Ptr(testLimit),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *alb.ApiListCredentialsRequest)) alb.ApiListCredentialsRequest {
request := testClient.ListCredentials(testCtx, testProjectId, testRegion)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
flagValues: map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
},
isValid: true,
expectedModel: fixtureInputModel(func(inputModel *inputModel) {
inputModel.Limit = nil
}),
},
{
description: "withoutLimit",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, "limit")
}),
isValid: true,
expectedModel: fixtureInputModel(func(inputModel *inputModel) {
inputModel.Limit = nil
}),
},
{
description: "invalid limit 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "invalid"
}),
isValid: false,
},
{
description: "invalid limit 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "0"
}),
isValid: false,
},
{
description: "label selector empty",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err = cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing input: %v", err)
}
if !tt.isValid {
t.Fatal("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest alb.ApiListCredentialsRequest
}{
{
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("request does not match: %s", diff)
}
})
}
}
func Test_outputResult(t *testing.T) {
type args struct {
outputFormat string
response []alb.CredentialsResponse
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty",
args: args{
outputFormat: "",
response: []alb.CredentialsResponse{
{},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := print.NewPrinter()
p.Cmd = NewCmd(p)
if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package list
import (
"context"
"encoding/json"
"fmt"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"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/alb/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
const (
limitFlag = "limit"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Limit *int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all credentials",
Long: "Lists all credentials.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Lists all credentials`,
"$ stackit beta alb observability-credentials list",
),
examples.NewExample(
`Lists all credentials in JSON format`,
"$ stackit beta alb observability-credentials list --output-format json",
),
examples.NewExample(
`Lists up to 10 credentials`,
"$ stackit beta alb observability-credentials list --limit 10",
),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("list credentials: %w", err)
}
if resp.Credentials == nil || len(*resp.Credentials) == 0 {
p.Info("No credentials found\n")
return nil
}
items := *resp.Credentials
if model.Limit != nil && len(items) > int(*model.Limit) {
items = items[:*model.Limit]
}
return outputResult(p, model.OutputFormat, items)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Int64(limitFlag, 0, "Number of credentials to list")
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
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,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.InfoLevel, modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiListCredentialsRequest {
req := apiClient.ListCredentials(ctx, model.ProjectId, model.Region)
return req
}
func outputResult(p *print.Printer, outputFormat string, items []alb.CredentialsResponse) error {
if items == nil {
p.Outputln("no credentials found")
return nil
}
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(items, "", " ")
if err != nil {
return fmt.Errorf("marshal credentials: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal credentials: %w", err)
}
p.Outputln(string(details))
default:
table := tables.NewTable()
table.SetHeader("CREDENTIAL REF", "DISPLAYNAME", "USERNAME", "REGION")
for _, item := range items {
table.AddRow(
utils.PtrString(item.CredentialsRef),
utils.PtrString(item.DisplayName),
utils.PtrString(item.Username),
utils.PtrString(item.Region),
)
}
p.Outputln(table.Render())
}
return nil
}
package credentials
import (
"github.com/spf13/cobra"
add "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/add"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/list"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/update"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
)
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "observability-credentials",
Short: "Provides functionality for application loadbalancer credentials",
Long: "Provides functionality for application loadbalancer credentials",
Args: cobra.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, p)
return cmd
}
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(add.NewCmd(p))
cmd.AddCommand(delete.NewCmd(p))
cmd.AddCommand(describe.NewCmd(p))
cmd.AddCommand(list.NewCmd(p))
cmd.AddCommand(update.NewCmd(p))
}
package update
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &alb.APIClient{}
var (
testProjectId = uuid.NewString()
testRegion = "eu01"
testCredentialRef = "credential-12345"
testDisplayname = "displayname"
testUsername = "testuser"
testPassword = "testpassword"
)
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
usernameFlag: testUsername,
displaynameFlag: testDisplayname,
passwordFlag: testPassword,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) inputModel {
model := inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
ProjectId: testProjectId,
Region: testRegion,
},
Username: &testUsername,
Displayname: &testDisplayname,
CredentialsRef: &testCredentialRef,
Password: &testPassword,
}
for _, mod := range mods {
mod(&model)
}
return model
}
func fixtureRequest(mods ...func(request *alb.ApiUpdateCredentialsRequest)) alb.ApiUpdateCredentialsRequest {
request := testClient.UpdateCredentials(testCtx, testProjectId, testRegion, testCredentialRef)
request = request.UpdateCredentialsPayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func fixturePayload(mods ...func(payload *alb.UpdateCredentialsPayload)) alb.UpdateCredentialsPayload {
payload := alb.UpdateCredentialsPayload{
DisplayName: &testDisplayname,
Password: &testPassword,
Username: &testUsername,
}
for _, mod := range mods {
mod(&payload)
}
return payload
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
args []string
isValid bool
expectedModel inputModel
}{
{
description: "base",
args: []string{testCredentialRef},
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
args: []string{testCredentialRef},
flagValues: map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
},
isValid: false,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Username = nil
model.Displayname = nil
}),
},
{
description: "required values",
args: []string{testCredentialRef},
flagValues: map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
usernameFlag: testUsername,
displaynameFlag: testDisplayname,
passwordFlag: testPassword,
},
isValid: true,
expectedModel: fixtureInputModel(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err = cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model := parseInput(p, cmd, tt.args)
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model inputModel
expectedRequest alb.ApiUpdateCredentialsRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request, err := buildRequest(testCtx, &tt.model, testClient)
if err != nil {
t.Fatalf("cannot build request: %v", err)
}
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
cmp.AllowUnexported(alb.NullableString{}),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func Test_outputResult(t *testing.T) {
type args struct {
item *alb.UpdateCredentialsResponse
model inputModel
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty",
args: args{
item: nil,
model: fixtureInputModel(),
},
wantErr: true,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(p)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.model, tt.args.item); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package update
import (
"context"
"encoding/json"
"fmt"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"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/alb/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
const (
usernameFlag = "username"
displaynameFlag = "displayname"
passwordFlag = "password"
credentialRefArg = "CREDENTIAL_REF_ARG" //nolint:gosec // false alert, these are not valid credentials
)
type inputModel struct {
*globalflags.GlobalFlagModel
Username *string
Displayname *string
Password *string
CredentialsRef *string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("update %s", credentialRefArg),
Short: "Update credentials",
Long: "Update credentials.",
Args: args.SingleArg(credentialRefArg, nil),
Example: examples.Build(
examples.NewExample(
`Update the password of observability credentials of Application Load Balancer with credentials reference "credentials-xxx", by providing the path to a file with the new password as flag`,
"$ stackit beta alb observability-credentials update credentials-xxx --username user1 --displayname user1 --password @./new-password.txt"),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model := parseInput(p, cmd, args)
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
req, err := buildRequest(ctx, &model, apiClient)
if err != nil {
return err
}
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
if err != nil {
p.Debug(print.ErrorLevel, "get project name: %v", err)
projectLabel = model.ProjectId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to update credential %q for %q?", *model.CredentialsRef, projectLabel)
err = p.PromptForConfirmation(prompt)
if err != nil {
return fmt.Errorf("update credential: %w", err)
}
}
// Call API
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("update credential: %w", err)
}
if resp == nil {
return fmt.Errorf("response is nil")
}
return outputResult(p, model, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(usernameFlag, "u", "", "Username for the credentials")
cmd.Flags().StringP(displaynameFlag, "d", "", "Displayname for the credentials")
cmd.Flags().Var(flags.ReadFromFileFlag(), passwordFlag, `Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).`)
cobra.CheckErr(flags.MarkFlagsRequired(cmd, displaynameFlag, usernameFlag))
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) (req alb.ApiUpdateCredentialsRequest, err error) {
req = apiClient.UpdateCredentials(ctx, model.ProjectId, model.Region, *model.CredentialsRef)
payload := alb.UpdateCredentialsPayload{
DisplayName: model.Displayname,
Password: model.Password,
Username: model.Username,
}
if model.Displayname == nil && model.Username == nil {
return req, fmt.Errorf("no attribute to change passed")
}
return req.UpdateCredentialsPayload(payload), nil
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) inputModel {
model := inputModel{
GlobalFlagModel: globalflags.Parse(p, cmd),
Username: flags.FlagToStringPointer(p, cmd, usernameFlag),
Displayname: flags.FlagToStringPointer(p, cmd, displaynameFlag),
CredentialsRef: &inputArgs[0],
Password: flags.FlagToStringPointer(p, cmd, passwordFlag),
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return model
}
func outputResult(p *print.Printer, model inputModel, response *alb.UpdateCredentialsResponse) error {
var outputFormat string
if model.GlobalFlagModel != nil {
outputFormat = model.GlobalFlagModel.OutputFormat
}
if response == nil {
return fmt.Errorf("no response passewd")
}
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(response.Credential, "", " ")
if err != nil {
return fmt.Errorf("marshal credential: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(response.Credential, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal credential: %w", err)
}
p.Outputln(string(details))
default:
p.Outputf("Updated credential %q\n", utils.PtrString(model.CredentialsRef))
}
return nil
}
package plans
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &alb.APIClient{}
testProjectId = uuid.NewString()
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},
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *alb.ApiListPlansRequest)) alb.ApiListPlansRequest {
request := testClient.ListPlans(testCtx, testRegion)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description 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 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
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) {
p := print.NewPrinter()
cmd := NewCmd(p)
if err := globalflags.Configure(cmd.Flags()); err != nil {
t.Errorf("cannot configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
if err := cmd.ValidateRequiredFlags(); err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest alb.ApiListPlansRequest
}{
{
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),
cmpopts.IgnoreFields(alb.ApiListLoadBalancersRequest{}, "ctx"),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func Test_outputResult(t *testing.T) {
type args struct {
outputFormat string
items []alb.PlanDetails
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty",
args: args{
outputFormat: "",
items: []alb.PlanDetails{},
},
wantErr: false,
},
{
name: "output format json",
args: args{
outputFormat: print.JSONOutputFormat,
items: []alb.PlanDetails{},
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(p)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package plans
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"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/projectname"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
type inputModel struct {
*globalflags.GlobalFlagModel
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "plans",
Short: "Lists the application load balancer plans",
Long: "Lists the available application load balancer plans.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all application load balancer plans`,
`$ stackit beta alb plans`,
),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
if err != nil {
p.Debug(print.ErrorLevel, "get project name: %v", err)
projectLabel = model.ProjectId
} else if projectLabel == "" {
projectLabel = model.ProjectId
}
// Call API
request := buildRequest(ctx, model, apiClient)
response, err := request.Execute()
if err != nil {
return fmt.Errorf("list plans: %w", err)
}
if items := response.ValidPlans; items == nil || len(*items) == 0 {
p.Info("No plans found for project %q", projectLabel)
} else {
if err := outputResult(p, model.OutputFormat, *items); err != nil {
return fmt.Errorf("output plans: %w", err)
}
}
return nil
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiListPlansRequest {
request := apiClient.ListPlans(ctx, model.Region)
return request
}
func outputResult(p *print.Printer, outputFormat string, items []alb.PlanDetails) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(items, "", " ")
if err != nil {
return fmt.Errorf("marshal plans: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal plans: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("PLAN ID", "NAME", "FLAVOR", "MAX CONNS", "DESCRIPTION")
for _, item := range items {
table.AddRow(utils.PtrString(item.PlanId),
utils.PtrString(item.Name),
utils.PtrString(item.FlavorName),
utils.PtrString(item.MaxConnections),
utils.Truncate(item.Description, 70),
)
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package pool
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/pool/update"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
)
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "pool",
Short: "Manages target pools for application loadbalancers",
Long: "Manage the lifecycle of target pools for application loadbalancers.",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, p)
return cmd
}
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(update.NewCmd(p))
}
{
"activeHealthCheck": {
"healthyThreshold": 1,
"httpHealthChecks": {
"okStatuses": [
"string"
],
"path": "string"
},
"interval": "3s",
"intervalJitter": "3s",
"timeout": "3s",
"unhealthyThreshold": 1
},
"name": "my-target-pool",
"targetPort": 5732,
"targets": [
{
"displayName": "my-target",
"ip": "192.0.2.5"
}
],
"tlsConfig": {
"customCa": "string",
"enabled": true,
"skipCertificateValidation": true
}
}
package update
import (
"context"
_ "embed"
"encoding/json"
"log"
"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/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
//go:embed testdata/testconfig.json
var testConfiguration []byte
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &alb.APIClient{}
testProjectId = uuid.NewString()
testRegion = "eu01"
testLoadBalancer = "my-load-balancer"
testPool = "my-target-pool"
testConfig = "testdata/testconfig.json"
)
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
configurationFlag: testConfig,
globalflags.RegionFlag: testRegion,
albNameFlag: testLoadBalancer,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
Region: testRegion,
},
Configuration: utils.Ptr(testConfig),
AlbName: &testLoadBalancer,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixturePayload(mods ...func(payload *alb.UpdateTargetPoolPayload)) (payload alb.UpdateTargetPoolPayload) {
if err := json.Unmarshal(testConfiguration, &payload); err != nil {
log.Panicf("cannot deserialize test configuration: %v", err)
}
for _, f := range mods {
f(&payload)
}
return payload
}
func fixtureRequest(mods ...func(request *alb.ApiUpdateTargetPoolRequest)) alb.ApiUpdateTargetPoolRequest {
request := testClient.UpdateTargetPool(testCtx, testProjectId, testRegion, testLoadBalancer, testPool)
request = request.UpdateTargetPoolPayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description 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: "required fields only",
flagValues: map[string]string{
projectIdFlag: testProjectId,
configurationFlag: testConfig,
albNameFlag: testLoadBalancer,
},
isValid: true,
expectedModel: &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
Configuration: &testConfig,
AlbName: &testLoadBalancer,
},
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest alb.ApiUpdateTargetPoolRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
{
description: "required fields only",
model: &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
Configuration: &testConfig,
AlbName: &testLoadBalancer,
},
expectedRequest: testClient.
UpdateTargetPool(testCtx, testProjectId, testRegion, testLoadBalancer, testPool).
UpdateTargetPoolPayload(fixturePayload()),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request, err := buildRequest(testCtx, tt.model, testClient)
if err != nil {
t.Fatalf("cannot create request: %v", err)
}
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 *alb.TargetPool
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty",
args: args{},
wantErr: true,
},
{
name: "empty response as argument",
args: args{
model: fixtureInputModel(),
resp: &alb.TargetPool{},
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(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 (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/goccy/go-yaml"
"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/alb/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
const (
configurationFlag = "configuration"
albNameFlag = "name"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Configuration *string
AlbName *string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "update",
Short: "Updates an application target pool",
Long: "Updates an application target pool.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Update an application target pool from a configuration file (the name of the pool is read from the file)`,
"$ stackit beta alb update --configuration my-target-pool.json --name my-load-balancer"),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
if err != nil {
p.Debug(print.ErrorLevel, "get project name: %v", err)
projectLabel = model.ProjectId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to update an application target pool for project %q?", projectLabel)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req, err := buildRequest(ctx, model, apiClient)
if err != nil {
return err
}
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("update application target pool: %w", err)
}
return outputResult(p, model, projectLabel, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(configurationFlag, "c", "", "Filename of the input configuration file")
cmd.Flags().StringP(albNameFlag, "n", "", "Name of the target pool name to update")
err := flags.MarkFlagsRequired(cmd, configurationFlag, albNameFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
Configuration: flags.FlagToStringPointer(p, cmd, configurationFlag),
AlbName: flags.FlagToStringPointer(p, cmd, albNameFlag),
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) (req alb.ApiUpdateTargetPoolRequest, err error) {
payload, err := readPayload(ctx, model)
if err != nil {
return req, err
}
if payload.Name == nil {
return req, fmt.Errorf("update target pool: no poolname provided")
}
req = apiClient.UpdateTargetPool(ctx, model.ProjectId, model.Region, *model.AlbName, *payload.Name)
return req.UpdateTargetPoolPayload(payload), nil
}
func readPayload(_ context.Context, model *inputModel) (payload alb.UpdateTargetPoolPayload, err error) {
if model.Configuration == nil {
return payload, fmt.Errorf("no configuration file defined")
}
file, err := os.Open(*model.Configuration)
if err != nil {
return payload, fmt.Errorf("cannot open configuration file %q: %w", *model.Configuration, err)
}
defer file.Close() // nolint:errcheck // at this point close errors are not relevant anymore
if strings.HasSuffix(*model.Configuration, ".yaml") {
decoder := yaml.NewDecoder(bufio.NewReader(file), yaml.UseJSONUnmarshaler())
if err := decoder.Decode(&payload); err != nil {
return payload, fmt.Errorf("cannot deserialize yaml configuration from %q: %w", *model.Configuration, err)
}
} else if strings.HasSuffix(*model.Configuration, ".json") {
decoder := json.NewDecoder(bufio.NewReader(file))
if err := decoder.Decode(&payload); err != nil {
return payload, fmt.Errorf("cannot deserialize json configuration from %q: %w", *model.Configuration, err)
}
} else {
return payload, fmt.Errorf("cannot determine configuration fileformat of %q by extension. Must be '.json' or '.yaml'", *model.Configuration)
}
return payload, nil
}
func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *alb.TargetPool) error {
if resp == nil {
return fmt.Errorf("update target pool response is empty")
}
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal target pool: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal target pool: %w", err)
}
p.Outputln(string(details))
return nil
default:
operationState := "Updated"
if model.Async {
operationState = "Triggered update of"
}
p.Outputf("%s application target pool for %q. Name: %s\n", operationState, projectLabel, utils.PtrString(resp.Name))
return nil
}
}
package quotas
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &alb.APIClient{}
testProjectId = uuid.NewString()
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},
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *alb.ApiGetQuotaRequest)) alb.ApiGetQuotaRequest {
request := testClient.GetQuota(testCtx, testProjectId, testRegion)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description 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 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
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) {
p := print.NewPrinter()
cmd := NewCmd(p)
if err := globalflags.Configure(cmd.Flags()); err != nil {
t.Errorf("cannot configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
if err := cmd.ValidateRequiredFlags(); err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest alb.ApiGetQuotaRequest
}{
{
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),
cmpopts.IgnoreFields(alb.ApiListLoadBalancersRequest{}, "ctx"),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func Test_outputResult(t *testing.T) {
type args struct {
outputFormat string
response alb.GetQuotaResponse
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty",
args: args{
outputFormat: "",
response: alb.GetQuotaResponse{},
},
wantErr: false,
},
{
name: "output format json",
args: args{
outputFormat: print.JSONOutputFormat,
response: alb.GetQuotaResponse{},
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(p)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package quotas
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"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/alb/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
type inputModel struct {
*globalflags.GlobalFlagModel
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "quotas",
Short: "Shows the application load balancer quotas",
Long: "Shows the application load balancer quotas for the application load balancers.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all application load balancer quotas`,
`$ stackit beta alb quotas`,
),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
request := buildRequest(ctx, model, apiClient)
response, err := request.Execute()
if err != nil {
return fmt.Errorf("get quotas: %w", err)
}
if response == nil {
p.Outputln("no quotas found")
return nil
}
if err := outputResult(p, model.OutputFormat, *response); err != nil {
return fmt.Errorf("output loadbalancers: %w", err)
}
return nil
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiGetQuotaRequest {
request := apiClient.GetQuota(ctx, model.ProjectId, model.Region)
return request
}
func outputResult(p *print.Printer, outputFormat string, response alb.GetQuotaResponse) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(response, "", " ")
if err != nil {
return fmt.Errorf("marshal quotas: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(response, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal quotas: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.AddRow("REGION", utils.PtrString(response.Region))
table.AddSeparator()
table.AddRow("MAX LOADBALANCERS", utils.PtrString(response.MaxLoadBalancers))
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package template
import (
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
)
var (
testProjectId = uuid.NewString()
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},
}
for _, mod := range mods {
mod(model)
}
return model
}
func TestParseInput(t *testing.T) {
tests := []struct {
description 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 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "alb with yaml",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[formatFlag] = "yaml"
flagValues[typeFlag] = "alb"
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Format = utils.Ptr("yaml")
model.Type = utils.Ptr("alb")
}),
}, {
description: "alb with yaml",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[formatFlag] = "yaml"
flagValues[typeFlag] = "alb"
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Format = utils.Ptr("yaml")
model.Type = utils.Ptr("alb")
}),
}, {
description: "alb with json",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[formatFlag] = "json"
flagValues[typeFlag] = "alb"
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Format = utils.Ptr("json")
model.Type = utils.Ptr("alb")
}),
}, {
description: "pool with yaml",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[formatFlag] = "yaml"
flagValues[typeFlag] = "pool"
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Format = utils.Ptr("yaml")
model.Type = utils.Ptr("pool")
}),
},
{
description: "pool with json",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[formatFlag] = "json"
flagValues[typeFlag] = "pool"
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Format = utils.Ptr("json")
model.Type = utils.Ptr("pool")
}),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
if err := globalflags.Configure(cmd.Flags()); err != nil {
t.Errorf("cannot configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
if err := cmd.ValidateRequiredFlags(); err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
name: my-load-balancer
# public ip, must be removed when ephemeral address option is true
externalAddress: 123.123.123.123
# public listening interfaces of the loadbalancer
listeners:
- displayName: listener1
# for plain http the https block must be removed
# http: {}
https:
certificateConfig:
certificateIds:
- cert-1
- cert-2
- cert-3
port: 443
# protocal may be PROTOCOL_HTTPS or PROTOCOL_HTTP
protocol: PROTOCOL_HTTPS
rules:
# fqdn of the virtual host of the load balancer
- host: front.facing.host
http:
subRules:
- cookiePersistence:
name: cookie1
ttl: 120s
headers:
- name: testheader1
exactMatch: X-test-header1
- name: testheader2
exactMatch: X-test-header2
- name: testheader3
exactMatch: X-test-header3
pathPrefix: /foo
queryParameters:
- name: query-param
exactMatch: q
- name: region
exactMatch: region
targetPool: my-target-pool
webSocket: false
networks:
- networkId: 00000000-0000-0000-0000-000000000000
role: ROLE_LISTENERS_AND_TARGETS
- networkId: 00000000-0000-0000-0000-000000000001
role: ROLE_LISTENERS_AND_TARGETS
options:
accessControl:
# which host may access the loadbalancer in prefix notation
allowedSourceRanges:
- 10.100.42.0/24
ephemeralAddress: true
privateNetworkOnly: true
# Enable observability features. Appropriate credentials must be made
# available using the credentials endpoint
# observability:
# logs:
# credentialsRef: my-credentials
# pushUrl: https://my.observability.host/<observability-instance-id>/loki/api/v1/push
# metrics:
# credentialsRef: my-credentials
# pushUrl: https://my.observability.host/<observability-instance-id>/<argus-instance-id>/api/v1/receive
planId: p10
# definition of the backend servers
targetPools:
- name: my-target-pool
activeHealthCheck:
healthyThreshold: 3
httpHealthChecks:
okStatuses:
- "200"
path: /health
interval: 10s
intervalJitter: 3s
timeout: 5s
unhealthyThreshold: 1
targetPort: 5732
targets:
# configuration of the backend servers
- displayName: my-target1
ip: 192.11.2.5
# if the backend servers must be accessed via TLS the following block
# allows defining the TLS configuration
# tlsConfig:
# # A PEM and base64 encoded certificate
# customCa: LS0t...
# enabled: true
# skipCertificateValidation: false
activeHealthCheck:
healthyThreshold: 1
httpHealthChecks:
okStatuses:
- "200"
path: /health
interval: 3s
intervalJitter: 3s
timeout: 3s
unhealthyThreshold: 1
name: my-target-pool
targetPort: 4000
targets:
- displayName: my-target
ip: 10.0.1.155
# if the backend servers must be accessed via TLS the following block
# allows defining the TLS configuration
# tlsConfig:
# # A PEM and base64 encoded certificate
# customCa: LS0...
# enabled: true
# skipCertificateValidation: false
package template
import (
_ "embed"
"encoding/json"
"fmt"
"os"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"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/utils"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
const (
formatFlag = "format"
typeFlag = "type"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Format *string
Type *string
}
var (
//go:embed template-alb.yaml
templateAlb string
//go:embed template-pool.yaml
templatePool string
)
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "template",
Short: "creates configuration templates to use for resource creation",
Long: "creates a json or yaml template file for creating/updating an application loadbalancer or target pool.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a yaml template`,
`$ stackit beta alb template --format=yaml --type alb`,
),
examples.NewExample(
`Create a json template`,
`$ stackit beta alb template --format=json --type pool`,
),
),
RunE: func(cmd *cobra.Command, _ []string) error {
model, err := parseInput(p, cmd)
if err != nil {
return err
}
var (
template string
target any
)
if model.Type != nil && *model.Type == "pool" {
template = templatePool
target = alb.CreateLoadBalancerPayload{}
} else if model.Type == nil || *model.Type == "alb" {
template = templateAlb
target = alb.UpdateTargetPoolPayload{}
} else {
return fmt.Errorf("invalid type %q", utils.PtrString(model.Type))
}
if model.Format == nil || *model.Format == "yaml" {
p.Outputln(template)
} else if *model.Format == "json" {
if err := yaml.Unmarshal([]byte(template), &target); err != nil {
return fmt.Errorf("cannot unmarshal template: %w", err)
}
encoder := json.NewEncoder(os.Stdout)
if err := encoder.Encode(target); err != nil {
return fmt.Errorf("cannot marshal template to yaml: %w", err)
}
} else {
return fmt.Errorf("invalid format %q defined. Must be 'json' or 'yaml'", *model.Format)
}
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().VarP(flags.EnumFlag(true, "json", "json", "yaml"), formatFlag, "f", "Defines the output format ('yaml' or 'json'), default is 'json'")
cmd.Flags().VarP(flags.EnumFlag(true, "alb", "alb", "pool"), typeFlag, "t", "Defines the output type ('alb' or 'pool'), default is 'alb'")
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
Format: flags.FlagToStringPointer(p, cmd, formatFlag),
Type: flags.FlagToStringPointer(p, cmd, typeFlag),
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
{
"externalAddress": "10.100.42.1",
"listeners": [
{
"displayName": "listener1",
"http": {},
"https": {
"certificateConfig": {
"certificateIds": [
"cert-1",
"cert-2",
"cert-3"
]
}
},
"port": 443,
"protocol": "PROTOCOL_HTTPS",
"rules": [
{
"host": "front.facing.host",
"http": {
"subRules": [
{
"cookiePersistence": {
"name": "cookie1",
"ttl": "120s"
},
"headers": [
{
"name": "testheader1",
"exactMatch": "X-test-header1"
},
{
"name": "testheader2",
"exactMatch": "X-test-header2"
},
{
"name": "testheader3",
"exactMatch": "X-test-header3"
}
],
"pathPrefix": "/foo",
"queryParameters": [
{
"name": "query-param",
"exactMatch": "q"
},
{
"name": "region",
"exactMatch": "region"
}
],
"targetPool": "my-target-pool",
"webSocket": false
}
]
}
}
]
}
],
"name": "my-load-balancer",
"networks": [
{
"networkId": "00000000-0000-0000-0000-000000000000",
"role": "ROLE_LISTENERS_AND_TARGETS"
},
{
"networkId": "00000000-0000-0000-0000-000000000001",
"role": "ROLE_LISTENERS_AND_TARGETS"
}
],
"options": {
"accessControl": {
"allowedSourceRanges": [
"192.168.42.0-192.168.42.10",
"192.168.54.0-192.168.54.10"
]
},
"ephemeralAddress": true,
"observability": {
"logs": {
"credentialsRef": "my-credentials",
"pushUrl": "https://my.observability.host/<observability-instance-id>/loki/api/v1/push"
},
"metrics": {
"credentialsRef": "my-credentials",
"pushUrl": "https://my.observability.host/<observability-instance-id>/<argus-instance-id>/api/v1/receive"
}
},
"privateNetworkOnly": true
},
"planId": "p10",
"targetPools": [
{
"activeHealthCheck": {
"healthyThreshold": 3,
"httpHealthChecks": {
"okStatuses": [
"200",
"204"
],
"path": "/health"
},
"interval": "10s",
"intervalJitter": "3s",
"timeout": "5s",
"unhealthyThreshold": 1
},
"name": "my-target-pool",
"targetPort": 5732,
"targets": [
{
"displayName": "my-target1",
"ip": "192.11.2.5"
}
],
"tlsConfig": {
"customCa": "my.private.ca",
"enabled": true,
"skipCertificateValidation": false
}
}
]
}
package update
import (
"context"
_ "embed"
"encoding/json"
"log"
"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/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
//go:embed testdata/testconfig.json
var testConfiguration []byte
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &alb.APIClient{}
testProjectId = uuid.NewString()
testRegion = "eu01"
testLoadBalancer = "my-load-balancer"
testConfig = "testdata/testconfig.json"
)
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
configurationFlag: testConfig,
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,
Verbosity: globalflags.VerbosityDefault,
Region: testRegion,
},
Configuration: utils.Ptr(testConfig),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixturePayload(mods ...func(payload *alb.UpdateLoadBalancerPayload)) (payload alb.UpdateLoadBalancerPayload) {
if err := json.Unmarshal(testConfiguration, &payload); err != nil {
log.Panicf("cannot deserialize test configuration: %v", err)
}
for _, f := range mods {
f(&payload)
}
return payload
}
func fixtureRequest(mods ...func(request *alb.ApiUpdateLoadBalancerRequest)) alb.ApiUpdateLoadBalancerRequest {
request := testClient.UpdateLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancer)
request = request.UpdateLoadBalancerPayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description 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: "required fields only",
flagValues: map[string]string{
projectIdFlag: testProjectId,
configurationFlag: testConfig,
},
isValid: true,
expectedModel: &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
Configuration: &testConfig,
},
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest alb.ApiUpdateLoadBalancerRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
{
description: "required fields only",
model: &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
Configuration: &testConfig,
},
expectedRequest: testClient.
UpdateLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancer).
UpdateLoadBalancerPayload(fixturePayload()),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request, err := buildRequest(testCtx, tt.model, testClient)
if err != nil {
t.Fatalf("cannot create request: %v", err)
}
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 *alb.LoadBalancer
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty",
args: args{},
wantErr: true,
},
{
name: "empty response as argument",
args: args{
model: fixtureInputModel(),
resp: &alb.LoadBalancer{},
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(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 (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/goccy/go-yaml"
"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/alb/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
"github.com/stackitcloud/stackit-sdk-go/services/alb/wait"
)
const (
configurationFlag = "configuration"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Configuration *string
Version *string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "update",
Short: "Updates an application loadbalancer",
Long: "Updates an application loadbalancer.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Update an application loadbalancer from a configuration file`,
"$ stackit beta alb update --configuration my-loadbalancer.json"),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
if err != nil {
p.Debug(print.ErrorLevel, "get project name: %v", err)
projectLabel = model.ProjectId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to update an application loadbalancer for project %q?", projectLabel)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// for updates of an existing ALB the current version must be passed to the request
model.Version, err = getCurrentAlbVersion(ctx, apiClient, model)
if err != nil {
return err
}
// Call API
req, err := buildRequest(ctx, model, apiClient)
if err != nil {
return err
}
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("update application loadbalancer: %w", err)
}
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(p)
s.Start("updating loadbalancer")
_, err = wait.CreateOrUpdateLoadbalancerWaitHandler(ctx, apiClient, model.ProjectId, model.Region, *resp.Name).
WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for loadbalancer update: %w", err)
}
s.Stop()
}
return outputResult(p, model, projectLabel, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(configurationFlag, "c", "", "Filename of the input configuration file")
err := flags.MarkFlagsRequired(cmd, configurationFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
Configuration: flags.FlagToStringPointer(p, cmd, configurationFlag),
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func getCurrentAlbVersion(ctx context.Context, apiClient *alb.APIClient, model *inputModel) (*string, error) {
// use the configuration file to find the name of the loadbalancer
updatePayload, err := readPayload(ctx, model)
if err != nil {
return nil, err
}
if updatePayload.Name == nil {
return nil, fmt.Errorf("no name found in configuration")
}
if err != nil {
return nil, err
}
resp, err := apiClient.GetLoadBalancer(ctx, model.ProjectId, model.Region, *updatePayload.Name).Execute()
if err != nil {
return nil, err
}
return resp.Version, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) (req alb.ApiUpdateLoadBalancerRequest, err error) {
payload, err := readPayload(ctx, model)
if err != nil {
return req, err
}
if payload.Name == nil {
return req, fmt.Errorf("no name found in loadbalancer configuration")
}
payload.Version = model.Version
req = apiClient.UpdateLoadBalancer(ctx, model.ProjectId, model.Region, *payload.Name)
return req.UpdateLoadBalancerPayload(payload), nil
}
func readPayload(_ context.Context, model *inputModel) (payload alb.UpdateLoadBalancerPayload, err error) {
if model.Configuration == nil {
return payload, fmt.Errorf("no configuration file defined")
}
file, err := os.Open(*model.Configuration)
if err != nil {
return payload, fmt.Errorf("cannot open configuration file %q: %w", *model.Configuration, err)
}
defer file.Close() // nolint:errcheck // at this point close errors are not relevant anymore
if strings.HasSuffix(*model.Configuration, ".yaml") {
decoder := yaml.NewDecoder(bufio.NewReader(file), yaml.UseJSONUnmarshaler())
if err := decoder.Decode(&payload); err != nil {
return payload, fmt.Errorf("cannot deserialize yaml configuration from %q: %w", *model.Configuration, err)
}
} else if strings.HasSuffix(*model.Configuration, ".json") {
decoder := json.NewDecoder(bufio.NewReader(file))
if err := decoder.Decode(&payload); err != nil {
return payload, fmt.Errorf("cannot deserialize json configuration from %q: %w", *model.Configuration, err)
}
} else {
return payload, fmt.Errorf("cannot determine configuration fileformat of %q by extension. Must be '.json' or '.yaml'", *model.Configuration)
}
return payload, nil
}
func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *alb.LoadBalancer) error {
if resp == nil {
return fmt.Errorf("update loadbalancer response is empty")
}
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal loadbalancer: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal loadbalancer: %w", err)
}
p.Outputln(string(details))
return nil
default:
operationState := "Updated"
if model.Async {
operationState = "Triggered update of"
}
p.Outputf("%s application loadbalancer for %q. Name: %s\n", operationState, projectLabel, utils.PtrString(resp.Name))
return nil
}
}
package client
import (
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
"github.com/stackitcloud/stackit-cli/internal/pkg/config"
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/spf13/viper"
sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/services/alb"
)
func ConfigureClient(p *print.Printer) (*alb.APIClient, error) {
var err error
var apiClient *alb.APIClient
var cfgOptions []sdkConfig.ConfigurationOption
authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser)
if err != nil {
p.Debug(print.ErrorLevel, "configure authentication: %v", err)
return nil, &errors.AuthError{}
}
cfgOptions = append(cfgOptions, authCfgOption)
customEndpoint := viper.GetString(config.IaaSCustomEndpointKey)
if customEndpoint != "" {
cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint))
} else {
cfgOptions = append(cfgOptions, authCfgOption)
}
if p.IsVerbosityDebug() {
cfgOptions = append(cfgOptions,
sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)),
)
}
apiClient, err = alb.NewAPIClient(cfgOptions...)
if err != nil {
p.Debug(print.ErrorLevel, "create new API client: %v", err)
return nil, &errors.AuthError{}
}
return apiClient, nil
}
package utils
type AlbClientMocked struct {
}
package utils
type AlbClient interface {
}
package utils
import (
"testing"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
)
func TestTruncate(t *testing.T) {
type args struct {
s *string
maxLen int
}
tests := []struct {
name string
args args
want string
}{
{"nil string", args{nil, 3}, ""},
{"empty string", args{utils.Ptr(""), 10}, ""},
{"length below maxlength", args{utils.Ptr("foo"), 10}, "foo"},
{"exactly maxlength", args{utils.Ptr("foo"), 3}, "foo"},
{"above maxlength", args{utils.Ptr("foobarbaz"), 3}, "foo…"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Truncate(tt.args.s, tt.args.maxLen); got != tt.want {
t.Errorf("Truncate() = %v, want %v", got, tt.want)
}
})
}
}
# Release
## Release cycle
A release should be created at least every 2 weeks.
## Release creation
> [!IMPORTANT]
> Consider informing / syncing with the team before creating a new release.
1. Check out latest main branch on your machine
2. Create git tag: `git tag vX.X.X`
3. Push the git tag: `git push origin --tags`
4. The [release pipeline](https://github.com/stackitcloud/stackit-cli/actions/workflows/release.yaml) will build the release and publish it on GitHub
5. Ensure the release was created properly using the [releases page](https://github.com/stackitcloud/stackit-cli/releases)
+1
-1

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

- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v2
uses: samuelmeuli/action-snapcraft@v3
- name: Run GoReleaser

@@ -69,0 +69,0 @@ uses: goreleaser/goreleaser-action@v6

@@ -16,5 +16,5 @@ name: Renovate

- name: Self-hosted Renovate
uses: renovatebot/github-action@v41.0.5
uses: renovatebot/github-action@v41.0.21
with:
configurationFile: .github/renovate.json
token: ${{ secrets.RENOVATE_TOKEN }}

@@ -111,2 +111,5 @@ version: 2

skip_upload: auto
install: |
bin.install "stackit"
generate_completions_from_executable(bin/"stackit", "completion")

@@ -113,0 +116,0 @@ snapcrafts:

@@ -44,3 +44,4 @@ ## stackit beta

* [stackit](./stackit.md) - Manage STACKIT resources using the command line
* [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers
* [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex

@@ -45,3 +45,3 @@ ## stackit image create

--secure-boot Enables Secure Boot.
--uefi Enables UEFI boot.
--uefi Enables UEFI boot. (default true)
--video-model string Sets Graphic device model.

@@ -48,0 +48,0 @@ --virtio-scsi Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block.

+5
-4

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

github.com/spf13/viper v1.20.1
github.com/stackitcloud/stackit-sdk-go/core v0.16.2
github.com/stackitcloud/stackit-sdk-go/core v0.17.1
github.com/stackitcloud/stackit-sdk-go/services/alb v0.2.1
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.6.1

@@ -38,3 +39,3 @@ github.com/stackitcloud/stackit-sdk-go/services/dns v0.13.1

golang.org/x/oauth2 v0.29.0
golang.org/x/term v0.30.0
golang.org/x/term v0.31.0
golang.org/x/text v0.24.0

@@ -85,3 +86,3 @@ k8s.io/apimachinery v0.32.3

github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.1.1
github.com/stackitcloud/stackit-sdk-go/services/observability v0.4.0
github.com/stackitcloud/stackit-sdk-go/services/observability v0.5.0
github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.22.0

@@ -91,3 +92,3 @@ github.com/stackitcloud/stackit-sdk-go/services/redis v0.22.0

go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/sys v0.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

@@ -94,0 +95,0 @@ k8s.io/api v0.32.3 // indirect

+10
-8

@@ -114,4 +114,6 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=

github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stackitcloud/stackit-sdk-go/core v0.16.2 h1:F8A4P/LLlQSbz0S0+G3m8rb3BUOK6EcR/CKx5UQY5jQ=
github.com/stackitcloud/stackit-sdk-go/core v0.16.2/go.mod h1:8KIw3czdNJ9sdil9QQimxjR6vHjeINFrRv0iZ67wfn0=
github.com/stackitcloud/stackit-sdk-go/core v0.17.1 h1:TTrVoB1lERd/qfWzpe6HpwCJSjtaGnUI7UE7ITb5IT0=
github.com/stackitcloud/stackit-sdk-go/core v0.17.1/go.mod h1:8KIw3czdNJ9sdil9QQimxjR6vHjeINFrRv0iZ67wfn0=
github.com/stackitcloud/stackit-sdk-go/services/alb v0.2.1 h1:7VOUNsnNBL2iQsZ3lJogNzKx4lrYEawDYllcXV1gEik=
github.com/stackitcloud/stackit-sdk-go/services/alb v0.2.1/go.mod h1:J2/Jk+leR6AjBotd7USJJzX+AEIHH11yxnmx+6ciJEk=
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.6.1 h1:2lq6SG8qOgPOx2OIA5Bca8mwRSlect3Yljk57bXqd5I=

@@ -133,4 +135,4 @@ github.com/stackitcloud/stackit-sdk-go/services/authorization v0.6.1/go.mod h1:in9kC4GIBU5DpzXKFDL7RDl0fKyvN/RUIc7YbyWYEUA=

github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.1.1/go.mod h1:c5/LHxZZ7qZYOboR01qOw1Mqq5v/VNyONw7WPHctNeY=
github.com/stackitcloud/stackit-sdk-go/services/observability v0.4.0 h1:CPa/SRuX1Gl810K0SearSFyH0k/xKF9JHUV+4j+Tcn4=
github.com/stackitcloud/stackit-sdk-go/services/observability v0.4.0/go.mod h1:/go4bZ76dxGfkvl48EYUmPZ41c+64Yrf/26RodfcFyw=
github.com/stackitcloud/stackit-sdk-go/services/observability v0.5.0 h1:BpHIZdoAZwtzYgXznFE7lhqpUvnDJjc+HaEAQmA7NOk=
github.com/stackitcloud/stackit-sdk-go/services/observability v0.5.0/go.mod h1:1gMNjPCqT868oIqdWGkiReS1G/qpM4bYKYBmDRi8sqg=
github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.21.0 h1:Au07XkATgBqrGpjO5t6ASAjuif7VXcKKF3t4edRh+Yw=

@@ -200,6 +202,6 @@ github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.21.0/go.mod h1:zyVy1/etVAq6NkdPG4kNOM2R7+AxXp5zLsiN4nDC7Fs=

golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

@@ -206,0 +208,0 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

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

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

@@ -40,2 +41,3 @@ "github.com/stackitcloud/stackit-cli/internal/pkg/args"

cmd.AddCommand(sqlserverflex.NewCmd(p))
cmd.AddCommand(alb.NewCmd(p))
}

@@ -108,3 +108,3 @@ package create

SecureBoot: &testSecureBoot,
Uefi: &testUefi,
Uefi: testUefi,
VideoModel: &testVideoModel,

@@ -251,2 +251,12 @@ VirtioScsi: &testVirtioScsi,

{
description: "uefi flag is set to false",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[uefiFlag] = strconv.FormatBool(false)
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Config.Uefi = false
}),
},
{
description: "no rescue device and no bus is valid",

@@ -352,3 +362,3 @@ flagValues: fixtureFlagValues(func(flagValues map[string]string) {

model: fixtureInputModel(func(model *inputModel) {
model.Config.Uefi = utils.Ptr(false)
model.Config.Uefi = false
}),

@@ -355,0 +365,0 @@ expectedRequest: fixtureRequest(func(request *iaas.ApiCreateImageRequest) {

@@ -65,3 +65,3 @@ package create

SecureBoot *bool
Uefi *bool
Uefi bool
VideoModel *string

@@ -274,3 +274,3 @@ VirtioScsi *bool

cmd.Flags().Bool(secureBootFlag, false, "Enables Secure Boot.")
cmd.Flags().Bool(uefiFlag, false, "Enables UEFI boot.")
cmd.Flags().Bool(uefiFlag, true, "Enables UEFI boot.")
cmd.Flags().String(videoModelFlag, "", "Sets Graphic device model.")

@@ -316,3 +316,3 @@ cmd.Flags().Bool(virtioScsiFlag, false, "Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block.")

SecureBoot: flags.FlagToBoolPointer(p, cmd, secureBootFlag),
Uefi: flags.FlagToBoolPointer(p, cmd, uefiFlag),
Uefi: flags.FlagToBoolValue(p, cmd, uefiFlag),
VideoModel: flags.FlagToStringPointer(p, cmd, videoModelFlag),

@@ -373,3 +373,3 @@ VirtioScsi: flags.FlagToBoolPointer(p, cmd, virtioScsiFlag),

SecureBoot: model.Config.SecureBoot,
Uefi: model.Config.Uefi,
Uefi: utils.Ptr(model.Config.Uefi),
VideoModel: iaas.NewNullableString(model.Config.VideoModel),

@@ -376,0 +376,0 @@ VirtioScsi: model.Config.VirtioScsi,

@@ -176,3 +176,3 @@ package list

table := tables.NewTable()
table.SetHeader("ID", "NAME", "NIC SECURITY", "STATUS", "TYPE")
table.SetHeader("ID", "NAME", "NIC SECURITY", "DEVICE ID", "IPv4 ADDRESS", "STATUS", "TYPE")

@@ -184,2 +184,4 @@ for _, nic := range nics {

utils.PtrString(nic.NicSecurity),
utils.PtrString(nic.Device),
utils.PtrString(nic.Ipv4),
utils.PtrString(nic.Status),

@@ -186,0 +188,0 @@ utils.PtrString(nic.Type),

@@ -192,3 +192,4 @@ package print

// -R: interprets ANSI color and style sequences
pagerCmd := exec.Command("less", "-F", "-S", "-w", "-R")
// -K: exits if an interrupt character is typed
pagerCmd := exec.Command("less", "-F", "-S", "-w", "-R", "-K")

@@ -195,0 +196,0 @@ pager, pagerExists := os.LookupEnv("PAGER")

@@ -5,2 +5,3 @@ package utils

"strings"
"unicode/utf8"
)

@@ -37,1 +38,23 @@

}
// Truncate trims the passed string (if it is not nil). If the input string is
// longer than the given length, it is truncated to _maxLen_ and a ellipsis (…)
// is attached. Therefore the resulting string has at most length _maxLen-1_
func Truncate(s *string, maxLen int) string {
if s == nil {
return ""
}
if utf8.RuneCountInString(*s) > maxLen {
var builder strings.Builder
for i, r := range *s {
if i >= maxLen {
break
}
builder.WriteRune(r)
}
builder.WriteRune('…')
return builder.String()
}
return *s
}

@@ -74,6 +74,5 @@ <div align="center">

| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| Observability | `observability` | :white_check_mark: |
| Infrastructure as a Service (IaaS) | `network-area` <br/> `network` <br/> `volume` <br/> `network-interface` <br/> `public-ip` <br/> `security-group` <br/> `key-pair` <br/> `image` <br/> `quota` | :white_check_mark:|
| Authorization | `project`, `organization` | :white_check_mark: |
| DNS | `dns` | :white_check_mark: |
| Infrastructure as a Service (IaaS) | `image` <br/> `key-pair` <br/> `network` <br/> `network-area` <br/> `network-interface` <br/> `public-ip` <br/> `quota` <br/> `security-group` <br/> `server` <br/> `volume` | :white_check_mark:|
| Kubernetes Engine (SKE) | `ske` | :white_check_mark: |

@@ -84,2 +83,3 @@ | Load Balancer | `load-balancer` | :white_check_mark: |

| MongoDB Flex | `mongodbflex` | :white_check_mark: |
| Observability | `observability` | :white_check_mark: |
| Object Storage | `object-storage` | :white_check_mark: |

@@ -92,4 +92,4 @@ | OpenSearch | `opensearch` | :white_check_mark: |

| Secrets Manager | `secrets-manager` | :white_check_mark: |
| Server Backup Management | `server backup` | :white_check_mark: |
| Server Command (Run Command) | `server command` | :white_check_mark: |
| Server Backup Management | `server backup` | :white_check_mark: |
| Server Command (Run Command) | `server command` | :white_check_mark: |
| Service Account | `service-account` | :white_check_mark: |

@@ -195,2 +195,6 @@ | SQLServer Flex | `beta sqlserverflex` | :white_check_mark: (beta) |

## Release creation
See the [release documentation](./RELEASE.md) for further information.
## License

@@ -197,0 +201,0 @@