mygithub.libinneed.workers.dev/stackitcloud/stackit-cli
Advanced tools
| version: 2 | ||
| updates: | ||
| - package-ecosystem: "gomod" | ||
| directory: "/" | ||
| schedule: | ||
| interval: "daily" | ||
| - package-ecosystem: "github-actions" | ||
| directory: "/" | ||
| schedule: | ||
| interval: "daily" |
| name: "Stale" | ||
| on: | ||
| schedule: | ||
| # every night at 01:30 | ||
| - cron: "30 1 * * *" | ||
| # run this workflow if the workflow definition gets changed within a PR | ||
| pull_request: | ||
| branches: ["main"] | ||
| paths: [".github/workflows/stale.yaml"] | ||
| env: | ||
| DAYS_BEFORE_PR_STALE: 7 | ||
| DAYS_BEFORE_PR_CLOSE: 7 | ||
| permissions: | ||
| issues: write | ||
| pull-requests: write | ||
| jobs: | ||
| stale: | ||
| name: "Stale" | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - name: "Mark old PRs as stale" | ||
| uses: actions/stale@v9 | ||
| with: | ||
| repo-token: ${{ secrets.GITHUB_TOKEN }} | ||
| stale-pr-message: "This PR was marked as stale after ${{ env.DAYS_BEFORE_PR_STALE }} days of inactivity and will be closed after another ${{ env.DAYS_BEFORE_PR_CLOSE }} days of further inactivity. If this PR should be kept open, just add a comment, remove the stale label or push new commits to it." | ||
| close-pr-message: "This PR was closed automatically because it has been stalled for ${{ env.DAYS_BEFORE_PR_CLOSE }} days with no activity. Feel free to re-open it at any time." | ||
| days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }} | ||
| days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }} | ||
| # never mark issues as stale or close them | ||
| days-before-issue-stale: -1 | ||
| days-before-issue-close: -1 |
| ## stackit beta alb create | ||
| Creates an application loadbalancer | ||
| ### Synopsis | ||
| Creates an application loadbalancer. | ||
| ``` | ||
| stackit beta alb create [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Create an application loadbalancer from a configuration file | ||
| $ stackit beta alb create --configuration my-loadbalancer.json | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -c, --configuration string Filename of the input configuration file | ||
| -h, --help Help for "stackit beta alb create" | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers | ||
| ## stackit beta alb delete | ||
| Deletes an application loadbalancer | ||
| ### Synopsis | ||
| Deletes an application loadbalancer. | ||
| ``` | ||
| stackit beta alb delete LOADBALANCER_NAME_ARG [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Delete an application loadbalancer with name "my-load-balancer" | ||
| $ stackit beta alb delete my-load-balancer | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta alb delete" | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers | ||
| ## stackit beta alb describe | ||
| Describes an application loadbalancer | ||
| ### Synopsis | ||
| Describes an application alb. | ||
| ``` | ||
| stackit beta alb describe LOADBALANCER_NAME_ARG [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Get details about an application loadbalancer with name "my-load-balancer" | ||
| $ stackit beta alb describe my-load-balancer | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta alb describe" | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers | ||
| ## stackit beta alb list | ||
| Lists albs | ||
| ### Synopsis | ||
| Lists application load balancers. | ||
| ``` | ||
| stackit beta alb list [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| List all load balancers | ||
| $ stackit beta alb list | ||
| List the first 10 application load balancers | ||
| $ stackit beta alb list --limit=10 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta alb list" | ||
| --limit int Limit the output to the first n elements | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers | ||
| ## stackit beta alb observability-credentials add | ||
| Adds observability credentials to an application load balancer | ||
| ### Synopsis | ||
| Adds observability credentials (username and password) to an application load balancer. The credentials can be for Observability or another monitoring tool. | ||
| ``` | ||
| stackit beta alb observability-credentials add [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Add observability credentials to a load balancer with username "xxx" and display name "yyy", providing the path to a file with the password as flag | ||
| $ stackit beta alb observability-credentials add --username xxx --password @./password.txt --display-name yyy | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -d, --displayname string Displayname for the credentials | ||
| -h, --help Help for "stackit beta alb observability-credentials add" | ||
| --password string Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt). | ||
| -u, --username string Username for the credentials | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb observability-credentials](./stackit_beta_alb_observability-credentials.md) - Provides functionality for application loadbalancer credentials | ||
| ## stackit beta alb observability-credentials delete | ||
| Deletes credentials | ||
| ### Synopsis | ||
| Deletes credentials. | ||
| ``` | ||
| stackit beta alb observability-credentials delete CREDENTIAL_REF [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Delete credential with name "credential-12345" | ||
| $ stackit beta alb observability-credentials delete credential-12345 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta alb observability-credentials delete" | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb observability-credentials](./stackit_beta_alb_observability-credentials.md) - Provides functionality for application loadbalancer credentials | ||
| ## stackit beta alb observability-credentials describe | ||
| Describes observability credentials for the Application Load Balancer | ||
| ### Synopsis | ||
| Describes observability credentials for the Application Load Balancer. | ||
| ``` | ||
| stackit beta alb observability-credentials describe CREDENTIAL_REF [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Get details about credentials with name "credential-12345" | ||
| $ stackit beta alb observability-credentials describe credential-12345 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta alb observability-credentials describe" | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb observability-credentials](./stackit_beta_alb_observability-credentials.md) - Provides functionality for application loadbalancer credentials | ||
| ## stackit beta alb observability-credentials list | ||
| Lists all credentials | ||
| ### Synopsis | ||
| Lists all credentials. | ||
| ``` | ||
| stackit beta alb observability-credentials list [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Lists all credentials | ||
| $ stackit beta alb observability-credentials list | ||
| Lists all credentials in JSON format | ||
| $ stackit beta alb observability-credentials list --output-format json | ||
| Lists up to 10 credentials | ||
| $ stackit beta alb observability-credentials list --limit 10 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta alb observability-credentials list" | ||
| --limit int Number of credentials to list | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb observability-credentials](./stackit_beta_alb_observability-credentials.md) - Provides functionality for application loadbalancer credentials | ||
| ## stackit beta alb observability-credentials update | ||
| Update credentials | ||
| ### Synopsis | ||
| Update credentials. | ||
| ``` | ||
| stackit beta alb observability-credentials update CREDENTIAL_REF_ARG [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Update the password of observability credentials of Application Load Balancer with credentials reference "credentials-xxx", by providing the path to a file with the new password as flag | ||
| $ stackit beta alb observability-credentials update credentials-xxx --username user1 --displayname user1 --password @./new-password.txt | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -d, --displayname string Displayname for the credentials | ||
| -h, --help Help for "stackit beta alb observability-credentials update" | ||
| --password string Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt). | ||
| -u, --username string Username for the credentials | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb observability-credentials](./stackit_beta_alb_observability-credentials.md) - Provides functionality for application loadbalancer credentials | ||
| ## stackit beta alb observability-credentials | ||
| Provides functionality for application loadbalancer credentials | ||
| ### Synopsis | ||
| Provides functionality for application loadbalancer credentials | ||
| ``` | ||
| stackit beta alb observability-credentials [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta alb observability-credentials" | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers | ||
| * [stackit beta alb observability-credentials add](./stackit_beta_alb_observability-credentials_add.md) - Adds observability credentials to an application load balancer | ||
| * [stackit beta alb observability-credentials delete](./stackit_beta_alb_observability-credentials_delete.md) - Deletes credentials | ||
| * [stackit beta alb observability-credentials describe](./stackit_beta_alb_observability-credentials_describe.md) - Describes observability credentials for the Application Load Balancer | ||
| * [stackit beta alb observability-credentials list](./stackit_beta_alb_observability-credentials_list.md) - Lists all credentials | ||
| * [stackit beta alb observability-credentials update](./stackit_beta_alb_observability-credentials_update.md) - Update credentials | ||
| ## stackit beta alb plans | ||
| Lists the application load balancer plans | ||
| ### Synopsis | ||
| Lists the available application load balancer plans. | ||
| ``` | ||
| stackit beta alb plans [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| List all application load balancer plans | ||
| $ stackit beta alb plans | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta alb plans" | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers | ||
| ## stackit beta alb pool update | ||
| Updates an application target pool | ||
| ### Synopsis | ||
| Updates an application target pool. | ||
| ``` | ||
| stackit beta alb pool update [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Update an application target pool from a configuration file (the name of the pool is read from the file) | ||
| $ stackit beta alb update --configuration my-target-pool.json --name my-load-balancer | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -c, --configuration string Filename of the input configuration file | ||
| -h, --help Help for "stackit beta alb pool update" | ||
| -n, --name string Name of the target pool name to update | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb pool](./stackit_beta_alb_pool.md) - Manages target pools for application loadbalancers | ||
| ## stackit beta alb pool | ||
| Manages target pools for application loadbalancers | ||
| ### Synopsis | ||
| Manage the lifecycle of target pools for application loadbalancers. | ||
| ``` | ||
| stackit beta alb pool [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta alb pool" | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers | ||
| * [stackit beta alb pool update](./stackit_beta_alb_pool_update.md) - Updates an application target pool | ||
| ## stackit beta alb quotas | ||
| Shows the application load balancer quotas | ||
| ### Synopsis | ||
| Shows the application load balancer quotas for the application load balancers. | ||
| ``` | ||
| stackit beta alb quotas [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| List all application load balancer quotas | ||
| $ stackit beta alb quotas | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta alb quotas" | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers | ||
| ## stackit beta alb template | ||
| creates configuration templates to use for resource creation | ||
| ### Synopsis | ||
| creates a json or yaml template file for creating/updating an application loadbalancer or target pool. | ||
| ``` | ||
| stackit beta alb template [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Create a yaml template | ||
| $ stackit beta alb template --format=yaml --type alb | ||
| Create a json template | ||
| $ stackit beta alb template --format=json --type pool | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -f, --format string Defines the output format ('yaml' or 'json'), default is 'json' (default "json") | ||
| -h, --help Help for "stackit beta alb template" | ||
| -t, --type string Defines the output type ('alb' or 'pool'), default is 'alb' (default "alb") | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers | ||
| ## stackit beta alb update | ||
| Updates an application loadbalancer | ||
| ### Synopsis | ||
| Updates an application loadbalancer. | ||
| ``` | ||
| stackit beta alb update [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Update an application loadbalancer from a configuration file | ||
| $ stackit beta alb update --configuration my-loadbalancer.json | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -c, --configuration string Filename of the input configuration file | ||
| -h, --help Help for "stackit beta alb update" | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers | ||
| ## stackit beta alb | ||
| Manages application loadbalancers | ||
| ### Synopsis | ||
| Manage the lifecycle of application loadbalancers. | ||
| ``` | ||
| stackit beta alb [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta alb" | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --region string Target region for region-specific requests | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands | ||
| * [stackit beta alb create](./stackit_beta_alb_create.md) - Creates an application loadbalancer | ||
| * [stackit beta alb delete](./stackit_beta_alb_delete.md) - Deletes an application loadbalancer | ||
| * [stackit beta alb describe](./stackit_beta_alb_describe.md) - Describes an application loadbalancer | ||
| * [stackit beta alb list](./stackit_beta_alb_list.md) - Lists albs | ||
| * [stackit beta alb observability-credentials](./stackit_beta_alb_observability-credentials.md) - Provides functionality for application loadbalancer credentials | ||
| * [stackit beta alb plans](./stackit_beta_alb_plans.md) - Lists the application load balancer plans | ||
| * [stackit beta alb pool](./stackit_beta_alb_pool.md) - Manages target pools for application loadbalancers | ||
| * [stackit beta alb quotas](./stackit_beta_alb_quotas.md) - Shows the application load balancer quotas | ||
| * [stackit beta alb template](./stackit_beta_alb_template.md) - creates configuration templates to use for resource creation | ||
| * [stackit beta alb update](./stackit_beta_alb_update.md) - Updates an application loadbalancer | ||
| package alb | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/create" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/delete" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/describe" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/list" | ||
| observabilitycredentials "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/plans" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/pool" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/quotas" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/template" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/update" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "alb", | ||
| Short: "Manages application loadbalancers", | ||
| Long: "Manage the lifecycle of application loadbalancers.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, p) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, p *print.Printer) { | ||
| cmd.AddCommand( | ||
| list.NewCmd(p), | ||
| template.NewCmd(p), | ||
| create.NewCmd(p), | ||
| update.NewCmd(p), | ||
| observabilitycredentials.NewCmd(p), | ||
| describe.NewCmd(p), | ||
| delete.NewCmd(p), | ||
| pool.NewCmd(p), | ||
| plans.NewCmd(p), | ||
| quotas.NewCmd(p), | ||
| ) | ||
| } |
| package create | ||
| import ( | ||
| "context" | ||
| _ "embed" | ||
| "encoding/json" | ||
| "log" | ||
| "testing" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| //go:embed testdata/testconfig.json | ||
| var testConfiguration []byte | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &alb.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testRegion = "eu01" | ||
| var testConfig = "testdata/testconfig.json" | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| configurationFlag: testConfig, | ||
| globalflags.RegionFlag: testRegion, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| Region: testRegion, | ||
| }, | ||
| Configuration: utils.Ptr(testConfig), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixturePayload(mods ...func(payload *alb.CreateLoadBalancerPayload)) (payload alb.CreateLoadBalancerPayload) { | ||
| if err := json.Unmarshal(testConfiguration, &payload); err != nil { | ||
| log.Panicf("cannot deserialize test configuration: %v", err) | ||
| } | ||
| for _, f := range mods { | ||
| f(&payload) | ||
| } | ||
| return payload | ||
| } | ||
| func fixtureRequest(mods ...func(request *alb.ApiCreateLoadBalancerRequest)) alb.ApiCreateLoadBalancerRequest { | ||
| request := testClient.CreateLoadBalancer(testCtx, testProjectId, testRegion) | ||
| request = request.CreateLoadBalancerPayload(fixturePayload()) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "required fields only", | ||
| flagValues: map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| configurationFlag: testConfig, | ||
| }, | ||
| isValid: true, | ||
| expectedModel: &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| Configuration: &testConfig, | ||
| }, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest alb.ApiCreateLoadBalancerRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| { | ||
| description: "required fields only", | ||
| model: &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| Configuration: &testConfig, | ||
| }, | ||
| expectedRequest: testClient. | ||
| CreateLoadBalancer(testCtx, testProjectId, testRegion). | ||
| CreateLoadBalancerPayload(fixturePayload()), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request, err := buildRequest(testCtx, tt.model, testClient) | ||
| if err != nil { | ||
| t.Fatalf("canno create request: %v", err) | ||
| } | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestOutputResult(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| projectLabel string | ||
| resp *alb.LoadBalancer | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{}, | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "empty response as argument", | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| resp: &alb.LoadBalancer{}, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| } | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(p) | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { | ||
| t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package create | ||
| import ( | ||
| "bufio" | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "os" | ||
| "strings" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb/wait" | ||
| ) | ||
| const ( | ||
| configurationFlag = "configuration" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Configuration *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "create", | ||
| Short: "Creates an application loadbalancer", | ||
| Long: "Creates an application loadbalancer.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Create an application loadbalancer from a configuration file`, | ||
| "$ stackit beta alb create --configuration my-loadbalancer.json"), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, p, cmd) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| projectLabel = model.ProjectId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create an application loadbalancer for project %q?", projectLabel) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req, err := buildRequest(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("create application loadbalancer: %w", err) | ||
| } | ||
| // Wait for async operation, if async mode not enabled | ||
| if !model.Async { | ||
| s := spinner.New(p) | ||
| s.Start("Creating loadbalancer") | ||
| _, err = wait.CreateOrUpdateLoadbalancerWaitHandler(ctx, apiClient, model.ProjectId, model.Region, *resp.Name).WaitWithContext(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("wait for loadbalancer creation: %w", err) | ||
| } | ||
| s.Stop() | ||
| } | ||
| return outputResult(p, model, projectLabel, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringP(configurationFlag, "c", "", "Filename of the input configuration file") | ||
| err := flags.MarkFlagsRequired(cmd, configurationFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Configuration: flags.FlagToStringPointer(p, cmd, configurationFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) (req alb.ApiCreateLoadBalancerRequest, err error) { | ||
| req = apiClient.CreateLoadBalancer(ctx, model.ProjectId, model.Region) | ||
| payload, err := readPayload(ctx, model) | ||
| if err != nil { | ||
| return req, err | ||
| } | ||
| return req.CreateLoadBalancerPayload(payload), nil | ||
| } | ||
| func readPayload(_ context.Context, model *inputModel) (payload alb.CreateLoadBalancerPayload, err error) { | ||
| if model.Configuration == nil { | ||
| return payload, fmt.Errorf("no configuration file defined") | ||
| } | ||
| file, err := os.Open(*model.Configuration) | ||
| if err != nil { | ||
| return payload, fmt.Errorf("cannot open configuration file %q: %w", *model.Configuration, err) | ||
| } | ||
| defer file.Close() // nolint:errcheck // at this point close errors are not relevant anymore | ||
| if strings.HasSuffix(*model.Configuration, ".yaml") { | ||
| decoder := yaml.NewDecoder(bufio.NewReader(file), yaml.UseJSONUnmarshaler()) | ||
| if err := decoder.Decode(&payload); err != nil { | ||
| return payload, fmt.Errorf("cannot deserialize yaml configuration from %q: %w", *model.Configuration, err) | ||
| } | ||
| } else if strings.HasSuffix(*model.Configuration, ".json") { | ||
| decoder := json.NewDecoder(bufio.NewReader(file)) | ||
| if err := decoder.Decode(&payload); err != nil { | ||
| return payload, fmt.Errorf("cannot deserialize json configuration from %q: %w", *model.Configuration, err) | ||
| } | ||
| } else { | ||
| return payload, fmt.Errorf("cannot determine configuration fileformat of %q by extension. Must be '.json' or '.yaml'", *model.Configuration) | ||
| } | ||
| return payload, nil | ||
| } | ||
| func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *alb.LoadBalancer) error { | ||
| if resp == nil { | ||
| return fmt.Errorf("create loadbalancer response is empty") | ||
| } | ||
| switch model.OutputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(resp, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal loadbalancer: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal loadbalancer: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| operationState := "Created" | ||
| if model.Async { | ||
| operationState = "Triggered creation of" | ||
| } | ||
| p.Outputf("%s application loadbalancer for %q. Name: %s\n", operationState, projectLabel, utils.PtrString(resp.Name)) | ||
| return nil | ||
| } | ||
| } |
| { | ||
| "externalAddress": "10.100.42.1", | ||
| "listeners": [ | ||
| { | ||
| "displayName": "listener1", | ||
| "http": {}, | ||
| "https": { | ||
| "certificateConfig": { | ||
| "certificateIds": [ | ||
| "cert-1", | ||
| "cert-2", | ||
| "cert-3" | ||
| ] | ||
| } | ||
| }, | ||
| "port": 443, | ||
| "protocol": "PROTOCOL_HTTPS", | ||
| "rules": [ | ||
| { | ||
| "host": "front.facing.host", | ||
| "http": { | ||
| "subRules": [ | ||
| { | ||
| "cookiePersistence": { | ||
| "name": "cookie1", | ||
| "ttl": "120s" | ||
| }, | ||
| "headers": [ | ||
| { | ||
| "name": "testheader1", | ||
| "exactMatch": "X-test-header1" | ||
| }, | ||
| { | ||
| "name": "testheader2", | ||
| "exactMatch": "X-test-header2" | ||
| }, | ||
| { | ||
| "name": "testheader3", | ||
| "exactMatch": "X-test-header3" | ||
| } | ||
| ], | ||
| "pathPrefix": "/foo", | ||
| "queryParameters": [ | ||
| { | ||
| "name": "query-param", | ||
| "exactMatch": "q" | ||
| }, | ||
| { | ||
| "name": "region", | ||
| "exactMatch": "region" | ||
| } | ||
| ], | ||
| "targetPool": "my-target-pool", | ||
| "webSocket": false | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "name": "my-load-balancer", | ||
| "networks": [ | ||
| { | ||
| "networkId": "00000000-0000-0000-0000-000000000000", | ||
| "role": "ROLE_LISTENERS_AND_TARGETS" | ||
| }, | ||
| { | ||
| "networkId": "00000000-0000-0000-0000-000000000001", | ||
| "role": "ROLE_LISTENERS_AND_TARGETS" | ||
| } | ||
| ], | ||
| "options": { | ||
| "accessControl": { | ||
| "allowedSourceRanges": [ | ||
| "192.168.42.0-192.168.42.10", | ||
| "192.168.54.0-192.168.54.10" | ||
| ] | ||
| }, | ||
| "ephemeralAddress": true, | ||
| "observability": { | ||
| "logs": { | ||
| "credentialsRef": "my-credentials", | ||
| "pushUrl": "https://my.observability.host/<observability-instance-id>/loki/api/v1/push" | ||
| }, | ||
| "metrics": { | ||
| "credentialsRef": "my-credentials", | ||
| "pushUrl": "https://my.observability.host/<observability-instance-id>/<argus-instance-id>/api/v1/receive" | ||
| } | ||
| }, | ||
| "privateNetworkOnly": true | ||
| }, | ||
| "planId": "p10", | ||
| "targetPools": [ | ||
| { | ||
| "activeHealthCheck": { | ||
| "healthyThreshold": 3, | ||
| "httpHealthChecks": { | ||
| "okStatuses": [ | ||
| "200", | ||
| "204" | ||
| ], | ||
| "path": "/health" | ||
| }, | ||
| "interval": "10s", | ||
| "intervalJitter": "3s", | ||
| "timeout": "5s", | ||
| "unhealthyThreshold": 1 | ||
| }, | ||
| "name": "my-target-pool", | ||
| "targetPort": 5732, | ||
| "targets": [ | ||
| { | ||
| "displayName": "my-target1", | ||
| "ip": "192.11.2.5" | ||
| } | ||
| ], | ||
| "tlsConfig": { | ||
| "customCa": "my.private.ca", | ||
| "enabled": true, | ||
| "skipCertificateValidation": false | ||
| } | ||
| } | ||
| ] | ||
| } |
| package delete | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testClient = &alb.APIClient{} | ||
| testLoadBalancerName = "my-test-loadbalancer" | ||
| ) | ||
| func fixtureArgValues(mods ...func(argVales []string)) []string { | ||
| argVales := []string{ | ||
| testLoadBalancerName, | ||
| } | ||
| for _, m := range mods { | ||
| m(argVales) | ||
| } | ||
| return argVales | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| } | ||
| for _, m := range mods { | ||
| m(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| }, | ||
| Name: testLoadBalancerName, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *alb.ApiDeleteLoadBalancerRequest)) alb.ApiDeleteLoadBalancerRequest { | ||
| request := testClient.DeleteLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argsValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argsValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argsValues: []string{}, | ||
| flagValues: map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| }, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no arg values", | ||
| argsValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flag values", | ||
| argsValues: fixtureArgValues(), | ||
| flagValues: map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| }, | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err = cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argsValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argsValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedResult alb.ApiDeleteLoadBalancerRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedResult: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedResult, | ||
| cmp.AllowUnexported(tt.expectedResult), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package delete | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| const ( | ||
| loadbalancerNameArg = "LOADBALANCER_NAME_ARG" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Name string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("delete %s", loadbalancerNameArg), | ||
| Short: "Deletes an application loadbalancer", | ||
| Long: "Deletes an application loadbalancer.", | ||
| Args: args.SingleArg(loadbalancerNameArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Delete an application loadbalancer with name "my-load-balancer"`, | ||
| "$ stackit beta alb delete my-load-balancer", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, p, cmd) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| projectLabel = model.ProjectId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the application loadbalancer %q for project %q?", model.Name, projectLabel) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| _, err = req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("delete loadbalancer: %w", err) | ||
| } | ||
| p.Outputf("Load balancer %q deleted.", model.Name) | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| loadbalancerName := inputArgs[0] | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Name: loadbalancerName, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiDeleteLoadBalancerRequest { | ||
| return apiClient.DeleteLoadBalancer(ctx, model.ProjectId, model.Region, model.Name) | ||
| } |
| package describe | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testClient = &alb.APIClient{} | ||
| testLoadBalancerName = "my-test-loadbalancer" | ||
| ) | ||
| func fixtureArgValues(mods ...func(argVales []string)) []string { | ||
| argVales := []string{ | ||
| testLoadBalancerName, | ||
| } | ||
| for _, m := range mods { | ||
| m(argVales) | ||
| } | ||
| return argVales | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| } | ||
| for _, m := range mods { | ||
| m(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| }, | ||
| Name: testLoadBalancerName, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *alb.ApiGetLoadBalancerRequest)) alb.ApiGetLoadBalancerRequest { | ||
| request := testClient.GetLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argsValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argsValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argsValues: []string{}, | ||
| flagValues: map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| }, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no arg values", | ||
| argsValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flag values", | ||
| argsValues: fixtureArgValues(), | ||
| flagValues: map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| }, | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err = cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argsValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argsValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedResult alb.ApiGetLoadBalancerRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedResult: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedResult, | ||
| cmp.AllowUnexported(tt.expectedResult), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func Test_outputResult(t *testing.T) { | ||
| type args struct { | ||
| outputFormat string | ||
| showOnlyPublicKey bool | ||
| response *alb.LoadBalancer | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "base", | ||
| args: args{ | ||
| outputFormat: "", | ||
| showOnlyPublicKey: false, | ||
| response: &alb.LoadBalancer{}, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| } | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(p) | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr { | ||
| t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package describe | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| const ( | ||
| loadbalancerNameArg = "LOADBALANCER_NAME_ARG" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Name string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("describe %s", loadbalancerNameArg), | ||
| Short: "Describes an application loadbalancer", | ||
| Long: "Describes an application alb.", | ||
| Args: args.SingleArg(loadbalancerNameArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Get details about an application loadbalancer with name "my-load-balancer"`, | ||
| "$ stackit beta alb describe my-load-balancer", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("read loadbalancer: %w", err) | ||
| } | ||
| if loadbalancer := resp; loadbalancer != nil { | ||
| return outputResult(p, model.OutputFormat, loadbalancer) | ||
| } | ||
| p.Outputln("No load balancer found.") | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| loadbalancerName := inputArgs[0] | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Name: loadbalancerName, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiGetLoadBalancerRequest { | ||
| return apiClient.GetLoadBalancer(ctx, model.ProjectId, model.Region, model.Name) | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, response *alb.LoadBalancer) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(response, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal loadbalancer: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(response, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal loadbalancer: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| if err := outputResultAsTable(p, response); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| func outputResultAsTable(p *print.Printer, loadbalancer *alb.LoadBalancer) error { | ||
| content := []tables.Table{} | ||
| content = append(content, buildLoadBalancerTable(loadbalancer)) | ||
| if loadbalancer.Listeners != nil { | ||
| content = append(content, buildListenersTable(*loadbalancer.Listeners)) | ||
| } | ||
| if loadbalancer.TargetPools != nil { | ||
| content = append(content, buildTargetPoolsTable(*loadbalancer.TargetPools)) | ||
| } | ||
| err := tables.DisplayTables(p, content) | ||
| if err != nil { | ||
| return fmt.Errorf("display output: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| func buildLoadBalancerTable(loadbalancer *alb.LoadBalancer) tables.Table { | ||
| acl := []string{} | ||
| privateAccessOnly := false | ||
| if loadbalancer.Options != nil { | ||
| if loadbalancer.Options.AccessControl != nil && loadbalancer.Options.AccessControl.AllowedSourceRanges != nil { | ||
| acl = *loadbalancer.Options.AccessControl.AllowedSourceRanges | ||
| } | ||
| if loadbalancer.Options.PrivateNetworkOnly != nil { | ||
| privateAccessOnly = *loadbalancer.Options.PrivateNetworkOnly | ||
| } | ||
| } | ||
| networkId := "-" | ||
| if loadbalancer.Networks != nil && len(*loadbalancer.Networks) > 0 { | ||
| networks := *loadbalancer.Networks | ||
| networkId = *networks[0].NetworkId | ||
| } | ||
| externalAddress := utils.PtrStringDefault(loadbalancer.ExternalAddress, "-") | ||
| errorDescriptions := []string{} | ||
| if loadbalancer.Errors != nil && len((*loadbalancer.Errors)) > 0 { | ||
| for _, err := range *loadbalancer.Errors { | ||
| errorDescriptions = append(errorDescriptions, *err.Description) | ||
| } | ||
| } | ||
| table := tables.NewTable() | ||
| table.SetTitle("Load Balancer") | ||
| table.AddRow("NAME", utils.PtrString(loadbalancer.Name)) | ||
| table.AddSeparator() | ||
| table.AddRow("STATE", utils.PtrString(loadbalancer.Status)) | ||
| table.AddSeparator() | ||
| if len(errorDescriptions) > 0 { | ||
| table.AddRow("ERROR DESCRIPTIONS", strings.Join(errorDescriptions, "\n")) | ||
| table.AddSeparator() | ||
| } | ||
| table.AddRow("PRIVATE ACCESS ONLY", privateAccessOnly) | ||
| table.AddSeparator() | ||
| table.AddRow("ATTACHED PUBLIC IP", externalAddress) | ||
| table.AddSeparator() | ||
| table.AddRow("ATTACHED NETWORK ID", networkId) | ||
| table.AddSeparator() | ||
| table.AddRow("ACL", acl) | ||
| return table | ||
| } | ||
| func buildListenersTable(listeners []alb.Listener) tables.Table { | ||
| table := tables.NewTable() | ||
| table.SetTitle("Listeners") | ||
| table.SetHeader("NAME", "PORT", "PROTOCOL", "TARGET POOL") | ||
| for i := range listeners { | ||
| listener := listeners[i] | ||
| table.AddRow( | ||
| utils.PtrString(listener.Name), | ||
| utils.PtrString(listener.Port), | ||
| utils.PtrString(listener.Protocol), | ||
| ) | ||
| } | ||
| return table | ||
| } | ||
| func buildTargetPoolsTable(targetPools []alb.TargetPool) tables.Table { | ||
| table := tables.NewTable() | ||
| table.SetTitle("Target Pools") | ||
| table.SetHeader("NAME", "PORT", "TARGETS") | ||
| for _, targetPool := range targetPools { | ||
| table.AddRow(utils.PtrString(targetPool.Name), utils.PtrString(targetPool.TargetPort), len(*targetPool.Targets)) | ||
| } | ||
| return table | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "strconv" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testClient = &alb.APIClient{} | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testLimit int64 = 10 | ||
| ) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| limitFlag: strconv.Itoa(int(testLimit)), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Region: testRegion, Verbosity: globalflags.VerbosityDefault}, | ||
| Limit: &testLimit, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *alb.ApiListLoadBalancersRequest)) alb.ApiListLoadBalancersRequest { | ||
| request := testClient.ListLoadBalancers(context.Background(), testProjectId, testRegion) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, globalflags.ProjectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| if err := globalflags.Configure(cmd.Flags()); err != nil { | ||
| t.Errorf("cannot configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| if err := cmd.ValidateRequiredFlags(); err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest alb.ApiListLoadBalancersRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| cmpopts.IgnoreFields(alb.ApiListLoadBalancersRequest{}, "ctx"), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func Test_outputResult(t *testing.T) { | ||
| type args struct { | ||
| outputFormat string | ||
| items []alb.LoadBalancer | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{ | ||
| outputFormat: "", | ||
| items: []alb.LoadBalancer{}, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| { | ||
| name: "output format json", | ||
| args: args{ | ||
| outputFormat: print.JSONOutputFormat, | ||
| items: []alb.LoadBalancer{}, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| } | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(p) | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr { | ||
| t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Limit *int64 | ||
| } | ||
| const ( | ||
| labelSelectorFlag = "label-selector" | ||
| limitFlag = "limit" | ||
| ) | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "Lists albs", | ||
| Long: "Lists application load balancers.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `List all load balancers`, | ||
| `$ stackit beta alb list`, | ||
| ), | ||
| examples.NewExample( | ||
| `List the first 10 application load balancers`, | ||
| `$ stackit beta alb list --limit=10`, | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, p, cmd) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| projectLabel = model.ProjectId | ||
| } else if projectLabel == "" { | ||
| projectLabel = model.ProjectId | ||
| } | ||
| // Call API | ||
| request := buildRequest(ctx, model, apiClient) | ||
| response, err := request.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("list load balancerse: %w", err) | ||
| } | ||
| if items := response.LoadBalancers; items == nil || len(*items) == 0 { | ||
| p.Info("No load balancers found for project %q", projectLabel) | ||
| } else { | ||
| if model.Limit != nil && len(*items) > int(*model.Limit) { | ||
| *items = (*items)[:*model.Limit] | ||
| } | ||
| if err := outputResult(p, model.OutputFormat, *items); err != nil { | ||
| return fmt.Errorf("output loadbalancers: %w", err) | ||
| } | ||
| } | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements") | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) | ||
| if limit != nil && *limit < 1 { | ||
| return nil, &errors.FlagValidationError{ | ||
| Flag: limitFlag, | ||
| Details: "must be greater than 0", | ||
| } | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Limit: limit, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiListLoadBalancersRequest { | ||
| request := apiClient.ListLoadBalancers(ctx, model.ProjectId, model.Region) | ||
| return request | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, items []alb.LoadBalancer) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(items, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal loadbalancer list: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal loadbalancer list: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| table.SetHeader("NAME", "EXTERNAL ADDRESS", "REGION", "STATUS", "VERSION", "ERRORS") | ||
| for _, item := range items { | ||
| var errNo int | ||
| if item.Errors != nil { | ||
| errNo = len(*item.Errors) | ||
| } | ||
| table.AddRow(utils.PtrString(item.Name), | ||
| utils.PtrString(item.ExternalAddress), | ||
| utils.PtrString(item.Region), | ||
| utils.PtrString(item.Status), | ||
| utils.PtrString(item.Version), | ||
| errNo, | ||
| ) | ||
| } | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| } |
| package add | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &alb.APIClient{} | ||
| var ( | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testDisplayname = "displayname" | ||
| testUsername = "testuser" | ||
| testPassword = "testpassword" | ||
| ) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| usernameFlag: testUsername, | ||
| displaynameFlag: testDisplayname, | ||
| passwordFlag: testPassword, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| }, | ||
| Username: &testUsername, | ||
| Displayname: &testDisplayname, | ||
| Password: &testPassword, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *alb.ApiCreateCredentialsRequest)) alb.ApiCreateCredentialsRequest { | ||
| request := testClient.CreateCredentials(testCtx, testProjectId, testRegion) | ||
| request = request.CreateCredentialsPayload(fixturePayload()) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func fixturePayload(mods ...func(payload *alb.CreateCredentialsPayload)) alb.CreateCredentialsPayload { | ||
| payload := alb.CreateCredentialsPayload{ | ||
| DisplayName: &testDisplayname, | ||
| Password: &testPassword, | ||
| Username: &testUsername, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(&payload) | ||
| } | ||
| return payload | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err = cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest alb.ApiCreateCredentialsRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| cmp.AllowUnexported(alb.NullableString{}), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func Test_outputResult(t *testing.T) { | ||
| type args struct { | ||
| item *alb.CreateCredentialsResponse | ||
| outputFormat string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{ | ||
| item: nil, | ||
| outputFormat: "", | ||
| }, | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "base", | ||
| args: args{ | ||
| item: &alb.CreateCredentialsResponse{}, | ||
| outputFormat: "", | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| } | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(p) | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if err := outputResult(p, tt.args.outputFormat, tt.args.item); (err != nil) != tt.wantErr { | ||
| t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package add | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| const ( | ||
| usernameFlag = "username" | ||
| displaynameFlag = "displayname" | ||
| passwordFlag = "password" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Username *string | ||
| Displayname *string | ||
| Password *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "add", | ||
| Short: "Adds observability credentials to an application load balancer", | ||
| Long: "Adds observability credentials (username and password) to an application load balancer. The credentials can be for Observability or another monitoring tool.", | ||
| Args: cobra.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Add observability credentials to a load balancer with username "xxx" and display name "yyy", providing the path to a file with the password as flag`, | ||
| "$ stackit beta alb observability-credentials add --username xxx --password @./password.txt --display-name yyy"), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := "Are your sure you want to add credentials?" | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("add credential: %w", err) | ||
| } | ||
| return outputResult(p, model.GlobalFlagModel.OutputFormat, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringP(usernameFlag, "u", "", "Username for the credentials") | ||
| cmd.Flags().StringP(displaynameFlag, "d", "", "Displayname for the credentials") | ||
| cmd.Flags().Var(flags.ReadFromFileFlag(), passwordFlag, `Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).`) | ||
| cobra.CheckErr(flags.MarkFlagsRequired(cmd, usernameFlag, displaynameFlag)) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Username: flags.FlagToStringPointer(p, cmd, usernameFlag), | ||
| Displayname: flags.FlagToStringPointer(p, cmd, displaynameFlag), | ||
| Password: flags.FlagToStringPointer(p, cmd, passwordFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string fo debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiCreateCredentialsRequest { | ||
| req := apiClient.CreateCredentials(ctx, model.ProjectId, model.Region) | ||
| payload := alb.CreateCredentialsPayload{ | ||
| DisplayName: model.Displayname, | ||
| Password: model.Password, | ||
| Username: model.Username, | ||
| } | ||
| return req.CreateCredentialsPayload(payload) | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, item *alb.CreateCredentialsResponse) error { | ||
| if item == nil { | ||
| return fmt.Errorf("no credential found") | ||
| } | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(item, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal credential: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(item, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal credential: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| default: | ||
| if item.Credential != nil { | ||
| p.Outputf("Created credential %s\n", | ||
| utils.PtrString(item.Credential.CredentialsRef), | ||
| ) | ||
| } | ||
| } | ||
| return nil | ||
| } |
| package delete | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testClient = &alb.APIClient{} | ||
| testCredentialRef = "credential-12345" | ||
| ) | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testCredentialRef, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| }, | ||
| CredentialsRef: testCredentialRef, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *alb.ApiDeleteCredentialsRequest)) alb.ApiDeleteCredentialsRequest { | ||
| request := testClient.DeleteCredentials(testCtx, testProjectId, testRegion, testCredentialRef) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| }, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no args", | ||
| argValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flags", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| }, | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err = cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest alb.ApiDeleteCredentialsRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package delete | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| const ( | ||
| credentialRefArg = "CREDENTIAL_REF" // nolint:gosec // false alert, these are not valid credentials | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| CredentialsRef string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("delete %s", credentialRefArg), | ||
| Short: "Deletes credentials", | ||
| Long: "Deletes credentials.", | ||
| Args: args.SingleArg(credentialRefArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Delete credential with name "credential-12345"`, | ||
| "$ stackit beta alb observability-credentials delete credential-12345", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %q?", model.CredentialsRef) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| _, err = req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("delete credential: %w", err) | ||
| } | ||
| p.Info("Deleted credential %q\n", model.CredentialsRef) | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| credentialRef := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| CredentialsRef: credentialRef, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiDeleteCredentialsRequest { | ||
| return apiClient.DeleteCredentials(ctx, model.ProjectId, model.Region, model.CredentialsRef) | ||
| } |
| package describe | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testClient = &alb.APIClient{} | ||
| testCredentialRef = "credential-12345" | ||
| ) | ||
| func fixtureArgValues(mods ...func(argVales []string)) []string { | ||
| argVales := []string{ | ||
| testCredentialRef, | ||
| } | ||
| for _, m := range mods { | ||
| m(argVales) | ||
| } | ||
| return argVales | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| } | ||
| for _, m := range mods { | ||
| m(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| }, | ||
| CredentialRef: testCredentialRef, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *alb.ApiGetCredentialsRequest)) alb.ApiGetCredentialsRequest { | ||
| request := testClient.GetCredentials(testCtx, testProjectId, testRegion, testCredentialRef) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argsValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argsValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argsValues: []string{}, | ||
| flagValues: map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| }, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no arg values", | ||
| argsValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flag values", | ||
| argsValues: fixtureArgValues(), | ||
| flagValues: map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| }, | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err = cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argsValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argsValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedResult alb.ApiGetCredentialsRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedResult: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedResult, | ||
| cmp.AllowUnexported(tt.expectedResult), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func Test_outputResult(t *testing.T) { | ||
| type args struct { | ||
| outputFormat string | ||
| showOnlyPublicKey bool | ||
| response alb.CredentialsResponse | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "base", | ||
| args: args{ | ||
| outputFormat: "", | ||
| showOnlyPublicKey: false, | ||
| response: alb.CredentialsResponse{}, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| } | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(p) | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr { | ||
| t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package describe | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| const ( | ||
| credentialRefArg = "CREDENTIAL_REF" // nolint:gosec // false alert, these are not valid credentials | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| CredentialRef string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("describe %s", credentialRefArg), | ||
| Short: "Describes observability credentials for the Application Load Balancer", | ||
| Long: "Describes observability credentials for the Application Load Balancer.", | ||
| Args: args.SingleArg(credentialRefArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Get details about credentials with name "credential-12345"`, | ||
| "$ stackit beta alb observability-credentials describe credential-12345", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("read credentials: %w", err) | ||
| } | ||
| if credential := resp; credential != nil && credential.Credential != nil { | ||
| return outputResult(p, model.OutputFormat, *credential.Credential) | ||
| } | ||
| p.Outputln("No credentials found.") | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| credentialRef := inputArgs[0] | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| CredentialRef: credentialRef, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiGetCredentialsRequest { | ||
| return apiClient.GetCredentials(ctx, model.ProjectId, model.Region, model.CredentialRef) | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, response alb.CredentialsResponse) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(response, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal credentials: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(response, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal credentials: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| table.AddRow("CREDENTIAL REF", utils.PtrString(response.CredentialsRef)) | ||
| table.AddSeparator() | ||
| table.AddRow("DISPLAYNAME", utils.PtrString(response.DisplayName)) | ||
| table.AddSeparator() | ||
| table.AddRow("UESRNAME", utils.PtrString(response.Username)) | ||
| table.AddSeparator() | ||
| table.AddRow("REGION", utils.PtrString(response.Region)) | ||
| table.AddSeparator() | ||
| p.Outputln(table.Render()) | ||
| } | ||
| return nil | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "strconv" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") | ||
| testClient = &alb.APIClient{} | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testLimit = int64(64) | ||
| ) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| limitFlag: strconv.FormatInt(testLimit, 10), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(inputModel *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| }, | ||
| Limit: utils.Ptr(testLimit), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *alb.ApiListCredentialsRequest)) alb.ApiListCredentialsRequest { | ||
| request := testClient.ListCredentials(testCtx, testProjectId, testRegion) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| }, | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(inputModel *inputModel) { | ||
| inputModel.Limit = nil | ||
| }), | ||
| }, | ||
| { | ||
| description: "withoutLimit", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, "limit") | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(inputModel *inputModel) { | ||
| inputModel.Limit = nil | ||
| }), | ||
| }, | ||
| { | ||
| description: "invalid limit 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "invalid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "invalid limit 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "0" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "label selector empty", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err = cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatal("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest alb.ApiListCredentialsRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("request does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func Test_outputResult(t *testing.T) { | ||
| type args struct { | ||
| outputFormat string | ||
| response []alb.CredentialsResponse | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{ | ||
| outputFormat: "", | ||
| response: []alb.CredentialsResponse{ | ||
| {}, | ||
| }, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(p) | ||
| if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr { | ||
| t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| const ( | ||
| limitFlag = "limit" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Limit *int64 | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "Lists all credentials", | ||
| Long: "Lists all credentials.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Lists all credentials`, | ||
| "$ stackit beta alb observability-credentials list", | ||
| ), | ||
| examples.NewExample( | ||
| `Lists all credentials in JSON format`, | ||
| "$ stackit beta alb observability-credentials list --output-format json", | ||
| ), | ||
| examples.NewExample( | ||
| `Lists up to 10 credentials`, | ||
| "$ stackit beta alb observability-credentials list --limit 10", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("list credentials: %w", err) | ||
| } | ||
| if resp.Credentials == nil || len(*resp.Credentials) == 0 { | ||
| p.Info("No credentials found\n") | ||
| return nil | ||
| } | ||
| items := *resp.Credentials | ||
| if model.Limit != nil && len(items) > int(*model.Limit) { | ||
| items = items[:*model.Limit] | ||
| } | ||
| return outputResult(p, model.OutputFormat, items) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Int64(limitFlag, 0, "Number of credentials to list") | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) | ||
| if limit != nil && *limit < 1 { | ||
| return nil, &errors.FlagValidationError{ | ||
| Flag: limitFlag, | ||
| Details: "must be greater than 0", | ||
| } | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Limit: limit, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.InfoLevel, modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiListCredentialsRequest { | ||
| req := apiClient.ListCredentials(ctx, model.ProjectId, model.Region) | ||
| return req | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, items []alb.CredentialsResponse) error { | ||
| if items == nil { | ||
| p.Outputln("no credentials found") | ||
| return nil | ||
| } | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(items, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal credentials: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal credentials: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| default: | ||
| table := tables.NewTable() | ||
| table.SetHeader("CREDENTIAL REF", "DISPLAYNAME", "USERNAME", "REGION") | ||
| for _, item := range items { | ||
| table.AddRow( | ||
| utils.PtrString(item.CredentialsRef), | ||
| utils.PtrString(item.DisplayName), | ||
| utils.PtrString(item.Username), | ||
| utils.PtrString(item.Region), | ||
| ) | ||
| } | ||
| p.Outputln(table.Render()) | ||
| } | ||
| return nil | ||
| } |
| package credentials | ||
| import ( | ||
| "github.com/spf13/cobra" | ||
| add "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/add" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/delete" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/describe" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/list" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/update" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| ) | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "observability-credentials", | ||
| Short: "Provides functionality for application loadbalancer credentials", | ||
| Long: "Provides functionality for application loadbalancer credentials", | ||
| Args: cobra.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, p) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, p *print.Printer) { | ||
| cmd.AddCommand(add.NewCmd(p)) | ||
| cmd.AddCommand(delete.NewCmd(p)) | ||
| cmd.AddCommand(describe.NewCmd(p)) | ||
| cmd.AddCommand(list.NewCmd(p)) | ||
| cmd.AddCommand(update.NewCmd(p)) | ||
| } |
| package update | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &alb.APIClient{} | ||
| var ( | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testCredentialRef = "credential-12345" | ||
| testDisplayname = "displayname" | ||
| testUsername = "testuser" | ||
| testPassword = "testpassword" | ||
| ) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| usernameFlag: testUsername, | ||
| displaynameFlag: testDisplayname, | ||
| passwordFlag: testPassword, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) inputModel { | ||
| model := inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| }, | ||
| Username: &testUsername, | ||
| Displayname: &testDisplayname, | ||
| CredentialsRef: &testCredentialRef, | ||
| Password: &testPassword, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(&model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *alb.ApiUpdateCredentialsRequest)) alb.ApiUpdateCredentialsRequest { | ||
| request := testClient.UpdateCredentials(testCtx, testProjectId, testRegion, testCredentialRef) | ||
| request = request.UpdateCredentialsPayload(fixturePayload()) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func fixturePayload(mods ...func(payload *alb.UpdateCredentialsPayload)) alb.UpdateCredentialsPayload { | ||
| payload := alb.UpdateCredentialsPayload{ | ||
| DisplayName: &testDisplayname, | ||
| Password: &testPassword, | ||
| Username: &testUsername, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(&payload) | ||
| } | ||
| return payload | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| args []string | ||
| isValid bool | ||
| expectedModel inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| args: []string{testCredentialRef}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| args: []string{testCredentialRef}, | ||
| flagValues: map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| }, | ||
| isValid: false, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Username = nil | ||
| model.Displayname = nil | ||
| }), | ||
| }, | ||
| { | ||
| description: "required values", | ||
| args: []string{testCredentialRef}, | ||
| flagValues: map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| usernameFlag: testUsername, | ||
| displaynameFlag: testDisplayname, | ||
| passwordFlag: testPassword, | ||
| }, | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err = cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model := parseInput(p, cmd, tt.args) | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model inputModel | ||
| expectedRequest alb.ApiUpdateCredentialsRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request, err := buildRequest(testCtx, &tt.model, testClient) | ||
| if err != nil { | ||
| t.Fatalf("cannot build request: %v", err) | ||
| } | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| cmp.AllowUnexported(alb.NullableString{}), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func Test_outputResult(t *testing.T) { | ||
| type args struct { | ||
| item *alb.UpdateCredentialsResponse | ||
| model inputModel | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{ | ||
| item: nil, | ||
| model: fixtureInputModel(), | ||
| }, | ||
| wantErr: true, | ||
| }, | ||
| } | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(p) | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if err := outputResult(p, tt.args.model, tt.args.item); (err != nil) != tt.wantErr { | ||
| t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package update | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| const ( | ||
| usernameFlag = "username" | ||
| displaynameFlag = "displayname" | ||
| passwordFlag = "password" | ||
| credentialRefArg = "CREDENTIAL_REF_ARG" //nolint:gosec // false alert, these are not valid credentials | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Username *string | ||
| Displayname *string | ||
| Password *string | ||
| CredentialsRef *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("update %s", credentialRefArg), | ||
| Short: "Update credentials", | ||
| Long: "Update credentials.", | ||
| Args: args.SingleArg(credentialRefArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Update the password of observability credentials of Application Load Balancer with credentials reference "credentials-xxx", by providing the path to a file with the new password as flag`, | ||
| "$ stackit beta alb observability-credentials update credentials-xxx --username user1 --displayname user1 --password @./new-password.txt"), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model := parseInput(p, cmd, args) | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| req, err := buildRequest(ctx, &model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, p, cmd) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| projectLabel = model.ProjectId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update credential %q for %q?", *model.CredentialsRef, projectLabel) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return fmt.Errorf("update credential: %w", err) | ||
| } | ||
| } | ||
| // Call API | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("update credential: %w", err) | ||
| } | ||
| if resp == nil { | ||
| return fmt.Errorf("response is nil") | ||
| } | ||
| return outputResult(p, model, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringP(usernameFlag, "u", "", "Username for the credentials") | ||
| cmd.Flags().StringP(displaynameFlag, "d", "", "Displayname for the credentials") | ||
| cmd.Flags().Var(flags.ReadFromFileFlag(), passwordFlag, `Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).`) | ||
| cobra.CheckErr(flags.MarkFlagsRequired(cmd, displaynameFlag, usernameFlag)) | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) (req alb.ApiUpdateCredentialsRequest, err error) { | ||
| req = apiClient.UpdateCredentials(ctx, model.ProjectId, model.Region, *model.CredentialsRef) | ||
| payload := alb.UpdateCredentialsPayload{ | ||
| DisplayName: model.Displayname, | ||
| Password: model.Password, | ||
| Username: model.Username, | ||
| } | ||
| if model.Displayname == nil && model.Username == nil { | ||
| return req, fmt.Errorf("no attribute to change passed") | ||
| } | ||
| return req.UpdateCredentialsPayload(payload), nil | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) inputModel { | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalflags.Parse(p, cmd), | ||
| Username: flags.FlagToStringPointer(p, cmd, usernameFlag), | ||
| Displayname: flags.FlagToStringPointer(p, cmd, displaynameFlag), | ||
| CredentialsRef: &inputArgs[0], | ||
| Password: flags.FlagToStringPointer(p, cmd, passwordFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return model | ||
| } | ||
| func outputResult(p *print.Printer, model inputModel, response *alb.UpdateCredentialsResponse) error { | ||
| var outputFormat string | ||
| if model.GlobalFlagModel != nil { | ||
| outputFormat = model.GlobalFlagModel.OutputFormat | ||
| } | ||
| if response == nil { | ||
| return fmt.Errorf("no response passewd") | ||
| } | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(response.Credential, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal credential: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(response.Credential, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal credential: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| default: | ||
| p.Outputf("Updated credential %q\n", utils.PtrString(model.CredentialsRef)) | ||
| } | ||
| return nil | ||
| } |
| package plans | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testClient = &alb.APIClient{} | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| ) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Region: testRegion, Verbosity: globalflags.VerbosityDefault}, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *alb.ApiListPlansRequest)) alb.ApiListPlansRequest { | ||
| request := testClient.ListPlans(testCtx, testRegion) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, globalflags.ProjectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| if err := globalflags.Configure(cmd.Flags()); err != nil { | ||
| t.Errorf("cannot configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| if err := cmd.ValidateRequiredFlags(); err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest alb.ApiListPlansRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| cmpopts.IgnoreFields(alb.ApiListLoadBalancersRequest{}, "ctx"), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func Test_outputResult(t *testing.T) { | ||
| type args struct { | ||
| outputFormat string | ||
| items []alb.PlanDetails | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{ | ||
| outputFormat: "", | ||
| items: []alb.PlanDetails{}, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| { | ||
| name: "output format json", | ||
| args: args{ | ||
| outputFormat: print.JSONOutputFormat, | ||
| items: []alb.PlanDetails{}, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| } | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(p) | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr { | ||
| t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package plans | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "plans", | ||
| Short: "Lists the application load balancer plans", | ||
| Long: "Lists the available application load balancer plans.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `List all application load balancer plans`, | ||
| `$ stackit beta alb plans`, | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, p, cmd) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| projectLabel = model.ProjectId | ||
| } else if projectLabel == "" { | ||
| projectLabel = model.ProjectId | ||
| } | ||
| // Call API | ||
| request := buildRequest(ctx, model, apiClient) | ||
| response, err := request.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("list plans: %w", err) | ||
| } | ||
| if items := response.ValidPlans; items == nil || len(*items) == 0 { | ||
| p.Info("No plans found for project %q", projectLabel) | ||
| } else { | ||
| if err := outputResult(p, model.OutputFormat, *items); err != nil { | ||
| return fmt.Errorf("output plans: %w", err) | ||
| } | ||
| } | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiListPlansRequest { | ||
| request := apiClient.ListPlans(ctx, model.Region) | ||
| return request | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, items []alb.PlanDetails) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(items, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal plans: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal plans: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| table.SetHeader("PLAN ID", "NAME", "FLAVOR", "MAX CONNS", "DESCRIPTION") | ||
| for _, item := range items { | ||
| table.AddRow(utils.PtrString(item.PlanId), | ||
| utils.PtrString(item.Name), | ||
| utils.PtrString(item.FlavorName), | ||
| utils.PtrString(item.MaxConnections), | ||
| utils.Truncate(item.Description, 70), | ||
| ) | ||
| } | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| } |
| package pool | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/pool/update" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "pool", | ||
| Short: "Manages target pools for application loadbalancers", | ||
| Long: "Manage the lifecycle of target pools for application loadbalancers.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, p) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, p *print.Printer) { | ||
| cmd.AddCommand(update.NewCmd(p)) | ||
| } |
| { | ||
| "activeHealthCheck": { | ||
| "healthyThreshold": 1, | ||
| "httpHealthChecks": { | ||
| "okStatuses": [ | ||
| "string" | ||
| ], | ||
| "path": "string" | ||
| }, | ||
| "interval": "3s", | ||
| "intervalJitter": "3s", | ||
| "timeout": "3s", | ||
| "unhealthyThreshold": 1 | ||
| }, | ||
| "name": "my-target-pool", | ||
| "targetPort": 5732, | ||
| "targets": [ | ||
| { | ||
| "displayName": "my-target", | ||
| "ip": "192.0.2.5" | ||
| } | ||
| ], | ||
| "tlsConfig": { | ||
| "customCa": "string", | ||
| "enabled": true, | ||
| "skipCertificateValidation": true | ||
| } | ||
| } |
| package update | ||
| import ( | ||
| "context" | ||
| _ "embed" | ||
| "encoding/json" | ||
| "log" | ||
| "testing" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| //go:embed testdata/testconfig.json | ||
| var testConfiguration []byte | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testClient = &alb.APIClient{} | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testLoadBalancer = "my-load-balancer" | ||
| testPool = "my-target-pool" | ||
| testConfig = "testdata/testconfig.json" | ||
| ) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| configurationFlag: testConfig, | ||
| globalflags.RegionFlag: testRegion, | ||
| albNameFlag: testLoadBalancer, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| Region: testRegion, | ||
| }, | ||
| Configuration: utils.Ptr(testConfig), | ||
| AlbName: &testLoadBalancer, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixturePayload(mods ...func(payload *alb.UpdateTargetPoolPayload)) (payload alb.UpdateTargetPoolPayload) { | ||
| if err := json.Unmarshal(testConfiguration, &payload); err != nil { | ||
| log.Panicf("cannot deserialize test configuration: %v", err) | ||
| } | ||
| for _, f := range mods { | ||
| f(&payload) | ||
| } | ||
| return payload | ||
| } | ||
| func fixtureRequest(mods ...func(request *alb.ApiUpdateTargetPoolRequest)) alb.ApiUpdateTargetPoolRequest { | ||
| request := testClient.UpdateTargetPool(testCtx, testProjectId, testRegion, testLoadBalancer, testPool) | ||
| request = request.UpdateTargetPoolPayload(fixturePayload()) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "required fields only", | ||
| flagValues: map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| configurationFlag: testConfig, | ||
| albNameFlag: testLoadBalancer, | ||
| }, | ||
| isValid: true, | ||
| expectedModel: &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| Configuration: &testConfig, | ||
| AlbName: &testLoadBalancer, | ||
| }, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest alb.ApiUpdateTargetPoolRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| { | ||
| description: "required fields only", | ||
| model: &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| Configuration: &testConfig, | ||
| AlbName: &testLoadBalancer, | ||
| }, | ||
| expectedRequest: testClient. | ||
| UpdateTargetPool(testCtx, testProjectId, testRegion, testLoadBalancer, testPool). | ||
| UpdateTargetPoolPayload(fixturePayload()), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request, err := buildRequest(testCtx, tt.model, testClient) | ||
| if err != nil { | ||
| t.Fatalf("cannot create request: %v", err) | ||
| } | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestOutputResult(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| projectLabel string | ||
| resp *alb.TargetPool | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{}, | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "empty response as argument", | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| resp: &alb.TargetPool{}, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| } | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(p) | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { | ||
| t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package update | ||
| import ( | ||
| "bufio" | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "os" | ||
| "strings" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| const ( | ||
| configurationFlag = "configuration" | ||
| albNameFlag = "name" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Configuration *string | ||
| AlbName *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "update", | ||
| Short: "Updates an application target pool", | ||
| Long: "Updates an application target pool.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Update an application target pool from a configuration file (the name of the pool is read from the file)`, | ||
| "$ stackit beta alb update --configuration my-target-pool.json --name my-load-balancer"), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, p, cmd) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| projectLabel = model.ProjectId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update an application target pool for project %q?", projectLabel) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req, err := buildRequest(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("update application target pool: %w", err) | ||
| } | ||
| return outputResult(p, model, projectLabel, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringP(configurationFlag, "c", "", "Filename of the input configuration file") | ||
| cmd.Flags().StringP(albNameFlag, "n", "", "Name of the target pool name to update") | ||
| err := flags.MarkFlagsRequired(cmd, configurationFlag, albNameFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Configuration: flags.FlagToStringPointer(p, cmd, configurationFlag), | ||
| AlbName: flags.FlagToStringPointer(p, cmd, albNameFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) (req alb.ApiUpdateTargetPoolRequest, err error) { | ||
| payload, err := readPayload(ctx, model) | ||
| if err != nil { | ||
| return req, err | ||
| } | ||
| if payload.Name == nil { | ||
| return req, fmt.Errorf("update target pool: no poolname provided") | ||
| } | ||
| req = apiClient.UpdateTargetPool(ctx, model.ProjectId, model.Region, *model.AlbName, *payload.Name) | ||
| return req.UpdateTargetPoolPayload(payload), nil | ||
| } | ||
| func readPayload(_ context.Context, model *inputModel) (payload alb.UpdateTargetPoolPayload, err error) { | ||
| if model.Configuration == nil { | ||
| return payload, fmt.Errorf("no configuration file defined") | ||
| } | ||
| file, err := os.Open(*model.Configuration) | ||
| if err != nil { | ||
| return payload, fmt.Errorf("cannot open configuration file %q: %w", *model.Configuration, err) | ||
| } | ||
| defer file.Close() // nolint:errcheck // at this point close errors are not relevant anymore | ||
| if strings.HasSuffix(*model.Configuration, ".yaml") { | ||
| decoder := yaml.NewDecoder(bufio.NewReader(file), yaml.UseJSONUnmarshaler()) | ||
| if err := decoder.Decode(&payload); err != nil { | ||
| return payload, fmt.Errorf("cannot deserialize yaml configuration from %q: %w", *model.Configuration, err) | ||
| } | ||
| } else if strings.HasSuffix(*model.Configuration, ".json") { | ||
| decoder := json.NewDecoder(bufio.NewReader(file)) | ||
| if err := decoder.Decode(&payload); err != nil { | ||
| return payload, fmt.Errorf("cannot deserialize json configuration from %q: %w", *model.Configuration, err) | ||
| } | ||
| } else { | ||
| return payload, fmt.Errorf("cannot determine configuration fileformat of %q by extension. Must be '.json' or '.yaml'", *model.Configuration) | ||
| } | ||
| return payload, nil | ||
| } | ||
| func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *alb.TargetPool) error { | ||
| if resp == nil { | ||
| return fmt.Errorf("update target pool response is empty") | ||
| } | ||
| switch model.OutputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(resp, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal target pool: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal target pool: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| operationState := "Updated" | ||
| if model.Async { | ||
| operationState = "Triggered update of" | ||
| } | ||
| p.Outputf("%s application target pool for %q. Name: %s\n", operationState, projectLabel, utils.PtrString(resp.Name)) | ||
| return nil | ||
| } | ||
| } |
| package quotas | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testClient = &alb.APIClient{} | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| ) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Region: testRegion, Verbosity: globalflags.VerbosityDefault}, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *alb.ApiGetQuotaRequest)) alb.ApiGetQuotaRequest { | ||
| request := testClient.GetQuota(testCtx, testProjectId, testRegion) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, globalflags.ProjectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| if err := globalflags.Configure(cmd.Flags()); err != nil { | ||
| t.Errorf("cannot configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| if err := cmd.ValidateRequiredFlags(); err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest alb.ApiGetQuotaRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| cmpopts.IgnoreFields(alb.ApiListLoadBalancersRequest{}, "ctx"), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func Test_outputResult(t *testing.T) { | ||
| type args struct { | ||
| outputFormat string | ||
| response alb.GetQuotaResponse | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{ | ||
| outputFormat: "", | ||
| response: alb.GetQuotaResponse{}, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| { | ||
| name: "output format json", | ||
| args: args{ | ||
| outputFormat: print.JSONOutputFormat, | ||
| response: alb.GetQuotaResponse{}, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| } | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(p) | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr { | ||
| t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package quotas | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "quotas", | ||
| Short: "Shows the application load balancer quotas", | ||
| Long: "Shows the application load balancer quotas for the application load balancers.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `List all application load balancer quotas`, | ||
| `$ stackit beta alb quotas`, | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
| request := buildRequest(ctx, model, apiClient) | ||
| response, err := request.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("get quotas: %w", err) | ||
| } | ||
| if response == nil { | ||
| p.Outputln("no quotas found") | ||
| return nil | ||
| } | ||
| if err := outputResult(p, model.OutputFormat, *response); err != nil { | ||
| return fmt.Errorf("output loadbalancers: %w", err) | ||
| } | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiGetQuotaRequest { | ||
| request := apiClient.GetQuota(ctx, model.ProjectId, model.Region) | ||
| return request | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, response alb.GetQuotaResponse) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(response, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal quotas: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(response, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal quotas: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| table.AddRow("REGION", utils.PtrString(response.Region)) | ||
| table.AddSeparator() | ||
| table.AddRow("MAX LOADBALANCERS", utils.PtrString(response.MaxLoadBalancers)) | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| } |
| package template | ||
| import ( | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/uuid" | ||
| ) | ||
| var ( | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| ) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Region: testRegion, Verbosity: globalflags.VerbosityDefault}, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, globalflags.ProjectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "alb with yaml", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[formatFlag] = "yaml" | ||
| flagValues[typeFlag] = "alb" | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Format = utils.Ptr("yaml") | ||
| model.Type = utils.Ptr("alb") | ||
| }), | ||
| }, { | ||
| description: "alb with yaml", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[formatFlag] = "yaml" | ||
| flagValues[typeFlag] = "alb" | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Format = utils.Ptr("yaml") | ||
| model.Type = utils.Ptr("alb") | ||
| }), | ||
| }, { | ||
| description: "alb with json", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[formatFlag] = "json" | ||
| flagValues[typeFlag] = "alb" | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Format = utils.Ptr("json") | ||
| model.Type = utils.Ptr("alb") | ||
| }), | ||
| }, { | ||
| description: "pool with yaml", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[formatFlag] = "yaml" | ||
| flagValues[typeFlag] = "pool" | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Format = utils.Ptr("yaml") | ||
| model.Type = utils.Ptr("pool") | ||
| }), | ||
| }, | ||
| { | ||
| description: "pool with json", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[formatFlag] = "json" | ||
| flagValues[typeFlag] = "pool" | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Format = utils.Ptr("json") | ||
| model.Type = utils.Ptr("pool") | ||
| }), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| if err := globalflags.Configure(cmd.Flags()); err != nil { | ||
| t.Errorf("cannot configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| if err := cmd.ValidateRequiredFlags(); err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| name: my-load-balancer | ||
| # public ip, must be removed when ephemeral address option is true | ||
| externalAddress: 123.123.123.123 | ||
| # public listening interfaces of the loadbalancer | ||
| listeners: | ||
| - displayName: listener1 | ||
| # for plain http the https block must be removed | ||
| # http: {} | ||
| https: | ||
| certificateConfig: | ||
| certificateIds: | ||
| - cert-1 | ||
| - cert-2 | ||
| - cert-3 | ||
| port: 443 | ||
| # protocal may be PROTOCOL_HTTPS or PROTOCOL_HTTP | ||
| protocol: PROTOCOL_HTTPS | ||
| rules: | ||
| # fqdn of the virtual host of the load balancer | ||
| - host: front.facing.host | ||
| http: | ||
| subRules: | ||
| - cookiePersistence: | ||
| name: cookie1 | ||
| ttl: 120s | ||
| headers: | ||
| - name: testheader1 | ||
| exactMatch: X-test-header1 | ||
| - name: testheader2 | ||
| exactMatch: X-test-header2 | ||
| - name: testheader3 | ||
| exactMatch: X-test-header3 | ||
| pathPrefix: /foo | ||
| queryParameters: | ||
| - name: query-param | ||
| exactMatch: q | ||
| - name: region | ||
| exactMatch: region | ||
| targetPool: my-target-pool | ||
| webSocket: false | ||
| networks: | ||
| - networkId: 00000000-0000-0000-0000-000000000000 | ||
| role: ROLE_LISTENERS_AND_TARGETS | ||
| - networkId: 00000000-0000-0000-0000-000000000001 | ||
| role: ROLE_LISTENERS_AND_TARGETS | ||
| options: | ||
| accessControl: | ||
| # which host may access the loadbalancer in prefix notation | ||
| allowedSourceRanges: | ||
| - 10.100.42.0/24 | ||
| ephemeralAddress: true | ||
| privateNetworkOnly: true | ||
| # Enable observability features. Appropriate credentials must be made | ||
| # available using the credentials endpoint | ||
| # observability: | ||
| # logs: | ||
| # credentialsRef: my-credentials | ||
| # pushUrl: https://my.observability.host/<observability-instance-id>/loki/api/v1/push | ||
| # metrics: | ||
| # credentialsRef: my-credentials | ||
| # pushUrl: https://my.observability.host/<observability-instance-id>/<argus-instance-id>/api/v1/receive | ||
| planId: p10 | ||
| # definition of the backend servers | ||
| targetPools: | ||
| - name: my-target-pool | ||
| activeHealthCheck: | ||
| healthyThreshold: 3 | ||
| httpHealthChecks: | ||
| okStatuses: | ||
| - "200" | ||
| path: /health | ||
| interval: 10s | ||
| intervalJitter: 3s | ||
| timeout: 5s | ||
| unhealthyThreshold: 1 | ||
| targetPort: 5732 | ||
| targets: | ||
| # configuration of the backend servers | ||
| - displayName: my-target1 | ||
| ip: 192.11.2.5 | ||
| # if the backend servers must be accessed via TLS the following block | ||
| # allows defining the TLS configuration | ||
| # tlsConfig: | ||
| # # A PEM and base64 encoded certificate | ||
| # customCa: LS0t... | ||
| # enabled: true | ||
| # skipCertificateValidation: false |
| activeHealthCheck: | ||
| healthyThreshold: 1 | ||
| httpHealthChecks: | ||
| okStatuses: | ||
| - "200" | ||
| path: /health | ||
| interval: 3s | ||
| intervalJitter: 3s | ||
| timeout: 3s | ||
| unhealthyThreshold: 1 | ||
| name: my-target-pool | ||
| targetPort: 4000 | ||
| targets: | ||
| - displayName: my-target | ||
| ip: 10.0.1.155 | ||
| # if the backend servers must be accessed via TLS the following block | ||
| # allows defining the TLS configuration | ||
| # tlsConfig: | ||
| # # A PEM and base64 encoded certificate | ||
| # customCa: LS0... | ||
| # enabled: true | ||
| # skipCertificateValidation: false | ||
| package template | ||
| import ( | ||
| _ "embed" | ||
| "encoding/json" | ||
| "fmt" | ||
| "os" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| const ( | ||
| formatFlag = "format" | ||
| typeFlag = "type" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Format *string | ||
| Type *string | ||
| } | ||
| var ( | ||
| //go:embed template-alb.yaml | ||
| templateAlb string | ||
| //go:embed template-pool.yaml | ||
| templatePool string | ||
| ) | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "template", | ||
| Short: "creates configuration templates to use for resource creation", | ||
| Long: "creates a json or yaml template file for creating/updating an application loadbalancer or target pool.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Create a yaml template`, | ||
| `$ stackit beta alb template --format=yaml --type alb`, | ||
| ), | ||
| examples.NewExample( | ||
| `Create a json template`, | ||
| `$ stackit beta alb template --format=json --type pool`, | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| var ( | ||
| template string | ||
| target any | ||
| ) | ||
| if model.Type != nil && *model.Type == "pool" { | ||
| template = templatePool | ||
| target = alb.CreateLoadBalancerPayload{} | ||
| } else if model.Type == nil || *model.Type == "alb" { | ||
| template = templateAlb | ||
| target = alb.UpdateTargetPoolPayload{} | ||
| } else { | ||
| return fmt.Errorf("invalid type %q", utils.PtrString(model.Type)) | ||
| } | ||
| if model.Format == nil || *model.Format == "yaml" { | ||
| p.Outputln(template) | ||
| } else if *model.Format == "json" { | ||
| if err := yaml.Unmarshal([]byte(template), &target); err != nil { | ||
| return fmt.Errorf("cannot unmarshal template: %w", err) | ||
| } | ||
| encoder := json.NewEncoder(os.Stdout) | ||
| if err := encoder.Encode(target); err != nil { | ||
| return fmt.Errorf("cannot marshal template to yaml: %w", err) | ||
| } | ||
| } else { | ||
| return fmt.Errorf("invalid format %q defined. Must be 'json' or 'yaml'", *model.Format) | ||
| } | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().VarP(flags.EnumFlag(true, "json", "json", "yaml"), formatFlag, "f", "Defines the output format ('yaml' or 'json'), default is 'json'") | ||
| cmd.Flags().VarP(flags.EnumFlag(true, "alb", "alb", "pool"), typeFlag, "t", "Defines the output type ('alb' or 'pool'), default is 'alb'") | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Format: flags.FlagToStringPointer(p, cmd, formatFlag), | ||
| Type: flags.FlagToStringPointer(p, cmd, typeFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } |
| { | ||
| "externalAddress": "10.100.42.1", | ||
| "listeners": [ | ||
| { | ||
| "displayName": "listener1", | ||
| "http": {}, | ||
| "https": { | ||
| "certificateConfig": { | ||
| "certificateIds": [ | ||
| "cert-1", | ||
| "cert-2", | ||
| "cert-3" | ||
| ] | ||
| } | ||
| }, | ||
| "port": 443, | ||
| "protocol": "PROTOCOL_HTTPS", | ||
| "rules": [ | ||
| { | ||
| "host": "front.facing.host", | ||
| "http": { | ||
| "subRules": [ | ||
| { | ||
| "cookiePersistence": { | ||
| "name": "cookie1", | ||
| "ttl": "120s" | ||
| }, | ||
| "headers": [ | ||
| { | ||
| "name": "testheader1", | ||
| "exactMatch": "X-test-header1" | ||
| }, | ||
| { | ||
| "name": "testheader2", | ||
| "exactMatch": "X-test-header2" | ||
| }, | ||
| { | ||
| "name": "testheader3", | ||
| "exactMatch": "X-test-header3" | ||
| } | ||
| ], | ||
| "pathPrefix": "/foo", | ||
| "queryParameters": [ | ||
| { | ||
| "name": "query-param", | ||
| "exactMatch": "q" | ||
| }, | ||
| { | ||
| "name": "region", | ||
| "exactMatch": "region" | ||
| } | ||
| ], | ||
| "targetPool": "my-target-pool", | ||
| "webSocket": false | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "name": "my-load-balancer", | ||
| "networks": [ | ||
| { | ||
| "networkId": "00000000-0000-0000-0000-000000000000", | ||
| "role": "ROLE_LISTENERS_AND_TARGETS" | ||
| }, | ||
| { | ||
| "networkId": "00000000-0000-0000-0000-000000000001", | ||
| "role": "ROLE_LISTENERS_AND_TARGETS" | ||
| } | ||
| ], | ||
| "options": { | ||
| "accessControl": { | ||
| "allowedSourceRanges": [ | ||
| "192.168.42.0-192.168.42.10", | ||
| "192.168.54.0-192.168.54.10" | ||
| ] | ||
| }, | ||
| "ephemeralAddress": true, | ||
| "observability": { | ||
| "logs": { | ||
| "credentialsRef": "my-credentials", | ||
| "pushUrl": "https://my.observability.host/<observability-instance-id>/loki/api/v1/push" | ||
| }, | ||
| "metrics": { | ||
| "credentialsRef": "my-credentials", | ||
| "pushUrl": "https://my.observability.host/<observability-instance-id>/<argus-instance-id>/api/v1/receive" | ||
| } | ||
| }, | ||
| "privateNetworkOnly": true | ||
| }, | ||
| "planId": "p10", | ||
| "targetPools": [ | ||
| { | ||
| "activeHealthCheck": { | ||
| "healthyThreshold": 3, | ||
| "httpHealthChecks": { | ||
| "okStatuses": [ | ||
| "200", | ||
| "204" | ||
| ], | ||
| "path": "/health" | ||
| }, | ||
| "interval": "10s", | ||
| "intervalJitter": "3s", | ||
| "timeout": "5s", | ||
| "unhealthyThreshold": 1 | ||
| }, | ||
| "name": "my-target-pool", | ||
| "targetPort": 5732, | ||
| "targets": [ | ||
| { | ||
| "displayName": "my-target1", | ||
| "ip": "192.11.2.5" | ||
| } | ||
| ], | ||
| "tlsConfig": { | ||
| "customCa": "my.private.ca", | ||
| "enabled": true, | ||
| "skipCertificateValidation": false | ||
| } | ||
| } | ||
| ] | ||
| } |
| package update | ||
| import ( | ||
| "context" | ||
| _ "embed" | ||
| "encoding/json" | ||
| "log" | ||
| "testing" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| //go:embed testdata/testconfig.json | ||
| var testConfiguration []byte | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testClient = &alb.APIClient{} | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testLoadBalancer = "my-load-balancer" | ||
| testConfig = "testdata/testconfig.json" | ||
| ) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| configurationFlag: testConfig, | ||
| globalflags.RegionFlag: testRegion, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| Region: testRegion, | ||
| }, | ||
| Configuration: utils.Ptr(testConfig), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixturePayload(mods ...func(payload *alb.UpdateLoadBalancerPayload)) (payload alb.UpdateLoadBalancerPayload) { | ||
| if err := json.Unmarshal(testConfiguration, &payload); err != nil { | ||
| log.Panicf("cannot deserialize test configuration: %v", err) | ||
| } | ||
| for _, f := range mods { | ||
| f(&payload) | ||
| } | ||
| return payload | ||
| } | ||
| func fixtureRequest(mods ...func(request *alb.ApiUpdateLoadBalancerRequest)) alb.ApiUpdateLoadBalancerRequest { | ||
| request := testClient.UpdateLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancer) | ||
| request = request.UpdateLoadBalancerPayload(fixturePayload()) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "required fields only", | ||
| flagValues: map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| configurationFlag: testConfig, | ||
| }, | ||
| isValid: true, | ||
| expectedModel: &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| Configuration: &testConfig, | ||
| }, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest alb.ApiUpdateLoadBalancerRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| { | ||
| description: "required fields only", | ||
| model: &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| Configuration: &testConfig, | ||
| }, | ||
| expectedRequest: testClient. | ||
| UpdateLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancer). | ||
| UpdateLoadBalancerPayload(fixturePayload()), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request, err := buildRequest(testCtx, tt.model, testClient) | ||
| if err != nil { | ||
| t.Fatalf("cannot create request: %v", err) | ||
| } | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestOutputResult(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| projectLabel string | ||
| resp *alb.LoadBalancer | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{}, | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "empty response as argument", | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| resp: &alb.LoadBalancer{}, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| } | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(p) | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { | ||
| t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package update | ||
| import ( | ||
| "bufio" | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "os" | ||
| "strings" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb/wait" | ||
| ) | ||
| const ( | ||
| configurationFlag = "configuration" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Configuration *string | ||
| Version *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "update", | ||
| Short: "Updates an application loadbalancer", | ||
| Long: "Updates an application loadbalancer.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Update an application loadbalancer from a configuration file`, | ||
| "$ stackit beta alb update --configuration my-loadbalancer.json"), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, p, cmd) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| projectLabel = model.ProjectId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update an application loadbalancer for project %q?", projectLabel) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // for updates of an existing ALB the current version must be passed to the request | ||
| model.Version, err = getCurrentAlbVersion(ctx, apiClient, model) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
| req, err := buildRequest(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("update application loadbalancer: %w", err) | ||
| } | ||
| // Wait for async operation, if async mode not enabled | ||
| if !model.Async { | ||
| s := spinner.New(p) | ||
| s.Start("updating loadbalancer") | ||
| _, err = wait.CreateOrUpdateLoadbalancerWaitHandler(ctx, apiClient, model.ProjectId, model.Region, *resp.Name). | ||
| WaitWithContext(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("wait for loadbalancer update: %w", err) | ||
| } | ||
| s.Stop() | ||
| } | ||
| return outputResult(p, model, projectLabel, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringP(configurationFlag, "c", "", "Filename of the input configuration file") | ||
| err := flags.MarkFlagsRequired(cmd, configurationFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Configuration: flags.FlagToStringPointer(p, cmd, configurationFlag), | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func getCurrentAlbVersion(ctx context.Context, apiClient *alb.APIClient, model *inputModel) (*string, error) { | ||
| // use the configuration file to find the name of the loadbalancer | ||
| updatePayload, err := readPayload(ctx, model) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if updatePayload.Name == nil { | ||
| return nil, fmt.Errorf("no name found in configuration") | ||
| } | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| resp, err := apiClient.GetLoadBalancer(ctx, model.ProjectId, model.Region, *updatePayload.Name).Execute() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return resp.Version, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) (req alb.ApiUpdateLoadBalancerRequest, err error) { | ||
| payload, err := readPayload(ctx, model) | ||
| if err != nil { | ||
| return req, err | ||
| } | ||
| if payload.Name == nil { | ||
| return req, fmt.Errorf("no name found in loadbalancer configuration") | ||
| } | ||
| payload.Version = model.Version | ||
| req = apiClient.UpdateLoadBalancer(ctx, model.ProjectId, model.Region, *payload.Name) | ||
| return req.UpdateLoadBalancerPayload(payload), nil | ||
| } | ||
| func readPayload(_ context.Context, model *inputModel) (payload alb.UpdateLoadBalancerPayload, err error) { | ||
| if model.Configuration == nil { | ||
| return payload, fmt.Errorf("no configuration file defined") | ||
| } | ||
| file, err := os.Open(*model.Configuration) | ||
| if err != nil { | ||
| return payload, fmt.Errorf("cannot open configuration file %q: %w", *model.Configuration, err) | ||
| } | ||
| defer file.Close() // nolint:errcheck // at this point close errors are not relevant anymore | ||
| if strings.HasSuffix(*model.Configuration, ".yaml") { | ||
| decoder := yaml.NewDecoder(bufio.NewReader(file), yaml.UseJSONUnmarshaler()) | ||
| if err := decoder.Decode(&payload); err != nil { | ||
| return payload, fmt.Errorf("cannot deserialize yaml configuration from %q: %w", *model.Configuration, err) | ||
| } | ||
| } else if strings.HasSuffix(*model.Configuration, ".json") { | ||
| decoder := json.NewDecoder(bufio.NewReader(file)) | ||
| if err := decoder.Decode(&payload); err != nil { | ||
| return payload, fmt.Errorf("cannot deserialize json configuration from %q: %w", *model.Configuration, err) | ||
| } | ||
| } else { | ||
| return payload, fmt.Errorf("cannot determine configuration fileformat of %q by extension. Must be '.json' or '.yaml'", *model.Configuration) | ||
| } | ||
| return payload, nil | ||
| } | ||
| func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *alb.LoadBalancer) error { | ||
| if resp == nil { | ||
| return fmt.Errorf("update loadbalancer response is empty") | ||
| } | ||
| switch model.OutputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(resp, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal loadbalancer: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal loadbalancer: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| operationState := "Updated" | ||
| if model.Async { | ||
| operationState = "Triggered update of" | ||
| } | ||
| p.Outputf("%s application loadbalancer for %q. Name: %s\n", operationState, projectLabel, utils.PtrString(resp.Name)) | ||
| return nil | ||
| } | ||
| } |
| package client | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/auth" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/config" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/spf13/viper" | ||
| sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/alb" | ||
| ) | ||
| func ConfigureClient(p *print.Printer) (*alb.APIClient, error) { | ||
| var err error | ||
| var apiClient *alb.APIClient | ||
| var cfgOptions []sdkConfig.ConfigurationOption | ||
| authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "configure authentication: %v", err) | ||
| return nil, &errors.AuthError{} | ||
| } | ||
| cfgOptions = append(cfgOptions, authCfgOption) | ||
| customEndpoint := viper.GetString(config.IaaSCustomEndpointKey) | ||
| if customEndpoint != "" { | ||
| cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) | ||
| } else { | ||
| cfgOptions = append(cfgOptions, authCfgOption) | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| cfgOptions = append(cfgOptions, | ||
| sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), | ||
| ) | ||
| } | ||
| apiClient, err = alb.NewAPIClient(cfgOptions...) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "create new API client: %v", err) | ||
| return nil, &errors.AuthError{} | ||
| } | ||
| return apiClient, nil | ||
| } |
| package utils | ||
| type AlbClientMocked struct { | ||
| } |
| package utils | ||
| type AlbClient interface { | ||
| } |
| package utils | ||
| import ( | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/utils" | ||
| ) | ||
| func TestTruncate(t *testing.T) { | ||
| type args struct { | ||
| s *string | ||
| maxLen int | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want string | ||
| }{ | ||
| {"nil string", args{nil, 3}, ""}, | ||
| {"empty string", args{utils.Ptr(""), 10}, ""}, | ||
| {"length below maxlength", args{utils.Ptr("foo"), 10}, "foo"}, | ||
| {"exactly maxlength", args{utils.Ptr("foo"), 3}, "foo"}, | ||
| {"above maxlength", args{utils.Ptr("foobarbaz"), 3}, "foo…"}, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if got := Truncate(tt.args.s, tt.args.maxLen); got != tt.want { | ||
| t.Errorf("Truncate() = %v, want %v", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } |
+17
| # Release | ||
| ## Release cycle | ||
| A release should be created at least every 2 weeks. | ||
| ## Release creation | ||
| > [!IMPORTANT] | ||
| > Consider informing / syncing with the team before creating a new release. | ||
| 1. Check out latest main branch on your machine | ||
| 2. Create git tag: `git tag vX.X.X` | ||
| 3. Push the git tag: `git push origin --tags` | ||
| 4. The [release pipeline](https://github.com/stackitcloud/stackit-cli/actions/workflows/release.yaml) will build the release and publish it on GitHub | ||
| 5. Ensure the release was created properly using the [releases page](https://github.com/stackitcloud/stackit-cli/releases) | ||
@@ -66,3 +66,3 @@ # STACKIT CLI release workflow. | ||
| - name: Install Snapcraft | ||
| uses: samuelmeuli/action-snapcraft@v2 | ||
| uses: samuelmeuli/action-snapcraft@v3 | ||
| - name: Run GoReleaser | ||
@@ -69,0 +69,0 @@ uses: goreleaser/goreleaser-action@v6 |
@@ -16,5 +16,5 @@ name: Renovate | ||
| - name: Self-hosted Renovate | ||
| uses: renovatebot/github-action@v41.0.5 | ||
| uses: renovatebot/github-action@v41.0.21 | ||
| with: | ||
| configurationFile: .github/renovate.json | ||
| token: ${{ secrets.RENOVATE_TOKEN }} |
+3
-0
@@ -111,2 +111,5 @@ version: 2 | ||
| skip_upload: auto | ||
| install: | | ||
| bin.install "stackit" | ||
| generate_completions_from_executable(bin/"stackit", "completion") | ||
@@ -113,0 +116,0 @@ snapcrafts: |
@@ -44,3 +44,4 @@ ## stackit beta | ||
| * [stackit](./stackit.md) - Manage STACKIT resources using the command line | ||
| * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers | ||
| * [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex | ||
@@ -45,3 +45,3 @@ ## stackit image create | ||
| --secure-boot Enables Secure Boot. | ||
| --uefi Enables UEFI boot. | ||
| --uefi Enables UEFI boot. (default true) | ||
| --video-model string Sets Graphic device model. | ||
@@ -48,0 +48,0 @@ --virtio-scsi Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block. |
+5
-4
@@ -18,3 +18,4 @@ module github.com/stackitcloud/stackit-cli | ||
| github.com/spf13/viper v1.20.1 | ||
| github.com/stackitcloud/stackit-sdk-go/core v0.16.2 | ||
| github.com/stackitcloud/stackit-sdk-go/core v0.17.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/alb v0.2.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/authorization v0.6.1 | ||
@@ -38,3 +39,3 @@ github.com/stackitcloud/stackit-sdk-go/services/dns v0.13.1 | ||
| golang.org/x/oauth2 v0.29.0 | ||
| golang.org/x/term v0.30.0 | ||
| golang.org/x/term v0.31.0 | ||
| golang.org/x/text v0.24.0 | ||
@@ -85,3 +86,3 @@ k8s.io/apimachinery v0.32.3 | ||
| github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.1.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/observability v0.4.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/observability v0.5.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.22.0 | ||
@@ -91,3 +92,3 @@ github.com/stackitcloud/stackit-sdk-go/services/redis v0.22.0 | ||
| go.uber.org/multierr v1.11.0 // indirect | ||
| golang.org/x/sys v0.31.0 // indirect | ||
| golang.org/x/sys v0.32.0 // indirect | ||
| gopkg.in/yaml.v3 v3.0.1 // indirect | ||
@@ -94,0 +95,0 @@ k8s.io/api v0.32.3 // indirect |
+10
-8
@@ -114,4 +114,6 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= | ||
| github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= | ||
| github.com/stackitcloud/stackit-sdk-go/core v0.16.2 h1:F8A4P/LLlQSbz0S0+G3m8rb3BUOK6EcR/CKx5UQY5jQ= | ||
| github.com/stackitcloud/stackit-sdk-go/core v0.16.2/go.mod h1:8KIw3czdNJ9sdil9QQimxjR6vHjeINFrRv0iZ67wfn0= | ||
| github.com/stackitcloud/stackit-sdk-go/core v0.17.1 h1:TTrVoB1lERd/qfWzpe6HpwCJSjtaGnUI7UE7ITb5IT0= | ||
| github.com/stackitcloud/stackit-sdk-go/core v0.17.1/go.mod h1:8KIw3czdNJ9sdil9QQimxjR6vHjeINFrRv0iZ67wfn0= | ||
| github.com/stackitcloud/stackit-sdk-go/services/alb v0.2.1 h1:7VOUNsnNBL2iQsZ3lJogNzKx4lrYEawDYllcXV1gEik= | ||
| github.com/stackitcloud/stackit-sdk-go/services/alb v0.2.1/go.mod h1:J2/Jk+leR6AjBotd7USJJzX+AEIHH11yxnmx+6ciJEk= | ||
| github.com/stackitcloud/stackit-sdk-go/services/authorization v0.6.1 h1:2lq6SG8qOgPOx2OIA5Bca8mwRSlect3Yljk57bXqd5I= | ||
@@ -133,4 +135,4 @@ github.com/stackitcloud/stackit-sdk-go/services/authorization v0.6.1/go.mod h1:in9kC4GIBU5DpzXKFDL7RDl0fKyvN/RUIc7YbyWYEUA= | ||
| github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.1.1/go.mod h1:c5/LHxZZ7qZYOboR01qOw1Mqq5v/VNyONw7WPHctNeY= | ||
| github.com/stackitcloud/stackit-sdk-go/services/observability v0.4.0 h1:CPa/SRuX1Gl810K0SearSFyH0k/xKF9JHUV+4j+Tcn4= | ||
| github.com/stackitcloud/stackit-sdk-go/services/observability v0.4.0/go.mod h1:/go4bZ76dxGfkvl48EYUmPZ41c+64Yrf/26RodfcFyw= | ||
| github.com/stackitcloud/stackit-sdk-go/services/observability v0.5.0 h1:BpHIZdoAZwtzYgXznFE7lhqpUvnDJjc+HaEAQmA7NOk= | ||
| github.com/stackitcloud/stackit-sdk-go/services/observability v0.5.0/go.mod h1:1gMNjPCqT868oIqdWGkiReS1G/qpM4bYKYBmDRi8sqg= | ||
| github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.21.0 h1:Au07XkATgBqrGpjO5t6ASAjuif7VXcKKF3t4edRh+Yw= | ||
@@ -200,6 +202,6 @@ github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.21.0/go.mod h1:zyVy1/etVAq6NkdPG4kNOM2R7+AxXp5zLsiN4nDC7Fs= | ||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= | ||
| golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||
| golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= | ||
| golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= | ||
| golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= | ||
| golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||
| golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= | ||
| golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= | ||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
@@ -206,0 +208,0 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
@@ -6,2 +6,3 @@ package beta | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex" | ||
@@ -40,2 +41,3 @@ "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cmd.AddCommand(sqlserverflex.NewCmd(p)) | ||
| cmd.AddCommand(alb.NewCmd(p)) | ||
| } |
@@ -108,3 +108,3 @@ package create | ||
| SecureBoot: &testSecureBoot, | ||
| Uefi: &testUefi, | ||
| Uefi: testUefi, | ||
| VideoModel: &testVideoModel, | ||
@@ -251,2 +251,12 @@ VirtioScsi: &testVirtioScsi, | ||
| { | ||
| description: "uefi flag is set to false", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[uefiFlag] = strconv.FormatBool(false) | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Config.Uefi = false | ||
| }), | ||
| }, | ||
| { | ||
| description: "no rescue device and no bus is valid", | ||
@@ -352,3 +362,3 @@ flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.Config.Uefi = utils.Ptr(false) | ||
| model.Config.Uefi = false | ||
| }), | ||
@@ -355,0 +365,0 @@ expectedRequest: fixtureRequest(func(request *iaas.ApiCreateImageRequest) { |
@@ -65,3 +65,3 @@ package create | ||
| SecureBoot *bool | ||
| Uefi *bool | ||
| Uefi bool | ||
| VideoModel *string | ||
@@ -274,3 +274,3 @@ VirtioScsi *bool | ||
| cmd.Flags().Bool(secureBootFlag, false, "Enables Secure Boot.") | ||
| cmd.Flags().Bool(uefiFlag, false, "Enables UEFI boot.") | ||
| cmd.Flags().Bool(uefiFlag, true, "Enables UEFI boot.") | ||
| cmd.Flags().String(videoModelFlag, "", "Sets Graphic device model.") | ||
@@ -316,3 +316,3 @@ cmd.Flags().Bool(virtioScsiFlag, false, "Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block.") | ||
| SecureBoot: flags.FlagToBoolPointer(p, cmd, secureBootFlag), | ||
| Uefi: flags.FlagToBoolPointer(p, cmd, uefiFlag), | ||
| Uefi: flags.FlagToBoolValue(p, cmd, uefiFlag), | ||
| VideoModel: flags.FlagToStringPointer(p, cmd, videoModelFlag), | ||
@@ -373,3 +373,3 @@ VirtioScsi: flags.FlagToBoolPointer(p, cmd, virtioScsiFlag), | ||
| SecureBoot: model.Config.SecureBoot, | ||
| Uefi: model.Config.Uefi, | ||
| Uefi: utils.Ptr(model.Config.Uefi), | ||
| VideoModel: iaas.NewNullableString(model.Config.VideoModel), | ||
@@ -376,0 +376,0 @@ VirtioScsi: model.Config.VirtioScsi, |
@@ -176,3 +176,3 @@ package list | ||
| table := tables.NewTable() | ||
| table.SetHeader("ID", "NAME", "NIC SECURITY", "STATUS", "TYPE") | ||
| table.SetHeader("ID", "NAME", "NIC SECURITY", "DEVICE ID", "IPv4 ADDRESS", "STATUS", "TYPE") | ||
@@ -184,2 +184,4 @@ for _, nic := range nics { | ||
| utils.PtrString(nic.NicSecurity), | ||
| utils.PtrString(nic.Device), | ||
| utils.PtrString(nic.Ipv4), | ||
| utils.PtrString(nic.Status), | ||
@@ -186,0 +188,0 @@ utils.PtrString(nic.Type), |
@@ -192,3 +192,4 @@ package print | ||
| // -R: interprets ANSI color and style sequences | ||
| pagerCmd := exec.Command("less", "-F", "-S", "-w", "-R") | ||
| // -K: exits if an interrupt character is typed | ||
| pagerCmd := exec.Command("less", "-F", "-S", "-w", "-R", "-K") | ||
@@ -195,0 +196,0 @@ pager, pagerExists := os.LookupEnv("PAGER") |
@@ -5,2 +5,3 @@ package utils | ||
| "strings" | ||
| "unicode/utf8" | ||
| ) | ||
@@ -37,1 +38,23 @@ | ||
| } | ||
| // Truncate trims the passed string (if it is not nil). If the input string is | ||
| // longer than the given length, it is truncated to _maxLen_ and a ellipsis (…) | ||
| // is attached. Therefore the resulting string has at most length _maxLen-1_ | ||
| func Truncate(s *string, maxLen int) string { | ||
| if s == nil { | ||
| return "" | ||
| } | ||
| if utf8.RuneCountInString(*s) > maxLen { | ||
| var builder strings.Builder | ||
| for i, r := range *s { | ||
| if i >= maxLen { | ||
| break | ||
| } | ||
| builder.WriteRune(r) | ||
| } | ||
| builder.WriteRune('…') | ||
| return builder.String() | ||
| } | ||
| return *s | ||
| } |
+8
-4
@@ -74,6 +74,5 @@ <div align="center"> | ||
| | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | ||
| | Observability | `observability` | :white_check_mark: | | ||
| | Infrastructure as a Service (IaaS) | `network-area` <br/> `network` <br/> `volume` <br/> `network-interface` <br/> `public-ip` <br/> `security-group` <br/> `key-pair` <br/> `image` <br/> `quota` | :white_check_mark:| | ||
| | Authorization | `project`, `organization` | :white_check_mark: | | ||
| | DNS | `dns` | :white_check_mark: | | ||
| | Infrastructure as a Service (IaaS) | `image` <br/> `key-pair` <br/> `network` <br/> `network-area` <br/> `network-interface` <br/> `public-ip` <br/> `quota` <br/> `security-group` <br/> `server` <br/> `volume` | :white_check_mark:| | ||
| | Kubernetes Engine (SKE) | `ske` | :white_check_mark: | | ||
@@ -84,2 +83,3 @@ | Load Balancer | `load-balancer` | :white_check_mark: | | ||
| | MongoDB Flex | `mongodbflex` | :white_check_mark: | | ||
| | Observability | `observability` | :white_check_mark: | | ||
| | Object Storage | `object-storage` | :white_check_mark: | | ||
@@ -92,4 +92,4 @@ | OpenSearch | `opensearch` | :white_check_mark: | | ||
| | Secrets Manager | `secrets-manager` | :white_check_mark: | | ||
| | Server Backup Management | `server backup` | :white_check_mark: | | ||
| | Server Command (Run Command) | `server command` | :white_check_mark: | | ||
| | Server Backup Management | `server backup` | :white_check_mark: | | ||
| | Server Command (Run Command) | `server command` | :white_check_mark: | | ||
| | Service Account | `service-account` | :white_check_mark: | | ||
@@ -195,2 +195,6 @@ | SQLServer Flex | `beta sqlserverflex` | :white_check_mark: (beta) | | ||
| ## Release creation | ||
| See the [release documentation](./RELEASE.md) for further information. | ||
| ## License | ||
@@ -197,0 +201,0 @@ |