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

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

Package Overview
Dependencies
Versions
174
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

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

Comparing version
v0.19.0
to
v0.20.0
+51
docs/stackit_beta_key-pair_create.md
## 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"
}
+10
-0

@@ -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
+2
-0

@@ -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