mygithub.libinneed.workers.dev/stackitcloud/stackit-cli
Advanced tools
| ## 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") | ||
| } |
@@ -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 |
+6
-6
@@ -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 |
+1
-0
@@ -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
| [](https://goreportcard.com/report/github.com/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) |