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.4.1-prerelease.1
to
v0.5.0
+50
docs/stackit_load-balancer_create.md
## stackit load-balancer create
Creates a Load Balancer
### Synopsis
Creates a Load Balancer.
The payload can be provided as a JSON string or a file path prefixed with "@".
See https://docs.api.stackit.cloud/documentation/load-balancer/version/v1#tag/Load-Balancer/operation/APIService_CreateLoadBalancer for information regarding the payload structure.
```
stackit load-balancer create [flags]
```
### Examples
```
Create a load balancer using an API payload sourced from the file "./payload.json"
$ stackit load-balancer create --payload @./payload.json
Create a load balancer using an API payload provided as a JSON string
$ stackit load-balancer create --payload "{...}"
Generate a payload with default values, and adapt it with custom values for the different configuration options
$ stackit load-balancer generate-payload > ./payload.json
<Modify payload in file>
$ stackit load-balancer create --payload @./payload.json
```
### Options
```
-h, --help Help for "stackit load-balancer create"
--payload string Request payload (JSON). Can be a string or a file path, if prefixed with "@" (example: @./payload.json).
```
### 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer](./stackit_load-balancer.md) - Provides functionality for Load Balancer
## stackit load-balancer delete
Deletes a Load Balancer
### Synopsis
Deletes a Load Balancer.
```
stackit load-balancer delete LOAD_BALANCER_NAME [flags]
```
### Examples
```
Deletes a load balancer with name "my-load-balancer"
$ stackit load-balancer delete my-load-balancer
```
### Options
```
-h, --help Help for "stackit load-balancer 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer](./stackit_load-balancer.md) - Provides functionality for Load Balancer
## stackit load-balancer describe
Shows details of a Load Balancer
### Synopsis
Shows details of a Load Balancer.
```
stackit load-balancer describe LOAD_BALANCER_NAME [flags]
```
### Examples
```
Get details of a load balancer with name "my-load-balancer"
$ stackit load-balancer describe my-load-balancer
Get details of a load-balancer with name "my-load-balancer" in a JSON format
$ stackit load-balancer describe my-load-balancer --output-format json
```
### Options
```
-h, --help Help for "stackit load-balancer 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer](./stackit_load-balancer.md) - Provides functionality for Load Balancer
## stackit load-balancer generate-payload
Generates a payload to create/update a Load Balancer
### Synopsis
Generates a JSON payload with values to be used as --payload input for load balancer creation or update.
See https://docs.api.stackit.cloud/documentation/load-balancer/version/v1#tag/Load-Balancer/operation/APIService_CreateLoadBalancer for information regarding the payload structure.
```
stackit load-balancer generate-payload [flags]
```
### Examples
```
Generate a payload, and adapt it with custom values for the different configuration options
$ stackit load-balancer generate-payload > ./payload.json
<Modify payload in file, if needed>
$ stackit load-balancer create --payload @./payload.json
Generate a payload with values of an existing load balancer, and adapt it with custom values for the different configuration options
$ stackit load-balancer generate-payload --lb-name xxx > ./payload.json
<Modify payload in file>
$ stackit load-balancer update xxx --payload @./payload.json
```
### Options
```
-h, --help Help for "stackit load-balancer generate-payload"
-n, --lb-name string If set, generates the payload with the current values of the given load balancer. If unset, generates the payload with empty values
```
### 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer](./stackit_load-balancer.md) - Provides functionality for Load Balancer
## stackit load-balancer list
Lists all Load Balancers
### Synopsis
Lists all Load Balancers.
```
stackit load-balancer list [flags]
```
### Examples
```
List all load balancers
$ stackit load-balancer list
List all loadbalancers in JSON format
$ stackit load-balancer list --output-format json
List up to 10 load balancers
$ stackit load-balancer list --limit 10
```
### Options
```
-h, --help Help for "stackit load-balancer list"
--limit int Maximum number of entries to list
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer](./stackit_load-balancer.md) - Provides functionality for Load Balancer
## stackit load-balancer observability-credentials add
Adds observability credentials to Load Balancer
### Synopsis
Adds existing observability credentials (username and password) to Load Balancer. The credentials can be for Argus or another monitoring tool.
```
stackit load-balancer observability-credentials add [flags]
```
### Examples
```
Add observability credentials to a load balancer with username "xxx" and display name "yyy". The password is entered using the terminal
$ stackit load-balancer observability-credentials add --username xxx --display-name yyy
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 load-balancer observability-credentials add --username xxx --password @./password.txt --display-name yyy
```
### Options
```
--display-name string Credentials display name
-h, --help Help for "stackit load-balancer observability-credentials add"
--password string Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).
--username string Username
```
### 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials
## stackit load-balancer observability-credentials delete
Deletes observability credentials for Load Balancer
### Synopsis
Deletes observability credentials for Load Balancer.
```
stackit load-balancer observability-credentials delete CREDENTIALS_REF [flags]
```
### Examples
```
Delete observability credentials with reference "credentials-xxx" for Load Balancer
$ stackit load-balancer observability-credentials delete credentials-xxx
```
### Options
```
-h, --help Help for "stackit load-balancer 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials
## stackit load-balancer observability-credentials describe
Shows details of observability credentials for Load Balancer
### Synopsis
Shows details of observability credentials for Load Balancer.
```
stackit load-balancer observability-credentials describe CREDENTIALS_REF [flags]
```
### Examples
```
Get details of observability credentials with reference "credentials-xxx"
$ stackit load-balancer observability-credentials describe credentials-xxx
```
### Options
```
-h, --help Help for "stackit load-balancer 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials
## stackit load-balancer observability-credentials list
Lists all observability credentials for Load Balancer
### Synopsis
Lists all observability credentials for Load Balancer.
```
stackit load-balancer observability-credentials list [flags]
```
### Examples
```
List all observability credentials for Load Balancer
$ stackit load-balancer observability-credentials list
List all observability credentials for Load Balancer in JSON format
$ stackit load-balancer observability-credentials list --output-format json
List up to 10 observability credentials for Load Balancer
$ stackit load-balancer observability-credentials list --limit 10
```
### Options
```
-h, --help Help for "stackit load-balancer observability-credentials list"
--limit int Maximum number of entries to list
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials
## stackit load-balancer observability-credentials update
Updates observability credentials for Load Balancer
### Synopsis
Updates existing observability credentials (username and password) for Load Balancer. The credentials can be for Argus or another monitoring tool.
```
stackit load-balancer observability-credentials update [flags]
```
### Examples
```
Update the password and username of observability credentials of Load Balancer with credentials reference "credentials-xxx". The password is entered using the terminal
$ stackit load-balancer observability-credentials update credentials-xxx --username new-username
Update the password of observability credentials of Load Balancer with credentials reference "credentials-xxx", by providing the path to a file with the new password as flag
$ stackit load-balancer observability-credentials update credentials-xxx --password @./new-password.txt
```
### Options
```
--display-name string Credentials name
-h, --help Help for "stackit load-balancer observability-credentials update"
--password string Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).
--username string Username
```
### 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials
## stackit load-balancer observability-credentials
Provides functionality for Load Balancer observability credentials
### Synopsis
Provides functionality for Load Balancer observability credentials. These commands can be used to store and update existing credentials, which are valid to be used for Load Balancer observability. This means, e.g. when using Argus, first of all these credentials must be created for that Argus instance (by using "stackit argus credentials create") and then can be managed for a Load Balancer by using the commands in this group.
```
stackit load-balancer observability-credentials [flags]
```
### Options
```
-h, --help Help for "stackit load-balancer 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer](./stackit_load-balancer.md) - Provides functionality for Load Balancer
* [stackit load-balancer observability-credentials add](./stackit_load-balancer_observability-credentials_add.md) - Adds observability credentials to Load Balancer
* [stackit load-balancer observability-credentials delete](./stackit_load-balancer_observability-credentials_delete.md) - Deletes observability credentials for Load Balancer
* [stackit load-balancer observability-credentials describe](./stackit_load-balancer_observability-credentials_describe.md) - Shows details of observability credentials for Load Balancer
* [stackit load-balancer observability-credentials list](./stackit_load-balancer_observability-credentials_list.md) - Lists all observability credentials for Load Balancer
* [stackit load-balancer observability-credentials update](./stackit_load-balancer_observability-credentials_update.md) - Updates observability credentials for Load Balancer
## stackit load-balancer quota
Shows the configured Load Balancer quota
### Synopsis
Shows the configured Load Balancer quota for the project. If you want to change the quota, please create a support ticket in the STACKIT Help Center (https://support.stackit.cloud)
```
stackit load-balancer quota [flags]
```
### Examples
```
Get the configured load balancer quota for the project
$ stackit load-balancer quota
```
### Options
```
-h, --help Help for "stackit load-balancer quota"
```
### 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer](./stackit_load-balancer.md) - Provides functionality for Load Balancer
## stackit load-balancer target-pool add-target
Adds a target to a target pool
### Synopsis
Adds a target to a target pool.
The target IP must by unique within a target pool and must be a valid IPv4 or IPv6.
```
stackit load-balancer target-pool add-target TARGET_IP [flags]
```
### Examples
```
Add a target with IP 1.2.3.4 and name "my-new-target" to target pool "my-target-pool" of load balancer with name "my-load-balancer"
$ stackit load-balancer target-pool add-target 1.2.3.4 --target-name my-new-target --target-pool-name my-target-pool --lb-name my-load-balancer
```
### Options
```
-h, --help Help for "stackit load-balancer target-pool add-target"
--lb-name string Load balancer name
-n, --target-name string Target name
--target-pool-name string Target pool name
```
### 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer target-pool](./stackit_load-balancer_target-pool.md) - Provides functionality for target pools
## stackit load-balancer target-pool describe
Shows details of a target pool in a Load Balancer
### Synopsis
Shows details of a target pool in a Load Balancer.
```
stackit load-balancer target-pool describe TARGET_POOL_NAME [flags]
```
### Examples
```
Get details of a target pool with name "pool" in load balancer with name "my-load-balancer"
$ stackit load-balancer target-pool describe pool --lb-name my-load-balancer
Get details of a target pool with name "pool" in load balancer with name "my-load-balancer in JSON output"
$ stackit load-balancer target-pool describe pool --lb-name my-load-balancer --output-format json
```
### Options
```
-h, --help Help for "stackit load-balancer target-pool describe"
--lb-name string Name of the load balancer
```
### 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer target-pool](./stackit_load-balancer_target-pool.md) - Provides functionality for target pools
## stackit load-balancer target-pool remove-target
Removes a target from a target pool
### Synopsis
Removes a target from a target pool.
```
stackit load-balancer target-pool remove-target TARGET_IP [flags]
```
### Examples
```
Remove target with IP 1.2.3.4 from target pool "my-target-pool" of load balancer with name "my-load-balancer"
$ stackit load-balancer target-pool remove-target 1.2.3.4 --target-pool-name my-target-pool --lb-name my-load-balancer
```
### Options
```
-h, --help Help for "stackit load-balancer target-pool remove-target"
--lb-name string Load balancer name
--target-pool-name string Target IP of the target to remove. Must be a valid IPv4 or IPv6
```
### 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer target-pool](./stackit_load-balancer_target-pool.md) - Provides functionality for target pools
## stackit load-balancer target-pool
Provides functionality for target pools
### Synopsis
Provides functionality for target pools.
```
stackit load-balancer target-pool [flags]
```
### Options
```
-h, --help Help for "stackit load-balancer target-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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer](./stackit_load-balancer.md) - Provides functionality for Load Balancer
* [stackit load-balancer target-pool add-target](./stackit_load-balancer_target-pool_add-target.md) - Adds a target to a target pool
* [stackit load-balancer target-pool describe](./stackit_load-balancer_target-pool_describe.md) - Shows details of a target pool in a Load Balancer
* [stackit load-balancer target-pool remove-target](./stackit_load-balancer_target-pool_remove-target.md) - Removes a target from a target pool
## stackit load-balancer update
Updates a Load Balancer
### Synopsis
Updates a load balancer.
The payload can be provided as a JSON string or a file path prefixed with "@".
See https://docs.api.stackit.cloud/documentation/load-balancer/version/v1#tag/Load-Balancer/operation/APIService_UpdateLoadBalancer for information regarding the payload structure.
```
stackit load-balancer update LOAD_BALANCER_NAME [flags]
```
### Examples
```
Update a load balancer with name "xxx", using an API payload sourced from the file "./payload.json"
$ stackit load-balancer update xxx --payload @./payload.json
Update a load balancer with name "xxx", using an API payload provided as a JSON string
$ stackit load-balancer update xxx --payload "{...}"
Generate a payload with the current values of an existing load balancer xxx, and adapt it with custom values for the different configuration options
$ stackit load-balancer generate-payload --lb-name xxx > ./payload.json
<Modify payload in file>
$ stackit load-balancer update xxx --payload @./payload.json
```
### Options
```
-h, --help Help for "stackit load-balancer update"
--payload string Request payload (JSON). Can be a string or a file path, if prefixed with "@". Example: @./payload.json
```
### 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit load-balancer](./stackit_load-balancer.md) - Provides functionality for Load Balancer
## stackit load-balancer
Provides functionality for Load Balancer
### Synopsis
Provides functionality for Load Balancer.
```
stackit load-balancer [flags]
```
### Options
```
-h, --help Help for "stackit load-balancer"
```
### 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"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit](./stackit.md) - Manage STACKIT resources using the command line
* [stackit load-balancer create](./stackit_load-balancer_create.md) - Creates a Load Balancer
* [stackit load-balancer delete](./stackit_load-balancer_delete.md) - Deletes a Load Balancer
* [stackit load-balancer describe](./stackit_load-balancer_describe.md) - Shows details of a Load Balancer
* [stackit load-balancer generate-payload](./stackit_load-balancer_generate-payload.md) - Generates a payload to create/update a Load Balancer
* [stackit load-balancer list](./stackit_load-balancer_list.md) - Lists all Load Balancers
* [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials
* [stackit load-balancer quota](./stackit_load-balancer_quota.md) - Shows the configured Load Balancer quota
* [stackit load-balancer target-pool](./stackit_load-balancer_target-pool.md) - Provides functionality for target pools
* [stackit load-balancer update](./stackit_load-balancer_update.md) - Updates a Load Balancer
package create
import (
"context"
"testing"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
"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"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &loadbalancer.APIClient{}
var testProjectId = uuid.NewString()
var testRequestId = xRequestId
var testPayload = &loadbalancer.CreateLoadBalancerPayload{
ExternalAddress: utils.Ptr(""),
Listeners: &[]loadbalancer.Listener{
{
DisplayName: utils.Ptr(""),
Port: utils.Ptr(int64(0)),
Protocol: utils.Ptr(""),
ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{
{
Name: utils.Ptr(""),
},
},
TargetPool: utils.Ptr(""),
Tcp: &loadbalancer.OptionsTCP{
IdleTimeout: utils.Ptr(""),
},
Udp: &loadbalancer.OptionsUDP{
IdleTimeout: utils.Ptr(""),
},
},
},
Name: utils.Ptr(""),
Networks: &[]loadbalancer.Network{
{
NetworkId: utils.Ptr(""),
Role: utils.Ptr(""),
},
},
Options: &loadbalancer.LoadBalancerOptions{
AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{
AllowedSourceRanges: &[]string{
"",
},
},
EphemeralAddress: utils.Ptr(false),
Observability: &loadbalancer.LoadbalancerOptionObservability{
Logs: &loadbalancer.LoadbalancerOptionLogs{
CredentialsRef: utils.Ptr(""),
PushUrl: utils.Ptr(""),
},
Metrics: &loadbalancer.LoadbalancerOptionMetrics{
CredentialsRef: utils.Ptr(""),
PushUrl: utils.Ptr(""),
},
},
PrivateNetworkOnly: utils.Ptr(false),
},
TargetPools: &[]loadbalancer.TargetPool{
{
ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{
HealthyThreshold: utils.Ptr(int64(0)),
Interval: utils.Ptr(""),
IntervalJitter: utils.Ptr(""),
Timeout: utils.Ptr(""),
UnhealthyThreshold: utils.Ptr(int64(0)),
},
Name: utils.Ptr(""),
SessionPersistence: &loadbalancer.SessionPersistence{
UseSourceIpAddress: utils.Ptr(false),
},
TargetPort: utils.Ptr(int64(0)),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr(""),
Ip: utils.Ptr(""),
},
},
},
},
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
payloadFlag: `{
"externalAddress": "",
"listeners": [
{
"displayName": "",
"port": 0,
"protocol": "",
"serverNameIndicators": [
{
"name": ""
}
],
"targetPool": "",
"tcp": {
"idleTimeout": ""
},
"udp": {
"idleTimeout": ""
}
}
],
"name": "",
"networks": [
{
"networkId": "",
"role": ""
}
],
"options": {
"accessControl": {
"allowedSourceRanges": [
""
]
},
"ephemeralAddress": false,
"observability": {
"logs": {
"credentialsRef": "",
"pushUrl": ""
},
"metrics": {
"credentialsRef": "",
"pushUrl": ""
}
},
"privateNetworkOnly": false
},
"targetPools": [
{
"activeHealthCheck": {
"healthyThreshold": 0,
"interval": "",
"intervalJitter": "",
"timeout": "",
"unhealthyThreshold": 0
},
"name": "",
"sessionPersistence": {
"useSourceIpAddress": false
},
"targetPort": 0,
"targets": [
{
"displayName": "",
"ip": ""
}
]
}
]
}`,
}
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,
},
Payload: testPayload,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiCreateLoadBalancerRequest)) loadbalancer.ApiCreateLoadBalancerRequest {
request := testClient.CreateLoadBalancer(testCtx, testProjectId)
request = request.CreateLoadBalancerPayload(*testPayload)
request = request.XRequestID(testRequestId)
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: "no flag values",
flagValues: map[string]string{},
isValid: false,
},
{
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,
},
{
description: "payload is missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, payloadFlag)
}),
isValid: false,
},
{
description: "payload is empty",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[payloadFlag] = ""
}),
isValid: false,
},
{
description: "invalid json",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[payloadFlag] = "not json"
}),
isValid: false,
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)
}
err = cmd.ValidateFlagGroups()
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,
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest loadbalancer.ApiCreateLoadBalancerRequest
isValid bool
}{
{
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 create
import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
"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/load-balancer/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/wait"
)
const (
payloadFlag = "payload"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Payload *loadbalancer.CreateLoadBalancerPayload
}
var (
xRequestId = uuid.NewString()
)
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a Load Balancer",
Long: fmt.Sprintf("%s\n%s\n%s",
"Creates a Load Balancer.",
"The payload can be provided as a JSON string or a file path prefixed with \"@\".",
"See https://docs.api.stackit.cloud/documentation/load-balancer/version/v1#tag/Load-Balancer/operation/APIService_CreateLoadBalancer for information regarding the payload structure.",
),
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a load balancer using an API payload sourced from the file "./payload.json"`,
"$ stackit load-balancer create --payload @./payload.json"),
examples.NewExample(
`Create a load balancer using an API payload provided as a JSON string`,
`$ stackit load-balancer create --payload "{...}"`),
examples.NewExample(
`Generate a payload with default values, and adapt it with custom values for the different configuration options`,
`$ stackit load-balancer generate-payload > ./payload.json`,
`<Modify payload in file>`,
`$ stackit load-balancer create --payload @./payload.json`),
),
RunE: func(cmd *cobra.Command, args []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 a load balancer for project %q?", 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("create load balancer: %w", err)
}
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(p)
s.Start("Creating load balancer")
_, err = wait.CreateLoadBalancerWaitHandler(ctx, apiClient, model.ProjectId, *model.Payload.Name).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for load balancer creation: %w", err)
}
s.Stop()
}
operationState := "Created"
if model.Async {
operationState = "Triggered creation of"
}
p.Outputf("%s load balancer with name %q \n", operationState, *model.Payload.Name)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.ReadFromFileFlag(), payloadFlag, `Request payload (JSON). Can be a string or a file path, if prefixed with "@" (example: @./payload.json).`)
err := flags.MarkFlagsRequired(cmd, payloadFlag)
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{}
}
payloadValue := flags.FlagToStringPointer(p, cmd, payloadFlag)
var payload *loadbalancer.CreateLoadBalancerPayload
if payloadValue != nil {
payload = &loadbalancer.CreateLoadBalancerPayload{}
err := json.Unmarshal([]byte(*payloadValue), payload)
if err != nil {
return nil, fmt.Errorf("encode payload: %w", err)
}
}
model := inputModel{
GlobalFlagModel: globalFlags,
Payload: payload,
}
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 *loadbalancer.APIClient) loadbalancer.ApiCreateLoadBalancerRequest {
req := apiClient.CreateLoadBalancer(ctx, model.ProjectId)
req = req.CreateLoadBalancerPayload(*model.Payload)
req = req.XRequestID(xRequestId)
return req
}
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/loadbalancer"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &loadbalancer.APIClient{}
var testProjectId = uuid.NewString()
var testLoadBalancerName = "loadBalancer"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testLoadBalancerName,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
}
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,
},
LoadBalancerName: testLoadBalancerName,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiDeleteLoadBalancerRequest)) loadbalancer.ApiDeleteLoadBalancerRequest {
request := testClient.DeleteLoadBalancer(testCtx, testProjectId, testLoadBalancerName)
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{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
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.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 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
isValid bool
expectedRequest loadbalancer.ApiDeleteLoadBalancerRequest
}{
{
description: "base",
model: fixtureInputModel(),
isValid: true,
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/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/load-balancer/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/wait"
)
const (
loadBalancerNameArg = "LOAD_BALANCER_NAME"
)
type inputModel struct {
*globalflags.GlobalFlagModel
LoadBalancerName string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("delete %s", loadBalancerNameArg),
Short: "Deletes a Load Balancer",
Long: "Deletes a Load Balancer.",
Args: args.SingleArg(loadBalancerNameArg, nil),
Example: examples.Build(
examples.NewExample(
`Deletes a load balancer with name "my-load-balancer"`,
"$ stackit load-balancer 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
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete load balancer %q? (This cannot be undone)", model.LoadBalancerName)
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 load balancer: %w", err)
}
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(p)
s.Start("Deleting load balancer")
_, err = wait.DeleteLoadBalancerWaitHandler(ctx, apiClient, model.ProjectId, model.LoadBalancerName).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for load balancer deletion: %w", err)
}
s.Stop()
}
operationState := "Deleted"
if model.Async {
operationState = "Triggered deletion of"
}
p.Info("%s load balancer %q\n", operationState, model.LoadBalancerName)
return nil
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
loadBalancerName := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
LoadBalancerName: 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 *loadbalancer.APIClient) loadbalancer.ApiDeleteLoadBalancerRequest {
req := apiClient.DeleteLoadBalancer(ctx, model.ProjectId, model.LoadBalancerName)
return req
}
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/loadbalancer"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &loadbalancer.APIClient{}
var testProjectId = uuid.NewString()
var testloadBalancerName = "loadBalancer"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testloadBalancerName,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
}
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,
},
LoadBalancerName: testloadBalancerName,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiGetLoadBalancerRequest)) loadbalancer.ApiGetLoadBalancerRequest {
request := testClient.GetLoadBalancer(testCtx, testProjectId, testloadBalancerName)
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{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
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.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 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
isValid bool
expectedRequest loadbalancer.ApiGetLoadBalancerRequest
}{
{
description: "base",
model: fixtureInputModel(),
isValid: true,
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 describe
import (
"context"
"encoding/json"
"fmt"
"strings"
"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/load-balancer/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
)
const (
loadBalancerNameArg = "LOAD_BALANCER_NAME"
)
type inputModel struct {
*globalflags.GlobalFlagModel
LoadBalancerName string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", loadBalancerNameArg),
Short: "Shows details of a Load Balancer",
Long: "Shows details of a Load Balancer.",
Args: args.SingleArg(loadBalancerNameArg, nil),
Example: examples.Build(
examples.NewExample(
`Get details of a load balancer with name "my-load-balancer"`,
"$ stackit load-balancer describe my-load-balancer"),
examples.NewExample(
`Get details of a load-balancer with name "my-load-balancer" in a JSON format`,
"$ stackit load-balancer describe my-load-balancer --output-format json"),
),
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 load balancer: %w", err)
}
return outputResult(p, model.OutputFormat, resp)
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
loadBalancerName := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
LoadBalancerName: 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 *loadbalancer.APIClient) loadbalancer.ApiGetLoadBalancerRequest {
req := apiClient.GetLoadBalancer(ctx, model.ProjectId, model.LoadBalancerName)
return req
}
func outputResult(p *print.Printer, outputFormat string, loadBalancer *loadbalancer.LoadBalancer) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(loadBalancer, "", " ")
if err != nil {
return fmt.Errorf("marshal load balancer: %w", err)
}
p.Outputln(string(details))
return nil
default:
return outputResultAsTable(p, loadBalancer)
}
}
func outputResultAsTable(p *print.Printer, loadBalancer *loadbalancer.LoadBalancer) error {
content := renderLoadBalancer(loadBalancer)
if loadBalancer.Listeners != nil {
content += renderListeners(*loadBalancer.Listeners)
}
if loadBalancer.TargetPools != nil {
content += renderTargetPools(*loadBalancer.TargetPools)
}
err := p.PagerDisplay(content)
if err != nil {
return fmt.Errorf("display output: %w", err)
}
return nil
}
func renderLoadBalancer(loadBalancer *loadbalancer.LoadBalancer) string {
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
}
externalAdress := "-"
if loadBalancer.ExternalAddress != nil {
externalAdress = *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.AddRow("NAME", *loadBalancer.Name)
table.AddSeparator()
table.AddRow("STATE", *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", externalAdress)
table.AddSeparator()
table.AddRow("ATTACHED NETWORK ID", networkId)
table.AddSeparator()
table.AddRow("ACL", acl)
return table.Render()
}
func renderListeners(listeners []loadbalancer.Listener) string {
table := tables.NewTable()
table.SetHeader("LISTENER NAME", "PORT", "PROTOCOL", "TARGET POOL")
for i := range listeners {
listener := listeners[i]
table.AddRow(*listener.Name, *listener.Port, *listener.Protocol, *listener.TargetPool)
}
return table.Render()
}
func renderTargetPools(targetPools []loadbalancer.TargetPool) string {
table := tables.NewTable()
table.SetHeader("TARGET POOL NAME", "PORT", "TARGETS")
for _, targetPool := range targetPools {
table.AddRow(*targetPool.Name, *targetPool.TargetPort, len(*targetPool.Targets))
}
return table.Render()
}
package generatepayload
import (
"context"
"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/loadbalancer"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &loadbalancer.APIClient{}
var testProjectId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
loadBalancerNameFlag: "example-name",
}
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,
},
LoadBalancerName: utils.Ptr("example-name"),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiGetLoadBalancerRequest)) loadbalancer.ApiGetLoadBalancerRequest {
request := testClient.GetLoadBalancer(testCtx, testProjectId, "example-name")
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: true,
expectedModel: &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{Verbosity: globalflags.VerbosityDefault},
},
},
{
description: "name missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, loadBalancerNameFlag)
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.LoadBalancerName = nil
}),
},
{
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)
}
err = cmd.ValidateFlagGroups()
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 loadbalancer.ApiGetLoadBalancerRequest
isValid bool
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestModifyListeners(t *testing.T) {
tests := []struct {
description string
response *loadbalancer.LoadBalancer
expected *[]loadbalancer.Listener
}{
{
description: "base",
response: &loadbalancer.LoadBalancer{
Listeners: &[]loadbalancer.Listener{
{
DisplayName: utils.Ptr(""),
Port: utils.Ptr(int64(0)),
Protocol: utils.Ptr(""),
Name: utils.Ptr(""),
ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{
{
Name: utils.Ptr(""),
},
},
TargetPool: utils.Ptr(""),
Tcp: &loadbalancer.OptionsTCP{
IdleTimeout: utils.Ptr(""),
},
Udp: &loadbalancer.OptionsUDP{
IdleTimeout: utils.Ptr(""),
},
},
{
DisplayName: utils.Ptr(""),
Port: utils.Ptr(int64(0)),
Protocol: utils.Ptr(""),
Name: utils.Ptr(""),
ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{
{
Name: utils.Ptr(""),
},
},
TargetPool: utils.Ptr(""),
Tcp: &loadbalancer.OptionsTCP{
IdleTimeout: utils.Ptr(""),
},
Udp: &loadbalancer.OptionsUDP{
IdleTimeout: utils.Ptr(""),
},
},
},
},
expected: &[]loadbalancer.Listener{
{
DisplayName: utils.Ptr(""),
Port: utils.Ptr(int64(0)),
Protocol: utils.Ptr(""),
Name: nil,
ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{
{
Name: utils.Ptr(""),
},
},
TargetPool: utils.Ptr(""),
Tcp: &loadbalancer.OptionsTCP{
IdleTimeout: utils.Ptr(""),
},
Udp: &loadbalancer.OptionsUDP{
IdleTimeout: utils.Ptr(""),
},
},
{
DisplayName: utils.Ptr(""),
Port: utils.Ptr(int64(0)),
Protocol: utils.Ptr(""),
Name: nil,
ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{
{
Name: utils.Ptr(""),
},
},
TargetPool: utils.Ptr(""),
Tcp: &loadbalancer.OptionsTCP{
IdleTimeout: utils.Ptr(""),
},
Udp: &loadbalancer.OptionsUDP{
IdleTimeout: utils.Ptr(""),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output := modifyListener(tt.response)
diff := cmp.Diff(output, tt.expected)
if diff != "" {
t.Errorf("expected output to be %+v, got %+v", tt.expected, output)
}
})
}
}
package generatepayload
import (
"context"
"encoding/json"
"fmt"
"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/load-balancer/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
"github.com/spf13/cobra"
)
const (
loadBalancerNameFlag = "lb-name"
)
type inputModel struct {
*globalflags.GlobalFlagModel
LoadBalancerName *string
}
var (
defaultPayloadListener = &loadbalancer.Listener{
DisplayName: utils.Ptr(""),
Port: utils.Ptr(int64(0)),
Protocol: utils.Ptr(""),
ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{
{
Name: utils.Ptr(""),
},
},
TargetPool: utils.Ptr(""),
Tcp: &loadbalancer.OptionsTCP{
IdleTimeout: utils.Ptr(""),
},
Udp: &loadbalancer.OptionsUDP{
IdleTimeout: utils.Ptr(""),
},
}
defaultPayloadNetwork = &loadbalancer.Network{
NetworkId: utils.Ptr(""),
Role: utils.Ptr(""),
}
defaultPayloadTargetPool = &loadbalancer.TargetPool{
ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{
HealthyThreshold: utils.Ptr(int64(0)),
Interval: utils.Ptr(""),
IntervalJitter: utils.Ptr(""),
Timeout: utils.Ptr(""),
UnhealthyThreshold: utils.Ptr(int64(0)),
},
Name: utils.Ptr(""),
SessionPersistence: &loadbalancer.SessionPersistence{
UseSourceIpAddress: utils.Ptr(false),
},
TargetPort: utils.Ptr(int64(0)),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr(""),
Ip: utils.Ptr(""),
},
},
}
DefaultCreateLoadBalancerPayload = loadbalancer.CreateLoadBalancerPayload{
ExternalAddress: utils.Ptr(""),
Listeners: &[]loadbalancer.Listener{
*defaultPayloadListener,
},
Name: utils.Ptr(""),
Networks: &[]loadbalancer.Network{
*defaultPayloadNetwork,
},
Options: &loadbalancer.LoadBalancerOptions{
AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{
AllowedSourceRanges: &[]string{
"",
},
},
EphemeralAddress: utils.Ptr(false),
Observability: &loadbalancer.LoadbalancerOptionObservability{
Logs: &loadbalancer.LoadbalancerOptionLogs{
CredentialsRef: utils.Ptr(""),
PushUrl: utils.Ptr(""),
},
Metrics: &loadbalancer.LoadbalancerOptionMetrics{
CredentialsRef: utils.Ptr(""),
PushUrl: utils.Ptr(""),
},
},
PrivateNetworkOnly: utils.Ptr(false),
},
TargetPools: &[]loadbalancer.TargetPool{
*defaultPayloadTargetPool,
},
}
)
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "generate-payload",
Short: "Generates a payload to create/update a Load Balancer",
Long: fmt.Sprintf("%s\n%s",
"Generates a JSON payload with values to be used as --payload input for load balancer creation or update.",
"See https://docs.api.stackit.cloud/documentation/load-balancer/version/v1#tag/Load-Balancer/operation/APIService_CreateLoadBalancer for information regarding the payload structure.",
),
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Generate a payload, and adapt it with custom values for the different configuration options`,
`$ stackit load-balancer generate-payload > ./payload.json`,
`<Modify payload in file, if needed>`,
`$ stackit load-balancer create --payload @./payload.json`),
examples.NewExample(
`Generate a payload with values of an existing load balancer, and adapt it with custom values for the different configuration options`,
`$ stackit load-balancer generate-payload --lb-name xxx > ./payload.json`,
`<Modify payload in file>`,
`$ stackit load-balancer update xxx --payload @./payload.json`),
),
RunE: func(cmd *cobra.Command, args []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
}
if model.LoadBalancerName == nil {
createPayload := DefaultCreateLoadBalancerPayload
return outputCreateResult(p, &createPayload)
}
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("read load balancer: %w", err)
}
listeners := modifyListener(resp)
updatePayload := &loadbalancer.UpdateLoadBalancerPayload{
ExternalAddress: resp.ExternalAddress,
Listeners: listeners,
Name: resp.Name,
Networks: resp.Networks,
Options: resp.Options,
TargetPools: resp.TargetPools,
Version: resp.Version,
}
return outputUpdateResult(p, updatePayload)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(loadBalancerNameFlag, "n", "", "If set, generates the payload with the current values of the given load balancer. If unset, generates the payload with empty values")
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
loadBalancerName := flags.FlagToStringPointer(p, cmd, loadBalancerNameFlag)
// If load balancer name is provided, projectId is needed as well
if loadBalancerName != nil && globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
LoadBalancerName: 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 *loadbalancer.APIClient) loadbalancer.ApiGetLoadBalancerRequest {
req := apiClient.GetLoadBalancer(ctx, model.ProjectId, *model.LoadBalancerName)
return req
}
func outputCreateResult(p *print.Printer, payload *loadbalancer.CreateLoadBalancerPayload) error {
payloadBytes, err := json.MarshalIndent(*payload, "", " ")
if err != nil {
return fmt.Errorf("marshal create load balancer payload: %w", err)
}
p.Outputln(string(payloadBytes))
return nil
}
func outputUpdateResult(p *print.Printer, payload *loadbalancer.UpdateLoadBalancerPayload) error {
payloadBytes, err := json.MarshalIndent(*payload, "", " ")
if err != nil {
return fmt.Errorf("marshal update load balancer payload: %w", err)
}
p.Outputln(string(payloadBytes))
return nil
}
func modifyListener(resp *loadbalancer.LoadBalancer) *[]loadbalancer.Listener {
listeners := *resp.Listeners
for i := range listeners {
listeners[i].Name = nil
}
return &listeners
}
package list
import (
"context"
"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/loadbalancer"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &loadbalancer.APIClient{}
var testProjectId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
limitFlag: "10",
}
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,
},
Limit: utils.Ptr(int64(10)),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiListLoadBalancersRequest)) loadbalancer.ApiListLoadBalancersRequest {
request := testClient.ListLoadBalancers(testCtx, testProjectId)
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, 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,
},
{
description: "limit invalid",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "invalid"
}),
isValid: false,
},
{
description: "limit invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "0"
}),
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 loadbalancer.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),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package list
import (
"context"
"encoding/json"
"fmt"
"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/load-balancer/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
)
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 Load Balancers",
Long: "Lists all Load Balancers.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all load balancers`,
"$ stackit load-balancer list"),
examples.NewExample(
`List all loadbalancers in JSON format`,
"$ stackit load-balancer list --output-format json"),
examples.NewExample(
`List up to 10 load balancers `,
"$ stackit load-balancer list --limit 10"),
),
RunE: func(cmd *cobra.Command, args []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("get load balancers: %w", err)
}
if resp.LoadBalancers == nil || (resp.LoadBalancers != nil && len(*resp.LoadBalancers) == 0) {
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
if err != nil {
p.Debug(print.ErrorLevel, "get project name: %v", err)
projectLabel = model.ProjectId
}
p.Info("No load balancers found for project %q\n", projectLabel)
return nil
}
loadBalancers := *resp.LoadBalancers
// Truncate output
if model.Limit != nil && len(loadBalancers) > int(*model.Limit) {
loadBalancers = loadBalancers[:*model.Limit]
}
return outputResult(p, model.OutputFormat, loadBalancers)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list")
}
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: flags.FlagToInt64Pointer(p, cmd, limitFlag),
}
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 *loadbalancer.APIClient) loadbalancer.ApiListLoadBalancersRequest {
req := apiClient.ListLoadBalancers(ctx, model.ProjectId)
return req
}
func outputResult(p *print.Printer, outputFormat string, loadBalancers []loadbalancer.LoadBalancer) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(loadBalancers, "", " ")
if err != nil {
return fmt.Errorf("marshal load balancer list: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("NAME", "STATE", "IP ADDRESS", "LISTENERS", "TARGET POOLS")
for i := range loadBalancers {
l := loadBalancers[i]
externalAdress := "-"
if l.ExternalAddress != nil {
externalAdress = *l.ExternalAddress
}
table.AddRow(*l.Name, *l.Status, externalAdress, len(*l.Listeners), len(*l.TargetPools))
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package loadbalancer
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/describe"
generatepayload "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/generate-payload"
"github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/list"
observabilitycredentials "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials"
"github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/quota"
targetpool "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/target-pool"
"github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/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: "load-balancer",
Aliases: []string{"lb"},
Short: "Provides functionality for Load Balancer",
Long: "Provides functionality for Load Balancer.",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, p)
return cmd
}
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(create.NewCmd(p))
cmd.AddCommand(delete.NewCmd(p))
cmd.AddCommand(describe.NewCmd(p))
cmd.AddCommand(generatepayload.NewCmd(p))
cmd.AddCommand(list.NewCmd(p))
cmd.AddCommand(quota.NewCmd(p))
cmd.AddCommand(observabilitycredentials.NewCmd(p))
cmd.AddCommand(targetpool.NewCmd(p))
cmd.AddCommand(update.NewCmd(p))
}
package add
import (
"context"
"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/stackitcloud/stackit-sdk-go/services/loadbalancer"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &loadbalancer.APIClient{}
var testProjectId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
displayNameFlag: "name",
usernameFlag: "username",
passwordFlag: "pwd",
}
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,
},
DisplayName: utils.Ptr("name"),
Username: utils.Ptr("username"),
Password: utils.Ptr("pwd"),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiCreateCredentialsRequest)) loadbalancer.ApiCreateCredentialsRequest {
request := testClient.CreateCredentials(testCtx, testProjectId)
request = request.CreateCredentialsPayload(loadbalancer.CreateCredentialsPayload{
DisplayName: utils.Ptr("name"),
Username: utils.Ptr("username"),
Password: utils.Ptr("pwd"),
})
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, 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,
},
{
description: "display name missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, displayNameFlag)
}),
isValid: false,
},
{
description: "username name missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, usernameFlag)
}),
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 loadbalancer.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),
cmpopts.IgnoreFields(loadbalancer.ApiCreateCredentialsRequest{}, "xRequestID"),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package add
import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
"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/load-balancer/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
)
const (
displayNameFlag = "display-name"
usernameFlag = "username"
passwordFlag = "password"
)
type inputModel struct {
*globalflags.GlobalFlagModel
DisplayName *string
Username *string
Password *string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "add",
Short: "Adds observability credentials to Load Balancer",
Long: "Adds existing observability credentials (username and password) to Load Balancer. The credentials can be for Argus or another monitoring tool.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Add observability credentials to a load balancer with username "xxx" and display name "yyy". The password is entered using the terminal`,
"$ stackit load-balancer observability-credentials add --username xxx --display-name yyy"),
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 load-balancer observability-credentials add --username xxx --password @./password.txt --display-name yyy"),
),
RunE: func(cmd *cobra.Command, args []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
}
// Prompt for password if not passed in as a flag
if model.Password == nil {
pwd, err := p.PromptForPassword("Enter user password: ")
if err != nil {
return fmt.Errorf("prompt for password: %w", err)
}
model.Password = utils.Ptr(pwd)
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to add observability credentials for Load Balancer on project %q?", projectLabel)
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 Load Balancer observability credentials: %w", err)
}
return outputResult(p, model, projectLabel, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().String(displayNameFlag, "", "Credentials display name")
cmd.Flags().String(usernameFlag, "", "Username")
cmd.Flags().Var(flags.ReadFromFileFlag(), passwordFlag, `Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).`)
err := flags.MarkFlagsRequired(cmd, displayNameFlag, usernameFlag)
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,
DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag),
Username: flags.FlagToStringPointer(p, cmd, usernameFlag),
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, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiCreateCredentialsRequest {
req := apiClient.CreateCredentials(ctx, model.ProjectId)
req = req.XRequestID(uuid.NewString())
req = req.CreateCredentialsPayload(loadbalancer.CreateCredentialsPayload{
DisplayName: model.DisplayName,
Username: model.Username,
Password: model.Password,
})
return req
}
func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *loadbalancer.CreateCredentialsResponse) error {
if resp.Credential == nil {
return fmt.Errorf("nil observability credentials response")
}
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal Load Balancer observability credentials: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Added Load Balancer observability credentials on project %q. Credentials reference: %q\n", projectLabel, *resp.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/loadbalancer"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &loadbalancer.APIClient{}
var testProjectId = uuid.NewString()
const testCredentialsRef = "credentials-xxx"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testCredentialsRef,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
}
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,
},
CredentialsRef: testCredentialsRef,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiDeleteCredentialsRequest)) loadbalancer.ApiDeleteCredentialsRequest {
request := testClient.DeleteCredentials(testCtx, testProjectId, testCredentialsRef)
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{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "credentials ref invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
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.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 loadbalancer.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/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/load-balancer/client"
loadbalancerUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
)
const (
credentialsRefArg = "CREDENTIALS_REF" //nolint:gosec // linter false positive
)
type inputModel struct {
*globalflags.GlobalFlagModel
CredentialsRef string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("delete %s", credentialsRefArg),
Short: "Deletes observability credentials for Load Balancer",
Long: "Deletes observability credentials for Load Balancer.",
Args: args.SingleArg(credentialsRefArg, nil),
Example: examples.Build(
examples.NewExample(
`Delete observability credentials with reference "credentials-xxx" for Load Balancer`,
"$ stackit load-balancer observability-credentials delete credentials-xxx"),
),
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
}
credentialsLabel, err := loadbalancerUtils.GetCredentialsDisplayName(ctx, apiClient, model.ProjectId, model.CredentialsRef)
if err != nil {
p.Debug(print.ErrorLevel, "get observability credentials display name: %v", err)
credentialsLabel = model.CredentialsRef
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete observability credentials %q on project %q?(This cannot be undone)", credentialsLabel, 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 Load Balancer observability credentials: %w", err)
}
p.Info("Deleted observability credentials %q on project %q\n", credentialsLabel, projectLabel)
return nil
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
credentialsRef := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
CredentialsRef: credentialsRef,
}
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 *loadbalancer.APIClient) loadbalancer.ApiDeleteCredentialsRequest {
req := apiClient.DeleteCredentials(ctx, model.ProjectId, model.CredentialsRef)
return req
}
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/loadbalancer"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &loadbalancer.APIClient{}
var testProjectId = uuid.NewString()
const testCredentialsRef = "credentials-test"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testCredentialsRef,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
}
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,
},
CredentialsRef: testCredentialsRef,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiGetCredentialsRequest)) loadbalancer.ApiGetCredentialsRequest {
request := testClient.GetCredentials(testCtx, testProjectId, testCredentialsRef)
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{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "credentials ref invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
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.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 loadbalancer.ApiGetCredentialsRequest
}{
{
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 describe
import (
"context"
"encoding/json"
"fmt"
"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/load-balancer/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
)
const (
credentialsRefArg = "CREDENTIALS_REF" //nolint:gosec // linter false positive
)
type inputModel struct {
*globalflags.GlobalFlagModel
CredentialsRef string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", credentialsRefArg),
Short: "Shows details of observability credentials for Load Balancer",
Long: "Shows details of observability credentials for Load Balancer.",
Args: args.SingleArg(credentialsRefArg, nil),
Example: examples.Build(
examples.NewExample(
`Get details of observability credentials with reference "credentials-xxx"`,
"$ stackit load-balancer observability-credentials describe credentials-xxx"),
),
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("describe Load Balancer observability credentials: %w", err)
}
return outputResult(p, model.OutputFormat, resp)
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
credentialsRef := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
CredentialsRef: credentialsRef,
}
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 *loadbalancer.APIClient) loadbalancer.ApiGetCredentialsRequest {
req := apiClient.GetCredentials(ctx, model.ProjectId, model.CredentialsRef)
return req
}
func outputResult(p *print.Printer, outputFormat string, credentials *loadbalancer.GetCredentialsResponse) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(credentials, "", " ")
if err != nil {
return fmt.Errorf("marshal Load Balancer observability credentials: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.AddRow("REFERENCE", *credentials.Credential.CredentialsRef)
table.AddSeparator()
table.AddRow("DISPLAY NAME", *credentials.Credential.DisplayName)
table.AddSeparator()
table.AddRow("USERNAME", *credentials.Credential.Username)
table.AddSeparator()
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package list
import (
"context"
"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/loadbalancer"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &loadbalancer.APIClient{}
var testProjectId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
limitFlag: "10",
}
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,
},
Limit: utils.Ptr(int64(10)),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiListCredentialsRequest)) loadbalancer.ApiListCredentialsRequest {
request := testClient.ListCredentials(testCtx, testProjectId)
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, 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,
},
{
description: "limit invalid",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "invalid"
}),
isValid: false,
},
{
description: "limit invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "0"
}),
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 loadbalancer.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("Data does not match: %s", diff)
}
})
}
}
package list
import (
"context"
"encoding/json"
"fmt"
"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/load-balancer/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
)
const (
instanceIdFlag = "instance-id"
limitFlag = "limit"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Limit *int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all observability credentials for Load Balancer",
Long: "Lists all observability credentials for Load Balancer.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all observability credentials for Load Balancer`,
"$ stackit load-balancer observability-credentials list"),
examples.NewExample(
`List all observability credentials for Load Balancer in JSON format`,
"$ stackit load-balancer observability-credentials list --output-format json"),
examples.NewExample(
`List up to 10 observability credentials for Load Balancer`,
"$ stackit load-balancer observability-credentials list --limit 10"),
),
RunE: func(cmd *cobra.Command, args []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
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("list Load Balancer observability credentials: %w", err)
}
credentialsPtr := resp.Credentials
if credentialsPtr == nil || (credentialsPtr != nil && len(*credentialsPtr) == 0) {
p.Info("No observability credentials found for Load Balancer on project %q\n", projectLabel)
return nil
}
credentials := *credentialsPtr
// Truncate output
if model.Limit != nil && len(credentials) > int(*model.Limit) {
credentials = credentials[:*model.Limit]
}
return outputResult(p, model.OutputFormat, credentials)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list")
}
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 *loadbalancer.APIClient) loadbalancer.ApiListCredentialsRequest {
req := apiClient.ListCredentials(ctx, model.ProjectId)
return req
}
func outputResult(p *print.Printer, outputFormat string, credentials []loadbalancer.CredentialsResponse) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(credentials, "", " ")
if err != nil {
return fmt.Errorf("marshal Load Balancer observability credentials list: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("REFERENCE", "DISPLAY NAME", "USERNAME")
for i := range credentials {
c := credentials[i]
table.AddRow(*c.CredentialsRef, *c.DisplayName, *c.Username)
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package credentials
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/add"
"github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/list"
"github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/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: "observability-credentials",
Short: "Provides functionality for Load Balancer observability credentials",
Long: `Provides functionality for Load Balancer observability credentials. These commands can be used to store and update existing credentials, which are valid to be used for Load Balancer observability. This means, e.g. when using Argus, first of all these credentials must be created for that Argus instance (by using "stackit argus credentials create") and then can be managed for a Load Balancer by using the commands in this group.`,
Args: args.NoArgs,
Aliases: []string{"credentials"},
Run: utils.CmdHelp,
}
addSubcommands(cmd, p)
return cmd
}
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(add.NewCmd(p))
cmd.AddCommand(describe.NewCmd(p))
cmd.AddCommand(delete.NewCmd(p))
cmd.AddCommand(update.NewCmd(p))
cmd.AddCommand(list.NewCmd(p))
}
package update
import (
"context"
"fmt"
"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/loadbalancer"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &loadbalancer.APIClient{}
type loadBalancerClientMocked struct {
getCredentialsError bool
getCredentialsResponse *loadbalancer.GetCredentialsResponse
}
func (c *loadBalancerClientMocked) UpdateCredentials(ctx context.Context, projectId, credentialsRef string) loadbalancer.ApiUpdateCredentialsRequest {
return testClient.UpdateCredentials(ctx, projectId, credentialsRef)
}
func (c *loadBalancerClientMocked) GetCredentialsExecute(_ context.Context, _, _ string) (*loadbalancer.GetCredentialsResponse, error) {
if c.getCredentialsError {
return nil, fmt.Errorf("get credentials failed")
}
return c.getCredentialsResponse, nil
}
var testProjectId = uuid.NewString()
const testCredentialsRef = "credentials-test"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testCredentialsRef,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
displayNameFlag: "name",
usernameFlag: "username",
passwordFlag: "pwd",
}
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,
},
DisplayName: utils.Ptr("name"),
Username: utils.Ptr("username"),
Password: utils.Ptr("pwd"),
CredentialsRef: testCredentialsRef,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiUpdateCredentialsRequest)) loadbalancer.ApiUpdateCredentialsRequest {
request := testClient.UpdateCredentials(testCtx, testProjectId, testCredentialsRef)
request = request.UpdateCredentialsPayload(loadbalancer.UpdateCredentialsPayload{
DisplayName: utils.Ptr("name"),
Username: utils.Ptr("username"),
Password: utils.Ptr("pwd"),
})
for _, mod := range mods {
mod(&request)
}
return request
}
func fixtureGetCredentialsResponse(mods ...func(response *loadbalancer.GetCredentialsResponse)) *loadbalancer.GetCredentialsResponse {
response := &loadbalancer.GetCredentialsResponse{
Credential: &loadbalancer.CredentialsResponse{
DisplayName: utils.Ptr("name"),
Username: utils.Ptr("username"),
},
}
for _, mod := range mods {
mod(response)
}
return response
}
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{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "credentials ref invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
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.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 loadbalancer.ApiUpdateCredentialsRequest
getCredentialsFails bool
getCredentialsResponse *loadbalancer.GetCredentialsResponse
isValid bool
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
getCredentialsResponse: fixtureGetCredentialsResponse(),
isValid: true,
},
{
description: "no display name",
model: fixtureInputModel(
func(model *inputModel) {
model.DisplayName = nil
},
),
expectedRequest: fixtureRequest(),
getCredentialsResponse: fixtureGetCredentialsResponse(),
isValid: true,
},
{
description: "no username name",
model: fixtureInputModel(
func(model *inputModel) {
model.Username = nil
},
),
expectedRequest: fixtureRequest(),
getCredentialsResponse: fixtureGetCredentialsResponse(),
isValid: true,
},
{
description: "get credentials fails",
model: fixtureInputModel(),
getCredentialsFails: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &loadBalancerClientMocked{
getCredentialsError: tt.getCredentialsFails,
getCredentialsResponse: tt.getCredentialsResponse,
}
request, err := buildRequest(testCtx, tt.model, client)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error building request: %v", err)
}
if !tt.isValid {
t.Fatal("expected error but none thrown")
}
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package update
import (
"context"
"fmt"
"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/load-balancer/client"
loadBalancerUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
)
const (
displayNameFlag = "display-name"
usernameFlag = "username"
passwordFlag = "password"
credentialsRefArg = "CREDENTIALS_REF" //nolint:gosec // linter false positive
)
type inputModel struct {
*globalflags.GlobalFlagModel
CredentialsRef string
DisplayName *string
Username *string
Password *string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "update",
Short: "Updates observability credentials for Load Balancer",
Long: "Updates existing observability credentials (username and password) for Load Balancer. The credentials can be for Argus or another monitoring tool.",
Args: args.SingleArg(credentialsRefArg, nil),
Example: examples.Build(
examples.NewExample(
`Update the password and username of observability credentials of Load Balancer with credentials reference "credentials-xxx". The password is entered using the terminal`,
"$ stackit load-balancer observability-credentials update credentials-xxx --username new-username"),
examples.NewExample(
`Update the password of observability credentials of Load Balancer with credentials reference "credentials-xxx", by providing the path to a file with the new password as flag`,
"$ stackit load-balancer observability-credentials update credentials-xxx --password @./new-password.txt"),
),
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
}
credentialsLabel, err := loadBalancerUtils.GetCredentialsDisplayName(ctx, apiClient, model.ProjectId, model.CredentialsRef)
if err != nil {
p.Debug(print.ErrorLevel, "get credentials display name: %v", err)
credentialsLabel = model.CredentialsRef
}
// Prompt for password if not passed in as a flag
if model.Password == nil {
pwd, err := p.PromptForPassword("Enter new password: ")
if err != nil {
return fmt.Errorf("prompt for password: %w", err)
}
model.Password = utils.Ptr(pwd)
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to update observability credentials %q for Load Balancer on project %q?", credentialsLabel, projectLabel)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req, err := buildRequest(ctx, model, apiClient)
if err != nil {
return err
}
_, err = req.Execute()
if err != nil {
return fmt.Errorf("update Load Balancer observability credentials: %w", err)
}
p.Info("Updated observability credentials %q for Load Balancer on project %q\n", credentialsLabel, projectLabel)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().String(displayNameFlag, "", "Credentials name")
cmd.Flags().String(usernameFlag, "", "Username")
cmd.Flags().Var(flags.ReadFromFileFlag(), passwordFlag, `Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).`)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
credentialsRef := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
displayName := flags.FlagToStringPointer(p, cmd, displayNameFlag)
username := flags.FlagToStringPointer(p, cmd, usernameFlag)
password := flags.FlagToStringPointer(p, cmd, passwordFlag)
return &inputModel{
GlobalFlagModel: globalFlags,
CredentialsRef: credentialsRef,
DisplayName: displayName,
Username: username,
Password: password,
}, nil
}
type loadBalancerClient interface {
UpdateCredentials(ctx context.Context, instanceId, projectId string) loadbalancer.ApiUpdateCredentialsRequest
GetCredentialsExecute(ctx context.Context, instanceId, projectId string) (*loadbalancer.GetCredentialsResponse, error)
}
func buildRequest(ctx context.Context, model *inputModel, apiClient loadBalancerClient) (loadbalancer.ApiUpdateCredentialsRequest, error) {
req := apiClient.UpdateCredentials(ctx, model.ProjectId, model.CredentialsRef)
currentCredentials, err := apiClient.GetCredentialsExecute(ctx, model.ProjectId, model.CredentialsRef)
if err != nil {
return req, fmt.Errorf("get Load Balancer observability credentials: %w", err)
}
payload := loadbalancer.UpdateCredentialsPayload{
DisplayName: currentCredentials.Credential.DisplayName,
Username: currentCredentials.Credential.Username,
Password: model.Password,
}
if model.DisplayName != nil {
payload.DisplayName = model.DisplayName
}
if model.Username != nil {
payload.Username = model.Username
}
req = req.UpdateCredentialsPayload(payload)
return req, nil
}
package quota
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/loadbalancer"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &loadbalancer.APIClient{}
var testProjectId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
}
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,
},
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiGetQuotaRequest)) loadbalancer.ApiGetQuotaRequest {
request := testClient.GetQuota(testCtx, testProjectId)
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: "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
isValid bool
expectedRequest loadbalancer.ApiGetQuotaRequest
}{
{
description: "base",
model: fixtureInputModel(),
isValid: true,
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 quota
import (
"context"
"encoding/json"
"fmt"
"strconv"
"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/load-balancer/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
)
type inputModel struct {
*globalflags.GlobalFlagModel
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "quota",
Short: "Shows the configured Load Balancer quota",
Long: "Shows the configured Load Balancer quota for the project. If you want to change the quota, please create a support ticket in the STACKIT Help Center (https://support.stackit.cloud)",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Get the configured load balancer quota for the project`,
"$ stackit load-balancer quota"),
),
RunE: func(cmd *cobra.Command, args []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("get load balancer quota: %w", err)
}
return outputResult(p, model.OutputFormat, resp)
},
}
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 *loadbalancer.APIClient) loadbalancer.ApiGetQuotaRequest {
req := apiClient.GetQuota(ctx, model.ProjectId)
return req
}
func outputResult(p *print.Printer, outputFormat string, quota *loadbalancer.GetQuotaResponse) error {
switch outputFormat {
case print.PrettyOutputFormat:
maxLoadBalancers := "Unlimited"
if quota.MaxLoadBalancers != nil && *quota.MaxLoadBalancers != -1 {
maxLoadBalancers = strconv.FormatInt(*quota.MaxLoadBalancers, 10)
}
p.Outputf("Maximum number of load balancers allowed: %s\n", maxLoadBalancers)
return nil
default:
details, err := json.MarshalIndent(quota, "", " ")
if err != nil {
return fmt.Errorf("marshal quota: %w", err)
}
p.Outputln(string(details))
return nil
}
}
package addtarget
import (
"context"
"fmt"
"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/stackitcloud/stackit-sdk-go/services/loadbalancer"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &loadbalancer.APIClient{}
testProjectId = uuid.NewString()
)
const (
testLBName = "my-load-balancer"
testTargetPoolName = "target-pool-1"
testTargetName = "my-target"
testIP = "1.1.1.1"
)
type loadBalancerClientMocked struct {
getCredentialsFails bool
getCredentialsResp *loadbalancer.GetCredentialsResponse
getLoadBalancerFails bool
getLoadBalancerResp *loadbalancer.LoadBalancer
}
func (m *loadBalancerClientMocked) GetCredentialsExecute(_ context.Context, _, _ string) (*loadbalancer.GetCredentialsResponse, error) {
if m.getCredentialsFails {
return nil, fmt.Errorf("could not get credentials")
}
return m.getCredentialsResp, nil
}
func (m *loadBalancerClientMocked) GetLoadBalancerExecute(_ context.Context, _, _ string) (*loadbalancer.LoadBalancer, error) {
if m.getLoadBalancerFails {
return nil, fmt.Errorf("could not get load balancer")
}
return m.getLoadBalancerResp, nil
}
func (m *loadBalancerClientMocked) UpdateTargetPool(ctx context.Context, projectId, loadBalancerName, targetPoolName string) loadbalancer.ApiUpdateTargetPoolRequest {
return testClient.UpdateTargetPool(ctx, projectId, loadBalancerName, targetPoolName)
}
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testIP,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
lbNameFlag: testLBName,
targetNameFlag: testTargetName,
targetPoolNameFlag: testTargetPoolName,
}
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,
},
TargetPoolName: testTargetPoolName,
LBName: testLBName,
TargetName: testTargetName,
IP: testIP,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureTargets() *[]loadbalancer.Target {
return &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("1.2.3.4"),
},
{
DisplayName: utils.Ptr("target-2"),
Ip: utils.Ptr("4.3.2.1"),
},
}
}
func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer.LoadBalancer {
lb := loadbalancer.LoadBalancer{
Name: utils.Ptr(testLBName),
TargetPools: &[]loadbalancer.TargetPool{
{
Name: utils.Ptr(testTargetPoolName),
Targets: fixtureTargets(),
ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{
UnhealthyThreshold: utils.Ptr(int64(3)),
},
SessionPersistence: &loadbalancer.SessionPersistence{
UseSourceIpAddress: utils.Ptr(true),
},
TargetPort: utils.Ptr(int64(80)),
},
{
Name: utils.Ptr("target-pool-2"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("6.7.8.9"),
},
{
DisplayName: utils.Ptr("target-2"),
Ip: utils.Ptr("9.8.7.6"),
},
},
},
},
}
for _, mod := range mods {
mod(&lb)
}
return &lb
}
func fixturePayload(mods ...func(payload *loadbalancer.UpdateTargetPoolPayload)) *loadbalancer.UpdateTargetPoolPayload {
payload := &loadbalancer.UpdateTargetPoolPayload{
Name: utils.Ptr("target-pool-1"),
ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{
UnhealthyThreshold: utils.Ptr(int64(3)),
},
SessionPersistence: &loadbalancer.SessionPersistence{
UseSourceIpAddress: utils.Ptr(true),
},
TargetPort: utils.Ptr(int64(80)),
Targets: fixtureTargets(),
}
for _, mod := range mods {
mod(payload)
}
return payload
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiUpdateTargetPoolRequest)) loadbalancer.ApiUpdateTargetPoolRequest {
request := testClient.UpdateTargetPool(testCtx, testProjectId, testLBName, testTargetPoolName)
request = request.UpdateTargetPoolPayload(*fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "ip missing",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "load balancer name missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, lbNameFlag)
}),
isValid: false,
},
{
description: "target name missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, targetNameFlag)
}),
isValid: false,
},
{
description: "target pool name missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, targetPoolNameFlag)
}),
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.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 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
isValid bool
getLoadBalancerFails bool
getLoadBalancerResp *loadbalancer.LoadBalancer
expectedRequest loadbalancer.ApiUpdateTargetPoolRequest
}{
{
description: "base",
model: fixtureInputModel(),
getLoadBalancerResp: fixtureLoadBalancer(),
isValid: true,
expectedRequest: fixtureRequest(func(request *loadbalancer.ApiUpdateTargetPoolRequest) {
payload := fixturePayload(func(payload *loadbalancer.UpdateTargetPoolPayload) {
payload.Targets = &[]loadbalancer.Target{
(*fixtureTargets())[0],
(*fixtureTargets())[1],
{
DisplayName: utils.Ptr(testTargetName),
Ip: utils.Ptr(testIP),
},
}
})
*request = request.UpdateTargetPoolPayload(*payload)
}),
},
{
description: "empty targets",
model: fixtureInputModel(),
getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) {
(*lb.TargetPools)[0].Targets = &[]loadbalancer.Target{}
}),
isValid: true,
expectedRequest: fixtureRequest(func(request *loadbalancer.ApiUpdateTargetPoolRequest) {
payload := fixturePayload(func(payload *loadbalancer.UpdateTargetPoolPayload) {
payload.Targets = &[]loadbalancer.Target{
{
DisplayName: utils.Ptr(testTargetName),
Ip: utils.Ptr(testIP),
},
}
})
*request = request.UpdateTargetPoolPayload(*payload)
}),
},
{
description: "nil targets",
model: fixtureInputModel(),
getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) {
(*lb.TargetPools)[0].Targets = nil
}),
isValid: true,
expectedRequest: fixtureRequest(func(request *loadbalancer.ApiUpdateTargetPoolRequest) {
payload := fixturePayload(func(payload *loadbalancer.UpdateTargetPoolPayload) {
payload.Targets = &[]loadbalancer.Target{
{
DisplayName: utils.Ptr(testTargetName),
Ip: utils.Ptr(testIP),
},
}
})
*request = request.UpdateTargetPoolPayload(*payload)
}),
},
{
description: "get load balancer fails",
model: fixtureInputModel(),
getLoadBalancerFails: true,
isValid: false,
},
{
description: "target pool not found",
model: fixtureInputModel(
func(model *inputModel) {
model.TargetPoolName = "not-existent"
}),
getLoadBalancerResp: fixtureLoadBalancer(),
isValid: false,
},
{
description: "nil target pool",
model: fixtureInputModel(),
getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) {
*lb.TargetPools = nil
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &loadBalancerClientMocked{
getLoadBalancerFails: tt.getLoadBalancerFails,
getLoadBalancerResp: tt.getLoadBalancerResp,
}
request, err := buildRequest(testCtx, tt.model, client)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error building 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)
}
})
}
}
package addtarget
import (
"context"
"fmt"
"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/load-balancer/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
"github.com/spf13/cobra"
)
const (
ipArg = "TARGET_IP"
lbNameFlag = "lb-name"
targetNameFlag = "target-name"
targetPoolNameFlag = "target-pool-name"
)
type inputModel struct {
*globalflags.GlobalFlagModel
TargetPoolName string
LBName string
TargetName string
IP string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("add-target %s", ipArg),
Short: "Adds a target to a target pool",
Long: fmt.Sprintf("%s\n%s",
"Adds a target to a target pool.",
"The target IP must by unique within a target pool and must be a valid IPv4 or IPv6."),
Args: args.SingleArg(ipArg, nil),
Example: examples.Build(
examples.NewExample(
`Add a target with IP 1.2.3.4 and name "my-new-target" to target pool "my-target-pool" of load balancer with name "my-load-balancer"`,
"$ stackit load-balancer target-pool add-target 1.2.3.4 --target-name my-new-target --target-pool-name my-target-pool --lb-name 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
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to add a target with IP %q to target pool %q of load balancer %q?", model.IP, model.TargetPoolName, model.LBName)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req, err := buildRequest(ctx, model, apiClient)
if err != nil {
return fmt.Errorf("build request: %w", err)
}
_, err = req.Execute()
if err != nil {
return fmt.Errorf("add target to target pool: %w", err)
}
p.Info("Added target to target pool of load balancer %q\n", model.LBName)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(targetNameFlag, "n", "", "Target name")
cmd.Flags().String(targetPoolNameFlag, "", "Target pool name")
cmd.Flags().String(lbNameFlag, "", "Load balancer name")
err := flags.MarkFlagsRequired(cmd, lbNameFlag, targetNameFlag, targetPoolNameFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
ip := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
TargetPoolName: cmd.Flag(targetPoolNameFlag).Value.String(),
LBName: cmd.Flag(lbNameFlag).Value.String(),
TargetName: cmd.Flag(targetNameFlag).Value.String(),
IP: ip,
}
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 utils.LoadBalancerClient) (loadbalancer.ApiUpdateTargetPoolRequest, error) {
req := apiClient.UpdateTargetPool(ctx, model.ProjectId, model.LBName, model.TargetPoolName)
targetPool, err := utils.GetLoadBalancerTargetPool(ctx, apiClient, model.ProjectId, model.LBName, model.TargetPoolName)
if err != nil {
return req, fmt.Errorf("get load balancer target pool: %w", err)
}
newTarget := &loadbalancer.Target{
DisplayName: &model.TargetName,
Ip: &model.IP,
}
err = utils.AddTargetToTargetPool(targetPool, newTarget)
if err != nil {
return req, fmt.Errorf("add target to target pool: %w", err)
}
payload := utils.ToPayloadTargetPool(targetPool)
if payload == nil {
return req, fmt.Errorf("nil payload")
}
return req.UpdateTargetPoolPayload(*payload), nil
}
package describe
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/loadbalancer"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &loadbalancer.APIClient{}
testProjectId = uuid.NewString()
)
const (
testLoadBalancerName = "my-load-balancer"
testTargetPoolName = "target-pool-1"
)
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testTargetPoolName,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
lbNameFlag: testLoadBalancerName,
}
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,
},
LBName: testLoadBalancerName,
TargetPoolName: testTargetPoolName,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiGetLoadBalancerRequest)) loadbalancer.ApiGetLoadBalancerRequest {
request := testClient.GetLoadBalancer(testCtx, testProjectId, testLoadBalancerName)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "target pool name empty",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "load balancer name missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, lbNameFlag)
}),
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.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 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
isValid bool
expectedRequest loadbalancer.ApiGetLoadBalancerRequest
}{
{
description: "base",
model: fixtureInputModel(),
isValid: true,
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 describe
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"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/load-balancer/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
)
const (
targetPoolNameArg = "TARGET_POOL_NAME"
lbNameFlag = "lb-name"
)
type inputModel struct {
*globalflags.GlobalFlagModel
TargetPoolName string
LBName string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", targetPoolNameArg),
Short: "Shows details of a target pool in a Load Balancer",
Long: "Shows details of a target pool in a Load Balancer.",
Args: args.SingleArg(targetPoolNameArg, nil),
Example: examples.Build(
examples.NewExample(
`Get details of a target pool with name "pool" in load balancer with name "my-load-balancer"`,
"$ stackit load-balancer target-pool describe pool --lb-name my-load-balancer"),
examples.NewExample(
`Get details of a target pool with name "pool" in load balancer with name "my-load-balancer in JSON output"`,
"$ stackit load-balancer target-pool describe pool --lb-name my-load-balancer --output-format json"),
),
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 load balancer: %w", err)
}
targetPool := utils.FindLoadBalancerTargetPoolByName(*resp.TargetPools, model.TargetPoolName)
if targetPool == nil {
return fmt.Errorf("target pool not found")
}
listener := utils.FindLoadBalancerListenerByTargetPool(*resp.Listeners, *targetPool.Name)
return outputResult(p, model.OutputFormat, *targetPool, listener)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().String(lbNameFlag, "", "Name of the load balancer")
err := flags.MarkFlagsRequired(cmd, lbNameFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
targetPoolName := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
TargetPoolName: targetPoolName,
LBName: cmd.Flag(lbNameFlag).Value.String(),
}
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 *loadbalancer.APIClient) loadbalancer.ApiGetLoadBalancerRequest {
req := apiClient.GetLoadBalancer(ctx, model.ProjectId, model.LBName)
return req
}
func outputResult(p *print.Printer, outputFormat string, targetPool loadbalancer.TargetPool, listener *loadbalancer.Listener) error {
switch outputFormat {
case print.JSONOutputFormat:
output := struct {
*loadbalancer.TargetPool
Listener *loadbalancer.Listener `json:"attached_listener"`
}{
&targetPool,
listener,
}
details, err := json.MarshalIndent(output, "", " ")
if err != nil {
return fmt.Errorf("marshal load balancer: %w", err)
}
p.Outputln(string(details))
return nil
default:
return outputResultAsTable(p, targetPool, listener)
}
}
func outputResultAsTable(p *print.Printer, targetPool loadbalancer.TargetPool, listener *loadbalancer.Listener) error {
sessionPersistence := "None"
if targetPool.SessionPersistence != nil && targetPool.SessionPersistence.UseSourceIpAddress != nil && *targetPool.SessionPersistence.UseSourceIpAddress {
sessionPersistence = "Use Source IP"
}
healthCheckInterval := "-"
healthCheckUnhealthyThreshold := "-"
healthCheckHealthyThreshold := "-"
if targetPool.ActiveHealthCheck != nil {
if targetPool.ActiveHealthCheck.Interval != nil {
healthCheckInterval = *targetPool.ActiveHealthCheck.Interval
}
if targetPool.ActiveHealthCheck.UnhealthyThreshold != nil {
healthCheckUnhealthyThreshold = strconv.FormatInt(*targetPool.ActiveHealthCheck.UnhealthyThreshold, 10)
}
if targetPool.ActiveHealthCheck.HealthyThreshold != nil {
healthCheckHealthyThreshold = strconv.FormatInt(*targetPool.ActiveHealthCheck.HealthyThreshold, 10)
}
}
targets := "-"
if targetPool.Targets != nil {
var targetsSlice []string
for _, target := range *targetPool.Targets {
targetStr := fmt.Sprintf("%s (%s)", *target.DisplayName, *target.Ip)
targetsSlice = append(targetsSlice, targetStr)
}
targets = strings.Join(targetsSlice, "\n")
}
listenerStr := "-"
if listener != nil {
listenerStr = fmt.Sprintf("%s (Port:%d, Protocol: %s)", *listener.Name, *listener.Port, *listener.Protocol)
}
table := tables.NewTable()
table.AddRow("NAME", *targetPool.Name)
table.AddSeparator()
table.AddRow("TARGET PORT", *targetPool.TargetPort)
table.AddSeparator()
table.AddRow("ATTACHED LISTENER", listenerStr)
table.AddSeparator()
table.AddRow("TARGETS", targets)
table.AddSeparator()
table.AddRow("SESSION PERSISTENCE", sessionPersistence)
table.AddSeparator()
table.AddRow("HEALTH CHECK INTERVAL", healthCheckInterval)
table.AddSeparator()
table.AddRow("HEALTH CHECK DOWN AFTER", healthCheckUnhealthyThreshold)
table.AddSeparator()
table.AddRow("HEALTH CHECK UP AFTER", healthCheckHealthyThreshold)
table.AddSeparator()
err := p.PagerDisplay(table.Render())
if err != nil {
return fmt.Errorf("display output: %w", err)
}
return nil
}
package removetarget
import (
"context"
"fmt"
"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/stackitcloud/stackit-sdk-go/services/loadbalancer"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &loadbalancer.APIClient{}
testProjectId = uuid.NewString()
)
const (
testLBName = "my-load-balancer"
testTargetPoolName = "target-pool-1"
testTargetName = "my-target"
testIP = "1.2.3.4"
)
type loadBalancerClientMocked struct {
getCredentialsFails bool
getCredentialsResp *loadbalancer.GetCredentialsResponse
getLoadBalancerFails bool
getLoadBalancerResp *loadbalancer.LoadBalancer
}
func (m *loadBalancerClientMocked) GetCredentialsExecute(_ context.Context, _, _ string) (*loadbalancer.GetCredentialsResponse, error) {
if m.getCredentialsFails {
return nil, fmt.Errorf("could not get credentials")
}
return m.getCredentialsResp, nil
}
func (m *loadBalancerClientMocked) GetLoadBalancerExecute(_ context.Context, _, _ string) (*loadbalancer.LoadBalancer, error) {
if m.getLoadBalancerFails {
return nil, fmt.Errorf("could not get load balancer")
}
return m.getLoadBalancerResp, nil
}
func (m *loadBalancerClientMocked) UpdateTargetPool(ctx context.Context, projectId, loadBalancerName, targetPoolName string) loadbalancer.ApiUpdateTargetPoolRequest {
return testClient.UpdateTargetPool(ctx, projectId, loadBalancerName, targetPoolName)
}
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testIP,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
lbNameFlag: testLBName,
targetPoolNameFlag: testTargetPoolName,
}
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,
},
LBName: testLBName,
TargetPoolName: testTargetPoolName,
IP: testIP,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureTargets() *[]loadbalancer.Target {
return &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("1.2.3.4"),
},
{
DisplayName: utils.Ptr("target-2"),
Ip: utils.Ptr("4.3.2.1"),
},
}
}
func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer.LoadBalancer {
lb := loadbalancer.LoadBalancer{
Name: utils.Ptr(testLBName),
TargetPools: &[]loadbalancer.TargetPool{
{
Name: utils.Ptr(testTargetPoolName),
Targets: fixtureTargets(),
ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{
UnhealthyThreshold: utils.Ptr(int64(3)),
},
SessionPersistence: &loadbalancer.SessionPersistence{
UseSourceIpAddress: utils.Ptr(true),
},
TargetPort: utils.Ptr(int64(80)),
},
{
Name: utils.Ptr("target-pool-2"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("6.7.8.9"),
},
{
DisplayName: utils.Ptr("target-2"),
Ip: utils.Ptr("9.8.7.6"),
},
},
},
},
}
for _, mod := range mods {
mod(&lb)
}
return &lb
}
func fixturePayload(mods ...func(payload *loadbalancer.UpdateTargetPoolPayload)) *loadbalancer.UpdateTargetPoolPayload {
payload := &loadbalancer.UpdateTargetPoolPayload{
Name: utils.Ptr("target-pool-1"),
ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{
UnhealthyThreshold: utils.Ptr(int64(3)),
},
SessionPersistence: &loadbalancer.SessionPersistence{
UseSourceIpAddress: utils.Ptr(true),
},
TargetPort: utils.Ptr(int64(80)),
Targets: fixtureTargets(),
}
for _, mod := range mods {
mod(payload)
}
return payload
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiUpdateTargetPoolRequest)) loadbalancer.ApiUpdateTargetPoolRequest {
request := testClient.UpdateTargetPool(testCtx, testProjectId, testLBName, testTargetPoolName)
request = request.UpdateTargetPoolPayload(*fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "ip missing",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "load balancer name missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, lbNameFlag)
}),
isValid: false,
},
{
description: "target pool name missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, targetPoolNameFlag)
}),
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.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 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
isValid bool
getLoadBalancerFails bool
getLoadBalancerResp *loadbalancer.LoadBalancer
expectedRequest loadbalancer.ApiUpdateTargetPoolRequest
}{
{
description: "base",
model: fixtureInputModel(),
getLoadBalancerResp: fixtureLoadBalancer(),
isValid: true,
expectedRequest: fixtureRequest(func(request *loadbalancer.ApiUpdateTargetPoolRequest) {
payload := fixturePayload(func(payload *loadbalancer.UpdateTargetPoolPayload) {
payload.Targets = utils.Ptr((*payload.Targets)[1:])
})
*request = request.UpdateTargetPoolPayload(*payload)
}),
},
{
description: "empty targets",
model: fixtureInputModel(),
getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) {
(*lb.TargetPools)[0].Targets = &[]loadbalancer.Target{}
}),
isValid: false,
},
{
description: "target not found",
model: fixtureInputModel(
func(model *inputModel) {
model.IP = "9.9.9.9"
}),
getLoadBalancerResp: fixtureLoadBalancer(),
isValid: false,
},
{
description: "nil targets",
model: fixtureInputModel(),
getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) {
(*lb.TargetPools)[0].Targets = nil
}),
isValid: false,
},
{
description: "get load balancer fails",
model: fixtureInputModel(),
getLoadBalancerFails: true,
isValid: false,
},
{
description: "target pool not found",
model: fixtureInputModel(
func(model *inputModel) {
model.TargetPoolName = "not-existent"
}),
getLoadBalancerResp: fixtureLoadBalancer(),
isValid: false,
},
{
description: "nil target pool",
model: fixtureInputModel(),
getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) {
*lb.TargetPools = nil
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &loadBalancerClientMocked{
getLoadBalancerFails: tt.getLoadBalancerFails,
getLoadBalancerResp: tt.getLoadBalancerResp,
}
request, err := buildRequest(testCtx, tt.model, client)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error building 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)
}
})
}
}
package removetarget
import (
"context"
"fmt"
"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/load-balancer/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
"github.com/spf13/cobra"
)
const (
ipArg = "TARGET_IP"
lbNameFlag = "lb-name"
targetPoolNameFlag = "target-pool-name"
)
type inputModel struct {
*globalflags.GlobalFlagModel
TargetPoolName string
LBName string
IP string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("remove-target %s", ipArg),
Short: "Removes a target from a target pool",
Long: "Removes a target from a target pool.",
Args: args.SingleArg(ipArg, nil),
Example: examples.Build(
examples.NewExample(
`Remove target with IP 1.2.3.4 from target pool "my-target-pool" of load balancer with name "my-load-balancer"`,
"$ stackit load-balancer target-pool remove-target 1.2.3.4 --target-pool-name my-target-pool --lb-name 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
}
targetLabel, err := utils.GetTargetName(ctx, apiClient, model.ProjectId, model.LBName, model.TargetPoolName, model.IP)
if err != nil {
p.Debug(print.ErrorLevel, "get target name: %v", err)
targetLabel = model.IP
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to remove target %q from target pool %q of load balancer %q?", targetLabel, model.TargetPoolName, model.LBName)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req, err := buildRequest(ctx, model, apiClient)
if err != nil {
return fmt.Errorf("build request: %w", err)
}
_, err = req.Execute()
if err != nil {
return fmt.Errorf("remove target from target pool: %w", err)
}
p.Info("Removed target from target pool of load balancer %q\n", model.LBName)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().String(lbNameFlag, "", "Load balancer name")
cmd.Flags().String(targetPoolNameFlag, "", "Target IP of the target to remove. Must be a valid IPv4 or IPv6")
err := flags.MarkFlagsRequired(cmd, lbNameFlag, targetPoolNameFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
ip := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
TargetPoolName: cmd.Flag(targetPoolNameFlag).Value.String(),
LBName: cmd.Flag(lbNameFlag).Value.String(),
IP: ip,
}
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 utils.LoadBalancerClient) (loadbalancer.ApiUpdateTargetPoolRequest, error) {
req := apiClient.UpdateTargetPool(ctx, model.ProjectId, model.LBName, model.TargetPoolName)
targetPool, err := utils.GetLoadBalancerTargetPool(ctx, apiClient, model.ProjectId, model.LBName, model.TargetPoolName)
if err != nil {
return req, fmt.Errorf("get load balancer target pool: %w", err)
}
err = utils.RemoveTargetFromTargetPool(targetPool, model.IP)
if err != nil {
return req, fmt.Errorf("remove target to target pool: %w", err)
}
payload := utils.ToPayloadTargetPool(targetPool)
if payload == nil {
return req, fmt.Errorf("nil payload")
}
return req.UpdateTargetPoolPayload(*payload), nil
}
package targetpool
import (
addtarget "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/target-pool/add-target"
"github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/target-pool/describe"
removetarget "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/target-pool/remove-target"
"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: "target-pool",
Short: "Provides functionality for target pools",
Long: "Provides functionality for target pools.",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, p)
return cmd
}
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(addtarget.NewCmd(p))
cmd.AddCommand(removetarget.NewCmd(p))
cmd.AddCommand(describe.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/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/loadbalancer"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &loadbalancer.APIClient{}
var testProjectId = uuid.NewString()
var testLoadBalancerName = "loadBalancer"
var testPayload = loadbalancer.UpdateLoadBalancerPayload{
ExternalAddress: utils.Ptr(""),
Listeners: &[]loadbalancer.Listener{
{
DisplayName: utils.Ptr(""),
Port: utils.Ptr(int64(0)),
Protocol: utils.Ptr(""),
ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{
{
Name: utils.Ptr(""),
},
},
TargetPool: utils.Ptr(""),
Tcp: &loadbalancer.OptionsTCP{
IdleTimeout: utils.Ptr(""),
},
Udp: &loadbalancer.OptionsUDP{
IdleTimeout: utils.Ptr(""),
},
},
},
Name: utils.Ptr(""),
Networks: &[]loadbalancer.Network{
{
NetworkId: utils.Ptr(""),
Role: utils.Ptr(""),
},
},
Options: &loadbalancer.LoadBalancerOptions{
AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{
AllowedSourceRanges: &[]string{
"",
},
},
EphemeralAddress: utils.Ptr(false),
Observability: &loadbalancer.LoadbalancerOptionObservability{
Logs: &loadbalancer.LoadbalancerOptionLogs{
CredentialsRef: utils.Ptr(""),
PushUrl: utils.Ptr(""),
},
Metrics: &loadbalancer.LoadbalancerOptionMetrics{
CredentialsRef: utils.Ptr(""),
PushUrl: utils.Ptr(""),
},
},
PrivateNetworkOnly: utils.Ptr(false),
},
TargetPools: &[]loadbalancer.TargetPool{
{
ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{
HealthyThreshold: utils.Ptr(int64(0)),
Interval: utils.Ptr(""),
IntervalJitter: utils.Ptr(""),
Timeout: utils.Ptr(""),
UnhealthyThreshold: utils.Ptr(int64(0)),
},
Name: utils.Ptr(""),
SessionPersistence: &loadbalancer.SessionPersistence{
UseSourceIpAddress: utils.Ptr(false),
},
TargetPort: utils.Ptr(int64(0)),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr(""),
Ip: utils.Ptr(""),
},
},
},
},
Version: utils.Ptr(""),
}
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testLoadBalancerName,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
payloadFlag: `
{
"externalAddress": "",
"listeners": [
{
"displayName": "",
"port": 0,
"protocol": "",
"serverNameIndicators": [
{
"name": ""
}
],
"targetPool": "",
"tcp": {
"idleTimeout": ""
},
"udp": {
"idleTimeout": ""
}
}
],
"name": "",
"networks": [
{
"networkId": "",
"role": ""
}
],
"options": {
"accessControl": {
"allowedSourceRanges": [
""
]
},
"ephemeralAddress": false,
"observability": {
"logs": {
"credentialsRef": "",
"pushUrl": ""
},
"metrics": {
"credentialsRef": "",
"pushUrl": ""
}
},
"privateNetworkOnly": false
},
"targetPools": [
{
"activeHealthCheck": {
"healthyThreshold": 0,
"interval": "",
"intervalJitter": "",
"timeout": "",
"unhealthyThreshold": 0
},
"name": "",
"sessionPersistence": {
"useSourceIpAddress": false
},
"targetPort": 0,
"targets": [
{
"displayName": "",
"ip": ""
}
]
}
],
"version": ""
}
`,
}
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,
},
LoadBalancerName: testLoadBalancerName,
Payload: testPayload,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *loadbalancer.ApiUpdateLoadBalancerRequest)) loadbalancer.ApiUpdateLoadBalancerRequest {
request := testClient.UpdateLoadBalancer(testCtx, testProjectId, testLoadBalancerName)
request = request.UpdateLoadBalancerPayload(testPayload)
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{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "invalid json",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[payloadFlag] = "not json"
}),
isValid: false,
},
{
description: "payload missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, payloadFlag)
}),
isValid: false,
},
{
description: "payload is empty",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[payloadFlag] = ""
}),
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.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 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 loadbalancer.ApiUpdateLoadBalancerRequest
isValid bool
}{
{
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 update
import (
"context"
"encoding/json"
"fmt"
"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/load-balancer/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
)
const (
loadBalancerNameArg = "LOAD_BALANCER_NAME"
payloadFlag = "payload"
)
type inputModel struct {
*globalflags.GlobalFlagModel
LoadBalancerName string
Payload loadbalancer.UpdateLoadBalancerPayload
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("update %s", loadBalancerNameArg),
Short: "Updates a Load Balancer",
Long: fmt.Sprintf("%s\n%s\n%s",
"Updates a load balancer.",
"The payload can be provided as a JSON string or a file path prefixed with \"@\".",
"See https://docs.api.stackit.cloud/documentation/load-balancer/version/v1#tag/Load-Balancer/operation/APIService_UpdateLoadBalancer for information regarding the payload structure.",
),
Args: args.SingleArg(loadBalancerNameArg, nil),
Example: examples.Build(
examples.NewExample(
`Update a load balancer with name "xxx", using an API payload sourced from the file "./payload.json"`,
"$ stackit load-balancer update xxx --payload @./payload.json"),
examples.NewExample(
`Update a load balancer with name "xxx", using an API payload provided as a JSON string`,
`$ stackit load-balancer update xxx --payload "{...}"`),
examples.NewExample(
`Generate a payload with the current values of an existing load balancer xxx, and adapt it with custom values for the different configuration options`,
`$ stackit load-balancer generate-payload --lb-name xxx > ./payload.json`,
`<Modify payload in file>`,
`$ stackit load-balancer update xxx --payload @./payload.json`),
),
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 update load balancer %q?", model.LoadBalancerName)
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("update load balancer: %w", err)
}
// The API has no status to wait on, so async mode is default
p.Info("Updated load balancer with name %q\n", model.LoadBalancerName)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.ReadFromFileFlag(), payloadFlag, `Request payload (JSON). Can be a string or a file path, if prefixed with "@". Example: @./payload.json`)
err := flags.MarkFlagsRequired(cmd, payloadFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
loadBalancerName := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
payloadString := flags.FlagToStringValue(p, cmd, payloadFlag)
var payload loadbalancer.UpdateLoadBalancerPayload
err := json.Unmarshal([]byte(payloadString), &payload)
if err != nil {
return nil, fmt.Errorf("encode payload: %w", err)
}
model := inputModel{
GlobalFlagModel: globalFlags,
LoadBalancerName: loadBalancerName,
Payload: payload,
}
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 *loadbalancer.APIClient) loadbalancer.ApiUpdateLoadBalancerRequest {
req := apiClient.UpdateLoadBalancer(ctx, model.ProjectId, model.LoadBalancerName)
req = req.UpdateLoadBalancerPayload(model.Payload)
return req
}
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/loadbalancer"
)
func ConfigureClient(p *print.Printer) (*loadbalancer.APIClient, error) {
var err error
var apiClient *loadbalancer.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.LoadBalancerCustomEndpointKey)
if customEndpoint != "" {
cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint))
} else {
cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion("eu01"))
}
if p.IsVerbosityDebug() {
cfgOptions = append(cfgOptions,
sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)),
)
}
apiClient, err = loadbalancer.NewAPIClient(cfgOptions...)
if err != nil {
p.Debug(print.ErrorLevel, "create new API client: %v", err)
return nil, &errors.AuthError{}
}
return apiClient, nil
}
package utils
import (
"context"
"fmt"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
)
var (
testProjectId = uuid.NewString()
)
const (
testCredentialsRef = "credentials-ref"
testCredentialsDisplayName = "credentials-name"
testLoadBalancerName = "my-load-balancer"
)
type loadBalancerClientMocked struct {
getCredentialsFails bool
getCredentialsResp *loadbalancer.GetCredentialsResponse
getLoadBalancerFails bool
getLoadBalancerResp *loadbalancer.LoadBalancer
}
func (m *loadBalancerClientMocked) GetCredentialsExecute(_ context.Context, _, _ string) (*loadbalancer.GetCredentialsResponse, error) {
if m.getCredentialsFails {
return nil, fmt.Errorf("could not get credentials")
}
return m.getCredentialsResp, nil
}
func (m *loadBalancerClientMocked) GetLoadBalancerExecute(_ context.Context, _, _ string) (*loadbalancer.LoadBalancer, error) {
if m.getLoadBalancerFails {
return nil, fmt.Errorf("could not get load balancer")
}
return m.getLoadBalancerResp, nil
}
func (m *loadBalancerClientMocked) UpdateTargetPool(_ context.Context, _, _, _ string) loadbalancer.ApiUpdateTargetPoolRequest {
return loadbalancer.ApiUpdateTargetPoolRequest{}
}
func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer.LoadBalancer {
lb := loadbalancer.LoadBalancer{
Name: utils.Ptr(testLoadBalancerName),
TargetPools: &[]loadbalancer.TargetPool{
{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("1.2.3.4"),
},
{
DisplayName: utils.Ptr("target-2"),
Ip: utils.Ptr("4.3.2.1"),
},
},
},
{
Name: utils.Ptr("target-pool-2"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("6.7.8.9"),
},
{
DisplayName: utils.Ptr("target-2"),
Ip: utils.Ptr("9.8.7.6"),
},
},
},
},
}
for _, mod := range mods {
mod(&lb)
}
return &lb
}
func fixtureTargets(mod ...func(*[]loadbalancer.Target)) *[]loadbalancer.Target {
targets := &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("1.2.3.4"),
},
{
DisplayName: utils.Ptr("target-2"),
Ip: utils.Ptr("2.2.2.2"),
},
{
DisplayName: utils.Ptr("target-3"),
Ip: utils.Ptr("6.6.6.6"),
},
}
for _, m := range mod {
m(targets)
}
return targets
}
func TestGetCredentialsDisplayName(t *testing.T) {
tests := []struct {
description string
getCredentialsFails bool
getCredentialsResp *loadbalancer.GetCredentialsResponse
isValid bool
expectedOutput string
}{
{
description: "base",
getCredentialsResp: &loadbalancer.GetCredentialsResponse{
Credential: &loadbalancer.CredentialsResponse{
DisplayName: utils.Ptr(testCredentialsDisplayName),
},
},
isValid: true,
expectedOutput: testCredentialsDisplayName,
},
{
description: "get credentials fails",
getCredentialsFails: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &loadBalancerClientMocked{
getCredentialsFails: tt.getCredentialsFails,
getCredentialsResp: tt.getCredentialsResp,
}
output, err := GetCredentialsDisplayName(context.Background(), client, testProjectId, testCredentialsRef)
if tt.isValid && err != nil {
t.Errorf("failed on valid input")
}
if !tt.isValid && err == nil {
t.Errorf("did not fail on invalid input")
}
if !tt.isValid {
return
}
if output != tt.expectedOutput {
t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output)
}
})
}
}
func TestGetLoadBalancerTargetPool(t *testing.T) {
tests := []struct {
description string
targetPoolName string
getLoadBalancerFails bool
getLoadBalancerResp *loadbalancer.LoadBalancer
isValid bool
expectedOutput *loadbalancer.TargetPool
}{
{
description: "base",
targetPoolName: "target-pool-1",
getLoadBalancerResp: fixtureLoadBalancer(),
isValid: true,
expectedOutput: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("1.2.3.4"),
},
{
DisplayName: utils.Ptr("target-2"),
Ip: utils.Ptr("4.3.2.1"),
},
},
},
},
{
description: "target pool not found",
targetPoolName: "target-pool-non-existent",
getLoadBalancerResp: fixtureLoadBalancer(),
isValid: false,
},
{
description: "no target pools",
getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) {
lb.TargetPools = &[]loadbalancer.TargetPool{}
}),
isValid: false,
},
{
description: "nil target pools",
getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) {
lb.TargetPools = nil
}),
isValid: false,
},
{
description: "get load balancer fails",
getLoadBalancerFails: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &loadBalancerClientMocked{
getLoadBalancerFails: tt.getLoadBalancerFails,
getLoadBalancerResp: tt.getLoadBalancerResp,
}
output, err := GetLoadBalancerTargetPool(context.Background(), client, testProjectId, testLoadBalancerName, tt.targetPoolName)
if tt.isValid && err != nil {
t.Errorf("failed on valid input")
}
if !tt.isValid && err == nil {
t.Errorf("did not fail on invalid input")
}
if !tt.isValid {
return
}
diff := cmp.Diff(output, tt.expectedOutput)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestFindLoadBalancerTargetPoolByName(t *testing.T) {
tests := []struct {
description string
targetPools []loadbalancer.TargetPool
targetPoolName string
expectedTargetPool *loadbalancer.TargetPool
}{
{
description: "base",
targetPools: []loadbalancer.TargetPool{
{
Name: utils.Ptr("target-pool-1"),
},
{
Name: utils.Ptr("target-pool-2"),
},
},
targetPoolName: "target-pool-1",
expectedTargetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
},
},
{
description: "target pool not found",
targetPools: []loadbalancer.TargetPool{
{
Name: utils.Ptr("target-pool-1"),
},
{
Name: utils.Ptr("target-pool-2"),
},
},
targetPoolName: "target-pool-3",
expectedTargetPool: nil,
},
{
description: "nil target pools",
targetPools: nil,
targetPoolName: "target-pool-1",
expectedTargetPool: nil,
},
{
description: "no target pools",
targetPools: []loadbalancer.TargetPool{},
targetPoolName: "target-pool-1",
expectedTargetPool: nil,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output := FindLoadBalancerTargetPoolByName(tt.targetPools, tt.targetPoolName)
diff := cmp.Diff(output, tt.expectedTargetPool)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestFindLoadBalancerListenerByTargetPool(t *testing.T) {
tests := []struct {
description string
listeners []loadbalancer.Listener
targetPoolName string
expected *loadbalancer.Listener
}{
{
description: "base",
listeners: []loadbalancer.Listener{
{
TargetPool: utils.Ptr("target-pool-1"),
},
{
TargetPool: utils.Ptr("target-pool-2"),
},
},
targetPoolName: "target-pool-1",
expected: &loadbalancer.Listener{
TargetPool: utils.Ptr("target-pool-1"),
},
},
{
description: "listener not found",
listeners: []loadbalancer.Listener{
{
TargetPool: utils.Ptr("target-pool-1"),
},
{
TargetPool: utils.Ptr("target-pool-2"),
},
},
targetPoolName: "target-pool-3",
expected: nil,
},
{
description: "nil listeners",
listeners: nil,
targetPoolName: "target-pool-1",
expected: nil,
},
{
description: "no listeners",
listeners: []loadbalancer.Listener{},
targetPoolName: "target-pool-1",
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output := FindLoadBalancerListenerByTargetPool(tt.listeners, tt.targetPoolName)
diff := cmp.Diff(output, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestAddTargetToTargetPool(t *testing.T) {
tests := []struct {
description string
targetPool *loadbalancer.TargetPool
target *loadbalancer.Target
isValid bool
expectedTargetPool *loadbalancer.TargetPool
}{
{
description: "base",
targetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("1.2.3.4"),
},
},
},
target: &loadbalancer.Target{
DisplayName: utils.Ptr("target-2"),
Ip: utils.Ptr("6.6.6.6"),
},
isValid: true,
expectedTargetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("1.2.3.4"),
},
{
DisplayName: utils.Ptr("target-2"),
Ip: utils.Ptr("6.6.6.6"),
},
},
},
},
{
description: "no target pool targets",
targetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{},
},
target: &loadbalancer.Target{
DisplayName: utils.Ptr("target-3"),
Ip: utils.Ptr("2.2.2.2"),
},
isValid: true,
expectedTargetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-3"),
Ip: utils.Ptr("2.2.2.2"),
},
},
},
},
{
description: "nil target pool targets",
targetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: nil,
},
target: &loadbalancer.Target{
DisplayName: utils.Ptr("target-3"),
Ip: utils.Ptr("2.2.2.2"),
},
isValid: true,
expectedTargetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-3"),
Ip: utils.Ptr("2.2.2.2"),
},
},
},
},
{
description: "nil target pool",
targetPool: nil,
target: &loadbalancer.Target{
DisplayName: utils.Ptr("target-3"),
Ip: utils.Ptr("2.2.2.2"),
},
expectedTargetPool: nil,
},
{
description: "nil new target",
targetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("1.2.3.4"),
},
},
},
target: nil,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := AddTargetToTargetPool(tt.targetPool, tt.target)
if tt.isValid && err != nil {
t.Errorf("failed on valid input")
}
if !tt.isValid && err == nil {
t.Errorf("did not fail on invalid input")
}
if !tt.isValid {
return
}
diff := cmp.Diff(tt.targetPool, tt.expectedTargetPool)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestRemoveTargetFromTargetPool(t *testing.T) {
tests := []struct {
description string
targetPool *loadbalancer.TargetPool
targetIp string
isValid bool
expectedTargetPool *loadbalancer.TargetPool
}{
{
description: "remove first target",
targetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: fixtureTargets(),
},
targetIp: "1.2.3.4",
isValid: true,
expectedTargetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-2"),
Ip: utils.Ptr("2.2.2.2"),
},
{
DisplayName: utils.Ptr("target-3"),
Ip: utils.Ptr("6.6.6.6"),
},
},
},
},
{
description: "remove last target",
targetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: fixtureTargets(),
},
targetIp: "6.6.6.6",
isValid: true,
expectedTargetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("1.2.3.4"),
},
{
DisplayName: utils.Ptr("target-2"),
Ip: utils.Ptr("2.2.2.2"),
},
},
},
},
{
description: "remove middle target",
targetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: fixtureTargets(),
},
targetIp: "2.2.2.2",
isValid: true,
expectedTargetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("1.2.3.4"),
},
{
DisplayName: utils.Ptr("target-3"),
Ip: utils.Ptr("6.6.6.6"),
},
},
},
},
{
description: "remove only target",
targetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("1.2.3.4"),
},
},
},
targetIp: "1.2.3.4",
isValid: true,
expectedTargetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{},
},
},
{
description: "no target pool targets",
targetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{},
},
targetIp: "2.2.2.2",
isValid: false,
},
{
description: "nil target pool targets",
targetPool: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
Targets: nil,
},
targetIp: "2.2.2.2",
isValid: false,
},
{
description: "nil target pool",
targetPool: nil,
targetIp: "2.2.2.2",
expectedTargetPool: nil,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := RemoveTargetFromTargetPool(tt.targetPool, tt.targetIp)
if tt.isValid && err != nil {
t.Errorf("failed on valid input")
}
if !tt.isValid && err == nil {
t.Errorf("did not fail on invalid input")
}
if !tt.isValid {
return
}
diff := cmp.Diff(tt.targetPool, tt.expectedTargetPool)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestToPayloadTargetPool(t *testing.T) {
tests := []struct {
description string
input *loadbalancer.TargetPool
expected *loadbalancer.UpdateTargetPoolPayload
}{
{
description: "base",
input: &loadbalancer.TargetPool{
Name: utils.Ptr("target-pool-1"),
ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{
UnhealthyThreshold: utils.Ptr(int64(3)),
},
SessionPersistence: &loadbalancer.SessionPersistence{
UseSourceIpAddress: utils.Ptr(true),
},
TargetPort: utils.Ptr(int64(80)),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("1.2.3.4"),
},
},
},
expected: &loadbalancer.UpdateTargetPoolPayload{
Name: utils.Ptr("target-pool-1"),
ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{
UnhealthyThreshold: utils.Ptr(int64(3)),
},
SessionPersistence: &loadbalancer.SessionPersistence{
UseSourceIpAddress: utils.Ptr(true),
},
TargetPort: utils.Ptr(int64(80)),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("target-1"),
Ip: utils.Ptr("1.2.3.4"),
},
},
},
},
{
description: "nil target pool",
input: nil,
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output := ToPayloadTargetPool(tt.input)
diff := cmp.Diff(output, tt.expected)
if diff != "" {
t.Errorf("expected output to be %+v, got %+v", tt.expected, output)
}
})
}
}
func TestGetTargetName(t *testing.T) {
tests := []struct {
description string
targetPoolName string
targetIp string
getLoadBalancerFails bool
getLoadBalancerResp *loadbalancer.LoadBalancer
isValid bool
expectedOutput string
}{
{
description: "base",
targetPoolName: "target-pool-1",
targetIp: "1.2.3.4",
getLoadBalancerResp: fixtureLoadBalancer(),
isValid: true,
expectedOutput: "target-1",
},
{
description: "target not found",
targetPoolName: "target-pool-1",
targetIp: "9.9.9.9",
getLoadBalancerResp: fixtureLoadBalancer(),
isValid: false,
},
{
description: "no targets",
targetPoolName: "target-pool-1",
targetIp: "1.2.3.4",
getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) {
lb.TargetPools = &[]loadbalancer.TargetPool{
{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{},
},
}
}),
isValid: false,
},
{
description: "nil targets",
targetPoolName: "target-pool-1",
targetIp: "1.2.3.4",
getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) {
lb.TargetPools = &[]loadbalancer.TargetPool{
{
Name: utils.Ptr("target-pool-1"),
Targets: nil,
},
}
}),
isValid: false,
},
{
description: "nil target name",
targetPoolName: "target-pool-1",
targetIp: "1.2.3.4",
getLoadBalancerResp: fixtureLoadBalancer(
func(lb *loadbalancer.LoadBalancer) {
lb.TargetPools = &[]loadbalancer.TargetPool{
{
Name: utils.Ptr("target-pool-1"),
Targets: &[]loadbalancer.Target{
{
DisplayName: nil,
Ip: utils.Ptr("1.2.3.4"),
},
},
},
}
}),
isValid: false,
},
{
description: "get target pool fails",
targetPoolName: "target-pool-1",
targetIp: "1.2.3.4",
getLoadBalancerFails: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &loadBalancerClientMocked{
getLoadBalancerResp: tt.getLoadBalancerResp,
}
output, err := GetTargetName(context.Background(), client, testProjectId, testLoadBalancerName, tt.targetPoolName, tt.targetIp)
if tt.isValid && err != nil {
t.Errorf("failed on valid input")
}
if !tt.isValid && err == nil {
t.Errorf("did not fail on invalid input")
}
if !tt.isValid {
return
}
if output != tt.expectedOutput {
t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output)
}
})
}
}
package utils
import (
"context"
"fmt"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
)
type LoadBalancerClient interface {
GetCredentialsExecute(ctx context.Context, projectId, credentialsRef string) (*loadbalancer.GetCredentialsResponse, error)
GetLoadBalancerExecute(ctx context.Context, projectId, name string) (*loadbalancer.LoadBalancer, error)
UpdateTargetPool(ctx context.Context, projectId, loadBalancerName, targetPoolName string) loadbalancer.ApiUpdateTargetPoolRequest
}
func GetCredentialsDisplayName(ctx context.Context, apiClient LoadBalancerClient, projectId, credentialsRef string) (string, error) {
resp, err := apiClient.GetCredentialsExecute(ctx, projectId, credentialsRef)
if err != nil {
return "", fmt.Errorf("get Load Balancer credentials: %w", err)
}
return *resp.Credential.DisplayName, nil
}
func GetLoadBalancerTargetPool(ctx context.Context, apiClient LoadBalancerClient, projectId, loadBalancerName, targetPoolName string) (*loadbalancer.TargetPool, error) {
resp, err := apiClient.GetLoadBalancerExecute(ctx, projectId, loadBalancerName)
if err != nil {
return nil, fmt.Errorf("get load balancer: %w", err)
}
if resp == nil {
return nil, fmt.Errorf("no load balancer found")
}
if resp.TargetPools == nil {
return nil, fmt.Errorf("no target pools found")
}
targetPool := FindLoadBalancerTargetPoolByName(*resp.TargetPools, targetPoolName)
if targetPool == nil {
return nil, fmt.Errorf("target pool not found")
}
return targetPool, nil
}
func FindLoadBalancerTargetPoolByName(targetPools []loadbalancer.TargetPool, targetPoolName string) *loadbalancer.TargetPool {
if targetPools == nil {
return nil
}
for _, targetPool := range targetPools {
if targetPool.Name != nil && *targetPool.Name == targetPoolName {
return &targetPool
}
}
return nil
}
func FindLoadBalancerListenerByTargetPool(listeners []loadbalancer.Listener, targetPoolName string) *loadbalancer.Listener {
if listeners == nil {
return nil
}
for _, listener := range listeners {
if listener.TargetPool != nil && *listener.TargetPool == targetPoolName {
return &listener
}
}
return nil
}
func AddTargetToTargetPool(targetPool *loadbalancer.TargetPool, target *loadbalancer.Target) error {
if targetPool == nil {
return fmt.Errorf("target pool is nil")
}
if target == nil {
return fmt.Errorf("target is nil")
}
if targetPool.Targets == nil {
targetPool.Targets = &[]loadbalancer.Target{*target}
return nil
}
*targetPool.Targets = append(*targetPool.Targets, *target)
return nil
}
func RemoveTargetFromTargetPool(targetPool *loadbalancer.TargetPool, ip string) error {
if targetPool == nil {
return fmt.Errorf("target pool is nil")
}
if targetPool.Targets == nil {
return fmt.Errorf("no targets found")
}
targets := *targetPool.Targets
for i, target := range targets {
if target.Ip != nil && *target.Ip == ip {
newTargets := targets[:i]
newTargets = append(newTargets, targets[i+1:]...)
*targetPool.Targets = newTargets
return nil
}
}
return fmt.Errorf("target not found")
}
func ToPayloadTargetPool(targetPool *loadbalancer.TargetPool) *loadbalancer.UpdateTargetPoolPayload {
if targetPool == nil {
return nil
}
return &loadbalancer.UpdateTargetPoolPayload{
Name: targetPool.Name,
ActiveHealthCheck: targetPool.ActiveHealthCheck,
SessionPersistence: targetPool.SessionPersistence,
TargetPort: targetPool.TargetPort,
Targets: targetPool.Targets,
}
}
func GetTargetName(ctx context.Context, apiClient LoadBalancerClient, projectId, loadBalancerName, targetPoolName, targetIp string) (string, error) {
targetPool, err := GetLoadBalancerTargetPool(ctx, apiClient, projectId, loadBalancerName, targetPoolName)
if err != nil {
return "", fmt.Errorf("get target pool: %w", err)
}
if targetPool.Targets == nil {
return "", fmt.Errorf("no targets found")
}
for _, target := range *targetPool.Targets {
if target.Ip != nil && *target.Ip == targetIp {
if target.DisplayName == nil {
return "", fmt.Errorf("nil target display name")
}
return *target.DisplayName, nil
}
}
return "", fmt.Errorf("target not found")
}
+5
-5

@@ -8,3 +8,3 @@ # Authentication Guide

You can use a [service account](https://docs.stackit.cloud/stackit/en/service-accounts-134415819.html) to authenticate to the STACKIT CLI.
The CLI will search for service account credentials similarly to the [STACKIT SDK](https://github.com/stackitcloud/stackit-sdk-go) and [Terraform Provider](https://github.com/stackitcloud/terraform-provider-stackit), so if you have setup you environment previously for those tools, you can just run:
The CLI will search for service account credentials similarly to the [STACKIT SDK](https://github.com/stackitcloud/stackit-sdk-go) and [STACKIT Terraform Provider](https://github.com/stackitcloud/terraform-provider-stackit), so if you have already set up your environment for those tools, you can just run:

@@ -19,3 +19,3 @@ ```bash

If you dont have a service account, create one in the STACKIT Portal an assign it the necessary permissions, e.g. `owner`. There are two ways to authenticate:
If you don't have a service account, create one in the [STACKIT Portal](https://portal.stackit.cloud/) and assign the necessary permissions to it, e.g. `owner`. There are two ways to authenticate:

@@ -44,3 +44,3 @@ - Key flow (recommended)

The following instructions assume that you have created a service account and assigned it the necessary permissions, e.g. `owner`.
The following instructions assume that you have created a service account and assigned the necessary permissions to it, e.g. `owner`.

@@ -51,3 +51,3 @@ To use the key flow, you need to have a service account key, which must have an RSA key-pair attached to it.

**Optionally**, you can provide your own private key when creating the service account key, which will then require you to also provide it explicitly to the CLI, additionaly to the service account key. Check the STACKIT Knowledge Base for an [example of how to create your own key-pair](https://docs.stackit.cloud/stackit/en/usage-of-the-service-account-keys-in-stackit-175112464.html#UsageoftheserviceaccountkeysinSTACKIT-CreatinganRSAkey-pair).
**Optionally**, you can provide your own private key when creating the service account key, which will then require you to also provide it explicitly to the CLI, additionally to the service account key. Check the STACKIT Knowledge Base for an [example of how to create your own key-pair](https://docs.stackit.cloud/stackit/en/usage-of-the-service-account-keys-in-stackit-175112464.html#UsageoftheserviceaccountkeysinSTACKIT-CreatinganRSAkey-pair).

@@ -59,3 +59,3 @@ To configure the key flow, follow this steps:

- In the CLI, run `stackit service-account key create --email <SERVICE_ACCOUNT_EMAIL>`
- As an alternative, use the STACKIT Portal: go to the `Service Accounts` tab, choose a `Service Account` and go to `Service Account Keys` to create a key. For more details, see [Create a service account key](https://docs.stackit.cloud/stackit/en/create-a-service-account-key-175112456.html)
- As an alternative, use the [STACKIT Portal](https://portal.stackit.cloud/): go to the `Service Accounts` tab, choose a `Service Account` and go to `Service Account Keys` to create a key. For more details, see [Create a service account key](https://docs.stackit.cloud/stackit/en/create-a-service-account-key-175112456.html)

@@ -62,0 +62,0 @@ 2. Save the content of the service account key by copying it and saving it in a JSON file.

@@ -45,3 +45,3 @@ # Autocompletion

Additionaly, you might also need to run:
Additionally, you might also need to run:

@@ -48,0 +48,0 @@ ```shell

@@ -41,3 +41,3 @@ # Contribute to the STACKIT CLI

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

@@ -47,7 +47,7 @@ 1. You would start by creating a new folder `bar/` inside `internal/cmd/foo/`

1. The Go package should be similar to the command usage, in this case `package bar` would be an adequate name
2. Please refer to the [Command file structure](./CONTRIBUTION.md/#command-file-structure) section for details on the strcutre of the file itself
2. Please refer to the [Command file structure](./CONTRIBUTION.md/#command-file-structure) section for details on the structure of the file itself
3. To register the command `bar` as a child of the existing command `foo`, add `cmd.AddCommand(bar.NewCmd(p))` to the `addSubcommands` method of the constructor of the `foo` command
1. In this case, `p` is the `printer` that is passed from the root command to all subcommands of the tree (refer to the [Outputs, prints and debug logs](./CONTRIBUTION.md/#outputs-prints-and-debug-logs) section for more details regarding the `printer`)
Please remeber to run `make generate-docs` after your changes to keep the commands' documentation updated.
Please remember to run `make generate-docs` after your changes to keep the commands' documentation updated.

@@ -217,3 +217,3 @@ #### Command file structure

2. Update the `stackit config unset` and `stackit config unset` commands by adding flags to set and unset a custom endpoint for the `foo` service API, respectively, and update their unit tests
3. Setup the SDK client configuration, using the authentication method configured in the CLI
3. Set up the SDK client configuration, using the authentication method configured in the CLI

@@ -297,4 +297,4 @@ 1. This is done in `internal/pkg/services/foo/client/client.go`

1. Go through the existing issues to check if your issue has already been reported.
2. Make sure you are using the latest version of the provider, we will not provide bug fixes for older versions. Also, latest versions may have the fix for your bug.
3. Please provide as much information as you can about your environment, e.g. your version of Go, your version of the provider, which operating system you are using and the corresponding version.
2. Make sure you are using the latest version of the STACKIT CLI, we will not provide bug fixes for older versions. Also, latest versions may have the fix for your bug.
3. Please provide as much information as you can about your environment, e.g. your version of Go, your version of the CLI, which operating system you are using and the corresponding version.
4. Include in your issue the steps to reproduce it, along with code snippets and/or information about your specific use case. This will make the support process much easier and efficient.

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

-h, --help Help for "stackit config set"
--load-balancer-custom-endpoint string Load Balancer API base URL, used in calls to this API
--logme-custom-endpoint string LogMe API base URL, used in calls to this API

@@ -38,0 +39,0 @@ --mariadb-custom-endpoint string MariaDB API base URL, used in calls to this API

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

-h, --help Help for "stackit config unset"
--load-balancer-custom-endpoint Load Balancer API base URL. If unset, uses the default base URL
--logme-custom-endpoint LogMe API base URL. If unset, uses the default base URL

@@ -36,0 +37,0 @@ --mariadb-custom-endpoint MariaDB API base URL. If unset, uses the default base URL

@@ -34,2 +34,3 @@ ## stackit

* [stackit dns](./stackit_dns.md) - Provides functionality for DNS
* [stackit load-balancer](./stackit_load-balancer.md) - Provides functionality for Load Balancer
* [stackit logme](./stackit_logme.md) - Provides functionality for LogMe

@@ -36,0 +37,0 @@ * [stackit mariadb](./stackit_mariadb.md) - Provides functionality for MariaDB

+3
-1

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

golang.org/x/oauth2 v0.20.0
golang.org/x/term v0.20.0
golang.org/x/text v0.15.0

@@ -57,2 +58,3 @@ )

github.com/stackitcloud/stackit-sdk-go/services/argus v0.10.0
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.12.0
github.com/stackitcloud/stackit-sdk-go/services/logme v0.13.0

@@ -66,3 +68,3 @@ github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.13.0

golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/sys v0.20.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect

@@ -69,0 +71,0 @@ gopkg.in/ini.v1 v1.67.0 // indirect

+6
-2

@@ -88,2 +88,4 @@ github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=

github.com/stackitcloud/stackit-sdk-go/services/dns v0.9.1/go.mod h1:MdZcRbs19s2NLeJmSLSoqTzm9IPIQhE1ZEMpo9gePq0=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.12.0 h1:LAteZO46XmqTsmPw0QV8n8WiGM205pxrcqHqWznNmyY=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.12.0/go.mod h1:wsO3+vXe1XiKLeCIctWAptaHQZ07Un7kmLTQ+drbj7w=
github.com/stackitcloud/stackit-sdk-go/services/logme v0.13.0 h1:/wqs+pfHSjFWTakJVQGD/KwArxmFN8qdYrJDUgA1Gxw=

@@ -134,4 +136,6 @@ github.com/stackitcloud/stackit-sdk-go/services/logme v0.13.0/go.mod h1:bj9cn1treNSxKTRCEmESwqfENN8vCYn60HUnEA0P83c=

golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=

@@ -138,0 +142,0 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=

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

dnsCustomEndpointFlag = "dns-custom-endpoint"
loadBalancerCustomEndpointFlag = "load-balancer-custom-endpoint"
logMeCustomEndpointFlag = "logme-custom-endpoint"

@@ -129,2 +130,3 @@ mariaDBCustomEndpointFlag = "mariadb-custom-endpoint"

cmd.Flags().String(dnsCustomEndpointFlag, "", "DNS API base URL, used in calls to this API")
cmd.Flags().String(loadBalancerCustomEndpointFlag, "", "Load Balancer API base URL, used in calls to this API")
cmd.Flags().String(logMeCustomEndpointFlag, "", "LogMe API base URL, used in calls to this API")

@@ -149,2 +151,4 @@ cmd.Flags().String(mariaDBCustomEndpointFlag, "", "MariaDB API base URL, used in calls to this API")

cobra.CheckErr(err)
err = viper.BindPFlag(config.LoadBalancerCustomEndpointKey, cmd.Flags().Lookup(loadBalancerCustomEndpointFlag))
cobra.CheckErr(err)
err = viper.BindPFlag(config.LogMeCustomEndpointKey, cmd.Flags().Lookup(logMeCustomEndpointFlag))

@@ -151,0 +155,0 @@ cobra.CheckErr(err)

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

dnsCustomEndpointFlag: true,
loadBalancerCustomEndpointFlag: true,
logMeCustomEndpointFlag: true,

@@ -52,2 +53,3 @@ mariaDBCustomEndpointFlag: true,

DNSCustomEndpoint: true,
LoadBalancerCustomEndpoint: true,
LogMeCustomEndpoint: true,

@@ -97,2 +99,3 @@ MariaDBCustomEndpoint: true,

model.DNSCustomEndpoint = false
model.LoadBalancerCustomEndpoint = false
model.LogMeCustomEndpoint = false

@@ -99,0 +102,0 @@ model.MariaDBCustomEndpoint = false

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

dnsCustomEndpointFlag = "dns-custom-endpoint"
loadBalancerCustomEndpointFlag = "load-balancer-custom-endpoint"
logMeCustomEndpointFlag = "logme-custom-endpoint"

@@ -53,2 +54,3 @@ mariaDBCustomEndpointFlag = "mariadb-custom-endpoint"

DNSCustomEndpoint bool
LoadBalancerCustomEndpoint bool
LogMeCustomEndpoint bool

@@ -113,2 +115,5 @@ MariaDBCustomEndpoint bool

}
if model.LoadBalancerCustomEndpoint {
viper.Set(config.LoadBalancerCustomEndpointKey, "")
}
if model.LogMeCustomEndpoint {

@@ -172,2 +177,3 @@ viper.Set(config.LogMeCustomEndpointKey, "")

cmd.Flags().Bool(dnsCustomEndpointFlag, false, "DNS API base URL. If unset, uses the default base URL")
cmd.Flags().Bool(loadBalancerCustomEndpointFlag, false, "Load Balancer API base URL. If unset, uses the default base URL")
cmd.Flags().Bool(logMeCustomEndpointFlag, false, "LogMe API base URL. If unset, uses the default base URL")

@@ -198,2 +204,3 @@ cmd.Flags().Bool(mariaDBCustomEndpointFlag, false, "MariaDB API base URL. If unset, uses the default base URL")

DNSCustomEndpoint: flags.FlagToBoolValue(p, cmd, dnsCustomEndpointFlag),
LoadBalancerCustomEndpoint: flags.FlagToBoolValue(p, cmd, loadBalancerCustomEndpointFlag),
LogMeCustomEndpoint: flags.FlagToBoolValue(p, cmd, logMeCustomEndpointFlag),

@@ -200,0 +207,0 @@ MariaDBCustomEndpoint: flags.FlagToBoolValue(p, cmd, mariaDBCustomEndpointFlag),

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

"github.com/stackitcloud/stackit-cli/internal/cmd/dns"
loadbalancer "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer"
"github.com/stackitcloud/stackit-cli/internal/cmd/logme"

@@ -110,2 +111,3 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/mariadb"

cmd.AddCommand(dns.NewCmd(p))
cmd.AddCommand(loadbalancer.NewCmd(p))
cmd.AddCommand(logme.NewCmd(p))

@@ -112,0 +114,0 @@ cmd.AddCommand(mariadb.NewCmd(p))

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

DNSCustomEndpointKey = "dns_custom_endpoint"
LoadBalancerCustomEndpointKey = "load_balancer_custom_endpoint"
LogMeCustomEndpointKey = "logme_custom_endpoint"

@@ -57,2 +58,3 @@ MariaDBCustomEndpointKey = "mariadb_custom_endpoint"

DNSCustomEndpointKey,
LoadBalancerCustomEndpointKey,
LogMeCustomEndpointKey,

@@ -59,0 +61,0 @@ MariaDBCustomEndpointKey,

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

"fmt"
"syscall"

@@ -19,2 +20,3 @@ "log/slog"

"github.com/stackitcloud/stackit-cli/internal/pkg/config"
"golang.org/x/term"
)

@@ -143,19 +145,24 @@

//
// Returns nil only if the user (explicitly) press directly enter.
// Returns ErrAborted if the user press anything else before pressing enter.
// Returns nil if the user presses Enter.
func (p *Printer) PromptForEnter(prompt string) error {
reader := bufio.NewReaderSize(p.Cmd.InOrStdin(), 1)
reader := bufio.NewReader(p.Cmd.InOrStdin())
p.Cmd.PrintErr(prompt)
answer, err := reader.ReadByte()
_, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("read user response: %w", err)
}
return nil
}
// ASCII code for Enter (newline) is 10.
if answer == 10 {
return nil
// Prompts the user for a password.
//
// Returns the password that was given, otherwise returns error
func (p *Printer) PromptForPassword(prompt string) (string, error) {
p.Cmd.PrintErr(prompt)
defer p.Outputln("")
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", fmt.Errorf("read password: %w", err)
}
p.Debug(ErrorLevel, "got byte %d", answer)
return errAborted
return string(bytePassword), nil
}

@@ -162,0 +169,0 @@

+38
-27
[![Go Report Card](https://goreportcard.com/badge/github.com/stackitcloud/stackit-cli)](https://goreportcard.com/report/github.com/stackitcloud/stackit-cli) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/stackitcloud/stackit-cli) [![GitHub License](https://img.shields.io/github/license/stackitcloud/stackit-cli)](https://www.apache.org/licenses/LICENSE-2.0)
# STACKIT CLI (BETA)

@@ -36,3 +35,3 @@

Some commands are implemented at the root, group or sub-group level:
Some commands are implemented at the root, group or subgroup level:

@@ -54,26 +53,28 @@ - `stackit config` to define variables to be used in future commands.

| Service | CLI Commands | Status |
| ----------------------- | ------------------------- |-------------------------|
| Argus | | Will be integrated soon |
| Authorization | `project`, `organization` | :white_check_mark: |
| DNS | `dns` | :white_check_mark: |
| Kubernetes Engine (SKE) | `ske` | :white_check_mark: |
| Load Balancer | | Will be integrated soon |
| LogMe | `logme` | :white_check_mark: |
| MariaDB | `mariadb` | :white_check_mark: |
| MongoDB Flex | `mongodbflex` | :white_check_mark: |
| Object Storage | `object-storage` | :white_check_mark: |
| OpenSearch | `opensearch` | :white_check_mark: |
| PostgreSQL Flex | `postgresflex` | :white_check_mark: |
| RabbitMQ | `rabbitmq` | :white_check_mark: |
| Redis | `redis` | :white_check_mark: |
| Resource Manager | `project` | :white_check_mark: |
| Secrets Manager | `secrets-manager` | :white_check_mark: |
| Service Account | `service-account` | :white_check_mark: |
| Service | CLI Commands | Status |
| ---------------------------------- | ------------------------- | ----------------------- |
| Argus | `argus` | :white_check_mark: |
| Infrastructure as a Service (IaaS) | | Will be integrated soon |
| Authorization | `project`, `organization` | :white_check_mark: |
| DNS | `dns` | :white_check_mark: |
| Kubernetes Engine (SKE) | `ske` | :white_check_mark: |
| Load Balancer | `load-balancer` | :white_check_mark: |
| LogMe | `logme` | :white_check_mark: |
| MariaDB | `mariadb` | :white_check_mark: |
| MongoDB Flex | `mongodbflex` | :white_check_mark: |
| Object Storage | `object-storage` | :white_check_mark: |
| OpenSearch | `opensearch` | :white_check_mark: |
| PostgreSQL Flex | `postgresflex` | :white_check_mark: |
| RabbitMQ | `rabbitmq` | :white_check_mark: |
| Redis | `redis` | :white_check_mark: |
| Resource Manager | `project` | :white_check_mark: |
| Secrets Manager | `secrets-manager` | :white_check_mark: |
| Service Account | `service-account` | :white_check_mark: |
| SQLServer Flex | | Will be integrated soon |
## Authentication
Most of the commands will require you to be authenticated. Currently it's possible to authenticate with your personal user or with a service account.
Most of the commands will require you to be authenticated. Currently, it's possible to authenticate with your personal user or with a service account.
After successful authentication, the CLI stores credentials in your OS keychain. You won't need to login again for the duration of your session, which is 2h by default but configurable by providing the `--session-time-limit` flag on the `config set` command (see [Configuration](#configuration)).
After successful authentication, the CLI stores credentials in your OS keychain. You won't need to log in again for the duration of your session, which is 2h by default but configurable by providing the `--session-time-limit` flag on the `config set` command (see [Configuration](#configuration)).

@@ -96,3 +97,3 @@ ### Login with a personal user account

For more details on how to setup authentication using a service account, check our [authentication guide](./AUTHENTICATION.md).
For more details on how to set up authentication using a service account, check our [authentication guide](./AUTHENTICATION.md).

@@ -125,5 +126,5 @@ ## Configuration

Run the `config set` command with the flag `--help` to get a list of all of the available configuration options.
Run the `config set` command with the flag `--help` to get a list of all the available configuration options.
You can lookup your current configuration by checking the configuration file or by running:
You can look up your current configuration by checking the configuration file or by running:

@@ -138,7 +139,7 @@ ```bash

If you wish to setup command autocompletion in your shell for the STACKIT CLI, please refer to our [autocompletion guide](./AUTOCOMPLETION.md).
If you wish to set up command autocompletion in your shell for the STACKIT CLI, please refer to our [autocompletion guide](./AUTOCOMPLETION.md).
## Reporting issues
If you encounter any issues or have suggestions for improvements, please reach out to the Developer Tools team or open a ticket through the [STACKIT Help Center](https://support.stackit.cloud/).
If you encounter any issues or have suggestions for improvements, please open an issue in the [repository](https://github.com/stackitcloud/stackit-cli/issues).

@@ -152,1 +153,11 @@ ## Contribute

Apache 2.0
## Useful Links
- [STACKIT Portal](https://portal.stackit.cloud/)
- [STACKIT](https://www.stackit.de/en/)
- [STACKIT Knowledge Base](https://docs.stackit.cloud/stackit/en/knowledge-base-85301704.html)
- [STACKIT Terraform Provider](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs)