mygithub.libinneed.workers.dev/stackitcloud/stackit-cli
Advanced tools
| ## stackit beta key-pair create | ||
| Creates a key pair | ||
| ### Synopsis | ||
| Creates a key pair. | ||
| ``` | ||
| stackit beta key-pair create [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Create a new key pair with public-key "ssh-rsa xxx" | ||
| $ stackit beta key-pair create --public-key `ssh-rsa xxx` | ||
| Create a new key pair with public-key from file "/Users/username/.ssh/id_rsa.pub" | ||
| $ stackit beta key-pair create --public-key `@/Users/username/.ssh/id_rsa.pub` | ||
| Create a new key pair with name "KEY_PAIR_NAME" and public-key "ssh-rsa yyy" | ||
| $ stackit beta key-pair create --name KEY_PAIR_NAME --public-key `ssh-rsa yyy` | ||
| Create a new key pair with public-key "ssh-rsa xxx" and labels "key=value,key1=value1" | ||
| $ stackit beta key-pair create --public-key `ssh-rsa xxx` --labels key=value,key1=value1 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta key-pair create" | ||
| --labels stringToString Labels are key-value string pairs which can be attached to a key pair. E.g. '--labels key1=value1,key2=value2,...' (default []) | ||
| --name string Key pair name | ||
| --public-key string Public key to be imported (format: ssh-rsa|ssh-ed25519) | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta key-pair](./stackit_beta_key-pair.md) - Provides functionality for SSH key pairs | ||
| ## stackit beta key-pair delete | ||
| Deletes a key pair | ||
| ### Synopsis | ||
| Deletes a key pair. | ||
| ``` | ||
| stackit beta key-pair delete [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Delete key pair with name "KEY_PAIR_NAME" | ||
| $ stackit beta key-pair delete KEY_PAIR_NAME | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta key-pair 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta key-pair](./stackit_beta_key-pair.md) - Provides functionality for SSH key pairs | ||
| ## stackit beta key-pair describe | ||
| Describes a key pair | ||
| ### Synopsis | ||
| Describes a key pair. | ||
| ``` | ||
| stackit beta key-pair describe [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Get details about a key pair with name "KEY_PAIR_NAME" | ||
| $ stackit beta key-pair describe KEY_PAIR_NAME | ||
| Get only the SSH public key of a key pair with name "KEY_PAIR_NAME" | ||
| $ stackit beta key-pair describe KEY_PAIR_NAME --public-key | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta key-pair describe" | ||
| --public-key Show only the public key | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta key-pair](./stackit_beta_key-pair.md) - Provides functionality for SSH key pairs | ||
| ## stackit beta key-pair list | ||
| Lists all key pairs | ||
| ### Synopsis | ||
| Lists all key pairs. | ||
| ``` | ||
| stackit beta key-pair list [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Lists all key pairs | ||
| $ stackit beta key-pair list | ||
| Lists all key pairs which contains the label xxx | ||
| $ stackit beta key-pair list --label-selector xxx | ||
| Lists all key pairs in JSON format | ||
| $ stackit beta key-pair list --output-format json | ||
| Lists up to 10 key pairs | ||
| $ stackit beta key-pair list --limit 10 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta key-pair list" | ||
| --label-selector string Filter by label | ||
| --limit int Number of key pairs 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta key-pair](./stackit_beta_key-pair.md) - Provides functionality for SSH key pairs | ||
| ## stackit beta key-pair update | ||
| Updates a key pair | ||
| ### Synopsis | ||
| Updates a key pair. | ||
| ``` | ||
| stackit beta key-pair update [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Update the labels of a key pair with name "KEY_PAIR_NAME" with "key=value,key1=value1" | ||
| $ stackit beta key-pair update KEY_PAIR_NAME --labels key=value,key1=value1 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta key-pair update" | ||
| --labels stringToString Labels are key-value string pairs which can be attached to a server. E.g. '--labels key1=value1,key2=value2,...' (default []) | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta key-pair](./stackit_beta_key-pair.md) - Provides functionality for SSH key pairs | ||
| ## stackit beta key-pair | ||
| Provides functionality for SSH key pairs | ||
| ### Synopsis | ||
| Provides functionality for SSH key pairs | ||
| ``` | ||
| stackit beta key-pair [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta key-pair" | ||
| ``` | ||
| ### 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 | ||
| --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 key-pair create](./stackit_beta_key-pair_create.md) - Creates a key pair | ||
| * [stackit beta key-pair delete](./stackit_beta_key-pair_delete.md) - Deletes a key pair | ||
| * [stackit beta key-pair describe](./stackit_beta_key-pair_describe.md) - Describes a key pair | ||
| * [stackit beta key-pair list](./stackit_beta_key-pair_list.md) - Lists all key pairs | ||
| * [stackit beta key-pair update](./stackit_beta_key-pair_update.md) - Updates a key pair | ||
| ## stackit beta security-group create | ||
| Creates security groups | ||
| ### Synopsis | ||
| Creates security groups. | ||
| ``` | ||
| stackit beta security-group create [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Create a named group | ||
| $ stackit beta security-group create --name my-new-group | ||
| Create a named group with labels | ||
| $ stackit beta security-group create --name my-new-group --labels label1=value1,label2=value2 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| --description string An optional description of the security group. | ||
| -h, --help Help for "stackit beta security-group create" | ||
| --labels stringToString Labels are key-value string pairs which can be attached to a network-interface. E.g. '--labels key1=value1,key2=value2,...' (default []) | ||
| --name string The name of the security group. | ||
| --stateful Create a stateful or a stateless security group | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta security-group](./stackit_beta_security-group.md) - Manage security groups | ||
| ## stackit beta security-group delete | ||
| Deletes a security group | ||
| ### Synopsis | ||
| Deletes a security group by its internal ID. | ||
| ``` | ||
| stackit beta security-group delete [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Delete a named group with ID "xxx" | ||
| $ stackit beta security-group delete xxx | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta security-group 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta security-group](./stackit_beta_security-group.md) - Manage security groups | ||
| ## stackit beta security-group describe | ||
| Describes security groups | ||
| ### Synopsis | ||
| Describes security groups by its internal ID. | ||
| ``` | ||
| stackit beta security-group describe [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Describe group "xxx" | ||
| $ stackit beta security-group describe xxx | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta security-group 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta security-group](./stackit_beta_security-group.md) - Manage security groups | ||
| ## stackit beta security-group list | ||
| Lists security groups | ||
| ### Synopsis | ||
| Lists security groups by its internal ID. | ||
| ``` | ||
| stackit beta security-group list [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| List all groups | ||
| $ stackit beta security-group list | ||
| List groups with labels | ||
| $ stackit beta security-group list --label-selector label1=value1,label2=value2 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta security-group list" | ||
| --label-selector string Filter by label | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta security-group](./stackit_beta_security-group.md) - Manage security groups | ||
| ## stackit beta security-group rule create | ||
| Creates a security group rule | ||
| ### Synopsis | ||
| Creates a security group rule. | ||
| ``` | ||
| stackit beta security-group rule create [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Create a security group rule for security group with ID "xxx" with direction "ingress" | ||
| $ stackit beta security-group rule create --security-group-id xxx --direction ingress | ||
| Create a security group rule for security group with ID "xxx" with direction "egress", protocol "icmp" and icmp parameters | ||
| $ stackit beta security-group rule create --security-group-id xxx --direction egress --protocol-name icmp --icmp-parameter-code 0 --icmp-parameter-type 8 | ||
| Create a security group rule for security group with ID "xxx" with direction "ingress", protocol "tcp" and port range values | ||
| $ stackit beta security-group rule create --security-group-id xxx --direction ingress --protocol-name tcp --port-range-max 24 --port-range-min 22 | ||
| Create a security group rule for security group with ID "xxx" with direction "ingress" and protocol number 1 | ||
| $ stackit beta security-group rule create --security-group-id xxx --direction ingress --protocol-number 1 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| --description string The rule description | ||
| --direction string The direction of the traffic which the rule should match. The possible values are: "ingress", "egress" | ||
| --ether-type string The ethertype which the rule should match | ||
| -h, --help Help for "stackit beta security-group rule create" | ||
| --icmp-parameter-code int ICMP code. Can be set if the protocol is ICMP | ||
| --icmp-parameter-type int ICMP type. Can be set if the protocol is ICMP | ||
| --ip-range string The remote IP range which the rule should match | ||
| --port-range-max int The maximum port number. Should be greater or equal to the minimum. This should only be provided if the protocol is not ICMP | ||
| --port-range-min int The minimum port number. Should be less or equal to the maximum. This should only be provided if the protocol is not ICMP | ||
| --protocol-name string The protocol name which the rule should match. If a protocol is to be defined, either "protocol-name" or "protocol-number" must be provided | ||
| --protocol-number int The protocol number which the rule should match. If a protocol is to be defined, either "protocol-name" or "protocol-number" must be provided | ||
| --remote-security-group-id string The remote security group which the rule should match | ||
| --security-group-id string The security group ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta security-group rule](./stackit_beta_security-group_rule.md) - Provides functionality for security group rules | ||
| ## stackit beta security-group rule delete | ||
| Deletes a security group rule | ||
| ### Synopsis | ||
| Deletes a security group rule. | ||
| If the security group rule is still in use, the deletion will fail | ||
| ``` | ||
| stackit beta security-group rule delete [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Delete security group rule with ID "xxx" in security group with ID "yyy" | ||
| $ stackit beta security-group rule delete xxx --security-group-id yyy | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta security-group rule delete" | ||
| --security-group-id string The security group ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta security-group rule](./stackit_beta_security-group_rule.md) - Provides functionality for security group rules | ||
| ## stackit beta security-group rule describe | ||
| Shows details of a security group rule | ||
| ### Synopsis | ||
| Shows details of a security group rule. | ||
| ``` | ||
| stackit beta security-group rule describe [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Show details of a security group rule with ID "xxx" in security group with ID "yyy" | ||
| $ stackit beta security-group rule describe xxx --security-group-id yyy | ||
| Show details of a security group rule with ID "xxx" in security group with ID "yyy" in JSON format | ||
| $ stackit beta security-group rule describe xxx --security-group-id yyy --output-format json | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta security-group rule describe" | ||
| --security-group-id string The security group ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta security-group rule](./stackit_beta_security-group_rule.md) - Provides functionality for security group rules | ||
| ## stackit beta security-group rule list | ||
| Lists all security group rules in a security group of a project | ||
| ### Synopsis | ||
| Lists all security group rules in a security group of a project. | ||
| ``` | ||
| stackit beta security-group rule list [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Lists all security group rules in security group with ID "xxx" | ||
| $ stackit beta security-group rule list --security-group-id xxx | ||
| Lists all security group rules in security group with ID "xxx" in JSON format | ||
| $ stackit beta security-group rule list --security-group-id xxx --output-format json | ||
| Lists up to 10 security group rules in security group with ID "xxx" | ||
| $ stackit beta security-group rule list --security-group-id xxx --limit 10 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta security-group rule list" | ||
| --limit int Maximum number of entries to list | ||
| --security-group-id string The security group ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta security-group rule](./stackit_beta_security-group_rule.md) - Provides functionality for security group rules | ||
| ## stackit beta security-group rule | ||
| Provides functionality for security group rules | ||
| ### Synopsis | ||
| Provides functionality for security group rules. | ||
| ``` | ||
| stackit beta security-group rule [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta security-group rule" | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta security-group](./stackit_beta_security-group.md) - Manage security groups | ||
| * [stackit beta security-group rule create](./stackit_beta_security-group_rule_create.md) - Creates a security group rule | ||
| * [stackit beta security-group rule delete](./stackit_beta_security-group_rule_delete.md) - Deletes a security group rule | ||
| * [stackit beta security-group rule describe](./stackit_beta_security-group_rule_describe.md) - Shows details of a security group rule | ||
| * [stackit beta security-group rule list](./stackit_beta_security-group_rule_list.md) - Lists all security group rules in a security group of a project | ||
| ## stackit beta security-group update | ||
| Updates a security group | ||
| ### Synopsis | ||
| Updates a named security group | ||
| ``` | ||
| stackit beta security-group update [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Update the name of group "xxx" | ||
| $ stackit beta security-group update xxx --name my-new-name | ||
| Update the labels of group "xxx" | ||
| $ stackit beta security-group update xxx --labels label1=value1,label2=value2 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| --description string An optional description of the security group. | ||
| -h, --help Help for "stackit beta security-group update" | ||
| --labels stringToString Labels are key-value string pairs which can be attached to a network-interface. E.g. '--labels key1=value1,key2=value2,...' (default []) | ||
| --name string The name of the security group. | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta security-group](./stackit_beta_security-group.md) - Manage security groups | ||
| ## stackit beta security-group | ||
| Manage security groups | ||
| ### Synopsis | ||
| Manage the lifecycle of security groups and rules. | ||
| ``` | ||
| stackit beta security-group [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta security-group" | ||
| ``` | ||
| ### 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 | ||
| --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 security-group create](./stackit_beta_security-group_create.md) - Creates security groups | ||
| * [stackit beta security-group delete](./stackit_beta_security-group_delete.md) - Deletes a security group | ||
| * [stackit beta security-group describe](./stackit_beta_security-group_describe.md) - Describes security groups | ||
| * [stackit beta security-group list](./stackit_beta_security-group_list.md) - Lists security groups | ||
| * [stackit beta security-group rule](./stackit_beta_security-group_rule.md) - Provides functionality for security group rules | ||
| * [stackit beta security-group update](./stackit_beta_security-group_update.md) - Updates a security group | ||
| ## stackit beta server console | ||
| Gets a URL for server remote console | ||
| ### Synopsis | ||
| Gets a URL for server remote console. | ||
| ``` | ||
| stackit beta server console [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Get a URL for the server remote console with server ID "xxx" | ||
| $ stackit beta server console xxx | ||
| Get a URL for the server remote console with server ID "xxx" in JSON format | ||
| $ stackit beta server console xxx --output-format json | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server console" | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers | ||
| ## stackit beta server deallocate | ||
| Deallocates an existing server | ||
| ### Synopsis | ||
| Deallocates an existing server. | ||
| ``` | ||
| stackit beta server deallocate [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Deallocate an existing server with ID "xxx" | ||
| $ stackit beta server deallocate xxx | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server deallocate" | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers | ||
| ## stackit beta server log | ||
| Gets server console log | ||
| ### Synopsis | ||
| Gets server console log. | ||
| ``` | ||
| stackit beta server log [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Get server console log for the server with ID "xxx" | ||
| $ stackit beta server log xxx | ||
| Get server console log for the server with ID "xxx" and limit output lines to 1000 | ||
| $ stackit beta server log xxx --length 1000 | ||
| Get server console log for the server with ID "xxx" in JSON format | ||
| $ stackit beta server log xxx --output-format json | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server log" | ||
| --length int Maximum number of lines to list (default 2000) | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers | ||
| ## stackit beta server reboot | ||
| Reboots a server | ||
| ### Synopsis | ||
| Reboots a server. | ||
| ``` | ||
| stackit beta server reboot [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Perform a soft reboot of a server with ID "xxx" | ||
| $ stackit beta server reboot xxx | ||
| Perform a hard reboot of a server with ID "xxx" | ||
| $ stackit beta server reboot xxx --hard | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -b, --hard Performs a hard reboot. (default false) | ||
| -h, --help Help for "stackit beta server reboot" | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers | ||
| ## stackit beta server rescue | ||
| Rescues an existing server | ||
| ### Synopsis | ||
| Rescues an existing server. | ||
| ``` | ||
| stackit beta server rescue [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Rescue an existing server with ID "xxx" using image with ID "yyy" as boot volume | ||
| $ stackit beta server rescue xxx --image-id yyy | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server rescue" | ||
| --image-id string The image ID to be used for a temporary boot volume. | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers | ||
| ## stackit beta server resize | ||
| Resizes the server to the given machine type | ||
| ### Synopsis | ||
| Resizes the server to the given machine type. | ||
| ``` | ||
| stackit beta server resize [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Resize a server with ID "xxx" to machine type "yyy" | ||
| $ stackit beta server resize xxx --machine-type yyy | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server resize" | ||
| --machine-type string Name of the type of the machine for the server. Possible values are documented in https://docs.stackit.cloud/stackit/en/virtual-machine-flavors-75137231.html | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers | ||
| ## stackit beta server service-account attach | ||
| Attach a service account to a server | ||
| ### Synopsis | ||
| Attach a service account to a server | ||
| ``` | ||
| stackit beta server service-account attach [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Attach a service account with mail "xxx@sa.stackit.cloud" to a server with ID "yyy" | ||
| $ stackit beta server service-account attach xxx@sa.stackit.cloud --server-id yyy | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server service-account attach" | ||
| -s, --server-id string Server ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server service-account](./stackit_beta_server_service-account.md) - Allows attaching/detaching service accounts to servers | ||
| ## stackit beta server service-account detach | ||
| Detach a service account from a server | ||
| ### Synopsis | ||
| Detach a service account from a server | ||
| ``` | ||
| stackit beta server service-account detach [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Detach a service account with mail "xxx@sa.stackit.cloud" from a server "yyy" | ||
| $ stackit beta server service-account detach xxx@sa.stackit.cloud --server-id yyy | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server service-account detach" | ||
| -s, --server-id string Server id | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server service-account](./stackit_beta_server_service-account.md) - Allows attaching/detaching service accounts to servers | ||
| ## stackit beta server service-account list | ||
| List all attached service accounts for a server | ||
| ### Synopsis | ||
| List all attached service accounts for a server | ||
| ``` | ||
| stackit beta server service-account list [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| List all attached service accounts for a server with ID "xxx" | ||
| $ stackit beta server service-account list --server-id xxx | ||
| List up to 10 attached service accounts for a server with ID "xxx" | ||
| $ stackit beta server service-account list --server-id xxx --limit 10 | ||
| List all attached service accounts for a server with ID "xxx" in JSON format | ||
| $ stackit beta server service-account list --server-id xxx --output-format json | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server service-account list" | ||
| --limit int Maximum number of entries to list | ||
| -s, --server-id string Server ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server service-account](./stackit_beta_server_service-account.md) - Allows attaching/detaching service accounts to servers | ||
| ## stackit beta server service-account | ||
| Allows attaching/detaching service accounts to servers | ||
| ### Synopsis | ||
| Allows attaching/detaching service accounts to servers | ||
| ``` | ||
| stackit beta server service-account [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server service-account" | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers | ||
| * [stackit beta server service-account attach](./stackit_beta_server_service-account_attach.md) - Attach a service account to a server | ||
| * [stackit beta server service-account detach](./stackit_beta_server_service-account_detach.md) - Detach a service account from a server | ||
| * [stackit beta server service-account list](./stackit_beta_server_service-account_list.md) - List all attached service accounts for a server | ||
| ## stackit beta server start | ||
| Starts an existing server or allocates the server if deallocated | ||
| ### Synopsis | ||
| Starts an existing server or allocates the server if deallocated. | ||
| ``` | ||
| stackit beta server start [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Start an existing server with ID "xxx" | ||
| $ stackit beta server start xxx | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server start" | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers | ||
| ## stackit beta server stop | ||
| Stops an existing server | ||
| ### Synopsis | ||
| Stops an existing server. | ||
| ``` | ||
| stackit beta server stop [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Stop an existing server with ID "xxx" | ||
| $ stackit beta server stop xxx | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server stop" | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers | ||
| ## stackit beta server unrescue | ||
| Unrescues an existing server | ||
| ### Synopsis | ||
| Unrescues an existing server. | ||
| ``` | ||
| stackit beta server unrescue [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Unrescue an existing server with ID "xxx" | ||
| $ stackit beta server unrescue xxx | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server unrescue" | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers | ||
| ## stackit config profile export | ||
| Exports a CLI configuration profile | ||
| ### Synopsis | ||
| Exports a CLI configuration profile. | ||
| ``` | ||
| stackit config profile export PROFILE_NAME [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Export a profile with name "PROFILE_NAME" to a file in your current directory | ||
| $ stackit config profile export PROFILE_NAME | ||
| Export a profile with name "PROFILE_NAME"" to a specific file path FILE_PATH | ||
| $ stackit config profile export PROFILE_NAME --file-path FILE_PATH | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -f, --file-path string If set, writes the config to the given file path. If unset, writes the config to you current directory with the name of the profile. E.g. '--file-path ~/my-config.json' | ||
| -h, --help Help for "stackit config profile export" | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit config profile](./stackit_config_profile.md) - Manage the CLI configuration profiles | ||
| ## stackit config profile import | ||
| Imports a CLI configuration profile | ||
| ### Synopsis | ||
| Imports a CLI configuration profile. | ||
| ``` | ||
| stackit config profile import [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Import a config with name "PROFILE_NAME" from file "./config.json" | ||
| $ stackit config profile import --name PROFILE_NAME --config `@./config.json` | ||
| Import a config with name "PROFILE_NAME" from file "./config.json" and do not set as active | ||
| $ stackit config profile import --name PROFILE_NAME --config `@./config.json` --no-set | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -c, --config string File where configuration will be imported from | ||
| -h, --help Help for "stackit config profile import" | ||
| --name string Profile name | ||
| --no-set Set the imported profile not as active | ||
| ``` | ||
| ### 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 | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit config profile](./stackit_config_profile.md) - Manage the CLI configuration profiles | ||
| package create | ||
| import ( | ||
| "context" | ||
| "os" | ||
| "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/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testPublicKey = "ssh-rsa <key>" | ||
| var testKeyPairName = "foobar_key" | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| publicKeyFlag: testPublicKey, | ||
| labelFlag: "foo=bar", | ||
| nameFlag: testKeyPairName, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| Labels: utils.Ptr(map[string]string{ | ||
| "foo": "bar", | ||
| }), | ||
| PublicKey: utils.Ptr(testPublicKey), | ||
| Name: utils.Ptr(testKeyPairName), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiCreateKeyPairRequest)) iaas.ApiCreateKeyPairRequest { | ||
| request := testClient.CreateKeyPair(testCtx) | ||
| request = request.CreateKeyPairPayload(fixturePayload()) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func fixturePayload(mods ...func(payload *iaas.CreateKeyPairPayload)) iaas.CreateKeyPairPayload { | ||
| payload := iaas.CreateKeyPairPayload{ | ||
| Labels: utils.Ptr(map[string]interface{}{ | ||
| "foo": "bar", | ||
| }), | ||
| PublicKey: utils.Ptr(testPublicKey), | ||
| Name: utils.Ptr(testKeyPairName), | ||
| } | ||
| 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: "required only", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, nameFlag) | ||
| delete(flagValues, labelFlag) | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Name = nil | ||
| model.Labels = nil | ||
| }), | ||
| }, | ||
| { | ||
| description: "read public key from file", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[publicKeyFlag] = "@./template/id_ed25519.pub" | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| file, err := os.ReadFile("./template/id_ed25519.pub") | ||
| if err != nil { | ||
| t.Fatal("could not create expected Model", err) | ||
| } | ||
| model.PublicKey = utils.Ptr(string(file)) | ||
| }), | ||
| }, | ||
| { | ||
| 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 iaas.ApiCreateKeyPairRequest | ||
| }{ | ||
| { | ||
| 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(iaas.NullableString{}), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package create | ||
| 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/iaas/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/iaas" | ||
| ) | ||
| const ( | ||
| nameFlag = "name" | ||
| publicKeyFlag = "public-key" | ||
| labelFlag = "labels" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Name *string | ||
| PublicKey *string | ||
| Labels *map[string]string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "create", | ||
| Short: "Creates a key pair", | ||
| Long: "Creates a key pair.", | ||
| Args: cobra.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Create a new key pair with public-key "ssh-rsa xxx"`, | ||
| "$ stackit beta key-pair create --public-key `ssh-rsa xxx`", | ||
| ), | ||
| examples.NewExample( | ||
| `Create a new key pair with public-key from file "/Users/username/.ssh/id_rsa.pub"`, | ||
| "$ stackit beta key-pair create --public-key `@/Users/username/.ssh/id_rsa.pub`", | ||
| ), | ||
| examples.NewExample( | ||
| `Create a new key pair with name "KEY_PAIR_NAME" and public-key "ssh-rsa yyy"`, | ||
| "$ stackit beta key-pair create --name KEY_PAIR_NAME --public-key `ssh-rsa yyy`", | ||
| ), | ||
| examples.NewExample( | ||
| `Create a new key pair with public-key "ssh-rsa xxx" and labels "key=value,key1=value1"`, | ||
| "$ stackit beta key-pair create --public-key `ssh-rsa xxx` --labels key=value,key1=value1", | ||
| ), | ||
| ), | ||
| 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 create a key pair?" | ||
| 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("create key pair: %w", err) | ||
| } | ||
| return outputResult(p, model, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().String(nameFlag, "", "Key pair name") | ||
| cmd.Flags().Var(flags.ReadFromFileFlag(), publicKeyFlag, "Public key to be imported (format: ssh-rsa|ssh-ed25519)") | ||
| cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a key pair. E.g. '--labels key1=value1,key2=value2,...'") | ||
| err := cmd.MarkFlagRequired(publicKeyFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), | ||
| Name: flags.FlagToStringPointer(p, cmd, nameFlag), | ||
| PublicKey: flags.FlagToStringPointer(p, cmd, publicKeyFlag), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiCreateKeyPairRequest { | ||
| req := apiClient.CreateKeyPair(ctx) | ||
| var labelsMap *map[string]interface{} | ||
| if model.Labels != nil && len(*model.Labels) > 0 { | ||
| // convert map[string]string to map[string]interface{} | ||
| labelsMap = utils.Ptr(map[string]interface{}{}) | ||
| for k, v := range *model.Labels { | ||
| (*labelsMap)[k] = v | ||
| } | ||
| } | ||
| payload := iaas.CreateKeyPairPayload{ | ||
| Name: model.Name, | ||
| Labels: labelsMap, | ||
| PublicKey: model.PublicKey, | ||
| } | ||
| return req.CreateKeyPairPayload(payload) | ||
| } | ||
| func outputResult(p *print.Printer, model *inputModel, item *iaas.Keypair) error { | ||
| switch model.OutputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(item, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal key pair: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(item, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal key pair: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| default: | ||
| p.Outputf("Created key pair %q.\nkey pair Fingerprint: %q\n", *item.Name, *item.Fingerprint) | ||
| } | ||
| return nil | ||
| } |
Sorry, the diff of this file is not supported yet
| 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/iaas" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") | ||
| var testClient = &iaas.APIClient{} | ||
| var testKeyPairName = "key-pair-name" | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testKeyPairName, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{} | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| KeyPairName: testKeyPairName, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiDeleteKeyPairRequest)) iaas.ApiDeleteKeyPairRequest { | ||
| request := testClient.DeleteKeyPair(testCtx, testKeyPairName) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no args", | ||
| argValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flags", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| 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 iaas.ApiDeleteKeyPairRequest | ||
| }{ | ||
| { | ||
| 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/iaas/client" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| const ( | ||
| keyPairNameArg = "KEY_PAIR_NAME" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| KeyPairName string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "delete", | ||
| Short: "Deletes a key pair", | ||
| Long: "Deletes a key pair.", | ||
| Args: args.SingleArg(keyPairNameArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Delete key pair with name "KEY_PAIR_NAME"`, | ||
| "$ stackit beta key-pair delete KEY_PAIR_NAME", | ||
| ), | ||
| ), | ||
| 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 key pair %q?", model.KeyPairName) | ||
| 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 key pair: %w", err) | ||
| } | ||
| p.Info("Deleted key pair %q\n", model.KeyPairName) | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| keyPairName := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| KeyPairName: keyPairName, | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiDeleteKeyPairRequest { | ||
| return apiClient.DeleteKeyPair(ctx, model.KeyPairName) | ||
| } |
| 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/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") | ||
| var testClient = &iaas.APIClient{} | ||
| var testKeyPairName = "foobar" | ||
| var testPublicKeyFlag = "true" | ||
| func fixtureArgValues(mods ...func(argVales []string)) []string { | ||
| argVales := []string{ | ||
| testKeyPairName, | ||
| } | ||
| for _, m := range mods { | ||
| m(argVales) | ||
| } | ||
| return argVales | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{} | ||
| for _, m := range mods { | ||
| m(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| KeyPairName: testKeyPairName, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiGetKeyPairRequest)) iaas.ApiGetKeyPairRequest { | ||
| request := testClient.GetKeyPair(testCtx, testKeyPairName) | ||
| 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{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no arg values", | ||
| argsValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flag values", | ||
| argsValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "set flag 'public-key' true", | ||
| argsValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[publicKeyFlag] = testPublicKeyFlag | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.PublicKey = true | ||
| }), | ||
| }, | ||
| } | ||
| 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 iaas.ApiGetKeyPairRequest | ||
| }{ | ||
| { | ||
| 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 describe | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| "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/services/iaas/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/iaas" | ||
| ) | ||
| const ( | ||
| keyPairNameArg = "KEY_PAIR_NAME" | ||
| publicKeyFlag = "public-key" | ||
| maxLengthPublicKey = 50 | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| KeyPairName string | ||
| PublicKey bool | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "describe", | ||
| Short: "Describes a key pair", | ||
| Long: "Describes a key pair.", | ||
| Args: args.SingleArg(keyPairNameArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Get details about a key pair with name "KEY_PAIR_NAME"`, | ||
| "$ stackit beta key-pair describe KEY_PAIR_NAME", | ||
| ), | ||
| examples.NewExample( | ||
| `Get only the SSH public key of a key pair with name "KEY_PAIR_NAME"`, | ||
| "$ stackit beta key-pair describe KEY_PAIR_NAME --public-key", | ||
| ), | ||
| ), | ||
| 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 key pair: %w", err) | ||
| } | ||
| return outputResult(p, model.OutputFormat, model.PublicKey, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Bool(publicKeyFlag, false, "Show only the public key") | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| keyPairName := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| KeyPairName: keyPairName, | ||
| PublicKey: flags.FlagToBoolValue(p, cmd, publicKeyFlag), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiGetKeyPairRequest { | ||
| return apiClient.GetKeyPair(ctx, model.KeyPairName) | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, showOnlyPublicKey bool, keyPair *iaas.Keypair) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(keyPair, "", " ") | ||
| if showOnlyPublicKey { | ||
| onlyPublicKey := map[string]string{ | ||
| "publicKey": *keyPair.PublicKey, | ||
| } | ||
| details, err = json.MarshalIndent(onlyPublicKey, "", " ") | ||
| } | ||
| if err != nil { | ||
| return fmt.Errorf("marshal key pair: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(keyPair, yaml.IndentSequence(true)) | ||
| if showOnlyPublicKey { | ||
| onlyPublicKey := map[string]string{ | ||
| "publicKey": *keyPair.PublicKey, | ||
| } | ||
| details, err = yaml.MarshalWithOptions(onlyPublicKey, yaml.IndentSequence(true)) | ||
| } | ||
| if err != nil { | ||
| return fmt.Errorf("marshal key pair: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| if showOnlyPublicKey { | ||
| p.Outputln(*keyPair.PublicKey) | ||
| return nil | ||
| } | ||
| table := tables.NewTable() | ||
| table.AddRow("KEY PAIR NAME", *keyPair.Name) | ||
| table.AddSeparator() | ||
| if *keyPair.Labels != nil && len(*keyPair.Labels) > 0 { | ||
| var labels []string | ||
| for key, value := range *keyPair.Labels { | ||
| labels = append(labels, fmt.Sprintf("%s: %s", key, value)) | ||
| } | ||
| table.AddRow("LABELS", strings.Join(labels, "\n")) | ||
| table.AddSeparator() | ||
| } | ||
| table.AddRow("FINGERPRINT", *keyPair.Fingerprint) | ||
| table.AddSeparator() | ||
| truncatedPublicKey := (*keyPair.PublicKey)[:maxLengthPublicKey] + "..." | ||
| table.AddRow("PUBLIC KEY", truncatedPublicKey) | ||
| table.AddSeparator() | ||
| table.AddRow("CREATED AT", *keyPair.CreatedAt) | ||
| table.AddSeparator() | ||
| table.AddRow("UPDATED AT", *keyPair.UpdatedAt) | ||
| table.AddSeparator() | ||
| p.Outputln(table.Render()) | ||
| } | ||
| return nil | ||
| } |
| package keypair | ||
| import ( | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/key-pair/create" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/key-pair/delete" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/key-pair/describe" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/key-pair/list" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/key-pair/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: "key-pair", | ||
| Short: "Provides functionality for SSH key pairs", | ||
| Long: "Provides functionality for SSH key pairs", | ||
| Args: cobra.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, p) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, p *print.Printer) { | ||
| cmd.AddCommand(create.NewCmd(p)) | ||
| cmd.AddCommand(delete.NewCmd(p)) | ||
| cmd.AddCommand(describe.NewCmd(p)) | ||
| cmd.AddCommand(list.NewCmd(p)) | ||
| cmd.AddCommand(update.NewCmd(p)) | ||
| } |
| 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/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") | ||
| var testClient = &iaas.APIClient{} | ||
| var testLabelSelector = "foo=bar" | ||
| var testLimit = int64(64) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| limitFlag: strconv.FormatInt(testLimit, 10), | ||
| labelSelectorFlag: testLabelSelector, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(inputModel *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| Limit: utils.Ptr(testLimit), | ||
| LabelSelector: utils.Ptr(testLabelSelector), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiListKeyPairsRequest)) iaas.ApiListKeyPairsRequest { | ||
| request := testClient.ListKeyPairs(testCtx) | ||
| request = request.LabelSelector(testLabelSelector) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(inputModel *inputModel) { | ||
| inputModel.Limit = nil | ||
| inputModel.LabelSelector = 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(func(flagValues map[string]string) { | ||
| flagValues[labelSelectorFlag] = "" | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(inputModel *inputModel) { | ||
| inputModel.LabelSelector = utils.Ptr("") | ||
| }), | ||
| }, | ||
| } | ||
| 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 iaas.ApiListKeyPairsRequest | ||
| }{ | ||
| { | ||
| 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) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/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/iaas/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/iaas" | ||
| ) | ||
| const ( | ||
| limitFlag = "limit" | ||
| labelSelectorFlag = "label-selector" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Limit *int64 | ||
| LabelSelector *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "Lists all key pairs", | ||
| Long: "Lists all key pairs.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Lists all key pairs`, | ||
| "$ stackit beta key-pair list", | ||
| ), | ||
| examples.NewExample( | ||
| `Lists all key pairs which contains the label xxx`, | ||
| "$ stackit beta key-pair list --label-selector xxx", | ||
| ), | ||
| examples.NewExample( | ||
| `Lists all key pairs in JSON format`, | ||
| "$ stackit beta key-pair list --output-format json", | ||
| ), | ||
| examples.NewExample( | ||
| `Lists up to 10 key pairs`, | ||
| "$ stackit beta key-pair 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 key pairs: %w", err) | ||
| } | ||
| if resp.Items == nil || len(*resp.Items) == 0 { | ||
| p.Info("No key pairs found\n") | ||
| return nil | ||
| } | ||
| items := *resp.Items | ||
| 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 key pairs to list") | ||
| cmd.Flags().String(labelSelectorFlag, "", "Filter by label") | ||
| } | ||
| 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, | ||
| LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiListKeyPairsRequest { | ||
| req := apiClient.ListKeyPairs(ctx) | ||
| if model.LabelSelector != nil { | ||
| req = req.LabelSelector(*model.LabelSelector) | ||
| } | ||
| return req | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, keyPairs []iaas.Keypair) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(keyPairs, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal key pairs: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(keyPairs, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal key pairs: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| default: | ||
| table := tables.NewTable() | ||
| table.SetHeader("KEY PAIR NAME", "LABELS", "FINGERPRINT", "CREATED AT", "UPDATED AT") | ||
| for idx := range keyPairs { | ||
| keyPair := keyPairs[idx] | ||
| var labels []string | ||
| for key, value := range *keyPair.Labels { | ||
| labels = append(labels, fmt.Sprintf("%s: %s", key, value)) | ||
| } | ||
| table.AddRow(*keyPair.Name, strings.Join(labels, ", "), *keyPair.Fingerprint, *keyPair.CreatedAt, *keyPair.UpdatedAt) | ||
| } | ||
| p.Outputln(table.Render()) | ||
| } | ||
| return nil | ||
| } |
| package update | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testKeyPairName = "foobar_key" | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testKeyPairName, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| labelsFlag: "foo=bar", | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| Labels: utils.Ptr(map[string]string{ | ||
| "foo": "bar", | ||
| }), | ||
| KeyPairName: utils.Ptr(testKeyPairName), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiUpdateKeyPairRequest)) iaas.ApiUpdateKeyPairRequest { | ||
| request := testClient.UpdateKeyPair(testCtx, testKeyPairName) | ||
| request = request.UpdateKeyPairPayload(fixturePayload()) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func fixturePayload(mods ...func(payload *iaas.UpdateKeyPairPayload)) iaas.UpdateKeyPairPayload { | ||
| payload := iaas.UpdateKeyPairPayload{ | ||
| Labels: utils.Ptr(map[string]interface{}{ | ||
| "foo": "bar", | ||
| }), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(&payload) | ||
| } | ||
| return payload | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flags", | ||
| argValues: fixtureArgValues(), | ||
| 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) | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiUpdateKeyPairRequest | ||
| }{ | ||
| { | ||
| 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(iaas.NullableString{}), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package update | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/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/iaas/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/iaas" | ||
| ) | ||
| const ( | ||
| keyPairNameArg = "KEY_PAIR_NAME" | ||
| labelsFlag = "labels" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Labels *map[string]string | ||
| KeyPairName *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "update", | ||
| Short: "Updates a key pair", | ||
| Long: "Updates a key pair.", | ||
| Args: args.SingleArg(keyPairNameArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Update the labels of a key pair with name "KEY_PAIR_NAME" with "key=value,key1=value1"`, | ||
| "$ stackit beta key-pair update KEY_PAIR_NAME --labels key=value,key1=value1", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update key pair %q?", *model.KeyPairName) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return fmt.Errorf("update key pair: %w", err) | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("update key pair: %w", err) | ||
| } | ||
| return outputResult(p, model, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringToString(labelsFlag, nil, "Labels are key-value string pairs which can be attached to a server. E.g. '--labels key1=value1,key2=value2,...'") | ||
| err := cmd.MarkFlagRequired(labelsFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateKeyPairRequest { | ||
| req := apiClient.UpdateKeyPair(ctx, *model.KeyPairName) | ||
| var labelsMap *map[string]interface{} | ||
| if model.Labels != nil && len(*model.Labels) > 0 { | ||
| // convert map[string]string to map[string]interface{} | ||
| labelsMap = utils.Ptr(map[string]interface{}{}) | ||
| for k, v := range *model.Labels { | ||
| (*labelsMap)[k] = v | ||
| } | ||
| } | ||
| payload := iaas.UpdateKeyPairPayload{ | ||
| Labels: labelsMap, | ||
| } | ||
| return req.UpdateKeyPairPayload(payload) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| keyPairName := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), | ||
| KeyPairName: utils.Ptr(keyPairName), | ||
| } | ||
| 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 outputResult(p *print.Printer, model *inputModel, keyPair *iaas.Keypair) error { | ||
| switch model.OutputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(keyPair, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal key pair: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(keyPair, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal key pair: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| default: | ||
| p.Outputf("Updated labels of key pair %q\n", *model.KeyPairName) | ||
| } | ||
| return nil | ||
| } |
| package create | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testClient = &iaas.APIClient{} | ||
| testProjectId = uuid.NewString() | ||
| testName = "new-security-group" | ||
| testDescription = "a test description" | ||
| testLabels = map[string]string{ | ||
| "fooKey": "fooValue", | ||
| "barKey": "barValue", | ||
| "bazKey": "bazValue", | ||
| } | ||
| testStateful = true | ||
| ) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| descriptionFlag: testDescription, | ||
| labelsFlag: "fooKey=fooValue,barKey=barValue,bazKey=bazValue", | ||
| statefulFlag: "true", | ||
| nameFlag: testName, | ||
| } | ||
| 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}, | ||
| Labels: &testLabels, | ||
| Description: &testDescription, | ||
| Name: &testName, | ||
| Stateful: &testStateful, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func toStringAnyMapPtr(m map[string]string) map[string]any { | ||
| if m == nil { | ||
| return nil | ||
| } | ||
| result := map[string]any{} | ||
| for k, v := range m { | ||
| result[k] = v | ||
| } | ||
| return result | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiCreateSecurityGroupRequest)) iaas.ApiCreateSecurityGroupRequest { | ||
| request := testClient.CreateSecurityGroup(testCtx, testProjectId) | ||
| request = request.CreateSecurityGroupPayload(iaas.CreateSecurityGroupPayload{ | ||
| Description: &testDescription, | ||
| Labels: utils.Ptr(toStringAnyMapPtr(testLabels)), | ||
| Name: &testName, | ||
| Rules: nil, | ||
| Stateful: &testStateful, | ||
| }) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "name missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, nameFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no labels", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, labelsFlag) | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Labels = nil | ||
| }), | ||
| }, | ||
| { | ||
| description: "single label", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[labelsFlag] = "foo=bar" | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Labels = &map[string]string{ | ||
| "foo": "bar", | ||
| } | ||
| }), | ||
| }, | ||
| { | ||
| description: "stateless security group", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[statefulFlag] = "false" | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Stateful = utils.Ptr(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 iaas.ApiCreateSecurityGroupRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| { | ||
| description: "no labels", | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.Labels = nil | ||
| }), | ||
| expectedRequest: fixtureRequest(func(request *iaas.ApiCreateSecurityGroupRequest) { | ||
| *request = request.CreateSecurityGroupPayload(iaas.CreateSecurityGroupPayload{ | ||
| Description: &testDescription, | ||
| Labels: nil, | ||
| Name: &testName, | ||
| Stateful: &testStateful, | ||
| }) | ||
| }), | ||
| }, | ||
| { | ||
| description: "stateless security group", | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.Stateful = utils.Ptr(false) | ||
| }), | ||
| expectedRequest: fixtureRequest(func(request *iaas.ApiCreateSecurityGroupRequest) { | ||
| *request = request.CreateSecurityGroupPayload(iaas.CreateSecurityGroupPayload{ | ||
| Description: &testDescription, | ||
| Labels: utils.Ptr(toStringAnyMapPtr(testLabels)), | ||
| Name: &testName, | ||
| Stateful: utils.Ptr(false), | ||
| }) | ||
| }), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package create | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/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/services/iaas/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| const ( | ||
| nameFlag = "name" | ||
| descriptionFlag = "description" | ||
| statefulFlag = "stateful" | ||
| labelsFlag = "labels" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Labels *map[string]string | ||
| Description *string | ||
| Name *string | ||
| Stateful *bool | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "create", | ||
| Short: "Creates security groups", | ||
| Long: "Creates security groups.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample(`Create a named group`, `$ stackit beta security-group create --name my-new-group`), | ||
| examples.NewExample(`Create a named group with labels`, `$ stackit beta security-group create --name my-new-group --labels label1=value1,label2=value2`), | ||
| ), | ||
| 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 | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create the security group %q?", *model.Name) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| request := buildRequest(ctx, model, apiClient) | ||
| group, err := request.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("create security group: %w", err) | ||
| } | ||
| if err := outputResult(p, model, group); err != nil { | ||
| return err | ||
| } | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().String(nameFlag, "", "The name of the security group.") | ||
| cmd.Flags().String(descriptionFlag, "", "An optional description of the security group.") | ||
| cmd.Flags().Bool(statefulFlag, false, "Create a stateful or a stateless security group") | ||
| cmd.Flags().StringToString(labelsFlag, nil, "Labels are key-value string pairs which can be attached to a network-interface. E.g. '--labels key1=value1,key2=value2,...'") | ||
| if err := flags.MarkFlagsRequired(cmd, nameFlag); err != nil { | ||
| 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{} | ||
| } | ||
| name := flags.FlagToStringValue(p, cmd, nameFlag) | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Name: &name, | ||
| Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), | ||
| Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), | ||
| Stateful: flags.FlagToBoolPointer(p, cmd, statefulFlag), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiCreateSecurityGroupRequest { | ||
| request := apiClient.CreateSecurityGroup(ctx, model.ProjectId) | ||
| var labelsMap *map[string]any | ||
| if model.Labels != nil && len(*model.Labels) > 0 { | ||
| // convert map[string]string to map[string]interface{} | ||
| labelsMap = utils.Ptr(map[string]interface{}{}) | ||
| for k, v := range *model.Labels { | ||
| (*labelsMap)[k] = v | ||
| } | ||
| } | ||
| payload := iaas.CreateSecurityGroupPayload{ | ||
| Description: model.Description, | ||
| Labels: labelsMap, | ||
| Name: model.Name, | ||
| Stateful: model.Stateful, | ||
| } | ||
| return request.CreateSecurityGroupPayload(payload) | ||
| } | ||
| func outputResult(p *print.Printer, model *inputModel, resp *iaas.SecurityGroup) error { | ||
| switch model.OutputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(resp, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal security group: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal security group: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| p.Outputf("Created security group %q\n", *model.Name) | ||
| 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/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testClient = &iaas.APIClient{} | ||
| testProjectId = uuid.NewString() | ||
| testGroupId = uuid.NewString() | ||
| ) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault}, | ||
| SecurityGroupId: testGroupId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiDeleteSecurityGroupRequest)) iaas.ApiDeleteSecurityGroupRequest { | ||
| request := testClient.DeleteSecurityGroup(testCtx, testProjectId, testGroupId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| args []string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| args: []string{testGroupId}, | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no arguments", | ||
| flagValues: fixtureFlagValues(), | ||
| args: nil, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "multiple arguments", | ||
| flagValues: fixtureFlagValues(), | ||
| args: []string{"foo", "bar"}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "invalid group id", | ||
| flagValues: fixtureFlagValues(), | ||
| args: []string{"foo"}, | ||
| 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) | ||
| } | ||
| cmd.SetArgs(tt.args) | ||
| 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.ValidateArgs(tt.args); err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.args) | ||
| 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 iaas.ApiDeleteSecurityGroupRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package delete | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/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/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| SecurityGroupId string | ||
| } | ||
| const groupIdArg = "GROUP_ID" | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "delete", | ||
| Short: "Deletes a security group", | ||
| Long: "Deletes a security group by its internal ID.", | ||
| Args: args.SingleArg(groupIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample(`Delete a named group with ID "xxx"`, `$ stackit beta security-group delete xxx`), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, p, cmd) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| projectLabel = model.ProjectId | ||
| } | ||
| groupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, model.SecurityGroupId) | ||
| if err != nil { | ||
| p.Warn("get security group name: %v", err) | ||
| groupLabel = model.SecurityGroupId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the security group %q for %q?", groupLabel, projectLabel) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| request := buildRequest(ctx, model, apiClient) | ||
| if err := request.Execute(); err != nil { | ||
| return fmt.Errorf("delete security group: %w", err) | ||
| } | ||
| p.Info("Deleted security group %q for %q\n", groupLabel, projectLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| SecurityGroupId: cliArgs[0], | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiDeleteSecurityGroupRequest { | ||
| request := apiClient.DeleteSecurityGroup(ctx, model.ProjectId, model.SecurityGroupId) | ||
| return request | ||
| } |
| 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/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testClient = &iaas.APIClient{} | ||
| testProjectId = uuid.NewString() | ||
| testSecurityGroupId = []string{uuid.NewString()} | ||
| ) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault}, | ||
| SecurityGroupId: testSecurityGroupId[0], | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiGetSecurityGroupRequest)) iaas.ApiGetSecurityGroupRequest { | ||
| request := testClient.GetSecurityGroup(testCtx, testProjectId, testSecurityGroupId[0]) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| args []string | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| expectedModel: fixtureInputModel(), | ||
| args: testSecurityGroupId, | ||
| isValid: true, | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| args: testSecurityGroupId, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| args: testSecurityGroupId, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| args: testSecurityGroupId, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| args: testSecurityGroupId, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no group id passed", | ||
| flagValues: fixtureFlagValues(), | ||
| args: nil, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "multiple group ids passed", | ||
| flagValues: fixtureFlagValues(), | ||
| args: []string{uuid.NewString(), uuid.NewString()}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "invalid group id passed", | ||
| flagValues: fixtureFlagValues(), | ||
| args: []string{"foobar"}, | ||
| }, | ||
| } | ||
| 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) | ||
| } | ||
| if err := cmd.ValidateArgs(tt.args); err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| } | ||
| model, err := parseInput(p, cmd, tt.args) | ||
| 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 iaas.ApiGetSecurityGroupRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package describe | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| "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/iaas/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| SecurityGroupId string | ||
| } | ||
| const groupIdArg = "GROUP_ID" | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "describe", | ||
| Short: "Describes security groups", | ||
| Long: "Describes security groups by its internal ID.", | ||
| Args: args.SingleArg(groupIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample(`Describe group "xxx"`, `$ stackit beta security-group describe xxx`), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
| request := buildRequest(ctx, model, apiClient) | ||
| group, err := request.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("get security group: %w", err) | ||
| } | ||
| if err := outputResult(p, model, group); err != nil { | ||
| return err | ||
| } | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetSecurityGroupRequest { | ||
| request := apiClient.GetSecurityGroup(ctx, model.ProjectId, model.SecurityGroupId) | ||
| return request | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| SecurityGroupId: cliArgs[0], | ||
| } | ||
| 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 outputResult(p *print.Printer, model *inputModel, resp *iaas.SecurityGroup) error { | ||
| switch model.OutputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(resp, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal security group: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal security group: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| if id := resp.Id; id != nil { | ||
| table.AddRow("ID", *id) | ||
| } | ||
| table.AddSeparator() | ||
| if name := resp.Name; name != nil { | ||
| table.AddRow("NAME", *name) | ||
| table.AddSeparator() | ||
| } | ||
| if description := resp.Description; description != nil { | ||
| table.AddRow("DESCRIPTION", *description) | ||
| table.AddSeparator() | ||
| } | ||
| if resp.Labels != nil && len(*resp.Labels) > 0 { | ||
| labels := []string{} | ||
| for key, value := range *resp.Labels { | ||
| labels = append(labels, fmt.Sprintf("%s: %s", key, value)) | ||
| } | ||
| table.AddRow("LABELS", strings.Join(labels, "\n")) | ||
| table.AddSeparator() | ||
| } | ||
| if err := table.Display(p); err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testClient = &iaas.APIClient{} | ||
| testProjectId = uuid.NewString() | ||
| testLabels = "fooKey=fooValue,barKey=barValue,bazKey=bazValue" | ||
| ) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| labelSelectorFlag: testLabels, | ||
| } | ||
| 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}, | ||
| LabelSelector: utils.Ptr(testLabels), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiListSecurityGroupsRequest)) iaas.ApiListSecurityGroupsRequest { | ||
| request := testClient.ListSecurityGroups(testCtx, testProjectId) | ||
| request = request.LabelSelector(testLabels) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no labels", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, labelSelectorFlag) | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.LabelSelector = nil | ||
| }), | ||
| }, | ||
| { | ||
| description: "single label", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[labelSelectorFlag] = "foo=bar" | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.LabelSelector = utils.Ptr("foo=bar") | ||
| }), | ||
| }, | ||
| } | ||
| 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 iaas.ApiListSecurityGroupsRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| { | ||
| description: "no labels", | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.LabelSelector = utils.Ptr("") | ||
| }), | ||
| expectedRequest: fixtureRequest(func(request *iaas.ApiListSecurityGroupsRequest) { | ||
| *request = request.LabelSelector("") | ||
| }), | ||
| }, | ||
| { | ||
| description: "single label", | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.LabelSelector = utils.Ptr("foo=bar") | ||
| }), | ||
| expectedRequest: fixtureRequest(func(request *iaas.ApiListSecurityGroupsRequest) { | ||
| *request = request.LabelSelector("foo=bar") | ||
| }), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/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/iaas/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| LabelSelector *string | ||
| } | ||
| const ( | ||
| labelSelectorFlag = "label-selector" | ||
| ) | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "Lists security groups", | ||
| Long: "Lists security groups by its internal ID.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample(`List all groups`, `$ stackit beta security-group list`), | ||
| examples.NewExample(`List groups with labels`, `$ stackit beta security-group list --label-selector label1=value1,label2=value2`), | ||
| ), | ||
| 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 | ||
| } | ||
| // Call API | ||
| request := buildRequest(ctx, model, apiClient) | ||
| response, err := request.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("list security group: %w", err) | ||
| } | ||
| if items := response.GetItems(); items == nil || len(*items) == 0 { | ||
| p.Info("No security groups found for project %q", projectLabel) | ||
| } else { | ||
| if err := outputResult(p, model.OutputFormat, *items); err != nil { | ||
| return fmt.Errorf("output security groups: %w", err) | ||
| } | ||
| } | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().String(labelSelectorFlag, "", "Filter by label") | ||
| } | ||
| 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, | ||
| LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiListSecurityGroupsRequest { | ||
| request := apiClient.ListSecurityGroups(ctx, model.ProjectId) | ||
| if model.LabelSelector != nil { | ||
| request = request.LabelSelector(*model.LabelSelector) | ||
| } | ||
| return request | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, items []iaas.SecurityGroup) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(items, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal PostgreSQL Flex instance list: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal PostgreSQL Flex instance list: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| table.SetHeader("ID", "NAME", "STATEFUL") | ||
| for _, item := range items { | ||
| table.AddRow(utils.PtrString(item.Id), utils.PtrString(item.Name), utils.PtrString(item.Stateful)) | ||
| } | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| } |
| package create | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testSecurityGroupId = uuid.NewString() | ||
| var testRemoteSecurityGroupId = uuid.NewString() | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| securityGroupIdFlag: testSecurityGroupId, | ||
| directionFlag: "ingress", | ||
| descriptionFlag: "example-description", | ||
| etherTypeFlag: "ether", | ||
| icmpParameterCodeFlag: "0", | ||
| icmpParameterTypeFlag: "8", | ||
| ipRangeFlag: "10.1.2.3", | ||
| portRangeMaxFlag: "24", | ||
| portRangeMinFlag: "22", | ||
| remoteSecurityGroupIdFlag: testRemoteSecurityGroupId, | ||
| protocolNumberFlag: "1", | ||
| protocolNameFlag: "icmp", | ||
| } | ||
| 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, | ||
| }, | ||
| SecurityGroupId: testSecurityGroupId, | ||
| Direction: utils.Ptr("ingress"), | ||
| Description: utils.Ptr("example-description"), | ||
| EtherType: utils.Ptr("ether"), | ||
| IcmpParameterCode: utils.Ptr(int64(0)), | ||
| IcmpParameterType: utils.Ptr(int64(8)), | ||
| IpRange: utils.Ptr("10.1.2.3"), | ||
| PortRangeMax: utils.Ptr(int64(24)), | ||
| PortRangeMin: utils.Ptr(int64(22)), | ||
| RemoteSecurityGroupId: utils.Ptr(testRemoteSecurityGroupId), | ||
| ProtocolNumber: utils.Ptr(int64(1)), | ||
| ProtocolName: utils.Ptr("icmp"), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiCreateSecurityGroupRuleRequest)) iaas.ApiCreateSecurityGroupRuleRequest { | ||
| request := testClient.CreateSecurityGroupRule(testCtx, testProjectId, testSecurityGroupId) | ||
| request = request.CreateSecurityGroupRulePayload(fixturePayload()) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func fixtureRequiredRequest(mods ...func(request *iaas.ApiCreateSecurityGroupRuleRequest)) iaas.ApiCreateSecurityGroupRuleRequest { | ||
| request := testClient.CreateSecurityGroupRule(testCtx, testProjectId, testSecurityGroupId) | ||
| request = request.CreateSecurityGroupRulePayload(iaas.CreateSecurityGroupRulePayload{ | ||
| Direction: utils.Ptr("ingress"), | ||
| }) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func fixturePayload(mods ...func(payload *iaas.CreateSecurityGroupRulePayload)) iaas.CreateSecurityGroupRulePayload { | ||
| payload := iaas.CreateSecurityGroupRulePayload{ | ||
| Direction: utils.Ptr("ingress"), | ||
| Description: utils.Ptr("example-description"), | ||
| Ethertype: utils.Ptr("ether"), | ||
| IcmpParameters: &iaas.ICMPParameters{ | ||
| Code: utils.Ptr(int64(0)), | ||
| Type: utils.Ptr(int64(8)), | ||
| }, | ||
| IpRange: utils.Ptr("10.1.2.3"), | ||
| PortRange: &iaas.PortRange{ | ||
| Max: utils.Ptr(int64(24)), | ||
| Min: utils.Ptr(int64(22)), | ||
| }, | ||
| Protocol: &iaas.CreateProtocol{ | ||
| Int64: utils.Ptr(int64(1)), | ||
| String: utils.Ptr("icmp"), | ||
| }, | ||
| RemoteSecurityGroupId: utils.Ptr(testRemoteSecurityGroupId), | ||
| } | ||
| 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(func(flagValues map[string]string) { | ||
| delete(flagValues, portRangeMaxFlag) | ||
| delete(flagValues, portRangeMinFlag) | ||
| delete(flagValues, protocolNumberFlag) | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.PortRangeMax = nil | ||
| model.PortRangeMin = nil | ||
| model.ProtocolNumber = nil | ||
| }), | ||
| }, | ||
| { | ||
| description: "required only", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, descriptionFlag) | ||
| delete(flagValues, etherTypeFlag) | ||
| delete(flagValues, icmpParameterCodeFlag) | ||
| delete(flagValues, icmpParameterTypeFlag) | ||
| delete(flagValues, ipRangeFlag) | ||
| delete(flagValues, portRangeMaxFlag) | ||
| delete(flagValues, portRangeMinFlag) | ||
| delete(flagValues, remoteSecurityGroupIdFlag) | ||
| delete(flagValues, protocolNumberFlag) | ||
| delete(flagValues, protocolNameFlag) | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Description = nil | ||
| model.EtherType = nil | ||
| model.IcmpParameterCode = nil | ||
| model.IcmpParameterType = nil | ||
| model.IpRange = nil | ||
| model.PortRangeMax = nil | ||
| model.PortRangeMin = nil | ||
| model.RemoteSecurityGroupId = nil | ||
| model.ProtocolNumber = nil | ||
| model.ProtocolName = nil | ||
| }), | ||
| }, | ||
| { | ||
| description: "direction missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, directionFlag) | ||
| delete(flagValues, protocolNumberFlag) | ||
| delete(flagValues, protocolNameFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "protocol is not icmp and port range values are provided", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[protocolNameFlag] = "not-icmp" | ||
| delete(flagValues, icmpParameterCodeFlag) | ||
| delete(flagValues, icmpParameterTypeFlag) | ||
| delete(flagValues, protocolNumberFlag) | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.IcmpParameterCode = nil | ||
| model.IcmpParameterType = nil | ||
| model.ProtocolName = utils.Ptr("not-icmp") | ||
| model.ProtocolNumber = nil | ||
| }), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, securityGroupIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[securityGroupIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[securityGroupIdFlag] = "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.ValidateFlagGroups() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flag groups: %v", 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) { | ||
| var tests = []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiCreateSecurityGroupRuleRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| { | ||
| description: "only direction and security group id in payload", | ||
| model: &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| Direction: utils.Ptr("ingress"), | ||
| SecurityGroupId: testSecurityGroupId, | ||
| }, | ||
| expectedRequest: fixtureRequiredRequest(), | ||
| }, | ||
| } | ||
| 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(iaas.NullableString{}), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package create | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| securityGroupIdFlag = "security-group-id" | ||
| directionFlag = "direction" | ||
| descriptionFlag = "description" | ||
| etherTypeFlag = "ether-type" | ||
| icmpParameterCodeFlag = "icmp-parameter-code" | ||
| icmpParameterTypeFlag = "icmp-parameter-type" | ||
| ipRangeFlag = "ip-range" | ||
| portRangeMaxFlag = "port-range-max" | ||
| portRangeMinFlag = "port-range-min" | ||
| remoteSecurityGroupIdFlag = "remote-security-group-id" | ||
| protocolNumberFlag = "protocol-number" | ||
| protocolNameFlag = "protocol-name" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| SecurityGroupId string | ||
| Direction *string | ||
| Description *string | ||
| EtherType *string | ||
| IcmpParameterCode *int64 | ||
| IcmpParameterType *int64 | ||
| IpRange *string | ||
| PortRangeMax *int64 | ||
| PortRangeMin *int64 | ||
| RemoteSecurityGroupId *string | ||
| ProtocolNumber *int64 | ||
| ProtocolName *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "create", | ||
| Short: "Creates a security group rule", | ||
| Long: "Creates a security group rule.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Create a security group rule for security group with ID "xxx" with direction "ingress"`, | ||
| `$ stackit beta security-group rule create --security-group-id xxx --direction ingress`, | ||
| ), | ||
| examples.NewExample( | ||
| `Create a security group rule for security group with ID "xxx" with direction "egress", protocol "icmp" and icmp parameters`, | ||
| `$ stackit beta security-group rule create --security-group-id xxx --direction egress --protocol-name icmp --icmp-parameter-code 0 --icmp-parameter-type 8`, | ||
| ), | ||
| examples.NewExample( | ||
| `Create a security group rule for security group with ID "xxx" with direction "ingress", protocol "tcp" and port range values`, | ||
| `$ stackit beta security-group rule create --security-group-id xxx --direction ingress --protocol-name tcp --port-range-max 24 --port-range-min 22`, | ||
| ), | ||
| examples.NewExample( | ||
| `Create a security group rule for security group with ID "xxx" with direction "ingress" and protocol number 1 `, | ||
| `$ stackit beta security-group rule create --security-group-id xxx --direction ingress --protocol-number 1`, | ||
| ), | ||
| ), | ||
| 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 | ||
| } | ||
| securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, model.SecurityGroupId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get security group name: %v", err) | ||
| securityGroupLabel = model.SecurityGroupId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a security group rule for security group %q for project %q?", securityGroupLabel, projectLabel) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("create security group rule : %w", err) | ||
| } | ||
| return outputResult(p, model, projectLabel, securityGroupLabel, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Var(flags.UUIDFlag(), securityGroupIdFlag, `The security group ID`) | ||
| cmd.Flags().String(directionFlag, "", `The direction of the traffic which the rule should match. The possible values are: "ingress", "egress"`) | ||
| cmd.Flags().String(descriptionFlag, "", `The rule description`) | ||
| cmd.Flags().String(etherTypeFlag, "", `The ethertype which the rule should match`) | ||
| cmd.Flags().Int64(icmpParameterCodeFlag, 0, `ICMP code. Can be set if the protocol is ICMP`) | ||
| cmd.Flags().Int64(icmpParameterTypeFlag, 0, `ICMP type. Can be set if the protocol is ICMP`) | ||
| cmd.Flags().String(ipRangeFlag, "", `The remote IP range which the rule should match`) | ||
| cmd.Flags().Int64(portRangeMaxFlag, 0, `The maximum port number. Should be greater or equal to the minimum. This should only be provided if the protocol is not ICMP`) | ||
| cmd.Flags().Int64(portRangeMinFlag, 0, `The minimum port number. Should be less or equal to the maximum. This should only be provided if the protocol is not ICMP`) | ||
| cmd.Flags().Var(flags.UUIDFlag(), remoteSecurityGroupIdFlag, `The remote security group which the rule should match`) | ||
| cmd.Flags().Int64(protocolNumberFlag, 0, `The protocol number which the rule should match. If a protocol is to be defined, either "protocol-name" or "protocol-number" must be provided`) | ||
| cmd.Flags().String(protocolNameFlag, "", `The protocol name which the rule should match. If a protocol is to be defined, either "protocol-name" or "protocol-number" must be provided`) | ||
| err := flags.MarkFlagsRequired(cmd, securityGroupIdFlag, directionFlag) | ||
| cmd.MarkFlagsMutuallyExclusive(protocolNumberFlag, protocolNameFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &cliErr.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| SecurityGroupId: flags.FlagToStringValue(p, cmd, securityGroupIdFlag), | ||
| Direction: flags.FlagToStringPointer(p, cmd, directionFlag), | ||
| Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), | ||
| EtherType: flags.FlagToStringPointer(p, cmd, etherTypeFlag), | ||
| IcmpParameterCode: flags.FlagToInt64Pointer(p, cmd, icmpParameterCodeFlag), | ||
| IcmpParameterType: flags.FlagToInt64Pointer(p, cmd, icmpParameterTypeFlag), | ||
| IpRange: flags.FlagToStringPointer(p, cmd, ipRangeFlag), | ||
| PortRangeMax: flags.FlagToInt64Pointer(p, cmd, portRangeMaxFlag), | ||
| PortRangeMin: flags.FlagToInt64Pointer(p, cmd, portRangeMinFlag), | ||
| RemoteSecurityGroupId: flags.FlagToStringPointer(p, cmd, remoteSecurityGroupIdFlag), | ||
| ProtocolNumber: flags.FlagToInt64Pointer(p, cmd, protocolNumberFlag), | ||
| ProtocolName: flags.FlagToStringPointer(p, cmd, protocolNameFlag), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiCreateSecurityGroupRuleRequest { | ||
| req := apiClient.CreateSecurityGroupRule(ctx, model.ProjectId, model.SecurityGroupId) | ||
| icmpParameters := &iaas.ICMPParameters{} | ||
| portRange := &iaas.PortRange{} | ||
| protocol := &iaas.CreateProtocol{} | ||
| payload := iaas.CreateSecurityGroupRulePayload{ | ||
| Direction: model.Direction, | ||
| Description: model.Description, | ||
| Ethertype: model.EtherType, | ||
| IpRange: model.IpRange, | ||
| RemoteSecurityGroupId: model.RemoteSecurityGroupId, | ||
| } | ||
| if model.IcmpParameterCode != nil || model.IcmpParameterType != nil { | ||
| icmpParameters.Code = model.IcmpParameterCode | ||
| icmpParameters.Type = model.IcmpParameterType | ||
| payload.IcmpParameters = icmpParameters | ||
| } | ||
| if model.PortRangeMax != nil || model.PortRangeMin != nil { | ||
| portRange.Max = model.PortRangeMax | ||
| portRange.Min = model.PortRangeMin | ||
| payload.PortRange = portRange | ||
| } | ||
| if model.ProtocolNumber != nil || model.ProtocolName != nil { | ||
| protocol.Int64 = model.ProtocolNumber | ||
| protocol.String = model.ProtocolName | ||
| payload.Protocol = protocol | ||
| } | ||
| if model.RemoteSecurityGroupId == nil { | ||
| payload.RemoteSecurityGroupId = nil | ||
| } | ||
| return req.CreateSecurityGroupRulePayload(payload) | ||
| } | ||
| func outputResult(p *print.Printer, model *inputModel, projectLabel, securityGroupName string, securityGroupRule *iaas.SecurityGroupRule) error { | ||
| switch model.OutputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(securityGroupRule, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal security group rule: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(securityGroupRule, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal security group rule: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| operationState := "Created" | ||
| if model.Async { | ||
| operationState = "Triggered creation of" | ||
| } | ||
| p.Outputf("%s security group rule for security group %q in project %q.\nSecurity group rule ID: %s\n", operationState, securityGroupName, projectLabel, *securityGroupRule.Id) | ||
| 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-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/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testSecurityGroupId = uuid.NewString() | ||
| var testSecurityGroupRuleId = uuid.NewString() | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testSecurityGroupRuleId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| securityGroupIdFlag: testSecurityGroupId, | ||
| } | ||
| 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, | ||
| }, | ||
| SecurityGroupId: utils.Ptr(testSecurityGroupId), | ||
| SecurityGroupRuleId: testSecurityGroupRuleId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiDeleteSecurityGroupRuleRequest)) iaas.ApiDeleteSecurityGroupRuleRequest { | ||
| request := testClient.DeleteSecurityGroupRule(testCtx, testProjectId, testSecurityGroupId, testSecurityGroupRuleId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| aclValues []string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, securityGroupIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[securityGroupIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[securityGroupIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group rule id invalid 1", | ||
| argValues: []string{""}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group rule id invalid 2", | ||
| argValues: []string{"invalid-uuid"}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing flags: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiDeleteSecurityGroupRuleRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package delete | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/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/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| const ( | ||
| securityGroupRuleIdArg = "SECURITY_GROUP_RULE_ID" | ||
| securityGroupIdFlag = "security-group-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| SecurityGroupRuleId string | ||
| SecurityGroupId *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "delete", | ||
| Short: "Deletes a security group rule", | ||
| Long: fmt.Sprintf("%s\n%s\n", | ||
| "Deletes a security group rule.", | ||
| "If the security group rule is still in use, the deletion will fail", | ||
| ), | ||
| Args: args.SingleArg(securityGroupRuleIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Delete security group rule with ID "xxx" in security group with ID "yyy"`, | ||
| "$ stackit beta security-group rule delete xxx --security-group-id yyy", | ||
| ), | ||
| ), | ||
| 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 | ||
| } | ||
| securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, *model.SecurityGroupId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get security group name: %v", err) | ||
| securityGroupLabel = *model.SecurityGroupId | ||
| } | ||
| securityGroupRuleLabel, err := iaasUtils.GetSecurityGroupRuleName(ctx, apiClient, model.ProjectId, model.SecurityGroupRuleId, *model.SecurityGroupId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get security group rule name: %v", err) | ||
| securityGroupRuleLabel = model.SecurityGroupRuleId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete security group rule %q from security group %q?", securityGroupRuleLabel, securityGroupLabel) | ||
| 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 security group rule: %w", err) | ||
| } | ||
| p.Info("Deleted security group rule %q from security group %q\n", securityGroupRuleLabel, securityGroupLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Var(flags.UUIDFlag(), securityGroupIdFlag, `The security group ID`) | ||
| err := flags.MarkFlagsRequired(cmd, securityGroupIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| securityGroupRuleId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| SecurityGroupRuleId: securityGroupRuleId, | ||
| SecurityGroupId: flags.FlagToStringPointer(p, cmd, securityGroupIdFlag), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiDeleteSecurityGroupRuleRequest { | ||
| return apiClient.DeleteSecurityGroupRule(ctx, model.ProjectId, *model.SecurityGroupId, model.SecurityGroupRuleId) | ||
| } |
| package describe | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-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/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testSecurityGroupId = uuid.NewString() | ||
| var testSecurityGroupRuleId = uuid.NewString() | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testSecurityGroupRuleId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| securityGroupIdFlag: testSecurityGroupId, | ||
| } | ||
| 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, | ||
| }, | ||
| SecurityGroupId: utils.Ptr(testSecurityGroupId), | ||
| SecurityGroupRuleId: testSecurityGroupRuleId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiGetSecurityGroupRuleRequest)) iaas.ApiGetSecurityGroupRuleRequest { | ||
| request := testClient.GetSecurityGroupRule(testCtx, testProjectId, testSecurityGroupId, testSecurityGroupRuleId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no arg values", | ||
| argValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flag values", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, securityGroupIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[securityGroupIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[securityGroupIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group rule id invalid 1", | ||
| argValues: []string{""}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group rule id invalid 2", | ||
| argValues: []string{"invalid-uuid"}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiGetSecurityGroupRuleRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package describe | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/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/services/iaas/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| securityGroupRuleIdArg = "SECURITY_GROUP_RULE_ID" | ||
| securityGroupIdFlag = "security-group-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| SecurityGroupRuleId string | ||
| SecurityGroupId *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "describe", | ||
| Short: "Shows details of a security group rule", | ||
| Long: "Shows details of a security group rule.", | ||
| Args: args.SingleArg(securityGroupRuleIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Show details of a security group rule with ID "xxx" in security group with ID "yyy"`, | ||
| "$ stackit beta security-group rule describe xxx --security-group-id yyy", | ||
| ), | ||
| examples.NewExample( | ||
| `Show details of a security group rule with ID "xxx" in security group with ID "yyy" in JSON format`, | ||
| "$ stackit beta security-group rule describe xxx --security-group-id yyy --output-format json", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("read security group rule: %w", err) | ||
| } | ||
| return outputResult(p, model.OutputFormat, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Var(flags.UUIDFlag(), securityGroupIdFlag, `The security group ID`) | ||
| err := flags.MarkFlagsRequired(cmd, securityGroupIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| securityGroupRuleId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| SecurityGroupRuleId: securityGroupRuleId, | ||
| SecurityGroupId: flags.FlagToStringPointer(p, cmd, securityGroupIdFlag), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiGetSecurityGroupRuleRequest { | ||
| return apiClient.GetSecurityGroupRule(ctx, model.ProjectId, *model.SecurityGroupId, model.SecurityGroupRuleId) | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, securityGroupRule *iaas.SecurityGroupRule) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(securityGroupRule, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal security group rule: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(securityGroupRule, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal security group rule: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| table.AddRow("ID", *securityGroupRule.Id) | ||
| table.AddSeparator() | ||
| if securityGroupRule.Protocol != nil { | ||
| if securityGroupRule.Protocol.Name != nil { | ||
| table.AddRow("PROTOCOL NAME", *securityGroupRule.Protocol.Name) | ||
| table.AddSeparator() | ||
| } | ||
| if securityGroupRule.Protocol.Number != nil { | ||
| table.AddRow("PROTOCOL NUMBER", *securityGroupRule.Protocol.Number) | ||
| table.AddSeparator() | ||
| } | ||
| } | ||
| table.AddRow("DIRECTION", *securityGroupRule.Direction) | ||
| table.AddSeparator() | ||
| if securityGroupRule.PortRange != nil { | ||
| if securityGroupRule.PortRange.Min != nil { | ||
| table.AddRow("START PORT", *securityGroupRule.PortRange.Min) | ||
| table.AddSeparator() | ||
| } | ||
| if securityGroupRule.PortRange.Max != nil { | ||
| table.AddRow("END PORT", *securityGroupRule.PortRange.Max) | ||
| table.AddSeparator() | ||
| } | ||
| } | ||
| if securityGroupRule.Ethertype != nil { | ||
| table.AddRow("ETHER TYPE", *securityGroupRule.Ethertype) | ||
| table.AddSeparator() | ||
| } | ||
| if securityGroupRule.IpRange != nil { | ||
| table.AddRow("IP RANGE", *securityGroupRule.IpRange) | ||
| table.AddSeparator() | ||
| } | ||
| if securityGroupRule.RemoteSecurityGroupId != nil { | ||
| table.AddRow("REMOTE SECURITY GROUP", *securityGroupRule.RemoteSecurityGroupId) | ||
| table.AddSeparator() | ||
| } | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testSecurityGroupId = uuid.NewString() | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| limitFlag: "10", | ||
| securityGroupIdFlag: testSecurityGroupId, | ||
| } | ||
| 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, | ||
| }, | ||
| Limit: utils.Ptr(int64(10)), | ||
| SecurityGroupId: utils.Ptr(testSecurityGroupId), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiListSecurityGroupRulesRequest)) iaas.ApiListSecurityGroupRulesRequest { | ||
| request := testClient.ListSecurityGroupRules(testCtx, testProjectId, testSecurityGroupId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flag values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, securityGroupIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[securityGroupIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "security group id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[securityGroupIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "limit invalid", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "invalid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "limit invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "0" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing 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 iaas.ApiListSecurityGroupRulesRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/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/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| limitFlag = "limit" | ||
| securityGroupIdFlag = "security-group-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Limit *int64 | ||
| SecurityGroupId *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "Lists all security group rules in a security group of a project", | ||
| Long: "Lists all security group rules in a security group of a project.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Lists all security group rules in security group with ID "xxx"`, | ||
| "$ stackit beta security-group rule list --security-group-id xxx", | ||
| ), | ||
| examples.NewExample( | ||
| `Lists all security group rules in security group with ID "xxx" in JSON format`, | ||
| "$ stackit beta security-group rule list --security-group-id xxx --output-format json", | ||
| ), | ||
| examples.NewExample( | ||
| `Lists up to 10 security group rules in security group with ID "xxx"`, | ||
| "$ stackit beta security-group rule list --security-group-id xxx --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 security group rules: %w", err) | ||
| } | ||
| if resp.Items == nil || len(*resp.Items) == 0 { | ||
| securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, *model.SecurityGroupId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get security group name: %v", err) | ||
| securityGroupLabel = *model.SecurityGroupId | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, p, cmd) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| projectLabel = model.ProjectId | ||
| } | ||
| p.Info("No rules found in security group %q for project %q\n", securityGroupLabel, projectLabel) | ||
| return nil | ||
| } | ||
| // Truncate output | ||
| items := *resp.Items | ||
| 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, `Maximum number of entries to list`) | ||
| cmd.Flags().Var(flags.UUIDFlag(), securityGroupIdFlag, `The security group ID`) | ||
| err := flags.MarkFlagsRequired(cmd, securityGroupIdFlag) | ||
| 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{} | ||
| } | ||
| 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, | ||
| SecurityGroupId: flags.FlagToStringPointer(p, cmd, securityGroupIdFlag), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiListSecurityGroupRulesRequest { | ||
| return apiClient.ListSecurityGroupRules(ctx, model.ProjectId, *model.SecurityGroupId) | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat string, securityGroupRules []iaas.SecurityGroupRule) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(securityGroupRules, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal security group rules: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(securityGroupRules, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal security group rules: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| table.SetHeader("ID", "ETHER TYPE", "DIRECTION", "PROTOCOL") | ||
| for _, securityGroupRule := range securityGroupRules { | ||
| etherType := "" | ||
| if securityGroupRule.Ethertype != nil { | ||
| etherType = *securityGroupRule.Ethertype | ||
| } | ||
| protocolName := "" | ||
| if securityGroupRule.Protocol != nil { | ||
| if securityGroupRule.Protocol.Name != nil { | ||
| protocolName = *securityGroupRule.Protocol.Name | ||
| } | ||
| } | ||
| table.AddRow(*securityGroupRule.Id, etherType, *securityGroupRule.Direction, protocolName) | ||
| table.AddSeparator() | ||
| } | ||
| p.Outputln(table.Render()) | ||
| return nil | ||
| } | ||
| } |
| package rule | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/rule/create" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/rule/delete" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/rule/describe" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/rule/list" | ||
| "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: "rule", | ||
| Short: "Provides functionality for security group rules", | ||
| Long: "Provides functionality for security group rules.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, p) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, p *print.Printer) { | ||
| cmd.AddCommand(create.NewCmd(p)) | ||
| cmd.AddCommand(delete.NewCmd(p)) | ||
| cmd.AddCommand(describe.NewCmd(p)) | ||
| cmd.AddCommand(list.NewCmd(p)) | ||
| } |
| package security_group | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/create" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/delete" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/describe" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/list" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/rule" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/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: "security-group", | ||
| Short: "Manage security groups", | ||
| Long: "Manage the lifecycle of security groups and rules.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, p) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, p *print.Printer) { | ||
| cmd.AddCommand( | ||
| rule.NewCmd(p), | ||
| create.NewCmd(p), | ||
| delete.NewCmd(p), | ||
| describe.NewCmd(p), | ||
| list.NewCmd(p), | ||
| 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/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/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testClient = &iaas.APIClient{} | ||
| testProjectId = uuid.NewString() | ||
| testGroupId = []string{uuid.NewString()} | ||
| testName = "new-security-group" | ||
| testDescription = "a test description" | ||
| testLabels = map[string]string{ | ||
| "fooKey": "fooValue", | ||
| "barKey": "barValue", | ||
| "bazKey": "bazValue", | ||
| } | ||
| ) | ||
| func toStringAnyMapPtr(m map[string]string) map[string]any { | ||
| if m == nil { | ||
| return nil | ||
| } | ||
| result := map[string]any{} | ||
| for k, v := range m { | ||
| result[k] = v | ||
| } | ||
| return result | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| descriptionArg: testDescription, | ||
| labelsArg: "fooKey=fooValue,barKey=barValue,bazKey=bazValue", | ||
| nameArg: testName, | ||
| } | ||
| 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}, | ||
| Labels: &testLabels, | ||
| Description: &testDescription, | ||
| Name: &testName, | ||
| SecurityGroupId: testGroupId[0], | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiUpdateSecurityGroupRequest)) iaas.ApiUpdateSecurityGroupRequest { | ||
| request := testClient.UpdateSecurityGroup(testCtx, testProjectId, testGroupId[0]) | ||
| request = request.UpdateSecurityGroupPayload(iaas.UpdateSecurityGroupPayload{ | ||
| Description: &testDescription, | ||
| Labels: utils.Ptr(toStringAnyMapPtr(testLabels)), | ||
| Name: &testName, | ||
| }) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| args []string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| args: testGroupId, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values but valid group id", | ||
| flagValues: map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| }, | ||
| args: testGroupId, | ||
| isValid: false, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Labels = nil | ||
| model.Name = nil | ||
| model.Description = nil | ||
| }), | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| args: testGroupId, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| args: testGroupId, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| args: testGroupId, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no name passed", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, nameArg) | ||
| }), | ||
| args: testGroupId, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Name = nil | ||
| }), | ||
| isValid: true, | ||
| }, | ||
| { | ||
| description: "no description passed", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, descriptionArg) | ||
| }), | ||
| args: testGroupId, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Description = nil | ||
| }), | ||
| isValid: true, | ||
| }, | ||
| { | ||
| description: "no labels", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, labelsArg) | ||
| }), | ||
| args: testGroupId, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Labels = nil | ||
| }), | ||
| isValid: true, | ||
| }, | ||
| { | ||
| description: "single label", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[labelsArg] = "foo=bar" | ||
| }), | ||
| args: testGroupId, | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Labels = &map[string]string{ | ||
| "foo": "bar", | ||
| } | ||
| }), | ||
| }, | ||
| { | ||
| description: "no group id passed", | ||
| flagValues: fixtureFlagValues(), | ||
| args: nil, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "invalid group id passed", | ||
| flagValues: fixtureFlagValues(), | ||
| args: []string{"foobar"}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "multiple group ids passed", | ||
| flagValues: fixtureFlagValues(), | ||
| args: []string{uuid.NewString(), uuid.NewString()}, | ||
| 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 { | ||
| if err := cmd.Flags().Set(flag, value); 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) | ||
| } | ||
| if err := cmd.ValidateArgs(tt.args); err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| } | ||
| model, err := parseInput(p, cmd, tt.args) | ||
| 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 iaas.ApiUpdateSecurityGroupRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| { | ||
| description: "no labels", | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.Labels = nil | ||
| }), | ||
| expectedRequest: fixtureRequest(func(request *iaas.ApiUpdateSecurityGroupRequest) { | ||
| *request = request.UpdateSecurityGroupPayload(iaas.UpdateSecurityGroupPayload{ | ||
| Description: &testDescription, | ||
| Labels: nil, | ||
| Name: &testName, | ||
| }) | ||
| }), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package update | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "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/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Labels *map[string]string | ||
| Description *string | ||
| Name *string | ||
| SecurityGroupId string | ||
| } | ||
| const groupNameArg = "GROUP_ID" | ||
| const ( | ||
| nameArg = "name" | ||
| descriptionArg = "description" | ||
| labelsArg = "labels" | ||
| ) | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "update", | ||
| Short: "Updates a security group", | ||
| Long: "Updates a named security group", | ||
| Args: args.SingleArg(groupNameArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample(`Update the name of group "xxx"`, `$ stackit beta security-group update xxx --name my-new-name`), | ||
| examples.NewExample(`Update the labels of group "xxx"`, `$ stackit beta security-group update xxx --labels label1=value1,label2=value2`), | ||
| ), | ||
| 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 | ||
| } | ||
| groupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, model.SecurityGroupId) | ||
| if err != nil { | ||
| p.Warn("cannot retrieve groupname: %v", err) | ||
| groupLabel = model.SecurityGroupId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update the security group %q?", groupLabel) | ||
| 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("update security group: %w", err) | ||
| } | ||
| p.Info("Updated security group \"%v\" for %q\n", utils.PtrString(resp.Name), projectLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().String(nameArg, "", "The name of the security group.") | ||
| cmd.Flags().String(descriptionArg, "", "An optional description of the security group.") | ||
| cmd.Flags().StringToString(labelsArg, nil, "Labels are key-value string pairs which can be attached to a network-interface. E.g. '--labels key1=value1,key2=value2,...'") | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Labels: flags.FlagToStringToStringPointer(p, cmd, labelsArg), | ||
| Description: flags.FlagToStringPointer(p, cmd, descriptionArg), | ||
| Name: flags.FlagToStringPointer(p, cmd, nameArg), | ||
| SecurityGroupId: cliArgs[0], | ||
| } | ||
| if model.Labels == nil && model.Description == nil && model.Name == nil { | ||
| return nil, fmt.Errorf("no flags have been passed") | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiUpdateSecurityGroupRequest { | ||
| request := apiClient.UpdateSecurityGroup(ctx, model.ProjectId, model.SecurityGroupId) | ||
| payload := iaas.NewUpdateSecurityGroupPayload() | ||
| payload.Description = model.Description | ||
| var labelsMap *map[string]any | ||
| if model.Labels != nil && len(*model.Labels) > 0 { | ||
| // convert map[string]string to map[string]interface{} | ||
| labelsMap = utils.Ptr(map[string]interface{}{}) | ||
| for k, v := range *model.Labels { | ||
| (*labelsMap)[k] = v | ||
| } | ||
| } | ||
| payload.Labels = labelsMap | ||
| payload.Name = model.Name | ||
| request = request.UpdateSecurityGroupPayload(*payload) | ||
| return request | ||
| } |
| package console | ||
| 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/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| }, | ||
| ServerId: testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiGetServerConsoleRequest)) iaas.ApiGetServerConsoleRequest { | ||
| request := testClient.GetServerConsole(testCtx, testProjectId, testServerId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "invalid-uuid" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiGetServerConsoleRequest | ||
| }{ | ||
| { | ||
| 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 console | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "net/url" | ||
| "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/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| serverIdArg = "SERVER_ID" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "console", | ||
| Short: "Gets a URL for server remote console", | ||
| Long: "Gets a URL for server remote console.", | ||
| Args: args.SingleArg(serverIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Get a URL for the server remote console with server ID "xxx"`, | ||
| "$ stackit beta server console xxx", | ||
| ), | ||
| examples.NewExample( | ||
| `Get a URL for the server remote console with server ID "xxx" in JSON format`, | ||
| "$ stackit beta server console xxx --output-format json", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = model.ServerId | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("server console: %w", err) | ||
| } | ||
| return outputResult(p, model, serverLabel, resp) | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| serverId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: serverId, | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiGetServerConsoleRequest { | ||
| return apiClient.GetServerConsole(ctx, model.ProjectId, model.ServerId) | ||
| } | ||
| func outputResult(p *print.Printer, model *inputModel, serverLabel string, serverUrl *iaas.ServerConsoleUrl) error { | ||
| outputFormat := model.OutputFormat | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(serverUrl, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal url: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(serverUrl, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal url: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| // unescape url in order to get rid of e.g. %40 | ||
| unescapedURL, err := url.PathUnescape(*serverUrl.GetUrl()) | ||
| if err != nil { | ||
| return fmt.Errorf("unescape url: %w", err) | ||
| } | ||
| p.Outputf("Remote console URL %q for server %q\n", unescapedURL, serverLabel) | ||
| return nil | ||
| } | ||
| } |
| package deallocate | ||
| 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/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| }, | ||
| ServerId: testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiDeallocateServerRequest)) iaas.ApiDeallocateServerRequest { | ||
| request := testClient.DeallocateServer(testCtx, testProjectId, testServerId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "invalid-uuid" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiDeallocateServerRequest | ||
| }{ | ||
| { | ||
| 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 deallocate | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| serverIdArg = "SERVER_ID" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "deallocate", | ||
| Short: "Deallocates an existing server", | ||
| Long: "Deallocates an existing server.", | ||
| Args: args.SingleArg(serverIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Deallocate an existing server with ID "xxx"`, | ||
| "$ stackit beta server deallocate xxx", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = model.ServerId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to deallocate server %q?", serverLabel) | ||
| 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("server deallocate: %w", err) | ||
| } | ||
| // Wait for async operation, if async mode not enabled | ||
| if !model.Async { | ||
| s := spinner.New(p) | ||
| s.Start("Deallocating server") | ||
| _, err = wait.DeallocateServerWaitHandler(ctx, apiClient, model.ProjectId, model.ServerId).WaitWithContext(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("wait for server deallocating: %w", err) | ||
| } | ||
| s.Stop() | ||
| } | ||
| operationState := "Deallocated" | ||
| if model.Async { | ||
| operationState = "Triggered deallocation of" | ||
| } | ||
| p.Info("%s server %q\n", operationState, serverLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| serverId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: serverId, | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiDeallocateServerRequest { | ||
| return apiClient.DeallocateServer(ctx, model.ProjectId, model.ServerId) | ||
| } |
| package log | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| lengthLimitFlag: "3000", | ||
| } | ||
| 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, | ||
| }, | ||
| ServerId: testServerId, | ||
| Length: utils.Ptr(int64(3000)), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiGetServerLogRequest)) iaas.ApiGetServerLogRequest { | ||
| request := testClient.GetServerLog(testCtx, testProjectId, testServerId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "invalid-uuid" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "optional length missing (test default value)", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, lengthLimitFlag) | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Length = utils.Ptr(int64(2000)) | ||
| }), | ||
| }, | ||
| } | ||
| 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 iaas.ApiGetServerLogRequest | ||
| }{ | ||
| { | ||
| 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 log | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "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/services/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| serverIdArg = "SERVER_ID" | ||
| lengthLimitFlag = "length" | ||
| defaultLengthLimit = 2000 // lines | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| Length *int64 | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "log", | ||
| Short: "Gets server console log", | ||
| Long: "Gets server console log.", | ||
| Args: args.SingleArg(serverIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Get server console log for the server with ID "xxx"`, | ||
| "$ stackit beta server log xxx", | ||
| ), | ||
| examples.NewExample( | ||
| `Get server console log for the server with ID "xxx" and limit output lines to 1000`, | ||
| "$ stackit beta server log xxx --length 1000", | ||
| ), | ||
| examples.NewExample( | ||
| `Get server console log for the server with ID "xxx" in JSON format`, | ||
| "$ stackit beta server log xxx --output-format json", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = model.ServerId | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("server log: %w", err) | ||
| } | ||
| log := *resp.GetOutput() | ||
| lines := strings.Split(log, "\n") | ||
| if len(lines) > int(*model.Length) { | ||
| // Truncate output and show most recent logs | ||
| start := len(lines) - int(*model.Length) | ||
| return outputResult(p, model, serverLabel, strings.Join(lines[start:], "\n")) | ||
| } | ||
| return outputResult(p, model, serverLabel, log) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Int64(lengthLimitFlag, defaultLengthLimit, "Maximum number of lines to list") | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| serverId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| length := flags.FlagWithDefaultToInt64Value(p, cmd, lengthLimitFlag) | ||
| if length < 0 { | ||
| return nil, &errors.FlagValidationError{ | ||
| Flag: lengthLimitFlag, | ||
| Details: "must not be negative", | ||
| } | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: serverId, | ||
| Length: utils.Ptr(length), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiGetServerLogRequest { | ||
| return apiClient.GetServerLog(ctx, model.ProjectId, model.ServerId) | ||
| } | ||
| func outputResult(p *print.Printer, model *inputModel, serverLabel, log string) error { | ||
| outputFormat := model.OutputFormat | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(log, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal url: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(log, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal url: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| p.Outputf("Log for server %q\n%s", serverLabel, log) | ||
| return nil | ||
| } | ||
| } |
| package reboot | ||
| 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/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| hardRebootFlag: "false", | ||
| } | ||
| 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, | ||
| }, | ||
| ServerId: testServerId, | ||
| HardReboot: false, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiRebootServerRequest)) iaas.ApiRebootServerRequest { | ||
| request := testClient.RebootServer(testCtx, testProjectId, testServerId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "invalid-uuid" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiRebootServerRequest | ||
| }{ | ||
| { | ||
| description: "base (soft reboot)", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| { | ||
| description: "hard reboot is set", | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.HardReboot = true | ||
| }), | ||
| expectedRequest: fixtureRequest(func(request *iaas.ApiRebootServerRequest) { | ||
| *request = request.Action("hard") | ||
| }), | ||
| }, | ||
| } | ||
| 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 reboot | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| serverIdArg = "SERVER_ID" | ||
| hardRebootFlag = "hard" | ||
| defaultHardReboot = false | ||
| hardRebootAction = "hard" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| HardReboot bool | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "reboot", | ||
| Short: "Reboots a server", | ||
| Long: "Reboots a server.", | ||
| Args: args.SingleArg(serverIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Perform a soft reboot of a server with ID "xxx"`, | ||
| "$ stackit beta server reboot xxx", | ||
| ), | ||
| examples.NewExample( | ||
| `Perform a hard reboot of a server with ID "xxx"`, | ||
| "$ stackit beta server reboot xxx --hard", | ||
| ), | ||
| ), | ||
| 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 | ||
| } | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = model.ServerId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to reboot server %q?", serverLabel) | ||
| 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("server reboot: %w", err) | ||
| } | ||
| p.Info("Server %q rebooted\n", serverLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().BoolP(hardRebootFlag, "b", defaultHardReboot, "Performs a hard reboot. (default false)") | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| serverId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: serverId, | ||
| HardReboot: flags.FlagToBoolValue(p, cmd, hardRebootFlag), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiRebootServerRequest { | ||
| req := apiClient.RebootServer(ctx, model.ProjectId, model.ServerId) | ||
| // if hard reboot is set the action must be set (soft is default) | ||
| if model.HardReboot { | ||
| req = req.Action(hardRebootAction) | ||
| } | ||
| return req | ||
| } |
| package rescue | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| var testImageId = uuid.NewString() | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| imageIdFlag: testImageId, | ||
| } | ||
| 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, | ||
| }, | ||
| ServerId: testServerId, | ||
| ImageId: utils.Ptr(testImageId), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiRescueServerRequest)) iaas.ApiRescueServerRequest { | ||
| request := testClient.RescueServer(testCtx, testProjectId, testServerId) | ||
| request = request.RescueServerPayload(iaas.RescueServerPayload{ | ||
| Image: utils.Ptr(testImageId), | ||
| }) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "required image id flag missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, imageIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "invalid-uuid" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiRescueServerRequest | ||
| }{ | ||
| { | ||
| 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 rescue | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| serverIdArg = "SERVER_ID" | ||
| imageIdFlag = "image-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| ImageId *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "rescue", | ||
| Short: "Rescues an existing server", | ||
| Long: "Rescues an existing server.", | ||
| Args: args.SingleArg(serverIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Rescue an existing server with ID "xxx" using image with ID "yyy" as boot volume`, | ||
| "$ stackit beta server rescue xxx --image-id yyy", | ||
| ), | ||
| ), | ||
| 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 | ||
| } | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = model.ServerId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to rescue server %q?", serverLabel) | ||
| 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("server rescue: %w", err) | ||
| } | ||
| // Wait for async operation, if async mode not enabled | ||
| if !model.Async { | ||
| s := spinner.New(p) | ||
| s.Start("Rescuing server") | ||
| _, err = wait.RescueServerWaitHandler(ctx, apiClient, model.ProjectId, model.ServerId).WaitWithContext(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("wait for server rescuing: %w", err) | ||
| } | ||
| s.Stop() | ||
| } | ||
| operationState := "Rescued" | ||
| if model.Async { | ||
| operationState = "Triggered rescue of" | ||
| } | ||
| p.Info("%s server %q. Image %q is used as temporary boot image\n", operationState, serverLabel, *model.ImageId) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Var(flags.UUIDFlag(), imageIdFlag, "The image ID to be used for a temporary boot volume.") | ||
| err := flags.MarkFlagsRequired(cmd, imageIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| serverId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: serverId, | ||
| ImageId: flags.FlagToStringPointer(p, cmd, imageIdFlag), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiRescueServerRequest { | ||
| req := apiClient.RescueServer(ctx, model.ProjectId, model.ServerId) | ||
| payload := iaas.RescueServerPayload{ | ||
| Image: model.ImageId, | ||
| } | ||
| return req.RescueServerPayload(payload) | ||
| } |
| package resize | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| machineTypeFlag: "t1.2", | ||
| } | ||
| 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, | ||
| }, | ||
| ServerId: testServerId, | ||
| MachineType: utils.Ptr("t1.2"), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiResizeServerRequest)) iaas.ApiResizeServerRequest { | ||
| request := testClient.ResizeServer(testCtx, testProjectId, testServerId) | ||
| request = request.ResizeServerPayload(iaas.ResizeServerPayload{ | ||
| MachineType: utils.Ptr("t1.2"), | ||
| }) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "required machine type flag missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, machineTypeFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "invalid-uuid" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiResizeServerRequest | ||
| }{ | ||
| { | ||
| 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 resize | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| serverIdArg = "SERVER_ID" | ||
| machineTypeFlag = "machine-type" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| MachineType *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "resize", | ||
| Short: "Resizes the server to the given machine type", | ||
| Long: "Resizes the server to the given machine type.", | ||
| Args: args.SingleArg(serverIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Resize a server with ID "xxx" to machine type "yyy"`, | ||
| "$ stackit beta server resize xxx --machine-type yyy", | ||
| ), | ||
| ), | ||
| 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 | ||
| } | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = model.ServerId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to resize server %q to machine type %q?", serverLabel, *model.MachineType) | ||
| 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("server resize: %w", err) | ||
| } | ||
| // Wait for async operation, if async mode not enabled | ||
| if !model.Async { | ||
| s := spinner.New(p) | ||
| s.Start("Resizing server") | ||
| _, err = wait.ResizeServerWaitHandler(ctx, apiClient, model.ProjectId, model.ServerId).WaitWithContext(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("wait for server resizing: %w", err) | ||
| } | ||
| s.Stop() | ||
| } | ||
| operationState := "Resized" | ||
| if model.Async { | ||
| operationState = "Triggered resize of" | ||
| } | ||
| p.Info("%s server %q\n", operationState, serverLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().String(machineTypeFlag, "", "Name of the type of the machine for the server. Possible values are documented in https://docs.stackit.cloud/stackit/en/virtual-machine-flavors-75137231.html") | ||
| err := flags.MarkFlagsRequired(cmd, machineTypeFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| serverId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: serverId, | ||
| MachineType: flags.FlagToStringPointer(p, cmd, machineTypeFlag), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiResizeServerRequest { | ||
| req := apiClient.ResizeServer(ctx, model.ProjectId, model.ServerId) | ||
| payload := iaas.ResizeServerPayload{ | ||
| MachineType: model.MachineType, | ||
| } | ||
| return req.ResizeServerPayload(payload) | ||
| } |
| package attach | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), &testCtxKey{}, "test") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| var testServiceAccount = "test@example.com" | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testServiceAccount, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| serverIdFlag: testServerId, | ||
| } | ||
| 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, | ||
| }, | ||
| ServerId: utils.Ptr(testServerId), | ||
| ServiceAccMail: testServiceAccount, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiAddServiceAccountToServerRequest)) iaas.ApiAddServiceAccountToServerRequest { | ||
| request := testClient.AddServiceAccountToServer(testCtx, testProjectId, testServerId, testServiceAccount) | ||
| 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: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, serverIdFlag) | ||
| }), | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[serverIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[serverIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "service account argument missing", | ||
| argValues: []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.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing 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 iaas.ApiAddServiceAccountToServerRequest | ||
| }{ | ||
| { | ||
| 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 attach | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| const ( | ||
| serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL" | ||
| serverIdFlag = "server-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId *string | ||
| ServiceAccMail string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "attach", | ||
| Short: "Attach a service account to a server", | ||
| Long: "Attach a service account to a server", | ||
| Args: args.SingleArg(serviceAccMailArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Attach a service account with mail "xxx@sa.stackit.cloud" to a server with ID "yyy"`, | ||
| "$ stackit beta server service-account attach xxx@sa.stackit.cloud --server-id yyy", | ||
| ), | ||
| ), | ||
| 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 | ||
| } | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = *model.ServerId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to attach service account %q to server %q?", model.ServiceAccMail, serverLabel) | ||
| 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("attach service account to server: %w", err) | ||
| } | ||
| return outputResult(p, model.OutputFormat, model.ServiceAccMail, serverLabel, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") | ||
| err := flags.MarkFlagsRequired(cmd, serverIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| serviceAccMail := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), | ||
| ServiceAccMail: serviceAccMail, | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiAddServiceAccountToServerRequest { | ||
| req := apiClient.AddServiceAccountToServer(ctx, model.ProjectId, *model.ServerId, model.ServiceAccMail) | ||
| return req | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat, serviceAccMail, serverLabel string, serviceAccounts *iaas.ServiceAccountMailListResponse) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(serviceAccounts, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal service account: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(serviceAccounts, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal service account: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| p.Outputf("Attached service account %q to server %q\n", serviceAccMail, serverLabel) | ||
| return nil | ||
| } | ||
| } |
| package detach | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), &testCtxKey{}, "test") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| var testServiceAccount = "test@example.com" | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testServiceAccount, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| serverIdFlag: testServerId, | ||
| } | ||
| 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, | ||
| }, | ||
| ServerId: utils.Ptr(testServerId), | ||
| ServiceAccMail: testServiceAccount, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiRemoveServiceAccountFromServerRequest)) iaas.ApiRemoveServiceAccountFromServerRequest { | ||
| request := testClient.RemoveServiceAccountFromServer(testCtx, testProjectId, testServerId, testServiceAccount) | ||
| 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: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, serverIdFlag) | ||
| }), | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[serverIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[serverIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "service account argument missing", | ||
| argValues: []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.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing 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 iaas.ApiRemoveServiceAccountFromServerRequest | ||
| }{ | ||
| { | ||
| 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 detach | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| const ( | ||
| serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL" | ||
| serverIdFlag = "server-id" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId *string | ||
| ServiceAccMail string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "detach", | ||
| Short: "Detach a service account from a server", | ||
| Long: "Detach a service account from a server", | ||
| Args: args.SingleArg(serviceAccMailArg, nil), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Detach a service account with mail "xxx@sa.stackit.cloud" from a server "yyy"`, | ||
| "$ stackit beta server service-account detach xxx@sa.stackit.cloud --server-id yyy", | ||
| ), | ||
| ), | ||
| 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 | ||
| } | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = *model.ServerId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are your sure you want to detach service account %q from a server %q?", model.ServiceAccMail, serverLabel) | ||
| 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("detach service account request: %w", err) | ||
| } | ||
| return outputResult(p, model.OutputFormat, model.ServiceAccMail, serverLabel, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server id") | ||
| err := flags.MarkFlagsRequired(cmd, serverIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| serviceAccMail := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), | ||
| ServiceAccMail: serviceAccMail, | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiRemoveServiceAccountFromServerRequest { | ||
| req := apiClient.RemoveServiceAccountFromServer(ctx, model.ProjectId, *model.ServerId, model.ServiceAccMail) | ||
| return req | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat, serviceAccMail, serverLabel string, service *iaas.ServiceAccountMailListResponse) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(service, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal service account: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(service, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal service account: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| p.Outputf("Detached service account %q from server %q\n", serviceAccMail, serverLabel) | ||
| 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/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), &testCtxKey{}, "test") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| var testLimit = int64(10) | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| serverIdFlag: testServerId, | ||
| 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, | ||
| }, | ||
| ServerId: utils.Ptr(testServerId), | ||
| Limit: utils.Ptr(testLimit), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiListServerServiceAccountsRequest)) iaas.ApiListServerServiceAccountsRequest { | ||
| request := testClient.ListServerServiceAccounts(testCtx, testProjectId, testServerId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, serverIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[serverIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[serverIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "without limit", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, limitFlag) | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Limit = nil | ||
| }), | ||
| }, | ||
| { | ||
| description: "limit invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "invalid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "limit invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "0" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing 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 iaas.ApiListServerServiceAccountsRequest | ||
| }{ | ||
| { | ||
| 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) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "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/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| const ( | ||
| serverIdFlag = "server-id" | ||
| limitFlag = "limit" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Limit *int64 | ||
| ServerId *string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "List all attached service accounts for a server", | ||
| Long: "List all attached service accounts for a server", | ||
| Args: cobra.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `List all attached service accounts for a server with ID "xxx"`, | ||
| "$ stackit beta server service-account list --server-id xxx", | ||
| ), | ||
| examples.NewExample( | ||
| `List up to 10 attached service accounts for a server with ID "xxx"`, | ||
| "$ stackit beta server service-account list --server-id xxx --limit 10", | ||
| ), | ||
| examples.NewExample( | ||
| `List all attached service accounts for a server with ID "xxx" in JSON format`, | ||
| "$ stackit beta server service-account list --server-id xxx --output-format 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 | ||
| } | ||
| serverName, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverName = *model.ServerId | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("list service accounts: %w", err) | ||
| } | ||
| serviceAccounts := *resp.Items | ||
| if len(serviceAccounts) == 0 { | ||
| p.Info("No service accounts found for server %s\n", *model.ServerId) | ||
| return nil | ||
| } | ||
| if model.Limit != nil && len(serviceAccounts) > int(*model.Limit) { | ||
| serviceAccounts = serviceAccounts[:int(*model.Limit)] | ||
| } | ||
| return outputResult(p, model.OutputFormat, *model.ServerId, serverName, serviceAccounts) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") | ||
| cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") | ||
| err := flags.MarkFlagsRequired(cmd, serverIdFlag) | ||
| 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{} | ||
| } | ||
| 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, | ||
| ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiListServerServiceAccountsRequest { | ||
| req := apiClient.ListServerServiceAccounts(ctx, model.ProjectId, *model.ServerId) | ||
| return req | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat, serverId, serverName string, serviceAccounts []string) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(serviceAccounts, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal service accounts list: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(serviceAccounts, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal service accounts list: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| table.SetHeader("SERVER ID", "SERVER NAME", "SERVICE ACCOUNT") | ||
| for i := range serviceAccounts { | ||
| table.AddRow(serverId, serverName, serviceAccounts[i]) | ||
| } | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("rednder table: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| } |
| package serviceaccount | ||
| import ( | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/service-account/attach" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/service-account/detach" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/service-account/list" | ||
| "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: "service-account", | ||
| Short: "Allows attaching/detaching service accounts to servers", | ||
| Long: "Allows attaching/detaching service accounts to servers", | ||
| Args: cobra.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, p) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, p *print.Printer) { | ||
| cmd.AddCommand(attach.NewCmd(p)) | ||
| cmd.AddCommand(detach.NewCmd(p)) | ||
| cmd.AddCommand(list.NewCmd(p)) | ||
| } |
| package start | ||
| 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/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| }, | ||
| ServerId: testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiStartServerRequest)) iaas.ApiStartServerRequest { | ||
| request := testClient.StartServer(testCtx, testProjectId, testServerId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "invalid-uuid" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiStartServerRequest | ||
| }{ | ||
| { | ||
| 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 start | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| serverIdArg = "SERVER_ID" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "start", | ||
| Short: "Starts an existing server or allocates the server if deallocated", | ||
| Long: "Starts an existing server or allocates the server if deallocated.", | ||
| Args: args.SingleArg(serverIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Start an existing server with ID "xxx"`, | ||
| "$ stackit beta server start xxx", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = model.ServerId | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| err = req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("server start: %w", err) | ||
| } | ||
| // Wait for async operation, if async mode not enabled | ||
| if !model.Async { | ||
| s := spinner.New(p) | ||
| s.Start("Starting server") | ||
| _, err = wait.StartServerWaitHandler(ctx, apiClient, model.ProjectId, model.ServerId).WaitWithContext(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("wait for server starting: %w", err) | ||
| } | ||
| s.Stop() | ||
| } | ||
| operationState := "Started" | ||
| if model.Async { | ||
| operationState = "Triggered start of" | ||
| } | ||
| p.Info("%s server %q\n", operationState, serverLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| serverId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: serverId, | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiStartServerRequest { | ||
| return apiClient.StartServer(ctx, model.ProjectId, model.ServerId) | ||
| } |
| package stop | ||
| 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/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| }, | ||
| ServerId: testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiStopServerRequest)) iaas.ApiStopServerRequest { | ||
| request := testClient.StopServer(testCtx, testProjectId, testServerId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "invalid-uuid" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiStopServerRequest | ||
| }{ | ||
| { | ||
| 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 stop | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| serverIdArg = "SERVER_ID" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "stop", | ||
| Short: "Stops an existing server", | ||
| Long: "Stops an existing server.", | ||
| Args: args.SingleArg(serverIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Stop an existing server with ID "xxx"`, | ||
| "$ stackit beta server stop xxx", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = model.ServerId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to stop server %q?", serverLabel) | ||
| 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("server stop: %w", err) | ||
| } | ||
| // Wait for async operation, if async mode not enabled | ||
| if !model.Async { | ||
| s := spinner.New(p) | ||
| s.Start("Stopping server") | ||
| _, err = wait.StopServerWaitHandler(ctx, apiClient, model.ProjectId, model.ServerId).WaitWithContext(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("wait for server stopping: %w", err) | ||
| } | ||
| s.Stop() | ||
| } | ||
| operationState := "Stopped" | ||
| if model.Async { | ||
| operationState = "Triggered stop of" | ||
| } | ||
| p.Info("%s server %q\n", operationState, serverLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| serverId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: serverId, | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiStopServerRequest { | ||
| return apiClient.StopServer(ctx, model.ProjectId, model.ServerId) | ||
| } |
| package unrescue | ||
| 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/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| func fixtureArgValues(mods ...func(argValues []string)) []string { | ||
| argValues := []string{ | ||
| testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(argValues) | ||
| } | ||
| return argValues | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| }, | ||
| ServerId: testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiUnrescueServerRequest)) iaas.ApiUnrescueServerRequest { | ||
| request := testClient.UnrescueServer(testCtx, testProjectId, testServerId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| argValues []string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| argValues: []string{}, | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| argValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| argValues: fixtureArgValues(func(argValues []string) { | ||
| argValues[0] = "invalid-uuid" | ||
| }), | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateArgs(tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating args: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd, tt.argValues) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiUnrescueServerRequest | ||
| }{ | ||
| { | ||
| 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 unrescue | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| serverIdArg = "SERVER_ID" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "unrescue", | ||
| Short: "Unrescues an existing server", | ||
| Long: "Unrescues an existing server.", | ||
| Args: args.SingleArg(serverIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Unrescue an existing server with ID "xxx"`, | ||
| "$ stackit beta server unrescue xxx", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = model.ServerId | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to unrescue server %q?", serverLabel) | ||
| 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("server unrescue: %w", err) | ||
| } | ||
| // Wait for async operation, if async mode not enabled | ||
| if !model.Async { | ||
| s := spinner.New(p) | ||
| s.Start("Unrescuing server") | ||
| _, err = wait.UnrescueServerWaitHandler(ctx, apiClient, model.ProjectId, model.ServerId).WaitWithContext(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("wait for server unrescuing: %w", err) | ||
| } | ||
| s.Stop() | ||
| } | ||
| operationState := "Unrescued" | ||
| if model.Async { | ||
| operationState = "Triggered unrescue of" | ||
| } | ||
| p.Info("%s server %q\n", operationState, serverLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| serverId := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: serverId, | ||
| } | ||
| 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 *iaas.APIClient) iaas.ApiUnrescueServerRequest { | ||
| return apiClient.UnrescueServer(ctx, model.ProjectId, model.ServerId) | ||
| } |
| package export | ||
| import ( | ||
| "fmt" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| ) | ||
| const ( | ||
| testProfileArg = "default" | ||
| testExportPath = "/tmp/stackit-profiles/" + testProfileArg + ".json" | ||
| ) | ||
| func fixtureArgValues(mods ...func(args []string)) []string { | ||
| args := []string{ | ||
| testProfileArg, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(args) | ||
| } | ||
| return args | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| filePathFlag: testExportPath, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(inputModel *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| ProfileName: testProfileArg, | ||
| FilePath: testExportPath, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| 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{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no args", | ||
| argsValues: []string{}, | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "no flags", | ||
| argsValues: fixtureArgValues(), | ||
| flagValues: map[string]string{}, | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(inputModel *inputModel) { | ||
| inputModel.FilePath = fmt.Sprintf("%s.json", testProfileArg) | ||
| }), | ||
| }, | ||
| { | ||
| description: "custom file-path without file extension", | ||
| argsValues: fixtureArgValues(), | ||
| flagValues: fixtureFlagValues( | ||
| func(flagValues map[string]string) { | ||
| flagValues[filePathFlag] = "./my-exported-config" | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(inputModel *inputModel) { | ||
| inputModel.FilePath = "./my-exported-config" | ||
| }), | ||
| }, | ||
| } | ||
| 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) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package export | ||
| import ( | ||
| "fmt" | ||
| "path/filepath" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/config" | ||
| "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/spf13/cobra" | ||
| ) | ||
| const ( | ||
| profileNameArg = "PROFILE_NAME" | ||
| filePathFlag = "file-path" | ||
| configFileExtension = "json" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ProfileName string | ||
| FilePath string | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("export %s", profileNameArg), | ||
| Short: "Exports a CLI configuration profile", | ||
| Long: "Exports a CLI configuration profile.", | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Export a profile with name "PROFILE_NAME" to a file in your current directory`, | ||
| "$ stackit config profile export PROFILE_NAME", | ||
| ), | ||
| examples.NewExample( | ||
| `Export a profile with name "PROFILE_NAME"" to a specific file path FILE_PATH`, | ||
| "$ stackit config profile export PROFILE_NAME --file-path FILE_PATH", | ||
| ), | ||
| ), | ||
| Args: args.SingleArg(profileNameArg, nil), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| model, err := parseInput(p, cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| err = config.ExportProfile(p, model.ProfileName, model.FilePath) | ||
| if err != nil { | ||
| return fmt.Errorf("could not export profile: %w", err) | ||
| } | ||
| p.Info("Exported profile %q to %q\n", model.ProfileName, model.FilePath) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringP(filePathFlag, "f", "", "If set, writes the config to the given file path. If unset, writes the config to you current directory with the name of the profile. E.g. '--file-path ~/my-config.json'") | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
| profileName := inputArgs[0] | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ProfileName: profileName, | ||
| FilePath: flags.FlagToStringValue(p, cmd, filePathFlag), | ||
| } | ||
| // If filePath contains does not contain a file name, then add a default name | ||
| if model.FilePath == "" { | ||
| exportFileName := fmt.Sprintf("%s.%s", model.ProfileName, configFileExtension) | ||
| model.FilePath = filepath.Join(model.FilePath, exportFileName) | ||
| } | ||
| 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 | ||
| } |
| package importProfile | ||
| import ( | ||
| _ "embed" | ||
| "strconv" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/google/go-cmp/cmp" | ||
| ) | ||
| const testProfile = "test-profile" | ||
| const testConfig = "@./template/profile.json" | ||
| const testNoSet = false | ||
| //go:embed template/profile.json | ||
| var testConfigContent string | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| nameFlag: testProfile, | ||
| configFlag: testConfig, | ||
| noSetFlag: strconv.FormatBool(testNoSet), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| ProfileName: testProfile, | ||
| Config: testConfigContent, | ||
| NoSet: testNoSet, | ||
| } | ||
| 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 flags", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "invalid path", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[configFlag] = "@./template/invalid-file" | ||
| }), | ||
| 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 { | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(tt.expectedModel, model) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package importProfile | ||
| import ( | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/config" | ||
| "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" | ||
| ) | ||
| const ( | ||
| nameFlag = "name" | ||
| configFlag = "config" | ||
| noSetFlag = "no-set" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ProfileName string | ||
| Config string | ||
| NoSet bool | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "import", | ||
| Short: "Imports a CLI configuration profile", | ||
| Long: "Imports a CLI configuration profile.", | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Import a config with name "PROFILE_NAME" from file "./config.json"`, | ||
| "$ stackit config profile import --name PROFILE_NAME --config `@./config.json`", | ||
| ), | ||
| examples.NewExample( | ||
| `Import a config with name "PROFILE_NAME" from file "./config.json" and do not set as active`, | ||
| "$ stackit config profile import --name PROFILE_NAME --config `@./config.json` --no-set", | ||
| ), | ||
| ), | ||
| Args: args.NoArgs, | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| err = config.ImportProfile(p, model.ProfileName, model.Config, !model.NoSet) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| p.Info("Successfully imported profile %q\n", model.ProfileName) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().String(nameFlag, "", "Profile name") | ||
| cmd.Flags().VarP(flags.ReadFromFileFlag(), configFlag, "c", "File where configuration will be imported from") | ||
| cmd.Flags().Bool(noSetFlag, false, "Set the imported profile not as active") | ||
| cobra.CheckErr(cmd.MarkFlagRequired(nameFlag)) | ||
| cobra.CheckErr(cmd.MarkFlagRequired(configFlag)) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| model := &inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ProfileName: flags.FlagToStringValue(p, cmd, nameFlag), | ||
| Config: flags.FlagToStringValue(p, cmd, configFlag), | ||
| NoSet: flags.FlagToBoolValue(p, cmd, noSetFlag), | ||
| } | ||
| if model.Config == "" { | ||
| return nil, &errors.FlagValidationError{ | ||
| Flag: configFlag, | ||
| Details: "must not be empty", | ||
| } | ||
| } | ||
| if model.ProfileName == "" { | ||
| return nil, &errors.FlagValidationError{ | ||
| Flag: nameFlag, | ||
| Details: "must not be empty", | ||
| } | ||
| } | ||
| 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 | ||
| } |
| { | ||
| "allowed_url_domain": "stackit.cloud", | ||
| "async": false, | ||
| "authorization_custom_endpoint": "", | ||
| "dns_custom_endpoint": "", | ||
| "iaas_custom_endpoint": "", | ||
| "identity_provider_custom_client_id": "", | ||
| "identity_provider_custom_well_known_configuration": "", | ||
| "load_balancer_custom_endpoint": "", | ||
| "logme_custom_endpoint": "", | ||
| "mariadb_custom_endpoint": "", | ||
| "mongodbflex_custom_endpoint": "", | ||
| "object_storage_custom_endpoint": "", | ||
| "observability_custom_endpoint": "", | ||
| "opensearch_custom_endpoint": "", | ||
| "output_format": "", | ||
| "postgresflex_custom_endpoint": "", | ||
| "project_id": "", | ||
| "project_name": "", | ||
| "rabbitmq_custom_endpoint": "", | ||
| "redis_custom_endpoint": "", | ||
| "resource_manager_custom_endpoint": "", | ||
| "runcommand_custom_endpoint": "", | ||
| "secrets_manager_custom_endpoint": "", | ||
| "serverbackup_custom_endpoint": "", | ||
| "service_account_custom_endpoint": "", | ||
| "service_enablement_custom_endpoint": "", | ||
| "session_time_limit": "2h", | ||
| "ske_custom_endpoint": "", | ||
| "sqlserverflex_custom_endpoint": "", | ||
| "token_custom_endpoint": "", | ||
| "verbosity": "info" | ||
| } |
| { | ||
| "allowed_url_domain": "stackit.cloud", | ||
| "async": false, | ||
| "authorization_custom_endpoint": "", | ||
| "dns_custom_endpoint": "", | ||
| "iaas_custom_endpoint": "", | ||
| "identity_provider_custom_client_id": "", | ||
| "identity_provider_custom_well_known_configuration": "", | ||
| "load_balancer_custom_endpoint": "", | ||
| "logme_custom_endpoint": "", | ||
| "mariadb_custom_endpoint": "", | ||
| "mongodbflex_custom_endpoint": "", | ||
| "object_storage_custom_endpoint": "", | ||
| "observability_custom_endpoint": "", | ||
| "opensearch_custom_endpoint": "", | ||
| "output_format": "", | ||
| "postgresflex_custom_endpoint": "", | ||
| "project_id": "", | ||
| "project_name": "", | ||
| "rabbitmq_custom_endpoint": "", | ||
| "redis_custom_endpoint": "", | ||
| "resource_manager_custom_endpoint": "", | ||
| "runcommand_custom_endpoint": "", | ||
| "secrets_manager_custom_endpoint": "", | ||
| "serverbackup_custom_endpoint": "", | ||
| "service_account_custom_endpoint": "", | ||
| "service_enablement_custom_endpoint": "", | ||
| "session_time_limit": "2h", | ||
| "ske_custom_endpoint": "", | ||
| "sqlserverflex_custom_endpoint": "", | ||
| "token_custom_endpoint": "", | ||
| "verbosity": "info" | ||
| } |
@@ -34,10 +34,20 @@ ## stackit beta server | ||
| * [stackit beta server command](./stackit_beta_server_command.md) - Provides functionality for Server Command | ||
| * [stackit beta server console](./stackit_beta_server_console.md) - Gets a URL for server remote console | ||
| * [stackit beta server create](./stackit_beta_server_create.md) - Creates a server | ||
| * [stackit beta server deallocate](./stackit_beta_server_deallocate.md) - Deallocates an existing server | ||
| * [stackit beta server delete](./stackit_beta_server_delete.md) - Deletes a server | ||
| * [stackit beta server describe](./stackit_beta_server_describe.md) - Shows details of a server | ||
| * [stackit beta server list](./stackit_beta_server_list.md) - Lists all servers of a project | ||
| * [stackit beta server log](./stackit_beta_server_log.md) - Gets server console log | ||
| * [stackit beta server network-interface](./stackit_beta_server_network-interface.md) - Allows attaching/detaching network interfaces to servers | ||
| * [stackit beta server public-ip](./stackit_beta_server_public-ip.md) - Allows attaching/detaching public IPs to servers | ||
| * [stackit beta server reboot](./stackit_beta_server_reboot.md) - Reboots a server | ||
| * [stackit beta server rescue](./stackit_beta_server_rescue.md) - Rescues an existing server | ||
| * [stackit beta server resize](./stackit_beta_server_resize.md) - Resizes the server to the given machine type | ||
| * [stackit beta server service-account](./stackit_beta_server_service-account.md) - Allows attaching/detaching service accounts to servers | ||
| * [stackit beta server start](./stackit_beta_server_start.md) - Starts an existing server or allocates the server if deallocated | ||
| * [stackit beta server stop](./stackit_beta_server_stop.md) - Stops an existing server | ||
| * [stackit beta server unrescue](./stackit_beta_server_unrescue.md) - Unrescues an existing server | ||
| * [stackit beta server update](./stackit_beta_server_update.md) - Updates a server | ||
| * [stackit beta server volume](./stackit_beta_server_volume.md) - Provides functionality for server volumes | ||
@@ -43,2 +43,3 @@ ## stackit beta | ||
| * [stackit](./stackit.md) - Manage STACKIT resources using the command line | ||
| * [stackit beta key-pair](./stackit_beta_key-pair.md) - Provides functionality for SSH key pairs | ||
| * [stackit beta network](./stackit_beta_network.md) - Provides functionality for networks | ||
@@ -48,2 +49,3 @@ * [stackit beta network-area](./stackit_beta_network-area.md) - Provides functionality for STACKIT Network Area (SNA) | ||
| * [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for public IPs | ||
| * [stackit beta security-group](./stackit_beta_security-group.md) - Manage security groups | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers | ||
@@ -50,0 +52,0 @@ * [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex |
@@ -37,2 +37,4 @@ ## stackit config profile | ||
| * [stackit config profile delete](./stackit_config_profile_delete.md) - Delete a CLI configuration profile | ||
| * [stackit config profile export](./stackit_config_profile_export.md) - Exports a CLI configuration profile | ||
| * [stackit config profile import](./stackit_config_profile_import.md) - Imports a CLI configuration profile | ||
| * [stackit config profile list](./stackit_config_profile_list.md) - Lists all CLI configuration profiles | ||
@@ -39,0 +41,0 @@ * [stackit config profile set](./stackit_config_profile_set.md) - Set a CLI configuration profile |
+19
-19
@@ -7,3 +7,3 @@ module github.com/stackitcloud/stackit-cli | ||
| github.com/fatih/color v1.18.0 | ||
| github.com/goccy/go-yaml v1.15.7 | ||
| github.com/goccy/go-yaml v1.15.11 | ||
| github.com/golang-jwt/jwt/v5 v5.2.1 | ||
@@ -13,4 +13,4 @@ github.com/google/go-cmp v0.6.0 | ||
| github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf | ||
| github.com/jedib0t/go-pretty/v6 v6.6.3 | ||
| github.com/lmittmann/tint v1.0.5 | ||
| github.com/jedib0t/go-pretty/v6 v6.6.5 | ||
| github.com/lmittmann/tint v1.0.6 | ||
| github.com/mattn/go-colorable v0.1.13 | ||
@@ -21,16 +21,16 @@ github.com/spf13/cobra v1.8.1 | ||
| github.com/stackitcloud/stackit-sdk-go/core v0.14.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/authorization v0.4.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/dns v0.12.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/iaas v0.16.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.16.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.19.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/authorization v0.4.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/dns v0.12.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/iaas v0.18.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.16.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.19.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.16.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.11.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/runcommand v0.2.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.10.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.11.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/runcommand v0.2.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.10.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.3.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.5.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.4.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.20.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.8.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.20.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.8.1 | ||
| github.com/zalando/go-keyring v0.2.6 | ||
@@ -85,8 +85,8 @@ golang.org/x/mod v0.22.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.17.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.11.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/observability v0.2.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.20.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/redis v0.20.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.11.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/observability v0.2.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.20.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/redis v0.20.1 | ||
| github.com/subosito/gotenv v1.6.0 // indirect | ||
@@ -93,0 +93,0 @@ go.uber.org/multierr v1.11.0 // indirect |
+38
-38
@@ -29,4 +29,4 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= | ||
| github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= | ||
| github.com/goccy/go-yaml v1.15.7 h1:L7XuKpd/A66X4w/dlk08lVfiIADdy79a1AzRoIefC98= | ||
| github.com/goccy/go-yaml v1.15.7/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= | ||
| github.com/goccy/go-yaml v1.15.11 h1:XeEd/2INF0TXXWMzJ9ALqJLGjGDl4PIi1gmrK+7KpAs= | ||
| github.com/goccy/go-yaml v1.15.11/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= | ||
| github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= | ||
@@ -60,4 +60,4 @@ github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||
| github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= | ||
| github.com/jedib0t/go-pretty/v6 v6.6.3 h1:nGqgS0tgIO1Hto47HSaaK4ac/I/Bu7usmdD3qvs0WvM= | ||
| github.com/jedib0t/go-pretty/v6 v6.6.3/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= | ||
| github.com/jedib0t/go-pretty/v6 v6.6.5 h1:9PgMJOVBedpgYLI56jQRJYqngxYAAzfEUua+3NgSqAo= | ||
| github.com/jedib0t/go-pretty/v6 v6.6.5/go.mod h1:Uq/HrbhuFty5WSVNfjpQQe47x16RwVGXIveNGEyGtHs= | ||
| github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= | ||
@@ -73,4 +73,4 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= | ||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||
| github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw= | ||
| github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= | ||
| github.com/lmittmann/tint v1.0.6 h1:vkkuDAZXc0EFGNzYjWcV0h7eEX+uujH48f/ifSkJWgc= | ||
| github.com/lmittmann/tint v1.0.6/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= | ||
| github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= | ||
@@ -126,34 +126,34 @@ github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= | ||
| github.com/stackitcloud/stackit-sdk-go/core v0.14.0/go.mod h1:mDX1mSTsB3mP+tNBGcFNx6gH1mGBN4T+dVt+lcw7nlw= | ||
| github.com/stackitcloud/stackit-sdk-go/services/authorization v0.4.0 h1:WXSIE4KfdHzaiiD0MF8CsoIv8I+Two/Bf/r28tYhRCU= | ||
| github.com/stackitcloud/stackit-sdk-go/services/authorization v0.4.0/go.mod h1:8spVqlPqZrvQQ63Qodbydk3qsZx7lr963ECft+sqFhY= | ||
| github.com/stackitcloud/stackit-sdk-go/services/dns v0.12.0 h1:NypnmRbvjCX7ANJej1epwmopBEjkMzklJT3SY5iVwWg= | ||
| github.com/stackitcloud/stackit-sdk-go/services/dns v0.12.0/go.mod h1:mv8U7kuclXo+0VpDHtBCkve/3i9h1yT+RAId/MUi+C8= | ||
| github.com/stackitcloud/stackit-sdk-go/services/iaas v0.16.0 h1:geyW780gqNxzSsPvmlxy3kUUJaRA4eiF9V3b2Ibcdjs= | ||
| github.com/stackitcloud/stackit-sdk-go/services/iaas v0.16.0/go.mod h1:YfuN+eXuqr846xeRyW2Vf1JM2jU0ikeQa76dDI66RsM= | ||
| github.com/stackitcloud/stackit-sdk-go/services/authorization v0.4.1 h1:sK7FZHN1tF8D/7rbWLhu4DX0qrAP5Psn2NusnwbCsEM= | ||
| github.com/stackitcloud/stackit-sdk-go/services/authorization v0.4.1/go.mod h1:8spVqlPqZrvQQ63Qodbydk3qsZx7lr963ECft+sqFhY= | ||
| github.com/stackitcloud/stackit-sdk-go/services/dns v0.12.1 h1:nzOZQ2X6joM2iStePoACZeYvXFBQIBjkYE9sLanOGvE= | ||
| github.com/stackitcloud/stackit-sdk-go/services/dns v0.12.1/go.mod h1:mv8U7kuclXo+0VpDHtBCkve/3i9h1yT+RAId/MUi+C8= | ||
| github.com/stackitcloud/stackit-sdk-go/services/iaas v0.18.0 h1:7QPYi7OZXUSO1uOtp1UXeCbxK0BGJbmjp71kaQOrMa8= | ||
| github.com/stackitcloud/stackit-sdk-go/services/iaas v0.18.0/go.mod h1:YfuN+eXuqr846xeRyW2Vf1JM2jU0ikeQa76dDI66RsM= | ||
| github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.17.0 h1:06CGP64CEk3Zg6i9kZCMRdmCzLLiyMWQqGK1teBr9Oc= | ||
| github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.17.0/go.mod h1:JL94zc8K0ebWs+DBGXR28vNCF0EFV54ZLUtrlXOvWgA= | ||
| github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.0 h1:V0UGP7JEa4Q8SsZFUJsKgLGaoPruLn2KVKnqQtaoWCU= | ||
| github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.0/go.mod h1:+NlUMcid2dEFSmqtbXJrcT57iP0oTdnIXHOQku0E04A= | ||
| github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.0 h1:Q59bOPYEI9HeFA4vFrpQN2z/KwbW9MFi7L4BJ2o40k8= | ||
| github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.0/go.mod h1:nuZK6OXyZ4zlGsC1gZDj9+ajJzzFi9vVgSSRQlEJAqA= | ||
| github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.16.0 h1:UYCEVpuv0Yq/sITf6yNCdJZw8PicWJJLT1TzBOego9g= | ||
| github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.16.0/go.mod h1:CvGSm9Goma2O1xkA0LEJbHfZpGlhy0AXJnMPpMmRdMM= | ||
| github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.11.0 h1:pCuhlSsRUW/3YV/u7l8iWJCjGBXANboVFdW5WanASOg= | ||
| github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.11.0/go.mod h1:V2LEHKyTaaiEBi9L3v62mNQ7xyJSred4OK+himLJOZQ= | ||
| github.com/stackitcloud/stackit-sdk-go/services/observability v0.2.0 h1:5UZrI0qs8CNAmv/Szhes6ebjQ4YrzShpanviJsX3gbg= | ||
| github.com/stackitcloud/stackit-sdk-go/services/observability v0.2.0/go.mod h1:okcRTrNDTI3d7MQcYJMliK0qoXeLq0b1wvZuEqgJIWE= | ||
| github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.19.0 h1:KFP7xkVGU8b7tB6Rk3arKM8y9Q8OlSB9B5hVi0e1Xvo= | ||
| github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.19.0/go.mod h1:U45gFwIAAdXWL/Wlp9rY3iPVWFHLGILz1C3Qc62o8KM= | ||
| github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.1 h1:ogo7Ce4wA9ln/Z0VwvckH0FT5/i7d9/34bG85aayHn8= | ||
| github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.1/go.mod h1:+NlUMcid2dEFSmqtbXJrcT57iP0oTdnIXHOQku0E04A= | ||
| github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.1 h1:J+GLgfDIDnNpq/z6ev1whfLbOwUn5XgSKh3aE2auHIs= | ||
| github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.1/go.mod h1:nuZK6OXyZ4zlGsC1gZDj9+ajJzzFi9vVgSSRQlEJAqA= | ||
| github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.16.1 h1:BCzShNI9TX6vZ8Q5vLzdG32gOdgcAwMWjEpy2AGHdnI= | ||
| github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.16.1/go.mod h1:CvGSm9Goma2O1xkA0LEJbHfZpGlhy0AXJnMPpMmRdMM= | ||
| github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.11.1 h1:Df3fTAHaVgyiiyp9LyTTQI8jXSVeGo49eW5ya4AATCY= | ||
| github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.11.1/go.mod h1:V2LEHKyTaaiEBi9L3v62mNQ7xyJSred4OK+himLJOZQ= | ||
| github.com/stackitcloud/stackit-sdk-go/services/observability v0.2.1 h1:sIz4wJIz6/9Eh6nSoi2sQ+Ef53iOrFsqLKIp2oRkmgo= | ||
| github.com/stackitcloud/stackit-sdk-go/services/observability v0.2.1/go.mod h1:okcRTrNDTI3d7MQcYJMliK0qoXeLq0b1wvZuEqgJIWE= | ||
| github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.19.1 h1:hwRkCCUSWMhKTc7fLakL89V6+9xkxsFQlRthVmrvi1U= | ||
| github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.19.1/go.mod h1:U45gFwIAAdXWL/Wlp9rY3iPVWFHLGILz1C3Qc62o8KM= | ||
| github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.16.0 h1:aRVPIaTtM2zyep3k22lu6ARG0j0P1N/7fjH7TS6ucio= | ||
| github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.16.0/go.mod h1:JnhCZzXprN/em1Uxpvl1ITMf6Hl/8N/4y5zNsRqEGlA= | ||
| github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.20.0 h1:823zATqn83jngf4XOzXxjoT338FhMUv9h+65dfk/SjY= | ||
| github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.20.0/go.mod h1:42oYZOqin+rIUrUqgtCIE4wzCWWY30H4UFhzvo1Wg2w= | ||
| github.com/stackitcloud/stackit-sdk-go/services/redis v0.20.0 h1:GVBQbLvJlIViURciLiX8z9zpbTuHAg2dGwVdwWSEqac= | ||
| github.com/stackitcloud/stackit-sdk-go/services/redis v0.20.0/go.mod h1:YJdkyuY7aK/clfE3lQDz7O369JLPcg0FO4yfCIPNUNE= | ||
| github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.11.0 h1:AScIIoDpq1rxpjW248Ahn/KZS5dCZ4xggjAES9M5XSI= | ||
| github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.11.0/go.mod h1:9Om4A5FI/wXZE/8zu5wF8eRBb70VddyPfnj/nlYXHX0= | ||
| github.com/stackitcloud/stackit-sdk-go/services/runcommand v0.2.0 h1:tb0w+0imdJhsaoWCBOuie8bu335ZbxruwR01Yx7FmRE= | ||
| github.com/stackitcloud/stackit-sdk-go/services/runcommand v0.2.0/go.mod h1:LgCIIj7jA2lWX4DI3bxUYD+m0TbWCr1VgAyBYNJeghc= | ||
| github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.10.0 h1:HrnEgRPt3eD/tEHyzDWyCRxNFzb9g/FLYfwIEEQZ+Rg= | ||
| github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.10.0/go.mod h1:268uoY2gKCa5xcDL169TGVjLUNTcZ2En77YdfYOcR1w= | ||
| github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.20.1 h1:6XfGxsPFqci/geSDd1gCtiaRJun85X5JepXn4edobXQ= | ||
| github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.20.1/go.mod h1:42oYZOqin+rIUrUqgtCIE4wzCWWY30H4UFhzvo1Wg2w= | ||
| github.com/stackitcloud/stackit-sdk-go/services/redis v0.20.1 h1:/EVm0bD9a3KCk9aj/v2ivJIURlGsTr4O2OwMQ4ey3e4= | ||
| github.com/stackitcloud/stackit-sdk-go/services/redis v0.20.1/go.mod h1:YJdkyuY7aK/clfE3lQDz7O369JLPcg0FO4yfCIPNUNE= | ||
| github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.11.1 h1:bICGCqRsGEzqidVCgQIH3hxB+SX1vJapZgrSP5nhvBo= | ||
| github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.11.1/go.mod h1:9Om4A5FI/wXZE/8zu5wF8eRBb70VddyPfnj/nlYXHX0= | ||
| github.com/stackitcloud/stackit-sdk-go/services/runcommand v0.2.1 h1:qAKT20siGhkIIg4gY0JBPD7TU+I/6UieYcivGU7hVKc= | ||
| github.com/stackitcloud/stackit-sdk-go/services/runcommand v0.2.1/go.mod h1:LgCIIj7jA2lWX4DI3bxUYD+m0TbWCr1VgAyBYNJeghc= | ||
| github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.10.1 h1:qShB0OuNR8EOffY36/DfJs/Yk12syy38xkE88Z15f4k= | ||
| github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.10.1/go.mod h1:268uoY2gKCa5xcDL169TGVjLUNTcZ2En77YdfYOcR1w= | ||
| github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.3.0 h1:Tlps8vBQmQ1mx2YFbzOzMIyWtXGJy7X3N9Qk3qk88Cc= | ||
@@ -165,6 +165,6 @@ github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.3.0/go.mod h1:+807U5ZLXns+CEbyIg483wNEwV10vaN6GjMnSZhw/64= | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.4.0/go.mod h1:zyg0hpiNdZLRbelkJb2KDf9OHQKLqqcTpePQ1qHL5dE= | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.20.0 h1:ssEywzCS8IdRtzyxweLUKBG5GFbgwjNWJh++wGqigJM= | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.20.0/go.mod h1:A4+9KslxCA31JvxnT+O/GC67eAOdw+iqhBzewZZaCD0= | ||
| github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.8.0 h1:1ByAgO10fxWF+UZ+RkJeAiv+h5AgqrzYz6r86Pn/BWE= | ||
| github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.8.0/go.mod h1:ui+yRLddE9mknzjZa45boyaU1ZomQuZy3p7wuwOufCY= | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.20.1 h1:Gw68D2U+cAh20sRX2GvhLCSvRVvbKF52Ew7FQ/AoCXc= | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.20.1/go.mod h1:A4+9KslxCA31JvxnT+O/GC67eAOdw+iqhBzewZZaCD0= | ||
| github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.8.1 h1:26MyaOGc3Sn4yuR3zoOOOFfb4MTbDV9PJwzU8yulWNc= | ||
| github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.8.1/go.mod h1:ui+yRLddE9mknzjZa45boyaU1ZomQuZy3p7wuwOufCY= | ||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
@@ -171,0 +171,0 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= |
@@ -6,2 +6,3 @@ package beta | ||
| keypair "github.com/stackitcloud/stackit-cli/internal/cmd/beta/key-pair" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/network" | ||
@@ -11,2 +12,3 @@ networkArea "github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area" | ||
| publicip "github.com/stackitcloud/stackit-cli/internal/cmd/beta/public-ip" | ||
| securitygroup "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server" | ||
@@ -53,2 +55,4 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex" | ||
| cmd.AddCommand(publicip.NewCmd(p)) | ||
| cmd.AddCommand(securitygroup.NewCmd(p)) | ||
| cmd.AddCommand(keypair.NewCmd(p)) | ||
| } |
@@ -6,10 +6,21 @@ package server | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/command" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/console" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/create" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/deallocate" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/delete" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/describe" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/list" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/log" | ||
| networkinterface "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/network-interface" | ||
| publicip "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/public-ip" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/reboot" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/rescue" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/resize" | ||
| serviceaccount "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/service-account" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/start" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/stop" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/unrescue" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/update" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/volume" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
@@ -42,5 +53,15 @@ "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| cmd.AddCommand(publicip.NewCmd(p)) | ||
| cmd.AddCommand(serviceaccount.NewCmd(p)) | ||
| cmd.AddCommand(update.NewCmd(p)) | ||
| cmd.AddCommand(volume.NewCmd(p)) | ||
| cmd.AddCommand(networkinterface.NewCmd(p)) | ||
| cmd.AddCommand(console.NewCmd(p)) | ||
| cmd.AddCommand(log.NewCmd(p)) | ||
| cmd.AddCommand(start.NewCmd(p)) | ||
| cmd.AddCommand(stop.NewCmd(p)) | ||
| cmd.AddCommand(reboot.NewCmd(p)) | ||
| cmd.AddCommand(deallocate.NewCmd(p)) | ||
| cmd.AddCommand(resize.NewCmd(p)) | ||
| cmd.AddCommand(rescue.NewCmd(p)) | ||
| cmd.AddCommand(unrescue.NewCmd(p)) | ||
| } |
@@ -8,2 +8,4 @@ package profile | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/delete" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/export" | ||
| importProfile "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/import" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/list" | ||
@@ -42,2 +44,4 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/set" | ||
| cmd.AddCommand(delete.NewCmd(p)) | ||
| cmd.AddCommand(importProfile.NewCmd(p)) | ||
| cmd.AddCommand(export.NewCmd(p)) | ||
| } |
@@ -108,3 +108,7 @@ package config | ||
| func InitConfig() { | ||
| defaultConfigFolderPath = getInitialConfigDir() | ||
| initConfig(getInitialConfigDir()) | ||
| } | ||
| func initConfig(configPath string) { | ||
| defaultConfigFolderPath = configPath | ||
| profileFilePath = getInitialProfileFilePath() // Profile file path is in the default config folder | ||
@@ -111,0 +115,0 @@ |
| package config | ||
| import ( | ||
| _ "embed" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| ) | ||
| //go:embed template/test_profile.json | ||
| var templateConfig string | ||
| func TestValidateProfile(t *testing.T) { | ||
@@ -118,1 +126,148 @@ tests := []struct { | ||
| } | ||
| func TestImportProfile(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| profile string | ||
| config string | ||
| setAsActive bool | ||
| isValid bool | ||
| }{ | ||
| { | ||
| description: "valid profile", | ||
| profile: "profile-name", | ||
| config: templateConfig, | ||
| setAsActive: false, | ||
| isValid: true, | ||
| }, | ||
| { | ||
| description: "invalid profile name", | ||
| profile: "invalid-profile-&", | ||
| config: templateConfig, | ||
| setAsActive: false, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "invalid config", | ||
| profile: "my-profile", | ||
| config: `{ "invalid": "json }`, | ||
| setAsActive: false, | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| err := ImportProfile(p, tt.profile, tt.config, tt.setAsActive) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("profile should be valid but got error: %v\n", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("profile should be invalid but got no error\n") | ||
| } | ||
| }) | ||
| t.Cleanup(func() { | ||
| p := print.NewPrinter() | ||
| err := DeleteProfile(p, tt.profile) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| fmt.Printf("could not clean up imported profile: %v\n", err) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestExportProfile(t *testing.T) { | ||
| // Create directory where the export configs should be stored | ||
| testDir, err := os.MkdirTemp(os.TempDir(), "stackit-cli-test") | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| t.Cleanup(func() { | ||
| func(path string) { | ||
| err := os.RemoveAll(path) | ||
| if err != nil { | ||
| fmt.Printf("could not clean up temp dir: %v\n", err) | ||
| } | ||
| }(testDir) | ||
| }) | ||
| // Create test config directory | ||
| testConfigFolderPath := filepath.Join(testDir, "config") | ||
| initConfig(testConfigFolderPath) | ||
| err = Write() | ||
| if err != nil { | ||
| t.Fatalf("could not write profile, %v", err) | ||
| } | ||
| // Create prerequisite profile | ||
| p := print.NewPrinter() | ||
| profileName := "export-profile-test" | ||
| err = CreateProfile(p, profileName, true, false) | ||
| if err != nil { | ||
| t.Fatalf("could not create prerequisite profile, %v", err) | ||
| } | ||
| t.Cleanup(func() { | ||
| func(p *print.Printer, profile string) { | ||
| err := DeleteProfile(p, profile) | ||
| if err != nil { | ||
| fmt.Printf("could not clean up prerequisite profile %q, %v", profileName, err) | ||
| } | ||
| }(p, profileName) | ||
| }) | ||
| tests := []struct { | ||
| description string | ||
| profile string | ||
| filePath string | ||
| isValid bool | ||
| }{ | ||
| { | ||
| description: "valid profile", | ||
| profile: profileName, | ||
| filePath: filepath.Join(testDir, fmt.Sprintf("custom-name.%s", configFileExtension)), | ||
| isValid: true, | ||
| }, | ||
| { | ||
| description: "invalid profile", | ||
| profile: "invalid-my-profile", | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "not existing path", | ||
| profile: profileName, | ||
| filePath: filepath.Join(testDir, "invalid", "path", fmt.Sprintf("custom-name.%s", configFileExtension)), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "export without file extension", | ||
| profile: profileName, | ||
| filePath: filepath.Join(testDir, "file-without-extension"), | ||
| isValid: true, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| err := ExportProfile(p, tt.profile, tt.filePath) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("export should be valid but got error: %v\n", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("export should be invalid but got no error\n") | ||
| } | ||
| }) | ||
| } | ||
| } |
| package config | ||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
@@ -333,1 +334,106 @@ "os" | ||
| } | ||
| // ImportProfile imports a profile configuration | ||
| // It imports the profile with the name profileName and a config json. | ||
| // If setAsActive is true, it set the new profile as the active profile. | ||
| func ImportProfile(p *print.Printer, profileName, config string, setAsActive bool) error { | ||
| err := ValidateProfile(profileName) | ||
| if err != nil || profileName == DefaultProfileName { | ||
| return &errors.InvalidProfileNameError{Profile: profileName} | ||
| } | ||
| exists, err := ProfileExists(profileName) | ||
| if err != nil { | ||
| return fmt.Errorf("check if profile exists: %w", err) | ||
| } | ||
| if exists { | ||
| return &errors.ProfileAlreadyExistsError{Profile: profileName} | ||
| } | ||
| importConfig := &map[string]interface{}{} | ||
| err = json.Unmarshal([]byte(config), importConfig) | ||
| if err != nil { | ||
| return fmt.Errorf("unmarshal config: %w", err) | ||
| } | ||
| configFolderPath = GetProfileFolderPath(profileName) | ||
| err = os.MkdirAll(configFolderPath, 0o750) | ||
| if err != nil { | ||
| return fmt.Errorf("create config folder: %w", err) | ||
| } | ||
| content, err := json.MarshalIndent(importConfig, "", " ") | ||
| if err != nil { | ||
| cleanupErr := os.RemoveAll(configFolderPath) | ||
| if cleanupErr != nil { | ||
| return fmt.Errorf("json marshal config: %w, cleanup directories: %w", err, cleanupErr) | ||
| } | ||
| return fmt.Errorf("marshal config file: %w", err) | ||
| } | ||
| filePath := getConfigFilePath(configFolderPath) | ||
| err = os.WriteFile(filePath, content, 0o600) | ||
| if err != nil { | ||
| cleanupErr := os.RemoveAll(configFolderPath) | ||
| if cleanupErr != nil { | ||
| return fmt.Errorf("write config file: %w, cleanup directories: %w", err, cleanupErr) | ||
| } | ||
| return fmt.Errorf("write config file: %w", err) | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| p.Debug(print.DebugLevel, "profile %q imported", profileName) | ||
| } | ||
| if setAsActive { | ||
| err := SetProfile(&print.Printer{}, profileName) | ||
| if err != nil { | ||
| return fmt.Errorf("set active profile: %w", err) | ||
| } | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| p.Debug(print.DebugLevel, "active profile %q is now active", profileName) | ||
| } | ||
| return nil | ||
| } | ||
| // ExportProfile exports a profile configuration | ||
| // Is exports the profile to the exportPath. The exportPath must contain the filename. | ||
| func ExportProfile(p *print.Printer, profile, exportPath string) error { | ||
| err := ValidateProfile(profile) | ||
| if err != nil { | ||
| return fmt.Errorf("validate profile name: %w", err) | ||
| } | ||
| exists, err := ProfileExists(profile) | ||
| if err != nil { | ||
| return fmt.Errorf("check if profile exists: %w", err) | ||
| } | ||
| if !exists { | ||
| return &errors.ProfileDoesNotExistError{Profile: profile} | ||
| } | ||
| profilePath := GetProfileFolderPath(profile) | ||
| configFile := getConfigFilePath(profilePath) | ||
| stats, err := os.Stat(exportPath) | ||
| if err == nil { | ||
| if stats.IsDir() { | ||
| return fmt.Errorf("export path %q is a directory. Please specify a full path", exportPath) | ||
| } | ||
| return &errors.FileAlreadyExistsError{Filename: exportPath} | ||
| } | ||
| err = fileutils.CopyFile(configFile, exportPath) | ||
| if err != nil { | ||
| return fmt.Errorf("export config file to %q: %w", exportPath, err) | ||
| } | ||
| if p != nil { | ||
| p.Debug(print.DebugLevel, "exported profile %q to %q", profile, exportPath) | ||
| } | ||
| return nil | ||
| } |
@@ -153,2 +153,14 @@ package errors | ||
| IAAS_SERVER_NIC_DETACH_MISSING_NIC_ID = `The "network-interface-id" flag must be provided if the "delete" flag is not provided.` | ||
| PROFILE_ALREADY_EXISTS = `profile %[1]q already exists. | ||
| To delete it, run: | ||
| $ stackit config profile delete %[1]s` | ||
| PROFILE_DOES_NOT_EXIST = `The profile %q does not exist. | ||
| To list all profiles, run: | ||
| $ stackit config profile list` | ||
| FILE_ALREADY_EXISTS = `file %q already exists in the export path. Delete the existing file or define a different export path` | ||
| ) | ||
@@ -441,1 +453,23 @@ | ||
| } | ||
| type ProfileAlreadyExistsError struct { | ||
| Profile string | ||
| } | ||
| func (e *ProfileAlreadyExistsError) Error() string { | ||
| return fmt.Sprintf(PROFILE_ALREADY_EXISTS, e.Profile) | ||
| } | ||
| type ProfileDoesNotExistError struct { | ||
| Profile string | ||
| } | ||
| func (e *ProfileDoesNotExistError) Error() string { | ||
| return fmt.Sprintf(PROFILE_DOES_NOT_EXIST, e.Profile) | ||
| } | ||
| type FileAlreadyExistsError struct { | ||
| Filename string | ||
| } | ||
| func (e *FileAlreadyExistsError) Error() string { return fmt.Sprintf(FILE_ALREADY_EXISTS, e.Filename) } |
@@ -14,18 +14,36 @@ package utils | ||
| type IaaSClientMocked struct { | ||
| GetPublicIpFails bool | ||
| GetPublicIpResp *iaas.PublicIp | ||
| GetServerFails bool | ||
| GetServerResp *iaas.Server | ||
| GetVolumeFails bool | ||
| GetVolumeResp *iaas.Volume | ||
| GetNetworkFails bool | ||
| GetNetworkResp *iaas.Network | ||
| GetNetworkAreaFails bool | ||
| GetNetworkAreaResp *iaas.NetworkArea | ||
| GetAttachedProjectsFails bool | ||
| GetAttachedProjectsResp *iaas.ProjectListResponse | ||
| GetNetworkAreaRangeFails bool | ||
| GetNetworkAreaRangeResp *iaas.NetworkRange | ||
| GetSecurityGroupRuleFails bool | ||
| GetSecurityGroupRuleResp *iaas.SecurityGroupRule | ||
| GetSecurityGroupFails bool | ||
| GetSecurityGroupResp *iaas.SecurityGroup | ||
| GetPublicIpFails bool | ||
| GetPublicIpResp *iaas.PublicIp | ||
| GetServerFails bool | ||
| GetServerResp *iaas.Server | ||
| GetVolumeFails bool | ||
| GetVolumeResp *iaas.Volume | ||
| GetNetworkFails bool | ||
| GetNetworkResp *iaas.Network | ||
| GetNetworkAreaFails bool | ||
| GetNetworkAreaResp *iaas.NetworkArea | ||
| GetAttachedProjectsFails bool | ||
| GetAttachedProjectsResp *iaas.ProjectListResponse | ||
| GetNetworkAreaRangeFails bool | ||
| GetNetworkAreaRangeResp *iaas.NetworkRange | ||
| } | ||
| func (m *IaaSClientMocked) GetSecurityGroupRuleExecute(_ context.Context, _, _, _ string) (*iaas.SecurityGroupRule, error) { | ||
| if m.GetSecurityGroupRuleFails { | ||
| return nil, fmt.Errorf("could not get security group rule") | ||
| } | ||
| return m.GetSecurityGroupRuleResp, nil | ||
| } | ||
| func (m *IaaSClientMocked) GetSecurityGroupExecute(_ context.Context, _, _ string) (*iaas.SecurityGroup, error) { | ||
| if m.GetSecurityGroupFails { | ||
| return nil, fmt.Errorf("could not get security group") | ||
| } | ||
| return m.GetSecurityGroupResp, nil | ||
| } | ||
| func (m *IaaSClientMocked) GetPublicIPExecute(_ context.Context, _, _ string) (*iaas.PublicIp, error) { | ||
@@ -80,2 +98,95 @@ if m.GetPublicIpFails { | ||
| func TestGetSecurityGroupRuleName(t *testing.T) { | ||
| type args struct { | ||
| getInstanceFails bool | ||
| getInstanceResp *iaas.SecurityGroupRule | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want string | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "base", | ||
| args: args{ | ||
| getInstanceResp: &iaas.SecurityGroupRule{ | ||
| Ethertype: utils.Ptr("IPv6"), | ||
| Direction: utils.Ptr("ingress"), | ||
| }, | ||
| }, | ||
| want: "IPv6, ingress", | ||
| }, | ||
| { | ||
| name: "get security group rule fails", | ||
| args: args{ | ||
| getInstanceFails: true, | ||
| }, | ||
| wantErr: true, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| m := &IaaSClientMocked{ | ||
| GetSecurityGroupRuleFails: tt.args.getInstanceFails, | ||
| GetSecurityGroupRuleResp: tt.args.getInstanceResp, | ||
| } | ||
| got, err := GetSecurityGroupRuleName(context.Background(), m, "", "", "") | ||
| if (err != nil) != tt.wantErr { | ||
| t.Errorf("GetSecurityGroupRuleName() error = %v, wantErr %v", err, tt.wantErr) | ||
| return | ||
| } | ||
| if got != tt.want { | ||
| t.Errorf("GetSecurityGroupRuleName() = %v, want %v", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestGetSecurityGroupName(t *testing.T) { | ||
| type args struct { | ||
| getInstanceFails bool | ||
| getInstanceResp *iaas.SecurityGroup | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want string | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "base", | ||
| args: args{ | ||
| getInstanceResp: &iaas.SecurityGroup{ | ||
| Name: utils.Ptr("test"), | ||
| }, | ||
| }, | ||
| want: "test", | ||
| }, | ||
| { | ||
| name: "get security group fails", | ||
| args: args{ | ||
| getInstanceFails: true, | ||
| }, | ||
| wantErr: true, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| m := &IaaSClientMocked{ | ||
| GetSecurityGroupFails: tt.args.getInstanceFails, | ||
| GetSecurityGroupResp: tt.args.getInstanceResp, | ||
| } | ||
| got, err := GetSecurityGroupName(context.Background(), m, "", "") | ||
| if (err != nil) != tt.wantErr { | ||
| t.Errorf("GetSecurityGroupName() error = %v, wantErr %v", err, tt.wantErr) | ||
| return | ||
| } | ||
| if got != tt.want { | ||
| t.Errorf("GetSecurityGroupName() = %v, want %v", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestGetPublicIp(t *testing.T) { | ||
@@ -82,0 +193,0 @@ type args struct { |
@@ -11,2 +11,4 @@ package utils | ||
| type IaaSClient interface { | ||
| GetSecurityGroupRuleExecute(ctx context.Context, projectId, securityGroupRuleId, securityGroupId string) (*iaas.SecurityGroupRule, error) | ||
| GetSecurityGroupExecute(ctx context.Context, projectId, securityGroupId string) (*iaas.SecurityGroup, error) | ||
| GetPublicIPExecute(ctx context.Context, projectId, publicIpId string) (*iaas.PublicIp, error) | ||
@@ -21,2 +23,19 @@ GetServerExecute(ctx context.Context, projectId, serverId string) (*iaas.Server, error) | ||
| func GetSecurityGroupRuleName(ctx context.Context, apiClient IaaSClient, projectId, securityGroupRuleId, securityGroupId string) (string, error) { | ||
| resp, err := apiClient.GetSecurityGroupRuleExecute(ctx, projectId, securityGroupRuleId, securityGroupId) | ||
| if err != nil { | ||
| return "", fmt.Errorf("get security group rule: %w", err) | ||
| } | ||
| securityGroupRuleName := *resp.Ethertype + ", " + *resp.Direction | ||
| return securityGroupRuleName, nil | ||
| } | ||
| func GetSecurityGroupName(ctx context.Context, apiClient IaaSClient, projectId, securityGroupId string) (string, error) { | ||
| resp, err := apiClient.GetSecurityGroupExecute(ctx, projectId, securityGroupId) | ||
| if err != nil { | ||
| return "", fmt.Errorf("get security group: %w", err) | ||
| } | ||
| return *resp.Name, nil | ||
| } | ||
| func GetPublicIP(ctx context.Context, apiClient IaaSClient, projectId, publicIpId string) (ip, associatedResource string, err error) { | ||
@@ -23,0 +42,0 @@ resp, err := apiClient.GetPublicIPExecute(ctx, projectId, publicIpId) |
@@ -19,2 +19,11 @@ package utils | ||
| // PtrString creates a string representation of a passed object pointer or returns | ||
| // an empty string, if the passed object is _nil_. | ||
| func PtrString[T any](t *T) string { | ||
| if t != nil { | ||
| return fmt.Sprintf("%v", *t) | ||
| } | ||
| return "" | ||
| } | ||
| // Int64Ptr returns a pointer to an int64 | ||
@@ -21,0 +30,0 @@ // Needed because the Ptr function only returns pointer to int |
+22
-22
@@ -68,24 +68,24 @@ <div align="center"> | ||
| | Service | CLI Commands | Status | | ||
| | ---------------------------------- |----------------------------------------------------------------------------------------------------------------------| ------------------------- | | ||
| | Observability | `observability` | :white_check_mark: | | ||
| | Infrastructure as a Service (IaaS) | `beta network-area` <br/> `beta network` <br/> `beta volume` <br/> `beta network-interface` <br/> `beta public-ip` | :white_check_mark: (beta) | | ||
| | Authorization | `project`, `organization` | :white_check_mark: | | ||
| | DNS | `dns` | :white_check_mark: | | ||
| | Kubernetes Engine (SKE) | `ske` | :white_check_mark: | | ||
| | Load Balancer | `load-balancer` | :white_check_mark: | | ||
| | LogMe | `logme` | :white_check_mark: | | ||
| | MariaDB | `mariadb` | :white_check_mark: | | ||
| | MongoDB Flex | `mongodbflex` | :white_check_mark: | | ||
| | Object Storage | `object-storage` | :white_check_mark: | | ||
| | OpenSearch | `opensearch` | :white_check_mark: | | ||
| | PostgreSQL Flex | `postgresflex` | :white_check_mark: | | ||
| | RabbitMQ | `rabbitmq` | :white_check_mark: | | ||
| | Redis | `redis` | :white_check_mark: | | ||
| | Resource Manager | `project` | :white_check_mark: | | ||
| | Secrets Manager | `secrets-manager` | :white_check_mark: | | ||
| | Server Backup Management | `beta server backup` | :white_check_mark: (beta) | | ||
| | Server Command (Run Command) | `beta server command` | :white_check_mark: (beta) | | ||
| | Service Account | `service-account` | :white_check_mark: | | ||
| | SQLServer Flex | `beta sqlserverflex` | :white_check_mark: (beta) | | ||
| | Service | CLI Commands | Status | | ||
| | ---------------------------------- |--------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------- | | ||
| | Observability | `observability` | :white_check_mark: | | ||
| | Infrastructure as a Service (IaaS) | `beta network-area` <br/> `beta network` <br/> `beta volume` <br/> `beta network-interface` <br/> `beta public-ip` <br/> `beta security-group` <br/> `beta key-pair` | :white_check_mark: (beta) | | ||
| | Authorization | `project`, `organization` | :white_check_mark: | | ||
| | DNS | `dns` | :white_check_mark: | | ||
| | Kubernetes Engine (SKE) | `ske` | :white_check_mark: | | ||
| | Load Balancer | `load-balancer` | :white_check_mark: | | ||
| | LogMe | `logme` | :white_check_mark: | | ||
| | MariaDB | `mariadb` | :white_check_mark: | | ||
| | MongoDB Flex | `mongodbflex` | :white_check_mark: | | ||
| | Object Storage | `object-storage` | :white_check_mark: | | ||
| | OpenSearch | `opensearch` | :white_check_mark: | | ||
| | PostgreSQL Flex | `postgresflex` | :white_check_mark: | | ||
| | RabbitMQ | `rabbitmq` | :white_check_mark: | | ||
| | Redis | `redis` | :white_check_mark: | | ||
| | Resource Manager | `project` | :white_check_mark: | | ||
| | Secrets Manager | `secrets-manager` | :white_check_mark: | | ||
| | Server Backup Management | `beta server backup` | :white_check_mark: (beta) | | ||
| | Server Command (Run Command) | `beta server command` | :white_check_mark: (beta) | | ||
| | Service Account | `service-account` | :white_check_mark: | | ||
| | SQLServer Flex | `beta sqlserverflex` | :white_check_mark: (beta) | | ||
@@ -92,0 +92,0 @@ ## Authentication |