🚀 Big News:Socket Has Acquired Secure Annex.Learn More
Socket
Book a DemoSign in
Socket

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

Package Overview
Dependencies
Versions
178
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.10.0
to
v0.11.0
+50
docs/stackit_beta_network_create.md
## stackit beta network create
Creates a network
### Synopsis
Creates a network.
```
stackit beta network create [flags]
```
### Examples
```
Create a network with name "network-1"
$ stackit beta network create --name network-1
Create an IPv4 network with name "network-1" with DNS name servers and a prefix length
$ stackit beta network create --name network-1 --ipv4-dns-name-servers "1.1.1.1,8.8.8.8,9.9.9.9" --ipv4-prefix-length 25
Create an IPv6 network with name "network-1" with DNS name servers and a prefix length
$ stackit beta network create --name network-1 --ipv6-dns-name-servers "2001:4860:4860::8888,2001:4860:4860::8844" --ipv6-prefix-length 56
```
### Options
```
-h, --help Help for "stackit beta network create"
--ipv4-dns-name-servers strings List of DNS name servers for IPv4
--ipv4-prefix-length int The prefix length of the IPv4 network
--ipv6-dns-name-servers strings List of DNS name servers for IPv6
--ipv6-prefix-length int The prefix length of the IPv6 network
-n, --name string Network name
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "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 network](./stackit_beta_network.md) - Provides functionality for Network
## stackit beta network delete
Deletes a network
### Synopsis
Deletes a network.
If the network is still in use, the deletion will fail
```
stackit beta network delete [flags]
```
### Examples
```
Delete network with ID "xxx"
$ stackit beta network delete xxx
```
### Options
```
-h, --help Help for "stackit beta network 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 network](./stackit_beta_network.md) - Provides functionality for Network
## stackit beta network describe
Shows details of a network
### Synopsis
Shows details of a network.
```
stackit beta network describe [flags]
```
### Examples
```
Show details of a network with ID "xxx"
$ stackit beta network describe xxx
Show details of a network with ID "xxx" in JSON format
$ stackit beta network describe xxx --output-format json
```
### Options
```
-h, --help Help for "stackit beta network 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 network](./stackit_beta_network.md) - Provides functionality for Network
## stackit beta network list
Lists all networks of a project
### Synopsis
Lists all network of a project.
```
stackit beta network list [flags]
```
### Examples
```
Lists all networks
$ stackit beta network list
Lists all networks in JSON format
$ stackit beta network list --output-format json
Lists up to 10 networks
$ stackit beta network list --limit 10
```
### Options
```
-h, --help Help for "stackit beta network list"
--limit int Maximum number of entries to list
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "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 network](./stackit_beta_network.md) - Provides functionality for Network
## stackit beta network update
Updates a network
### Synopsis
Updates a network.
```
stackit beta network update [flags]
```
### Examples
```
Update network with ID "xxx" with new name "network-1-new"
$ stackit beta network update xxx --name network-1-new
Update IPv4 network with ID "xxx" with new name "network-1-new" and new DNS name servers
$ stackit beta network update xxx --name network-1-new --ipv4-dns-name-servers "2.2.2.2"
Update IPv6 network with ID "xxx" with new name "network-1-new" and new DNS name servers
$ stackit beta network update xxx --name network-1-new --ipv6-dns-name-servers "2001:4860:4860::8888"
```
### Options
```
-h, --help Help for "stackit beta network update"
--ipv4-dns-name-servers strings List of DNS name servers IPv4
--ipv6-dns-name-servers strings List of DNS name servers for IPv6
-n, --name string Network name
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "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 network](./stackit_beta_network.md) - Provides functionality for Network
## stackit beta network-area create
Creates a STACKIT Network Area (SNA)
### Synopsis
Creates a STACKIT Network Area (SNA) in an organization.
```
stackit beta network-area create [flags]
```
### Examples
```
Create a network area with name "network-area-1" in organization with ID "xxx" with network ranges and a transfer network
$ stackit beta network-area create --name network-area-1 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24"
Create a network area with name "network-area-2" in organization with ID "xxx" with network ranges, transfer network and DNS name server
$ stackit beta network-area create --name network-area-2 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --dns-name-servers "1.1.1.1"
Create a network area with name "network-area-3" in organization with ID "xxx" with network ranges, transfer network and additional options
$ stackit beta network-area create --name network-area-3 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --default-prefix-length 25 --max-prefix-length 29 --min-prefix-length 24
```
### Options
```
--default-prefix-length int The default prefix length for networks in the network area
--dns-name-servers strings List of DNS name server IPs
-h, --help Help for "stackit beta network-area create"
--max-prefix-length int The maximum prefix length for networks in the network area
--min-prefix-length int The minimum prefix length for networks in the network area
-n, --name string Network area name
--network-ranges strings List of network ranges (default [])
--organization-id string Organization ID
--transfer-network string Transfer network in CIDR notation
```
### 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 network-area](./stackit_beta_network-area.md) - Provides functionality for STACKIT Network Area (SNA)
## stackit beta network-area delete
Deletes a STACKIT Network Area (SNA)
### Synopsis
Deletes a STACKIT Network Area (SNA) in an organization.
If the SNA is attached to any projects, the deletion will fail
```
stackit beta network-area delete [flags]
```
### Examples
```
Delete network area with ID "xxx" in organization with ID "yyy"
$ stackit beta network-area delete xxx --organization-id yyy
```
### Options
```
-h, --help Help for "stackit beta network-area delete"
--organization-id string Organization 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 network-area](./stackit_beta_network-area.md) - Provides functionality for STACKIT Network Area (SNA)
## stackit beta network-area describe
Shows details of a STACKIT Network Area
### Synopsis
Shows details of a STACKIT Network Area in an organization.
```
stackit beta network-area describe [flags]
```
### Examples
```
Show details of a network area with ID "xxx" in organization with ID "yyy"
$ stackit beta network-area describe xxx --organization-id yyy
Show details of a network area with ID "xxx" in organization with ID "yyy" and show attached projects
$ stackit beta network-area describe xxx --organization-id yyy --show-attached-projects
Show details of a network area with ID "xxx" in organization with ID "yyy" in JSON format
$ stackit beta network-area describe xxx --organization-id yyy --output-format json
```
### Options
```
-h, --help Help for "stackit beta network-area describe"
--organization-id string Organization ID
--show-attached-projects Whether to show attached projects. If a network area has several attached projects, their retrieval may take some time and the output may be extensive.
```
### 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 network-area](./stackit_beta_network-area.md) - Provides functionality for STACKIT Network Area (SNA)
## stackit beta network-area list
Lists all STACKIT Network Areas (SNA) of an organization
### Synopsis
Lists all STACKIT Network Areas (SNA) of an organization.
```
stackit beta network-area list [flags]
```
### Examples
```
Lists all network areas of organization "xxx"
$ stackit beta network-area list --organization-id xxx
Lists all network areas of organization "xxx" in JSON format
$ stackit beta network-area list --organization-id xxx --output-format json
Lists up to 10 network areas of organization "xxx"
$ stackit beta network-area list --organization-id xxx --limit 10
```
### Options
```
-h, --help Help for "stackit beta network-area list"
--limit int Maximum number of entries to list
--organization-id string Organization 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 network-area](./stackit_beta_network-area.md) - Provides functionality for STACKIT Network Area (SNA)
## stackit beta network-area network-range create
Creates a network range in a STACKIT Network Area (SNA)
### Synopsis
Creates a network range in a STACKIT Network Area (SNA).
```
stackit beta network-area network-range create [flags]
```
### Examples
```
Create a network range in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"
$ stackit beta network-area network-range create --network-area-id xxx --organization-id yyy --network-range "1.1.1.0/24"
```
### Options
```
-h, --help Help for "stackit beta network-area network-range create"
--network-area-id string STACKIT Network Area (SNA) ID
--network-range string Network range to create in CIDR notation
--organization-id string Organization 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 network-area network-range](./stackit_beta_network-area_network-range.md) - Provides functionality for network ranges in STACKIT Network Areas
## stackit beta network-area network-range delete
Deletes a network range in a STACKIT Network Area (SNA)
### Synopsis
Deletes a network range in a STACKIT Network Area (SNA).
```
stackit beta network-area network-range delete [flags]
```
### Examples
```
Delete network range with id "xxx" in a STACKIT Network Area with ID "yyy" in organization with ID "zzz"
$ stackit beta network-area network-range delete xxx --network-area-id yyy --organization-id zzz
```
### Options
```
-h, --help Help for "stackit beta network-area network-range delete"
--network-area-id string STACKIT Network Area (SNA) ID
--organization-id string Organization 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 network-area network-range](./stackit_beta_network-area_network-range.md) - Provides functionality for network ranges in STACKIT Network Areas
## stackit beta network-area network-range describe
Shows details of a network range in a STACKIT Network Area (SNA)
### Synopsis
Shows details of a network range in a STACKIT Network Area (SNA).
```
stackit beta network-area network-range describe [flags]
```
### Examples
```
Show details of a network range with ID "xxx" in a STACKIT Network Area with ID "yyy" in organization with ID "zzz"
$ stackit beta network-area network-range describe xxx --network-area-id yyy --organization-id zzz
```
### Options
```
-h, --help Help for "stackit beta network-area network-range describe"
--network-area-id string STACKIT Network Area (SNA) ID
--organization-id string Organization 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 network-area network-range](./stackit_beta_network-area_network-range.md) - Provides functionality for network ranges in STACKIT Network Areas
## stackit beta network-area network-range list
Lists all network ranges in a STACKIT Network Area (SNA)
### Synopsis
Lists all network ranges in a STACKIT Network Area (SNA).
```
stackit beta network-area network-range list [flags]
```
### Examples
```
Lists all network ranges in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"
$ stackit beta network-area network-range list --network-area-id xxx --organization-id yyy
Lists all network ranges in a STACKIT Network Area with ID "xxx" in organization with ID "yyy" in JSON format
$ stackit beta network-area network-range list --network-area-id xxx --organization-id yyy --output-format json
Lists up to 10 network ranges in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"
$ stackit beta network-area network-range list --network-area-id xxx --organization-id yyy --limit 10
```
### Options
```
-h, --help Help for "stackit beta network-area network-range list"
--limit int Maximum number of entries to list
--network-area-id string STACKIT Network Area (SNA) ID
--organization-id string Organization 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 network-area network-range](./stackit_beta_network-area_network-range.md) - Provides functionality for network ranges in STACKIT Network Areas
## stackit beta network-area network-range
Provides functionality for network ranges in STACKIT Network Areas
### Synopsis
Provides functionality for network ranges in STACKIT Network Areas.
```
stackit beta network-area network-range [flags]
```
### Options
```
-h, --help Help for "stackit beta network-area network-range"
```
### 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 network-area](./stackit_beta_network-area.md) - Provides functionality for STACKIT Network Area (SNA)
* [stackit beta network-area network-range create](./stackit_beta_network-area_network-range_create.md) - Creates a network range in a STACKIT Network Area (SNA)
* [stackit beta network-area network-range delete](./stackit_beta_network-area_network-range_delete.md) - Deletes a network range in a STACKIT Network Area (SNA)
* [stackit beta network-area network-range describe](./stackit_beta_network-area_network-range_describe.md) - Shows details of a network range in a STACKIT Network Area (SNA)
* [stackit beta network-area network-range list](./stackit_beta_network-area_network-range_list.md) - Lists all network ranges in a STACKIT Network Area (SNA)
## stackit beta network-area route create
Creates a static route in a STACKIT Network Area (SNA)
### Synopsis
Creates a static route in a STACKIT Network Area (SNA).
This command is currently asynchonous only due to limitations in the waiting functionality of the SDK. This will be updated in a future release.
```
stackit beta network-area route create [flags]
```
### Examples
```
Create a static route with prefix "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"
$ stackit beta network-area route create --organization-id yyy --network-area-id xxx --prefix 1.1.1.0/24 --next-hop 1.1.1.1
```
### Options
```
-h, --help Help for "stackit beta network-area route create"
--network-area-id string STACKIT Network Area ID
--next-hop string Next hop IP address. Must be a valid IPv4
--organization-id string Organization ID
--prefix string Static route prefix
```
### 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 network-area route](./stackit_beta_network-area_route.md) - Provides functionality for static routes in STACKIT Network Areas
## stackit beta network-area route delete
Deletes a static route in a STACKIT Network Area (SNA)
### Synopsis
Deletes a static route in a STACKIT Network Area (SNA).
```
stackit beta network-area route delete [flags]
```
### Examples
```
Delete a static route with ID "xxx" in a STACKIT Network Area with ID "yyy" in organization with ID "zzz"
$ stackit beta network-area route delete xxx --organization-id zzz --network-area-id yyy
```
### Options
```
-h, --help Help for "stackit beta network-area route delete"
--network-area-id string STACKIT Network Area ID
--organization-id string Organization 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 network-area route](./stackit_beta_network-area_route.md) - Provides functionality for static routes in STACKIT Network Areas
## stackit beta network-area route describe
Shows details of a static route in a STACKIT Network Area (SNA)
### Synopsis
Shows details of a static route in a STACKIT Network Area (SNA).
```
stackit beta network-area route describe [flags]
```
### Examples
```
Show details of a static route with ID "xxx" in a STACKIT Network Area with ID "yyy" in organization with ID "zzz"
$ stackit beta network-area route describe xxx --network-area-id yyy --organization-id zzz
Show details of a static route with ID "xxx" in a STACKIT Network Area with ID "yyy" in organization with ID "zzz" in JSON format
$ stackit beta network-area route describe xxx --network-area-id yyy --organization-id zzz --output-format json
```
### Options
```
-h, --help Help for "stackit beta network-area route describe"
--network-area-id string STACKIT Network Area ID
--organization-id string Organization 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 network-area route](./stackit_beta_network-area_route.md) - Provides functionality for static routes in STACKIT Network Areas
## stackit beta network-area route list
Lists all static routes in a STACKIT Network Area (SNA)
### Synopsis
Lists all static routes in a STACKIT Network Area (SNA).
```
stackit beta network-area route list [flags]
```
### Examples
```
Lists all static routes in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"
$ stackit beta network-area route list --network-area-id xxx --organization-id yyy
Lists all static routes in a STACKIT Network Area with ID "xxx" in organization with ID "yyy" in JSON format
$ stackit beta network-area route list --network-area-id xxx --organization-id yyy --output-format json
Lists up to 10 static routes in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"
$ stackit beta network-area route list --network-area-id xxx --organization-id yyy --limit 10
```
### Options
```
-h, --help Help for "stackit beta network-area route list"
--limit int Maximum number of entries to list
--network-area-id string STACKIT Network Area ID
--organization-id string Organization 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 network-area route](./stackit_beta_network-area_route.md) - Provides functionality for static routes in STACKIT Network Areas
## stackit beta network-area route
Provides functionality for static routes in STACKIT Network Areas
### Synopsis
Provides functionality for static routes in STACKIT Network Areas.
```
stackit beta network-area route [flags]
```
### Options
```
-h, --help Help for "stackit beta network-area route"
```
### 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 network-area](./stackit_beta_network-area.md) - Provides functionality for STACKIT Network Area (SNA)
* [stackit beta network-area route create](./stackit_beta_network-area_route_create.md) - Creates a static route in a STACKIT Network Area (SNA)
* [stackit beta network-area route delete](./stackit_beta_network-area_route_delete.md) - Deletes a static route in a STACKIT Network Area (SNA)
* [stackit beta network-area route describe](./stackit_beta_network-area_route_describe.md) - Shows details of a static route in a STACKIT Network Area (SNA)
* [stackit beta network-area route list](./stackit_beta_network-area_route_list.md) - Lists all static routes in a STACKIT Network Area (SNA)
## stackit beta network-area update
Updates a STACKIT Network Area (SNA)
### Synopsis
Updates a STACKIT Network Area (SNA) in an organization.
```
stackit beta network-area update [flags]
```
### Examples
```
Update network area with ID "xxx" in organization with ID "yyy" with new name "network-area-1-new"
$ stackit beta network-area update xxx --organization-id yyy --name network-area-1-new
```
### Options
```
--default-prefix-length int The default prefix length for networks in the network area
--dns-name-servers strings List of DNS name server IPs
-h, --help Help for "stackit beta network-area update"
--max-prefix-length int The maximum prefix length for networks in the network area
--min-prefix-length int The minimum prefix length for networks in the network area
-n, --name string Network area name
--organization-id string Organization 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 network-area](./stackit_beta_network-area.md) - Provides functionality for STACKIT Network Area (SNA)
## stackit beta network-area
Provides functionality for STACKIT Network Area (SNA)
### Synopsis
Provides functionality for STACKIT Network Area (SNA).
```
stackit beta network-area [flags]
```
### Options
```
-h, --help Help for "stackit beta network-area"
```
### 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 network-area create](./stackit_beta_network-area_create.md) - Creates a STACKIT Network Area (SNA)
* [stackit beta network-area delete](./stackit_beta_network-area_delete.md) - Deletes a STACKIT Network Area (SNA)
* [stackit beta network-area describe](./stackit_beta_network-area_describe.md) - Shows details of a STACKIT Network Area
* [stackit beta network-area list](./stackit_beta_network-area_list.md) - Lists all STACKIT Network Areas (SNA) of an organization
* [stackit beta network-area network-range](./stackit_beta_network-area_network-range.md) - Provides functionality for network ranges in STACKIT Network Areas
* [stackit beta network-area route](./stackit_beta_network-area_route.md) - Provides functionality for static routes in STACKIT Network Areas
* [stackit beta network-area update](./stackit_beta_network-area_update.md) - Updates a STACKIT Network Area (SNA)
## stackit beta network
Provides functionality for Network
### Synopsis
Provides functionality for Network.
```
stackit beta network [flags]
```
### Options
```
-h, --help Help for "stackit beta network"
```
### 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 network create](./stackit_beta_network_create.md) - Creates a network
* [stackit beta network delete](./stackit_beta_network_delete.md) - Deletes a network
* [stackit beta network describe](./stackit_beta_network_describe.md) - Shows details of a network
* [stackit beta network list](./stackit_beta_network_list.md) - Lists all networks of a project
* [stackit beta network update](./stackit_beta_network_update.md) - Updates a network
## stackit beta server command create
Creates a Server Command
### Synopsis
Creates a Server Command.
```
stackit beta server command create [flags]
```
### Examples
```
Create a server command for server with ID "xxx", template name "RunShellScript" and a script from a file (using the @{...} format)
$ stackit beta server command create --server-id xxx --template-name=RunShellScript --params script='@{/path/to/script.sh}'
Create a server command for server with ID "xxx", template name "RunShellScript" and a script provided on the command line
$ stackit beta server command create --server-id xxx --template-name=RunShellScript --params script='echo hello'
```
### Options
```
-h, --help Help for "stackit beta server command create"
-r, --params stringToString Params can be provided with the format key=value and the flag can be used multiple times to provide a list of labels (default [])
-s, --server-id string Server ID
-n, --template-name string Template name
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "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 command](./stackit_beta_server_command.md) - Provides functionality for Server Command
## stackit beta server command describe
Shows details of a Server Command
### Synopsis
Shows details of a Server Command.
```
stackit beta server command describe COMMAND_ID [flags]
```
### Examples
```
Get details of a Server Command with ID "xxx" for server with ID "yyy"
$ stackit beta server command describe xxx --server-id=yyy
Get details of a Server Command with ID "xxx" for server with ID "yyy" in JSON format
$ stackit beta server command describe xxx --server-id=yyy --output-format json
```
### Options
```
-h, --help Help for "stackit beta server command describe"
-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 command](./stackit_beta_server_command.md) - Provides functionality for Server Command
## stackit beta server command list
Lists all server commands
### Synopsis
Lists all server commands.
```
stackit beta server command list [flags]
```
### Examples
```
List all commands for a server with ID "xxx"
$ stackit beta server command list --server-id xxx
List all commands for a server with ID "xxx" in JSON format
$ stackit beta server command list --server-id xxx --output-format json
```
### Options
```
-h, --help Help for "stackit beta server command 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 command](./stackit_beta_server_command.md) - Provides functionality for Server Command
## stackit beta server command template describe
Shows details of a Server Command Template
### Synopsis
Shows details of a Server Command Template.
```
stackit beta server command template describe COMMAND_TEMPLATE_NAME [flags]
```
### Examples
```
Get details of a Server Command Template with name "RunShellScript" for server with ID "xxx"
$ stackit beta server command template describe RunShellScript --server-id=xxx
Get details of a Server Command Template with name "RunShellScript" for server with ID "xxx" in JSON format
$ stackit beta server command template describe RunShellScript --server-id=xxx --output-format json
```
### Options
```
-h, --help Help for "stackit beta server command template describe"
-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 command template](./stackit_beta_server_command_template.md) - Provides functionality for Server Command Template
## stackit beta server command template list
Lists all server command templates
### Synopsis
Lists all server command templates.
```
stackit beta server command template list [flags]
```
### Examples
```
List all command templates
$ stackit beta server command template list
List all commands templates in JSON format
$ stackit beta server command template list --output-format json
```
### Options
```
-h, --help Help for "stackit beta server command template list"
--limit int Maximum number of entries to list
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "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 command template](./stackit_beta_server_command_template.md) - Provides functionality for Server Command Template
## stackit beta server command template
Provides functionality for Server Command Template
### Synopsis
Provides functionality for Server Command Template.
```
stackit beta server command template [flags]
```
### Options
```
-h, --help Help for "stackit beta server command template"
```
### 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 command](./stackit_beta_server_command.md) - Provides functionality for Server Command
* [stackit beta server command template describe](./stackit_beta_server_command_template_describe.md) - Shows details of a Server Command Template
* [stackit beta server command template list](./stackit_beta_server_command_template_list.md) - Lists all server command templates
## stackit beta server command
Provides functionality for Server Command
### Synopsis
Provides functionality for Server Command.
```
stackit beta server command [flags]
```
### Options
```
-h, --help Help for "stackit beta server command"
```
### 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 Server
* [stackit beta server command create](./stackit_beta_server_command_create.md) - Creates a Server Command
* [stackit beta server command describe](./stackit_beta_server_command_describe.md) - Shows details of a Server Command
* [stackit beta server command list](./stackit_beta_server_command_list.md) - Lists all server commands
* [stackit beta server command template](./stackit_beta_server_command_template.md) - Provides functionality for Server Command Template
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"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testOrgId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
nameFlag: "example-network-area-name",
organizationIdFlag: testOrgId,
dnsNameServersFlag: "1.1.1.0,1.1.2.0",
networkRangesFlag: "192.0.0.0/24,102.0.0.0/24",
transferNetworkFlag: "100.0.0.0/24",
defaultPrefixLengthFlag: "24",
maxPrefixLengthFlag: "24",
minPrefixLengthFlag: "24",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
Name: utils.Ptr("example-network-area-name"),
OrganizationId: utils.Ptr(testOrgId),
DnsNameServers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}),
NetworkRanges: utils.Ptr([]string{"192.0.0.0/24", "102.0.0.0/24"}),
TransferNetwork: utils.Ptr("100.0.0.0/24"),
DefaultPrefixLength: utils.Ptr(int64(24)),
MaxPrefixLength: utils.Ptr(int64(24)),
MinPrefixLength: utils.Ptr(int64(24)),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkAreaRequest)) iaas.ApiCreateNetworkAreaRequest {
request := testClient.CreateNetworkArea(testCtx, testOrgId)
request = request.CreateNetworkAreaPayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func fixturePayload(mods ...func(payload *iaas.CreateNetworkAreaPayload)) iaas.CreateNetworkAreaPayload {
payload := iaas.CreateNetworkAreaPayload{
Name: utils.Ptr("example-network-area-name"),
AddressFamily: &iaas.CreateAreaAddressFamily{
Ipv4: &iaas.CreateAreaIPv4{
DefaultNameservers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}),
NetworkRanges: &[]iaas.NetworkRange{
{
Prefix: utils.Ptr("192.0.0.0/24"),
},
{
Prefix: utils.Ptr("102.0.0.0/24"),
},
},
TransferNetwork: utils.Ptr("100.0.0.0/24"),
DefaultPrefixLen: utils.Ptr(int64(24)),
MaxPrefixLen: utils.Ptr(int64(24)),
MinPrefixLen: utils.Ptr(int64(24)),
},
},
}
for _, mod := range mods {
mod(&payload)
}
return payload
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
aclValues []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, dnsNameServersFlag)
delete(flagValues, defaultPrefixLengthFlag)
delete(flagValues, maxPrefixLengthFlag)
delete(flagValues, minPrefixLengthFlag)
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.DnsNameServers = nil
model.DefaultPrefixLength = nil
model.MaxPrefixLength = nil
model.MinPrefixLength = nil
}),
},
{
description: "name missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, nameFlag)
}),
isValid: false,
},
{
description: "network ranges missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, networkRangesFlag)
}),
isValid: false,
},
{
description: "transfer network missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, transferNetworkFlag)
}),
isValid: false,
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "org id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, organizationIdFlag)
}),
isValid: false,
},
{
description: "org id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = ""
}),
isValid: false,
},
{
description: "org id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest iaas.ApiCreateNetworkAreaRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package create
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"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"
rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client"
rmUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/spf13/cobra"
)
const (
nameFlag = "name"
organizationIdFlag = "organization-id"
dnsNameServersFlag = "dns-name-servers"
networkRangesFlag = "network-ranges"
transferNetworkFlag = "transfer-network"
defaultPrefixLengthFlag = "default-prefix-length"
maxPrefixLengthFlag = "max-prefix-length"
minPrefixLengthFlag = "min-prefix-length"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Name *string
OrganizationId *string
DnsNameServers *[]string
NetworkRanges *[]string
TransferNetwork *string
DefaultPrefixLength *int64
MaxPrefixLength *int64
MinPrefixLength *int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a STACKIT Network Area (SNA)",
Long: "Creates a STACKIT Network Area (SNA) in an organization.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a network area with name "network-area-1" in organization with ID "xxx" with network ranges and a transfer network`,
`$ stackit beta network-area create --name network-area-1 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24"`,
),
examples.NewExample(
`Create a network area with name "network-area-2" in organization with ID "xxx" with network ranges, transfer network and DNS name server`,
`$ stackit beta network-area create --name network-area-2 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --dns-name-servers "1.1.1.1"`,
),
examples.NewExample(
`Create a network area with name "network-area-3" in organization with ID "xxx" with network ranges, transfer network and additional options`,
`$ stackit beta network-area create --name network-area-3 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --default-prefix-length 25 --max-prefix-length 29 --min-prefix-length 24`,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
var orgLabel string
rmApiClient, err := rmClient.ConfigureClient(p)
if err == nil {
orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient, *model.OrganizationId)
if err != nil {
p.Debug(print.ErrorLevel, "get organization name: %v", err)
orgLabel = *model.OrganizationId
}
} else {
p.Debug(print.ErrorLevel, "configure resource manager client: %v", err)
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to create a network area for organization %q?", orgLabel)
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 network area: %w", err)
}
return outputResult(p, model, orgLabel, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(nameFlag, "n", "", "Network area name")
cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID")
cmd.Flags().StringSlice(dnsNameServersFlag, nil, "List of DNS name server IPs")
cmd.Flags().Var(flags.CIDRSliceFlag(), networkRangesFlag, "List of network ranges")
cmd.Flags().Var(flags.CIDRFlag(), transferNetworkFlag, "Transfer network in CIDR notation")
cmd.Flags().Int64(defaultPrefixLengthFlag, 0, "The default prefix length for networks in the network area")
cmd.Flags().Int64(maxPrefixLengthFlag, 0, "The maximum prefix length for networks in the network area")
cmd.Flags().Int64(minPrefixLengthFlag, 0, "The minimum prefix length for networks in the network area")
err := flags.MarkFlagsRequired(cmd, nameFlag, organizationIdFlag, networkRangesFlag, transferNetworkFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
model := inputModel{
GlobalFlagModel: globalFlags,
Name: flags.FlagToStringPointer(p, cmd, nameFlag),
OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag),
DnsNameServers: flags.FlagToStringSlicePointer(p, cmd, dnsNameServersFlag),
NetworkRanges: flags.FlagToStringSlicePointer(p, cmd, networkRangesFlag),
TransferNetwork: flags.FlagToStringPointer(p, cmd, transferNetworkFlag),
DefaultPrefixLength: flags.FlagToInt64Pointer(p, cmd, defaultPrefixLengthFlag),
MaxPrefixLength: flags.FlagToInt64Pointer(p, cmd, maxPrefixLengthFlag),
MinPrefixLength: flags.FlagToInt64Pointer(p, cmd, minPrefixLengthFlag),
}
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.ApiCreateNetworkAreaRequest {
req := apiClient.CreateNetworkArea(ctx, *model.OrganizationId)
networkRanges := make([]iaas.NetworkRange, len(*model.NetworkRanges))
for i, networkRange := range *model.NetworkRanges {
networkRanges[i] = iaas.NetworkRange{
Prefix: utils.Ptr(networkRange),
}
}
payload := iaas.CreateNetworkAreaPayload{
Name: model.Name,
AddressFamily: &iaas.CreateAreaAddressFamily{
Ipv4: &iaas.CreateAreaIPv4{
DefaultNameservers: model.DnsNameServers,
NetworkRanges: utils.Ptr(networkRanges),
TransferNetwork: model.TransferNetwork,
DefaultPrefixLen: model.DefaultPrefixLength,
MaxPrefixLen: model.MaxPrefixLength,
MinPrefixLen: model.MinPrefixLength,
},
},
}
return req.CreateNetworkAreaPayload(payload)
}
func outputResult(p *print.Printer, model *inputModel, orgLabel string, networkArea *iaas.NetworkArea) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(networkArea, "", " ")
if err != nil {
return fmt.Errorf("marshal network area: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(networkArea, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal network area: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Created STACKIT Network Area for organization %q.\nNetwork area ID: %s\n", orgLabel, *networkArea.AreaId)
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"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testOrganizationId = uuid.NewString()
var testNetworkAreaId = uuid.NewString()
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testNetworkAreaId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
organizationIdFlag: testOrganizationId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
OrganizationId: &testOrganizationId,
AreaId: testNetworkAreaId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiDeleteNetworkAreaRequest)) iaas.ApiDeleteNetworkAreaRequest {
request := testClient.DeleteNetworkArea(testCtx, testOrganizationId, testNetworkAreaId)
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: "organization id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, organizationIdFlag)
}),
isValid: false,
},
{
description: "organization id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = ""
}),
isValid: false,
},
{
description: "organization id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "instance id invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "instance 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.ApiDeleteNetworkAreaRequest
}{
{
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/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 (
areaIdArg = "AREA_ID"
organizationIdFlag = "organization-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
OrganizationId *string
AreaId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Short: "Deletes a STACKIT Network Area (SNA)",
Long: fmt.Sprintf("%s\n%s\n",
"Deletes a STACKIT Network Area (SNA) in an organization.",
"If the SNA is attached to any projects, the deletion will fail",
),
Args: args.SingleArg(areaIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Delete network area with ID "xxx" in organization with ID "yyy"`,
"$ stackit beta network-area delete xxx --organization-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
}
networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, model.AreaId)
if err != nil {
p.Debug(print.ErrorLevel, "get network area name: %v", err)
networkAreaLabel = model.AreaId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete network area %q?", networkAreaLabel)
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 network area: %w", err)
}
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(p)
s.Start("Deleting network area")
_, err = wait.DeleteNetworkAreaWaitHandler(ctx, apiClient, *model.OrganizationId, model.AreaId).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for network area deletion: %w", err)
}
s.Stop()
}
operationState := "Deleted"
if model.Async {
operationState = "Triggered deletion of"
}
p.Info("%s STACKIT Network Area %q\n", operationState, networkAreaLabel)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID")
err := flags.MarkFlagsRequired(cmd, organizationIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
areaId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
model := inputModel{
GlobalFlagModel: globalFlags,
OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag),
AreaId: areaId,
}
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.ApiDeleteNetworkAreaRequest {
return apiClient.DeleteNetworkArea(ctx, *model.OrganizationId, model.AreaId)
}
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"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testOrganizationId = uuid.NewString()
var testNetworkAreaId = uuid.NewString()
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testNetworkAreaId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
organizationIdFlag: testOrganizationId,
showAttachedProjectsFlag: "false",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
OrganizationId: &testOrganizationId,
AreaId: testNetworkAreaId,
ShowAttachedProjects: false,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiGetNetworkAreaRequest)) iaas.ApiGetNetworkAreaRequest {
request := testClient.GetNetworkArea(testCtx, testOrganizationId, testNetworkAreaId)
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: "organization id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, organizationIdFlag)
}),
isValid: false,
},
{
description: "organization id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = ""
}),
isValid: false,
},
{
description: "organization id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "instance id invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "instance id invalid 2",
argValues: []string{"invalid-uuid"},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "show attached projects true",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[showAttachedProjectsFlag] = "true"
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.ShowAttachedProjects = 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.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.ApiGetNetworkAreaRequest
}{
{
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/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"
iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils"
"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 (
areaIdArg = "AREA_ID"
organizationIdFlag = "organization-id"
showAttachedProjectsFlag = "show-attached-projects"
)
type inputModel struct {
*globalflags.GlobalFlagModel
OrganizationId *string
AreaId string
ShowAttachedProjects bool
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "describe",
Short: "Shows details of a STACKIT Network Area",
Long: "Shows details of a STACKIT Network Area in an organization.",
Args: args.SingleArg(areaIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Show details of a network area with ID "xxx" in organization with ID "yyy"`,
"$ stackit beta network-area describe xxx --organization-id yyy",
),
examples.NewExample(
`Show details of a network area with ID "xxx" in organization with ID "yyy" and show attached projects`,
"$ stackit beta network-area describe xxx --organization-id yyy --show-attached-projects",
),
examples.NewExample(
`Show details of a network area with ID "xxx" in organization with ID "yyy" in JSON format`,
"$ stackit beta network-area describe xxx --organization-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 network area: %w", err)
}
var projects []string
if model.ShowAttachedProjects {
projects, err = iaasUtils.ListAttachedProjects(ctx, apiClient, *model.OrganizationId, model.AreaId)
if err != nil {
return fmt.Errorf("get attached projects: %w", err)
}
}
return outputResult(p, model.OutputFormat, resp, projects)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID")
cmd.Flags().Bool(showAttachedProjectsFlag, false, "Whether to show attached projects. If a network area has several attached projects, their retrieval may take some time and the output may be extensive.")
err := flags.MarkFlagsRequired(cmd, organizationIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
areaId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
model := inputModel{
GlobalFlagModel: globalFlags,
OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag),
AreaId: areaId,
ShowAttachedProjects: flags.FlagToBoolValue(p, cmd, showAttachedProjectsFlag),
}
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.ApiGetNetworkAreaRequest {
return apiClient.GetNetworkArea(ctx, *model.OrganizationId, model.AreaId)
}
func outputResult(p *print.Printer, outputFormat string, networkArea *iaas.NetworkArea, attachedProjects []string) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(networkArea, "", " ")
if err != nil {
return fmt.Errorf("marshal network area: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(networkArea, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal network area: %w", err)
}
p.Outputln(string(details))
return nil
default:
var routes []string
if networkArea.Ipv4.Routes != nil {
for _, route := range *networkArea.Ipv4.Routes {
routes = append(routes, fmt.Sprintf("next hop: %s\nprefix: %s", *route.Nexthop, *route.Prefix))
}
}
var networkRanges []string
if networkArea.Ipv4.NetworkRanges != nil {
for _, networkRange := range *networkArea.Ipv4.NetworkRanges {
networkRanges = append(networkRanges, *networkRange.Prefix)
}
}
table := tables.NewTable()
table.AddRow("ID", *networkArea.AreaId)
table.AddSeparator()
table.AddRow("NAME", *networkArea.Name)
table.AddSeparator()
table.AddRow("STATE", *networkArea.State)
table.AddSeparator()
if len(networkRanges) > 0 {
table.AddRow("NETWORK RANGES", strings.Join(networkRanges, ","))
}
table.AddSeparator()
for i, route := range routes {
table.AddRow(fmt.Sprintf("STATIC ROUTE %d", i+1), route)
table.AddSeparator()
}
if networkArea.Ipv4.TransferNetwork != nil {
table.AddRow("TRANSFER RANGE", *networkArea.Ipv4.TransferNetwork)
table.AddSeparator()
}
if networkArea.Ipv4.DefaultNameservers != nil {
table.AddRow("DNS NAME SERVERS", strings.Join(*networkArea.Ipv4.DefaultNameservers, ","))
table.AddSeparator()
}
if networkArea.Ipv4.DefaultPrefixLen != nil {
table.AddRow("DEFAULT PREFIX LENGTH", *networkArea.Ipv4.DefaultPrefixLen)
table.AddSeparator()
}
if networkArea.Ipv4.MaxPrefixLen != nil {
table.AddRow("MAX PREFIX LENGTH", *networkArea.Ipv4.MaxPrefixLen)
table.AddSeparator()
}
if networkArea.Ipv4.MinPrefixLen != nil {
table.AddRow("MIN PREFIX LENGTH", *networkArea.Ipv4.MinPrefixLen)
table.AddSeparator()
}
if len(attachedProjects) > 0 {
table.AddRow("ATTACHED PROJECTS IDS", strings.Join(attachedProjects, "\n"))
table.AddSeparator()
} else {
table.AddRow("# ATTACHED PROJECTS", *networkArea.ProjectCount)
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"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testOrganizationId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
organizationIdFlag: testOrganizationId,
limitFlag: "10",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
OrganizationId: &testOrganizationId,
Limit: utils.Ptr(int64(10)),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiListNetworkAreasRequest)) iaas.ApiListNetworkAreasRequest {
request := testClient.ListNetworkAreas(testCtx, testOrganizationId)
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: "organization id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, organizationIdFlag)
}),
isValid: false,
},
{
description: "organization id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = ""
}),
isValid: false,
},
{
description: "organization id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = "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.ApiListNetworkAreasRequest
}{
{
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/services/iaas/client"
rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client"
rmUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/spf13/cobra"
)
const (
limitFlag = "limit"
organizationIdFlag = "organization-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Limit *int64
OrganizationId *string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all STACKIT Network Areas (SNA) of an organization",
Long: "Lists all STACKIT Network Areas (SNA) of an organization.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Lists all network areas of organization "xxx"`,
"$ stackit beta network-area list --organization-id xxx",
),
examples.NewExample(
`Lists all network areas of organization "xxx" in JSON format`,
"$ stackit beta network-area list --organization-id xxx --output-format json",
),
examples.NewExample(
`Lists up to 10 network areas of organization "xxx"`,
"$ stackit beta network-area list --organization-id xxx --limit 10",
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("list network areas: %w", err)
}
if resp.Items == nil || len(*resp.Items) == 0 {
var orgLabel string
rmApiClient, err := rmClient.ConfigureClient(p)
if err == nil {
orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient, *model.OrganizationId)
if err != nil {
p.Debug(print.ErrorLevel, "get organization name: %v", err)
orgLabel = *model.OrganizationId
}
} else {
p.Debug(print.ErrorLevel, "configure resource manager client: %v", err)
}
p.Info("No STACKIT Network Areas found for organization %q\n", orgLabel)
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(), organizationIdFlag, "Organization ID")
err := flags.MarkFlagsRequired(cmd, organizationIdFlag)
cobra.CheckErr(err)
}
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,
OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag),
}
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.ApiListNetworkAreasRequest {
return apiClient.ListNetworkAreas(ctx, *model.OrganizationId)
}
func outputResult(p *print.Printer, outputFormat string, networkAreas []iaas.NetworkArea) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(networkAreas, "", " ")
if err != nil {
return fmt.Errorf("marshal network area: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(networkAreas, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal area: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("ID", "Name", "Status", "Network Ranges", "# Attached Projects")
for _, networkArea := range networkAreas {
table.AddRow(*networkArea.AreaId, *networkArea.Name, *networkArea.State, len(*networkArea.Ipv4.NetworkRanges), *networkArea.ProjectCount)
table.AddSeparator()
}
p.Outputln(table.Render())
return nil
}
}
package networkarea
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/list"
networkrange "github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/network-range"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/route"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/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: "network-area",
Short: "Provides functionality for STACKIT Network Area (SNA)",
Long: "Provides functionality for STACKIT Network Area (SNA).",
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))
cmd.AddCommand(networkrange.NewCmd(p))
cmd.AddCommand(route.NewCmd(p))
cmd.AddCommand(update.NewCmd(p))
}
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"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testOrgId = uuid.NewString()
var testNetworkAreaId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
organizationIdFlag: testOrgId,
networkAreaIdFlag: testNetworkAreaId,
networkRangeFlag: "1.1.1.0/24",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
OrganizationId: utils.Ptr(testOrgId),
NetworkAreaId: utils.Ptr(testNetworkAreaId),
NetworkRange: utils.Ptr("1.1.1.0/24"),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkAreaRangeRequest)) iaas.ApiCreateNetworkAreaRangeRequest {
request := testClient.CreateNetworkAreaRange(testCtx, testOrgId, testNetworkAreaId)
request = request.CreateNetworkAreaRangePayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func fixturePayload(mods ...func(payload *iaas.CreateNetworkAreaRangePayload)) iaas.CreateNetworkAreaRangePayload {
payload := iaas.CreateNetworkAreaRangePayload{
Ipv4: &[]iaas.NetworkRange{
{
Prefix: utils.Ptr("1.1.1.0/24"),
},
},
}
for _, mod := range mods {
mod(&payload)
}
return payload
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
aclValues []string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "network range missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, networkRangeFlag)
}),
isValid: false,
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "org id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, organizationIdFlag)
}),
isValid: false,
},
{
description: "org id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = ""
}),
isValid: false,
},
{
description: "org area id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "network area id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, networkAreaIdFlag)
}),
isValid: false,
},
{
description: "network area id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = ""
}),
isValid: false,
},
{
description: "network area id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest iaas.ApiCreateNetworkAreaRangeRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package create
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"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/services/iaas/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/spf13/cobra"
)
const (
organizationIdFlag = "organization-id"
networkAreaIdFlag = "network-area-id"
networkRangeFlag = "network-range"
)
type inputModel struct {
*globalflags.GlobalFlagModel
OrganizationId *string
NetworkAreaId *string
NetworkRange *string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a network range in a STACKIT Network Area (SNA)",
Long: "Creates a network range in a STACKIT Network Area (SNA).",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a network range in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`,
`$ stackit beta network-area network-range create --network-area-id xxx --organization-id yyy --network-range "1.1.1.0/24"`,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Get network area label
networkAreaLabel, err := utils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId)
if err != nil {
p.Debug(print.ErrorLevel, "get network area name: %v", err)
networkAreaLabel = *model.NetworkAreaId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to create a network range for STACKIT Network Area (SNA) %q?", networkAreaLabel)
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 network range: %w", err)
}
if resp.Items == nil || len(*resp.Items) == 0 {
return fmt.Errorf("empty response from API")
}
networkRange, err := utils.GetNetworkRangeFromAPIResponse(*model.NetworkRange, resp.Items)
if err != nil {
return err
}
return outputResult(p, model, networkAreaLabel, networkRange)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID")
cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID")
cmd.Flags().Var(flags.CIDRFlag(), networkRangeFlag, "Network range to create in CIDR notation")
err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, networkRangeFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
model := inputModel{
GlobalFlagModel: globalFlags,
OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag),
NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag),
NetworkRange: flags.FlagToStringPointer(p, cmd, networkRangeFlag),
}
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.ApiCreateNetworkAreaRangeRequest {
req := apiClient.CreateNetworkAreaRange(ctx, *model.OrganizationId, *model.NetworkAreaId)
payload := iaas.CreateNetworkAreaRangePayload{
Ipv4: &[]iaas.NetworkRange{
{
Prefix: model.NetworkRange,
},
},
}
return req.CreateNetworkAreaRangePayload(payload)
}
func outputResult(p *print.Printer, model *inputModel, networkAreaLabel string, networkRange iaas.NetworkRange) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(networkRange, "", " ")
if err != nil {
return fmt.Errorf("marshal network range: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(networkRange, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal network range: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Created network range for SNA %q.\nNetwork range ID: %s\n", networkAreaLabel, *networkRange.NetworkRangeId)
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"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testOrgId = uuid.NewString()
var testNetworkAreaId = uuid.NewString()
var testNetworkRangeId = uuid.NewString()
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testNetworkRangeId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
organizationIdFlag: testOrgId,
networkAreaIdFlag: testNetworkAreaId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
OrganizationId: utils.Ptr(testOrgId),
NetworkAreaId: utils.Ptr(testNetworkAreaId),
NetworkRangeId: testNetworkRangeId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiDeleteNetworkAreaRangeRequest)) iaas.ApiDeleteNetworkAreaRangeRequest {
request := testClient.DeleteNetworkAreaRange(testCtx, testOrgId, testNetworkAreaId, testNetworkRangeId)
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: "org id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, organizationIdFlag)
}),
isValid: false,
},
{
description: "org id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = ""
}),
isValid: false,
},
{
description: "org area id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "network area id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, networkAreaIdFlag)
}),
isValid: false,
},
{
description: "network area id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = ""
}),
isValid: false,
},
{
description: "network area id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "network range id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, networkRangeIdArg)
}),
isValid: false,
},
{
description: "network range id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkRangeIdArg] = ""
}),
isValid: false,
},
{
description: "network range id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkRangeIdArg] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error 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.ApiDeleteNetworkAreaRangeRequest
}{
{
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/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 (
networkRangeIdArg = "NETWORK_RANGE_ID"
organizationIdFlag = "organization-id"
networkAreaIdFlag = "network-area-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
OrganizationId *string
NetworkAreaId *string
NetworkRangeId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Short: "Deletes a network range in a STACKIT Network Area (SNA)",
Long: "Deletes a network range in a STACKIT Network Area (SNA).",
Args: args.SingleArg(networkRangeIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Delete network range with id "xxx" in a STACKIT Network Area with ID "yyy" in organization with ID "zzz"`,
`$ stackit beta network-area network-range delete xxx --network-area-id yyy --organization-id zzz`,
),
),
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
}
networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId)
if err != nil {
p.Debug(print.ErrorLevel, "get network area name: %v", err)
networkAreaLabel = *model.NetworkAreaId
}
networkRangeLabel, err := iaasUtils.GetNetworkRangePrefix(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId, model.NetworkRangeId)
if err != nil {
p.Debug(print.ErrorLevel, "get network range prefix: %v", err)
networkRangeLabel = model.NetworkRangeId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete network range %q on STACKIT Network Area (SNA) %q?", networkRangeLabel, networkAreaLabel)
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 network range: %w", err)
}
p.Info("Deleted network range %q on SNA %q\n", networkRangeLabel, networkAreaLabel)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID")
cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID")
err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
networkRangeId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
model := inputModel{
GlobalFlagModel: globalFlags,
OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag),
NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag),
NetworkRangeId: networkRangeId,
}
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.ApiDeleteNetworkAreaRangeRequest {
req := apiClient.DeleteNetworkAreaRange(ctx, *model.OrganizationId, *model.NetworkAreaId, model.NetworkRangeId)
return req
}
package describe
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/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"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testOrgId = uuid.NewString()
var testNetworkAreaId = uuid.NewString()
var testNetworkRangeId = uuid.NewString()
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testNetworkRangeId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
organizationIdFlag: testOrgId,
networkAreaIdFlag: testNetworkAreaId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
OrganizationId: utils.Ptr(testOrgId),
NetworkAreaId: utils.Ptr(testNetworkAreaId),
NetworkRangeId: testNetworkRangeId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiGetNetworkAreaRangeRequest)) iaas.ApiGetNetworkAreaRangeRequest {
request := testClient.GetNetworkAreaRange(testCtx, testOrgId, testNetworkAreaId, testNetworkRangeId)
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: "org id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, organizationIdFlag)
}),
isValid: false,
},
{
description: "org id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = ""
}),
isValid: false,
},
{
description: "org area id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "network area id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, networkAreaIdFlag)
}),
isValid: false,
},
{
description: "network area id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = ""
}),
isValid: false,
},
{
description: "network area id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "network range id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, networkRangeIdArg)
}),
isValid: false,
},
{
description: "network range id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkRangeIdArg] = ""
}),
isValid: false,
},
{
description: "network range id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkRangeIdArg] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error 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.ApiGetNetworkAreaRangeRequest
}{
{
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/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 (
networkRangeIdArg = "NETWORK_RANGE_ID"
organizationIdFlag = "organization-id"
networkAreaIdFlag = "network-area-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
OrganizationId *string
NetworkAreaId *string
NetworkRangeId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "describe",
Short: "Shows details of a network range in a STACKIT Network Area (SNA)",
Long: "Shows details of a network range in a STACKIT Network Area (SNA).",
Args: args.SingleArg(networkRangeIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Show details of a network range with ID "xxx" in a STACKIT Network Area with ID "yyy" in organization with ID "zzz"`,
`$ stackit beta network-area network-range describe xxx --network-area-id yyy --organization-id zzz`,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("describe network range: %w", err)
}
return outputResult(p, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID")
cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID")
err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
networkRangeId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
model := inputModel{
GlobalFlagModel: globalFlags,
OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag),
NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag),
NetworkRangeId: networkRangeId,
}
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.ApiGetNetworkAreaRangeRequest {
req := apiClient.GetNetworkAreaRange(ctx, *model.OrganizationId, *model.NetworkAreaId, model.NetworkRangeId)
return req
}
func outputResult(p *print.Printer, outputFormat string, networkRange *iaas.NetworkRange) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(networkRange, "", " ")
if err != nil {
return fmt.Errorf("marshal network range: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(networkRange, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal network range: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.AddRow("ID", *networkRange.NetworkRangeId)
table.AddSeparator()
table.AddRow("Network range", *networkRange.Prefix)
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"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testOrganizationId = uuid.NewString()
var testNetworkAreaId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
organizationIdFlag: testOrganizationId,
networkAreaIdFlag: testNetworkAreaId,
limitFlag: "10",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
OrganizationId: &testOrganizationId,
NetworkAreaId: &testNetworkAreaId,
Limit: utils.Ptr(int64(10)),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiListNetworkAreaRangesRequest)) iaas.ApiListNetworkAreaRangesRequest {
request := testClient.ListNetworkAreaRanges(testCtx, testOrganizationId, testNetworkAreaId)
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: "organization id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, organizationIdFlag)
}),
isValid: false,
},
{
description: "organization id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = ""
}),
isValid: false,
},
{
description: "organization id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "network area id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, networkAreaIdFlag)
}),
isValid: false,
},
{
description: "network area id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = ""
}),
isValid: false,
},
{
description: "network area id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = "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.ApiListNetworkAreaRangesRequest
}{
{
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/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"
organizationIdFlag = "organization-id"
networkAreaIdFlag = "network-area-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Limit *int64
OrganizationId *string
NetworkAreaId *string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all network ranges in a STACKIT Network Area (SNA)",
Long: "Lists all network ranges in a STACKIT Network Area (SNA).",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Lists all network ranges in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`,
"$ stackit beta network-area network-range list --network-area-id xxx --organization-id yyy",
),
examples.NewExample(
`Lists all network ranges in a STACKIT Network Area with ID "xxx" in organization with ID "yyy" in JSON format`,
"$ stackit beta network-area network-range list --network-area-id xxx --organization-id yyy --output-format json",
),
examples.NewExample(
`Lists up to 10 network ranges in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`,
"$ stackit beta network-area network-range list --network-area-id xxx --organization-id yyy --limit 10",
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("list network ranges: %w", err)
}
if resp.Items == nil || len(*resp.Items) == 0 {
var networkAreaLabel string
networkAreaLabel, err = iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId)
if err != nil {
p.Debug(print.ErrorLevel, "get organization name: %v", err)
networkAreaLabel = *model.NetworkAreaId
}
p.Info("No network ranges found for SNA %q\n", networkAreaLabel)
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(), organizationIdFlag, "Organization ID")
cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID")
err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag)
cobra.CheckErr(err)
}
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,
OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag),
NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag),
}
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.ApiListNetworkAreaRangesRequest {
return apiClient.ListNetworkAreaRanges(ctx, *model.OrganizationId, *model.NetworkAreaId)
}
func outputResult(p *print.Printer, outputFormat string, networkRanges []iaas.NetworkRange) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(networkRanges, "", " ")
if err != nil {
return fmt.Errorf("marshal network ranges: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(networkRanges, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal network ranges: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("ID", "Network Range")
for _, networkRange := range networkRanges {
table.AddRow(*networkRange.NetworkRangeId, *networkRange.Prefix)
}
p.Outputln(table.Render())
return nil
}
}
package networkranges
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/network-range/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/network-range/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/network-range/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/network-range/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: "network-range",
Aliases: []string{"range"},
Short: "Provides functionality for network ranges in STACKIT Network Areas",
Long: "Provides functionality for network ranges in STACKIT Network Areas.",
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 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"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testOrgId = uuid.NewString()
var testNetworkAreaId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
organizationIdFlag: testOrgId,
networkAreaIdFlag: testNetworkAreaId,
prefixFlag: "1.1.1.0/24",
nexthopFlag: "1.1.1.1",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
OrganizationId: utils.Ptr(testOrgId),
NetworkAreaId: utils.Ptr(testNetworkAreaId),
Prefix: utils.Ptr("1.1.1.0/24"),
Nexthop: utils.Ptr("1.1.1.1"),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkAreaRouteRequest)) iaas.ApiCreateNetworkAreaRouteRequest {
request := testClient.CreateNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId)
request = request.CreateNetworkAreaRoutePayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func fixturePayload(mods ...func(payload *iaas.CreateNetworkAreaRoutePayload)) iaas.CreateNetworkAreaRoutePayload {
payload := iaas.CreateNetworkAreaRoutePayload{
Ipv4: &[]iaas.Route{
{
Prefix: utils.Ptr("1.1.1.0/24"),
Nexthop: utils.Ptr("1.1.1.1"),
},
},
}
for _, mod := range mods {
mod(&payload)
}
return payload
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
aclValues []string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "next hop missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, nexthopFlag)
}),
isValid: false,
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "org id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, organizationIdFlag)
}),
isValid: false,
},
{
description: "org id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = ""
}),
isValid: false,
},
{
description: "org area id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "network area id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, networkAreaIdFlag)
}),
isValid: false,
},
{
description: "network area id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = ""
}),
isValid: false,
},
{
description: "network area id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "prefix missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, prefixFlag)
}),
isValid: false,
},
{
description: "prefix invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[prefixFlag] = ""
}),
isValid: false,
},
{
description: "prefix invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[prefixFlag] = "invalid-prefix"
}),
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.ApiCreateNetworkAreaRouteRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package create
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"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/services/iaas/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/spf13/cobra"
)
const (
organizationIdFlag = "organization-id"
networkAreaIdFlag = "network-area-id"
prefixFlag = "prefix"
nexthopFlag = "next-hop"
)
type inputModel struct {
*globalflags.GlobalFlagModel
OrganizationId *string
NetworkAreaId *string
Prefix *string
Nexthop *string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a static route in a STACKIT Network Area (SNA)",
Long: fmt.Sprintf("%s\n%s\n",
"Creates a static route in a STACKIT Network Area (SNA).",
"This command is currently asynchonous only due to limitations in the waiting functionality of the SDK. This will be updated in a future release.",
),
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a static route with prefix "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`,
"$ stackit beta network-area route create --organization-id yyy --network-area-id xxx --prefix 1.1.1.0/24 --next-hop 1.1.1.1",
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Get network area label
networkAreaLabel, err := utils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId)
if err != nil {
p.Debug(print.ErrorLevel, "get network area name: %v", err)
networkAreaLabel = *model.NetworkAreaId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to create a static route for STACKIT Network Area (SNA) %q?", networkAreaLabel)
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 static route: %w", err)
}
if resp.Items == nil || len(*resp.Items) == 0 {
return fmt.Errorf("empty response from API")
}
route, err := utils.GetRouteFromAPIResponse(*model.Prefix, *model.Nexthop, resp.Items)
if err != nil {
return err
}
return outputResult(p, model, networkAreaLabel, route)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID")
cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area ID")
cmd.Flags().Var(flags.CIDRFlag(), prefixFlag, "Static route prefix")
cmd.Flags().String(nexthopFlag, "", "Next hop IP address. Must be a valid IPv4")
err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, prefixFlag, nexthopFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
model := inputModel{
GlobalFlagModel: globalFlags,
OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag),
NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag),
Prefix: flags.FlagToStringPointer(p, cmd, prefixFlag),
Nexthop: flags.FlagToStringPointer(p, cmd, nexthopFlag),
}
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.ApiCreateNetworkAreaRouteRequest {
req := apiClient.CreateNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId)
payload := iaas.CreateNetworkAreaRoutePayload{
Ipv4: &[]iaas.Route{
{
Prefix: model.Prefix,
Nexthop: model.Nexthop,
},
},
}
return req.CreateNetworkAreaRoutePayload(payload)
}
func outputResult(p *print.Printer, model *inputModel, networkAreaLabel string, route iaas.Route) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(route, "", " ")
if err != nil {
return fmt.Errorf("marshal static route: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(route, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal static route: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Created static route for SNA %q.\nStatic route ID: %s\n", networkAreaLabel, *route.RouteId)
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"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testOrgId = uuid.NewString()
var testNetworkAreaId = uuid.NewString()
var testRouteId = uuid.NewString()
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testRouteId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
organizationIdFlag: testOrgId,
networkAreaIdFlag: testNetworkAreaId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
OrganizationId: utils.Ptr(testOrgId),
NetworkAreaId: utils.Ptr(testNetworkAreaId),
RouteId: testRouteId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiDeleteNetworkAreaRouteRequest)) iaas.ApiDeleteNetworkAreaRouteRequest {
request := testClient.DeleteNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRouteId)
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: "org id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, organizationIdFlag)
}),
isValid: false,
},
{
description: "org id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = ""
}),
isValid: false,
},
{
description: "org area id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "network area id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, networkAreaIdFlag)
}),
isValid: false,
},
{
description: "network area id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = ""
}),
isValid: false,
},
{
description: "network area id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "route id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, routeIdArg)
}),
isValid: false,
},
{
description: "route id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[routeIdArg] = ""
}),
isValid: false,
},
{
description: "route id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[routeIdArg] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error 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.ApiDeleteNetworkAreaRouteRequest
}{
{
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/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 (
routeIdArg = "ROUTE_ID"
organizationIdFlag = "organization-id"
networkAreaIdFlag = "network-area-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
OrganizationId *string
NetworkAreaId *string
RouteId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Short: "Deletes a static route in a STACKIT Network Area (SNA)",
Long: "Deletes a static route in a STACKIT Network Area (SNA).",
Args: args.SingleArg(routeIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Delete a static route with ID "xxx" in a STACKIT Network Area with ID "yyy" in organization with ID "zzz"`,
"$ stackit beta network-area route delete xxx --organization-id zzz --network-area-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
}
networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId)
if err != nil {
p.Debug(print.ErrorLevel, "get network area name: %v", err)
networkAreaLabel = *model.NetworkAreaId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete static route %q on STACKIT Network Area (SNA) %q?", model.RouteId, networkAreaLabel)
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 static route: %w", err)
}
p.Info("Deleted static route %q on SNA %q\n", model.RouteId, networkAreaLabel)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID")
cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area ID")
err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
routeId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
model := inputModel{
GlobalFlagModel: globalFlags,
OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag),
NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag),
RouteId: routeId,
}
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.ApiDeleteNetworkAreaRouteRequest {
req := apiClient.DeleteNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.RouteId)
return req
}
package describe
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/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"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testOrgId = uuid.NewString()
var testNetworkAreaId = uuid.NewString()
var testRouteId = uuid.NewString()
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testRouteId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
organizationIdFlag: testOrgId,
networkAreaIdFlag: testNetworkAreaId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
OrganizationId: utils.Ptr(testOrgId),
NetworkAreaId: utils.Ptr(testNetworkAreaId),
RouteId: testRouteId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiGetNetworkAreaRouteRequest)) iaas.ApiGetNetworkAreaRouteRequest {
request := testClient.GetNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRouteId)
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: "org id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, organizationIdFlag)
}),
isValid: false,
},
{
description: "org id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = ""
}),
isValid: false,
},
{
description: "org id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "network area id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, networkAreaIdFlag)
}),
isValid: false,
},
{
description: "network area id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = ""
}),
isValid: false,
},
{
description: "network area id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "network route id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, routeIdArg)
}),
isValid: false,
},
{
description: "network route id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[routeIdArg] = ""
}),
isValid: false,
},
{
description: "network route id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[routeIdArg] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error 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.ApiGetNetworkAreaRouteRequest
}{
{
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/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 (
routeIdArg = "ROUTE_ID"
organizationIdFlag = "organization-id"
networkAreaIdFlag = "network-area-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
OrganizationId *string
NetworkAreaId *string
RouteId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "describe",
Short: "Shows details of a static route in a STACKIT Network Area (SNA)",
Long: "Shows details of a static route in a STACKIT Network Area (SNA).",
Args: args.SingleArg(routeIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Show details of a static route with ID "xxx" in a STACKIT Network Area with ID "yyy" in organization with ID "zzz"`,
`$ stackit beta network-area route describe xxx --network-area-id yyy --organization-id zzz`,
),
examples.NewExample(
`Show details of a static route with ID "xxx" in a STACKIT Network Area with ID "yyy" in organization with ID "zzz" in JSON format`,
`$ stackit beta network-area route describe xxx --network-area-id yyy --organization-id zzz --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("describe static route: %w", err)
}
return outputResult(p, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID")
cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area ID")
err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
routeId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
model := inputModel{
GlobalFlagModel: globalFlags,
OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag),
NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag),
RouteId: routeId,
}
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.ApiGetNetworkAreaRouteRequest {
req := apiClient.GetNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.RouteId)
return req
}
func outputResult(p *print.Printer, outputFormat string, route *iaas.Route) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(route, "", " ")
if err != nil {
return fmt.Errorf("marshal static route: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(route, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal static route: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.AddRow("ID", *route.RouteId)
table.AddSeparator()
table.AddRow("Prefix", *route.Prefix)
table.AddSeparator()
table.AddRow("Nexthop", *route.Nexthop)
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"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testOrganizationId = uuid.NewString()
var testNetworkAreaId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
organizationIdFlag: testOrganizationId,
networkAreaIdFlag: testNetworkAreaId,
limitFlag: "10",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
OrganizationId: &testOrganizationId,
NetworkAreaId: &testNetworkAreaId,
Limit: utils.Ptr(int64(10)),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiListNetworkAreaRoutesRequest)) iaas.ApiListNetworkAreaRoutesRequest {
request := testClient.ListNetworkAreaRoutes(testCtx, testOrganizationId, testNetworkAreaId)
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: "organization id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, organizationIdFlag)
}),
isValid: false,
},
{
description: "organization id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = ""
}),
isValid: false,
},
{
description: "organization id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "network area id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, networkAreaIdFlag)
}),
isValid: false,
},
{
description: "network area id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = ""
}),
isValid: false,
},
{
description: "network area id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = "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.ApiListNetworkAreaRoutesRequest
}{
{
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/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"
organizationIdFlag = "organization-id"
networkAreaIdFlag = "network-area-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Limit *int64
OrganizationId *string
NetworkAreaId *string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all static routes in a STACKIT Network Area (SNA)",
Long: "Lists all static routes in a STACKIT Network Area (SNA).",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Lists all static routes in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`,
"$ stackit beta network-area route list --network-area-id xxx --organization-id yyy",
),
examples.NewExample(
`Lists all static routes in a STACKIT Network Area with ID "xxx" in organization with ID "yyy" in JSON format`,
"$ stackit beta network-area route list --network-area-id xxx --organization-id yyy --output-format json",
),
examples.NewExample(
`Lists up to 10 static routes in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`,
"$ stackit beta network-area route list --network-area-id xxx --organization-id yyy --limit 10",
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("list static routes: %w", err)
}
if resp.Items == nil || len(*resp.Items) == 0 {
var networkAreaLabel string
networkAreaLabel, err = iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId)
if err != nil {
p.Debug(print.ErrorLevel, "get network area name: %v", err)
networkAreaLabel = *model.NetworkAreaId
}
p.Info("No static routes found for STACKIT Network Area %q\n", networkAreaLabel)
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(), organizationIdFlag, "Organization ID")
cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area ID")
err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag)
cobra.CheckErr(err)
}
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,
OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag),
NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag),
}
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.ApiListNetworkAreaRoutesRequest {
return apiClient.ListNetworkAreaRoutes(ctx, *model.OrganizationId, *model.NetworkAreaId)
}
func outputResult(p *print.Printer, outputFormat string, routes []iaas.Route) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(routes, "", " ")
if err != nil {
return fmt.Errorf("marshal static routes: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(routes, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal static routes: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("Static Route ID", "Next Hop", "Prefix")
for _, route := range routes {
table.AddRow(*route.RouteId, *route.Nexthop, *route.Prefix)
}
p.Outputln(table.Render())
return nil
}
}
package route
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/route/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/route/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/route/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area/route/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: "route",
Short: "Provides functionality for static routes in STACKIT Network Areas",
Long: "Provides functionality for static routes in STACKIT Network Areas.",
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 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"
)
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testOrgId = uuid.NewString()
var testAreaId = uuid.NewString()
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testAreaId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
nameFlag: "example-network-area-name",
organizationIdFlag: testOrgId,
dnsNameServersFlag: "1.1.1.0,1.1.2.0",
defaultPrefixLengthFlag: "24",
maxPrefixLengthFlag: "24",
minPrefixLengthFlag: "24",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
Name: utils.Ptr("example-network-area-name"),
OrganizationId: utils.Ptr(testOrgId),
AreaId: testAreaId,
DnsNameServers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}),
DefaultPrefixLength: utils.Ptr(int64(24)),
MaxPrefixLength: utils.Ptr(int64(24)),
MinPrefixLength: utils.Ptr(int64(24)),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiPartialUpdateNetworkAreaRequest)) iaas.ApiPartialUpdateNetworkAreaRequest {
request := testClient.PartialUpdateNetworkArea(testCtx, testOrgId, testAreaId)
request = request.PartialUpdateNetworkAreaPayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func fixturePayload(mods ...func(payload *iaas.PartialUpdateNetworkAreaPayload)) iaas.PartialUpdateNetworkAreaPayload {
payload := iaas.PartialUpdateNetworkAreaPayload{
Name: utils.Ptr("example-network-area-name"),
AddressFamily: &iaas.UpdateAreaAddressFamily{
Ipv4: &iaas.UpdateAreaIPv4{
DefaultNameservers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}),
DefaultPrefixLen: utils.Ptr(int64(24)),
MaxPrefixLen: utils.Ptr(int64(24)),
MinPrefixLen: utils.Ptr(int64(24)),
},
},
}
for _, mod := range mods {
mod(&payload)
}
return payload
}
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: "required only",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, dnsNameServersFlag)
delete(flagValues, defaultPrefixLengthFlag)
delete(flagValues, maxPrefixLengthFlag)
delete(flagValues, minPrefixLengthFlag)
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.DnsNameServers = nil
model.DefaultPrefixLength = nil
model.MaxPrefixLength = nil
model.MinPrefixLength = nil
}),
},
{
description: "no values",
argValues: []string{},
flagValues: map[string]string{},
isValid: false,
},
{
description: "org id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, organizationIdFlag)
}),
isValid: false,
},
{
description: "org id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = ""
}),
isValid: false,
},
{
description: "org id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[organizationIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "area id missing",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "area id invalid 1",
argValues: fixtureArgValues(func(argValues []string) {
argValues[0] = ""
}),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[areaIdFlag] = ""
}),
isValid: false,
},
{
description: "area id invalid 2",
argValues: fixtureArgValues(func(argValues []string) {
argValues[0] = "invalid-uuid"
}),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[areaIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
err = cmd.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.ApiPartialUpdateNetworkAreaRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package update
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"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"
rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client"
rmUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/spf13/cobra"
)
const (
areaIdArg = "AREA_ID"
nameFlag = "name"
organizationIdFlag = "organization-id"
areaIdFlag = "area-id"
dnsNameServersFlag = "dns-name-servers"
defaultPrefixLengthFlag = "default-prefix-length"
maxPrefixLengthFlag = "max-prefix-length"
minPrefixLengthFlag = "min-prefix-length"
)
type inputModel struct {
*globalflags.GlobalFlagModel
AreaId string
Name *string
OrganizationId *string
DnsNameServers *[]string
DefaultPrefixLength *int64
MaxPrefixLength *int64
MinPrefixLength *int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "update",
Short: "Updates a STACKIT Network Area (SNA)",
Long: "Updates a STACKIT Network Area (SNA) in an organization.",
Args: args.SingleArg(areaIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Update network area with ID "xxx" in organization with ID "yyy" with new name "network-area-1-new"`,
"$ stackit beta network-area update xxx --organization-id yyy --name network-area-1-new",
),
),
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
}
var orgLabel string
rmApiClient, err := rmClient.ConfigureClient(p)
if err == nil {
orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient, *model.OrganizationId)
if err != nil {
p.Debug(print.ErrorLevel, "get organization name: %v", err)
orgLabel = *model.OrganizationId
}
} else {
p.Debug(print.ErrorLevel, "configure resource manager client: %v", err)
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to update a network area for organization %q?", orgLabel)
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 network area: %w", err)
}
return outputResult(p, model, orgLabel, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(nameFlag, "n", "", "Network area name")
cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID")
cmd.Flags().StringSlice(dnsNameServersFlag, nil, "List of DNS name server IPs")
cmd.Flags().Int64(defaultPrefixLengthFlag, 0, "The default prefix length for networks in the network area")
cmd.Flags().Int64(maxPrefixLengthFlag, 0, "The maximum prefix length for networks in the network area")
cmd.Flags().Int64(minPrefixLengthFlag, 0, "The minimum prefix length for networks in the network area")
err := flags.MarkFlagsRequired(cmd, organizationIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
areaId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
model := inputModel{
GlobalFlagModel: globalFlags,
Name: flags.FlagToStringPointer(p, cmd, nameFlag),
OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag),
AreaId: areaId,
DnsNameServers: flags.FlagToStringSlicePointer(p, cmd, dnsNameServersFlag),
DefaultPrefixLength: flags.FlagToInt64Pointer(p, cmd, defaultPrefixLengthFlag),
MaxPrefixLength: flags.FlagToInt64Pointer(p, cmd, maxPrefixLengthFlag),
MinPrefixLength: flags.FlagToInt64Pointer(p, cmd, minPrefixLengthFlag),
}
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.ApiPartialUpdateNetworkAreaRequest {
req := apiClient.PartialUpdateNetworkArea(ctx, *model.OrganizationId, model.AreaId)
payload := iaas.PartialUpdateNetworkAreaPayload{
Name: model.Name,
AddressFamily: &iaas.UpdateAreaAddressFamily{
Ipv4: &iaas.UpdateAreaIPv4{
DefaultNameservers: model.DnsNameServers,
DefaultPrefixLen: model.DefaultPrefixLength,
MaxPrefixLen: model.MaxPrefixLength,
MinPrefixLen: model.MinPrefixLength,
},
},
}
return req.PartialUpdateNetworkAreaPayload(payload)
}
func outputResult(p *print.Printer, model *inputModel, projectLabel string, networkArea *iaas.NetworkArea) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(networkArea, "", " ")
if err != nil {
return fmt.Errorf("marshal network area: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(networkArea, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal network area: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Updated STACKIT Network Area for project %q.\n", projectLabel)
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()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
nameFlag: "example-network-name",
ipv4DnsNameServersFlag: "1.1.1.0,1.1.2.0",
ipv4PrefixLengthFlag: "24",
ipv6DnsNameServersFlag: "2001:4860:4860::8888,2001:4860:4860::8844",
ipv6PrefixLengthFlag: "24",
}
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,
},
Name: utils.Ptr("example-network-name"),
IPv4DnsNameServers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}),
IPv4PrefixLength: utils.Ptr(int64(24)),
IPv6DnsNameServers: utils.Ptr([]string{"2001:4860:4860::8888", "2001:4860:4860::8844"}),
IPv6PrefixLength: utils.Ptr(int64(24)),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkRequest)) iaas.ApiCreateNetworkRequest {
request := testClient.CreateNetwork(testCtx, testProjectId)
request = request.CreateNetworkPayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func fixturePayload(mods ...func(payload *iaas.CreateNetworkPayload)) iaas.CreateNetworkPayload {
payload := iaas.CreateNetworkPayload{
Name: utils.Ptr("example-network-name"),
AddressFamily: &iaas.CreateNetworkAddressFamily{
Ipv4: &iaas.CreateNetworkIPv4{
Nameservers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}),
PrefixLength: utils.Ptr(int64(24)),
},
Ipv6: &iaas.V1CreateNetworkIPv6{
Nameservers: utils.Ptr([]string{"2001:4860:4860::8888", "2001:4860:4860::8844"}),
PrefixLength: utils.Ptr(int64(24)),
},
},
}
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, ipv4DnsNameServersFlag)
delete(flagValues, ipv4PrefixLengthFlag)
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.IPv4DnsNameServers = nil
model.IPv4PrefixLength = nil
}),
},
{
description: "name missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, nameFlag)
}),
isValid: false,
},
{
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: "use dns servers and prefix",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[ipv4DnsNameServersFlag] = "1.1.1.1"
flagValues[ipv4PrefixLengthFlag] = "25"
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.IPv4DnsNameServers = utils.Ptr([]string{"1.1.1.1"})
model.IPv4PrefixLength = utils.Ptr(int64(25))
}),
},
{
description: "use ipv6 dns servers and prefix",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[ipv6DnsNameServersFlag] = "2001:4860:4860::8888"
flagValues[ipv6PrefixLengthFlag] = "25"
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.IPv6DnsNameServers = utils.Ptr([]string{"2001:4860:4860::8888"})
model.IPv6PrefixLength = utils.Ptr(int64(25))
}),
},
}
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.ApiCreateNetworkRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package create
import (
"context"
"encoding/json"
"fmt"
"github.com/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"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/stackit-sdk-go/services/iaas/wait"
"github.com/spf13/cobra"
)
const (
nameFlag = "name"
ipv4DnsNameServersFlag = "ipv4-dns-name-servers"
ipv4PrefixLengthFlag = "ipv4-prefix-length"
ipv6DnsNameServersFlag = "ipv6-dns-name-servers"
ipv6PrefixLengthFlag = "ipv6-prefix-length"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Name *string
IPv4DnsNameServers *[]string
IPv4PrefixLength *int64
IPv6DnsNameServers *[]string
IPv6PrefixLength *int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a network",
Long: "Creates a network.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a network with name "network-1"`,
`$ stackit beta network create --name network-1`,
),
examples.NewExample(
`Create an IPv4 network with name "network-1" with DNS name servers and a prefix length`,
`$ stackit beta network create --name network-1 --ipv4-dns-name-servers "1.1.1.1,8.8.8.8,9.9.9.9" --ipv4-prefix-length 25`,
),
examples.NewExample(
`Create an IPv6 network with name "network-1" with DNS name servers and a prefix length`,
`$ stackit beta network create --name network-1 --ipv6-dns-name-servers "2001:4860:4860::8888,2001:4860:4860::8844" --ipv6-prefix-length 56`,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
if err != nil {
p.Debug(print.ErrorLevel, "get project name: %v", err)
projectLabel = model.ProjectId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to create a network for project %q?", projectLabel)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("create network : %w", err)
}
networkId := *resp.NetworkId
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(p)
s.Start("Creating network")
_, err = wait.CreateNetworkWaitHandler(ctx, apiClient, model.ProjectId, networkId).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for network creation: %w", err)
}
s.Stop()
}
return outputResult(p, model, projectLabel, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(nameFlag, "n", "", "Network name")
cmd.Flags().StringSlice(ipv4DnsNameServersFlag, []string{}, "List of DNS name servers for IPv4")
cmd.Flags().Int64(ipv4PrefixLengthFlag, 0, "The prefix length of the IPv4 network")
cmd.Flags().StringSlice(ipv6DnsNameServersFlag, []string{}, "List of DNS name servers for IPv6")
cmd.Flags().Int64(ipv6PrefixLengthFlag, 0, "The prefix length of the IPv6 network")
err := flags.MarkFlagsRequired(cmd, nameFlag)
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,
Name: flags.FlagToStringPointer(p, cmd, nameFlag),
IPv4DnsNameServers: flags.FlagToStringSlicePointer(p, cmd, ipv4DnsNameServersFlag),
IPv4PrefixLength: flags.FlagToInt64Pointer(p, cmd, ipv4PrefixLengthFlag),
IPv6DnsNameServers: flags.FlagToStringSlicePointer(p, cmd, ipv6DnsNameServersFlag),
IPv6PrefixLength: flags.FlagToInt64Pointer(p, cmd, ipv6PrefixLengthFlag),
}
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.ApiCreateNetworkRequest {
req := apiClient.CreateNetwork(ctx, model.ProjectId)
addressFamily := &iaas.CreateNetworkAddressFamily{}
if model.IPv6DnsNameServers != nil {
addressFamily.Ipv6 = &iaas.V1CreateNetworkIPv6{
Nameservers: model.IPv6DnsNameServers,
PrefixLength: model.IPv6PrefixLength,
}
}
if model.IPv4DnsNameServers != nil {
addressFamily.Ipv4 = &iaas.CreateNetworkIPv4{
Nameservers: model.IPv4DnsNameServers,
PrefixLength: model.IPv4PrefixLength,
}
}
payload := iaas.CreateNetworkPayload{
Name: model.Name,
AddressFamily: addressFamily,
}
return req.CreateNetworkPayload(payload)
}
func outputResult(p *print.Printer, model *inputModel, projectLabel string, network *iaas.Network) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(network, "", " ")
if err != nil {
return fmt.Errorf("marshal network: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(network, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal network: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Created network for project %q.\nNetwork ID: %s\n", projectLabel, *network.NetworkId)
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")
var testClient = &iaas.APIClient{}
var testNetworkId = uuid.NewString()
var testProjectId = uuid.NewString()
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testNetworkId,
}
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,
},
NetworkId: testNetworkId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiDeleteNetworkRequest)) iaas.ApiDeleteNetworkRequest {
request := testClient.DeleteNetwork(testCtx, testProjectId, testNetworkId)
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: "network id invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "network 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.ApiDeleteNetworkRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package delete
import (
"context"
"fmt"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/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 (
networkIdArg = "NETWORK_ID"
)
type inputModel struct {
*globalflags.GlobalFlagModel
NetworkId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Short: "Deletes a network",
Long: fmt.Sprintf("%s\n%s\n",
"Deletes a network.",
"If the network is still in use, the deletion will fail",
),
Args: args.SingleArg(networkIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Delete network with ID "xxx"`,
"$ stackit beta network 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
}
networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, model.NetworkId)
if err != nil {
p.Debug(print.ErrorLevel, "get network name: %v", err)
networkLabel = model.NetworkId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete network %q?", networkLabel)
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 network: %w", err)
}
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(p)
s.Start("Deleting network")
_, err = wait.DeleteNetworkWaitHandler(ctx, apiClient, model.ProjectId, model.NetworkId).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for network deletion: %w", err)
}
s.Stop()
}
operationState := "Deleted"
if model.Async {
operationState = "Triggered deletion of"
}
p.Info("%s network %q\n", operationState, networkLabel)
return nil
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
networkId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
NetworkId: networkId,
}
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.ApiDeleteNetworkRequest {
return apiClient.DeleteNetwork(ctx, model.ProjectId, model.NetworkId)
}
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")
var testClient = &iaas.APIClient{}
var testProjectId = uuid.NewString()
var testNetworkId = uuid.NewString()
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testNetworkId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
NetworkId: testNetworkId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiGetNetworkRequest)) iaas.ApiGetNetworkRequest {
request := testClient.GetNetwork(testCtx, testProjectId, testNetworkId)
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: "network id invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "network 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.ApiGetNetworkRequest
}{
{
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/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"
"github.com/spf13/cobra"
)
const (
networkIdArg = "NETWORK_ID"
)
type inputModel struct {
*globalflags.GlobalFlagModel
NetworkId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "describe",
Short: "Shows details of a network",
Long: "Shows details of a network.",
Args: args.SingleArg(networkIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Show details of a network with ID "xxx"`,
"$ stackit beta network describe xxx",
),
examples.NewExample(
`Show details of a network with ID "xxx" in JSON format`,
"$ stackit beta network describe 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
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("read network: %w", err)
}
return outputResult(p, model.OutputFormat, resp)
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
networkId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
NetworkId: networkId,
}
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.ApiGetNetworkRequest {
return apiClient.GetNetwork(ctx, model.ProjectId, model.NetworkId)
}
func outputResult(p *print.Printer, outputFormat string, network *iaas.Network) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(network, "", " ")
if err != nil {
return fmt.Errorf("marshal network: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(network, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal network: %w", err)
}
p.Outputln(string(details))
return nil
default:
var ipv4nameservers []string
if network.Nameservers != nil {
ipv4nameservers = append(ipv4nameservers, *network.Nameservers...)
}
var ipv4prefixes []string
if network.Prefixes != nil {
ipv4prefixes = append(ipv4prefixes, *network.Prefixes...)
}
var ipv6nameservers []string
if network.NameserversV6 != nil {
ipv6nameservers = append(ipv6nameservers, *network.NameserversV6...)
}
var ipv6prefixes []string
if network.PrefixesV6 != nil {
ipv6prefixes = append(ipv6prefixes, *network.PrefixesV6...)
}
table := tables.NewTable()
table.AddRow("ID", *network.NetworkId)
table.AddSeparator()
table.AddRow("NAME", *network.Name)
table.AddSeparator()
table.AddRow("STATE", *network.State)
table.AddSeparator()
table.AddRow("PUBLIC IP", *network.PublicIp)
table.AddSeparator()
if len(ipv4nameservers) > 0 {
table.AddRow("IPv4 NAME SERVERS", strings.Join(ipv4nameservers, ", "))
}
table.AddSeparator()
if len(ipv4prefixes) > 0 {
table.AddRow("IPv4 PREFIXES", strings.Join(ipv4prefixes, ", "))
}
table.AddSeparator()
if len(ipv6nameservers) > 0 {
table.AddRow("IPv6 NAME SERVERS", strings.Join(ipv6nameservers, ", "))
}
table.AddSeparator()
if len(ipv6prefixes) > 0 {
table.AddRow("IPv6 PREFIXES", strings.Join(ipv6prefixes, ", "))
}
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()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
limitFlag: "10",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
ProjectId: testProjectId,
},
Limit: utils.Ptr(int64(10)),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiListNetworksRequest)) iaas.ApiListNetworksRequest {
request := testClient.ListNetworks(testCtx, testProjectId)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "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: "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.ApiListNetworksRequest
}{
{
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"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/spf13/cobra"
)
const (
limitFlag = "limit"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Limit *int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all networks of a project",
Long: "Lists all network of a project.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Lists all networks`,
"$ stackit beta network list",
),
examples.NewExample(
`Lists all networks in JSON format`,
"$ stackit beta network list --output-format json",
),
examples.NewExample(
`Lists up to 10 networks`,
"$ stackit beta network list --limit 10",
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("list networks: %w", err)
}
if resp.Items == nil || len(*resp.Items) == 0 {
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
if err != nil {
p.Debug(print.ErrorLevel, "get project name: %v", err)
projectLabel = model.ProjectId
}
p.Info("No networks found for project %q\n", 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")
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
limit := flags.FlagToInt64Pointer(p, cmd, limitFlag)
if limit != nil && *limit < 1 {
return nil, &errors.FlagValidationError{
Flag: limitFlag,
Details: "must be greater than 0",
}
}
model := inputModel{
GlobalFlagModel: globalFlags,
Limit: limit,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNetworksRequest {
return apiClient.ListNetworks(ctx, model.ProjectId)
}
func outputResult(p *print.Printer, outputFormat string, networks []iaas.Network) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(networks, "", " ")
if err != nil {
return fmt.Errorf("marshal network: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(networks, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal network: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("ID", "Name", "Status", "Public IP")
for _, network := range networks {
table.AddRow(*network.NetworkId, *network.Name, *network.State, *network.PublicIp)
table.AddSeparator()
}
p.Outputln(table.Render())
return nil
}
}
package network
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network/list"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network/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: "network",
Short: "Provides functionality for Network",
Long: "Provides functionality for Network.",
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))
cmd.AddCommand(update.NewCmd(p))
}
package update
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/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 testNetworkId = uuid.NewString()
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testNetworkId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
nameFlag: "example-network-name",
projectIdFlag: testProjectId,
ipv4DnsNameServersFlag: "1.1.1.0,1.1.2.0",
ipv6DnsNameServersFlag: "2001:4860:4860::8888,2001:4860:4860::8844",
}
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,
},
Name: utils.Ptr("example-network-name"),
NetworkId: testNetworkId,
IPv4DnsNameServers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}),
IPv6DnsNameServers: utils.Ptr([]string{"2001:4860:4860::8888", "2001:4860:4860::8844"}),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiPartialUpdateNetworkRequest)) iaas.ApiPartialUpdateNetworkRequest {
request := testClient.PartialUpdateNetwork(testCtx, testProjectId, testNetworkId)
request = request.PartialUpdateNetworkPayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func fixturePayload(mods ...func(payload *iaas.PartialUpdateNetworkPayload)) iaas.PartialUpdateNetworkPayload {
payload := iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("example-network-name"),
AddressFamily: &iaas.UpdateNetworkAddressFamily{
Ipv4: &iaas.UpdateNetworkIPv4{
Nameservers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}),
},
Ipv6: &iaas.V1UpdateNetworkIPv6{
Nameservers: utils.Ptr([]string{"2001:4860:4860::8888", "2001:4860:4860::8844"}),
},
},
}
for _, mod := range mods {
mod(&payload)
}
return payload
}
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: "required only",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, ipv4DnsNameServersFlag)
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.IPv4DnsNameServers = nil
}),
},
{
description: "no values",
argValues: []string{},
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: "network id invalid 1",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "network id invalid 2",
argValues: []string{"invalid-uuid"},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "use dns servers and prefix",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[ipv4DnsNameServersFlag] = "1.1.1.1"
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.IPv4DnsNameServers = utils.Ptr([]string{"1.1.1.1"})
}),
},
{
description: "use ipv6 dns servers and prefix",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[ipv6DnsNameServersFlag] = "2001:4860:4860::8888"
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.IPv6DnsNameServers = utils.Ptr([]string{"2001:4860:4860::8888"})
}),
},
}
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.ApiPartialUpdateNetworkRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package update
import (
"context"
"fmt"
"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/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 (
networkIdArg = "NETWORK_ID"
nameFlag = "name"
ipv4DnsNameServersFlag = "ipv4-dns-name-servers"
ipv6DnsNameServersFlag = "ipv6-dns-name-servers"
)
type inputModel struct {
*globalflags.GlobalFlagModel
NetworkId string
Name *string
IPv4DnsNameServers *[]string
IPv6DnsNameServers *[]string
IPv6PrefixLength *int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "update",
Short: "Updates a network",
Long: "Updates a network.",
Args: args.SingleArg(networkIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Update network with ID "xxx" with new name "network-1-new"`,
`$ stackit beta network update xxx --name network-1-new`,
),
examples.NewExample(
`Update IPv4 network with ID "xxx" with new name "network-1-new" and new DNS name servers`,
`$ stackit beta network update xxx --name network-1-new --ipv4-dns-name-servers "2.2.2.2"`,
),
examples.NewExample(
`Update IPv6 network with ID "xxx" with new name "network-1-new" and new DNS name servers`,
`$ stackit beta network update xxx --name network-1-new --ipv6-dns-name-servers "2001:4860:4860::8888"`,
),
),
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
}
networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, model.NetworkId)
if err != nil {
p.Debug(print.ErrorLevel, "get network name: %v", err)
networkLabel = model.NetworkId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to update network %q?", networkLabel)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
err = req.Execute()
if err != nil {
return fmt.Errorf("update network area: %w", err)
}
networkId := model.NetworkId
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(p)
s.Start("Updating network")
_, err = wait.UpdateNetworkWaitHandler(ctx, apiClient, model.ProjectId, networkId).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for network update: %w", err)
}
s.Stop()
}
operationState := "Updated"
if model.Async {
operationState = "Triggered update of"
}
p.Info("%s network %q\n", operationState, networkLabel)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(nameFlag, "n", "", "Network name")
cmd.Flags().StringSlice(ipv4DnsNameServersFlag, nil, "List of DNS name servers IPv4")
cmd.Flags().StringSlice(ipv6DnsNameServersFlag, nil, "List of DNS name servers for IPv6")
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
networkId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &cliErr.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
Name: flags.FlagToStringPointer(p, cmd, nameFlag),
NetworkId: networkId,
IPv4DnsNameServers: flags.FlagToStringSlicePointer(p, cmd, ipv4DnsNameServersFlag),
IPv6DnsNameServers: flags.FlagToStringSlicePointer(p, cmd, ipv6DnsNameServersFlag),
}
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.ApiPartialUpdateNetworkRequest {
req := apiClient.PartialUpdateNetwork(ctx, model.ProjectId, model.NetworkId)
addressFamily := &iaas.UpdateNetworkAddressFamily{}
if model.IPv6DnsNameServers != nil {
addressFamily.Ipv6 = &iaas.V1UpdateNetworkIPv6{
Nameservers: model.IPv6DnsNameServers,
}
}
if model.IPv4DnsNameServers != nil {
addressFamily.Ipv4 = &iaas.UpdateNetworkIPv4{
Nameservers: model.IPv4DnsNameServers,
}
}
payload := iaas.PartialUpdateNetworkPayload{
Name: model.Name,
AddressFamily: addressFamily,
}
return req.PartialUpdateNetworkPayload(payload)
}
package command
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/command/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/command/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/command/list"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/command/template"
"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: "command",
Short: "Provides functionality for Server Command",
Long: "Provides functionality for Server Command.",
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(describe.NewCmd(p))
cmd.AddCommand(list.NewCmd(p))
cmd.AddCommand(template.NewCmd(p))
}
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/runcommand"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &runcommand.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
serverIdFlag: testServerId,
commandTemplateNameFlag: "RunShellScript",
paramsFlag: `script='echo hello'`,
}
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,
},
ServerId: testServerId,
CommandTemplateName: "RunShellScript",
Params: &map[string]string{"script": "'echo hello'"},
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *runcommand.ApiCreateCommandRequest)) runcommand.ApiCreateCommandRequest {
request := testClient.CreateCommand(testCtx, testProjectId, testServerId)
request = request.CreateCommandPayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func fixturePayload(mods ...func(payload *runcommand.CreateCommandPayload)) runcommand.CreateCommandPayload {
payload := runcommand.CreateCommandPayload{
CommandTemplateName: utils.Ptr("RunShellScript"),
Parameters: &map[string]string{"script": "'echo hello'"},
}
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: "with defaults",
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,
},
}
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 runcommand.ApiCreateCommandRequest
isValid bool
}{
{
description: "base",
model: fixtureInputModel(),
isValid: true,
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request, err := buildRequest(testCtx, tt.model, testClient)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error building request: %v", err)
}
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package 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/services/runcommand/client"
runcommandUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/runcommand/utils"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/runcommand"
)
const (
serverIdFlag = "server-id"
commandTemplateNameFlag = "template-name"
paramsFlag = "params"
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
CommandTemplateName string
Params *map[string]string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a Server Command",
Long: "Creates a Server Command.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a server command for server with ID "xxx", template name "RunShellScript" and a script from a file (using the @{...} format)`,
`$ stackit beta server command create --server-id xxx --template-name=RunShellScript --params script='@{/path/to/script.sh}'`),
examples.NewExample(
`Create a server command for server with ID "xxx", template name "RunShellScript" and a script provided on the command line`,
`$ stackit beta server command create --server-id xxx --template-name=RunShellScript --params script='echo hello'`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to create a Command for server %s?", model.ServerId)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req, err := buildRequest(ctx, model, apiClient)
if err != nil {
return err
}
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("create Server Command: %w", err)
}
return outputResult(p, model, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID")
cmd.Flags().StringP(commandTemplateNameFlag, "n", "", "Template name")
cmd.Flags().StringToStringP(paramsFlag, "r", nil, "Params can be provided with the format key=value and the flag can be used multiple times to provide a list of labels")
err := flags.MarkFlagsRequired(cmd, serverIdFlag, commandTemplateNameFlag)
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,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
CommandTemplateName: flags.FlagToStringValue(p, cmd, commandTemplateNameFlag),
Params: flags.FlagToStringToStringPointer(p, cmd, paramsFlag),
}
parsedParams, err := runcommandUtils.ParseScriptParams(*model.Params)
if err != nil {
return nil, &cliErr.FlagValidationError{
Flag: paramsFlag,
Details: err.Error(),
}
}
model.Params = &parsedParams
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 *runcommand.APIClient) (runcommand.ApiCreateCommandRequest, error) {
req := apiClient.CreateCommand(ctx, model.ProjectId, model.ServerId)
req = req.CreateCommandPayload(runcommand.CreateCommandPayload{
CommandTemplateName: &model.CommandTemplateName,
Parameters: model.Params,
})
return req, nil
}
func outputResult(p *print.Printer, model *inputModel, resp *runcommand.NewCommandResponse) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal server command: %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 server command: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Created server command for server %s. Command ID: %d\n", model.ServerId, *resp.Id)
return nil
}
}
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/runcommand"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &runcommand.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
var testCommandId = "5"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testCommandId,
}
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{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
ServerId: testServerId,
CommandId: testCommandId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *runcommand.ApiGetCommandRequest)) runcommand.ApiGetCommandRequest {
request := testClient.GetCommand(testCtx, testProjectId, testServerId, testCommandId)
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: "server id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, serverIdFlag)
}),
isValid: false,
},
{
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: "command id invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %v", err)
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd, tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
isValid bool
expectedRequest runcommand.ApiGetCommandRequest
}{
{
description: "base",
model: fixtureInputModel(),
isValid: true,
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package describe
import (
"context"
"encoding/json"
"fmt"
"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/runcommand/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/runcommand"
)
const (
commandIdArg = "COMMAND_ID"
serverIdFlag = "server-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
CommandId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", commandIdArg),
Short: "Shows details of a Server Command",
Long: "Shows details of a Server Command.",
Args: args.SingleArg(commandIdArg, nil),
Example: examples.Build(
examples.NewExample(
`Get details of a Server Command with ID "xxx" for server with ID "yyy"`,
"$ stackit beta server command describe xxx --server-id=yyy"),
examples.NewExample(
`Get details of a Server Command with ID "xxx" for server with ID "yyy" in JSON format`,
"$ stackit beta server command describe xxx --server-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 server command: %w", err)
}
return outputResult(p, model.OutputFormat, 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) {
commandId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
CommandId: commandId,
}
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 *runcommand.APIClient) runcommand.ApiGetCommandRequest {
req := apiClient.GetCommand(ctx, model.ProjectId, model.ServerId, model.CommandId)
return req
}
func outputResult(p *print.Printer, outputFormat string, command *runcommand.CommandDetails) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(command, "", " ")
if err != nil {
return fmt.Errorf("marshal server command: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(command, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal server command: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.AddRow("ID", *command.Id)
table.AddSeparator()
table.AddRow("COMMAND TEMPLATE NAME", *command.CommandTemplateName)
table.AddSeparator()
table.AddRow("COMMAND TEMPLATE TITLE", *command.CommandTemplateTitle)
table.AddSeparator()
table.AddRow("STATUS", *command.Status)
table.AddSeparator()
table.AddRow("STARTED AT", *command.StartedAt)
table.AddSeparator()
table.AddRow("FINISHED AT", *command.FinishedAt)
table.AddSeparator()
table.AddRow("EXIT CODE", *command.ExitCode)
table.AddSeparator()
table.AddRow("COMMAND SCRIPT", *command.Script)
table.AddSeparator()
table.AddRow("COMMAND OUTPUT", *command.Output)
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/runcommand"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &runcommand.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
limitFlag: "10",
serverIdFlag: testServerId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
Limit: utils.Ptr(int64(10)),
ServerId: testServerId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *runcommand.ApiListCommandsRequest)) runcommand.ApiListCommandsRequest {
request := testClient.ListCommands(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: "limit invalid",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "invalid"
}),
isValid: false,
},
{
description: "limit invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "0"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest runcommand.ApiListCommandsRequest
}{
{
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/services/runcommand/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/runcommand"
)
const (
limitFlag = "limit"
serverIdFlag = "server-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
Limit *int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all server commands",
Long: "Lists all server commands.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all commands for a server with ID "xxx"`,
"$ stackit beta server command list --server-id xxx"),
examples.NewExample(
`List all commands for a server with ID "xxx" in JSON format`,
"$ stackit beta server command list --server-id xxx --output-format json"),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("list server commands: %w", err)
}
commands := *resp.Items
if len(commands) == 0 {
p.Info("No commands found for server %s\n", model.ServerId)
return nil
}
// Truncate output
if model.Limit != nil && len(commands) > int(*model.Limit) {
commands = commands[:*model.Limit]
}
return outputResult(p, model.OutputFormat, commands)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list")
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) (*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,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
Limit: limit,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *runcommand.APIClient) runcommand.ApiListCommandsRequest {
req := apiClient.ListCommands(ctx, model.ProjectId, model.ServerId)
return req
}
func outputResult(p *print.Printer, outputFormat string, commands []runcommand.Commands) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(commands, "", " ")
if err != nil {
return fmt.Errorf("marshal server command list: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(commands, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal server command list: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("ID", "TEMPLATE NAME", "TEMPLATE TITLE", "STATUS", "STARTED_AT", "FINISHED_AT")
for i := range commands {
s := commands[i]
table.AddRow(*s.Id, *s.CommandTemplateName, *s.CommandTemplateTitle, *s.Status, *s.StartedAt, *s.FinishedAt)
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
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/runcommand"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &runcommand.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
var testCommandTemplateName = "RunShellScript"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testCommandTemplateName,
}
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{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
ServerId: testServerId,
CommandTemplateName: testCommandTemplateName,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *runcommand.ApiGetCommandTemplateRequest)) runcommand.ApiGetCommandTemplateRequest {
request := testClient.GetCommandTemplate(testCtx, testProjectId, testServerId, testCommandTemplateName)
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: "server id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, serverIdFlag)
}),
isValid: false,
},
{
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: "command template name invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %v", err)
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd, tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
isValid bool
expectedRequest runcommand.ApiGetCommandTemplateRequest
}{
{
description: "base",
model: fixtureInputModel(),
isValid: true,
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package describe
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/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/runcommand/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/runcommand"
)
const (
commandTemplateNameArg = "COMMAND_TEMPLATE_NAME"
serverIdFlag = "server-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
CommandTemplateName string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", commandTemplateNameArg),
Short: "Shows details of a Server Command Template",
Long: "Shows details of a Server Command Template.",
Args: args.SingleArg(commandTemplateNameArg, nil),
Example: examples.Build(
examples.NewExample(
`Get details of a Server Command Template with name "RunShellScript" for server with ID "xxx"`,
"$ stackit beta server command template describe RunShellScript --server-id=xxx"),
examples.NewExample(
`Get details of a Server Command Template with name "RunShellScript" for server with ID "xxx" in JSON format`,
"$ stackit beta server command template describe RunShellScript --server-id=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
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("read server command template: %w", err)
}
return outputResult(p, model.OutputFormat, 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) {
commandTemplateName := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
CommandTemplateName: commandTemplateName,
}
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 *runcommand.APIClient) runcommand.ApiGetCommandTemplateRequest {
req := apiClient.GetCommandTemplate(ctx, model.ProjectId, model.ServerId, model.CommandTemplateName)
return req
}
func outputResult(p *print.Printer, outputFormat string, commandTemplate *runcommand.CommandTemplateSchema) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(commandTemplate, "", " ")
if err != nil {
return fmt.Errorf("marshal server command template: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(commandTemplate, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal server command template: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.AddRow("NAME", *commandTemplate.Name)
table.AddSeparator()
table.AddRow("TITLE", *commandTemplate.Title)
table.AddSeparator()
table.AddRow("DESCRIPTION", *commandTemplate.Description)
table.AddSeparator()
table.AddRow("OS TYPE", strings.Join(*commandTemplate.OsType, "\n"))
table.AddSeparator()
if commandTemplate.ParameterSchema != nil {
table.AddRow("PARAMS", *commandTemplate.ParameterSchema)
} else {
table.AddRow("PARAMS", "")
}
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/runcommand"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &runcommand.APIClient{}
var testProjectId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
limitFlag: "10",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
Limit: utils.Ptr(int64(10)),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *runcommand.ApiListCommandTemplatesRequest)) runcommand.ApiListCommandTemplatesRequest {
request := testClient.ListCommandTemplates(testCtx)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "limit invalid",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "invalid"
}),
isValid: false,
},
{
description: "limit invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "0"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest runcommand.ApiListCommandTemplatesRequest
}{
{
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"
"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/runcommand/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/runcommand"
)
const (
limitFlag = "limit"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Limit *int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all server command templates",
Long: "Lists all server command templates.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all command templates`,
"$ stackit beta server command template list"),
examples.NewExample(
`List all commands templates in JSON format`,
"$ stackit beta server command template list --output-format json"),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("list server command templates: %w", err)
}
templates := *resp.Items
if len(templates) == 0 {
p.Info("No commands templates found\n")
return nil
}
// Truncate output
if model.Limit != nil && len(templates) > int(*model.Limit) {
templates = templates[:*model.Limit]
}
return outputResult(p, model.OutputFormat, templates)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list")
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
limit := flags.FlagToInt64Pointer(p, cmd, limitFlag)
if limit != nil && *limit < 1 {
return nil, &errors.FlagValidationError{
Flag: limitFlag,
Details: "must be greater than 0",
}
}
model := inputModel{
GlobalFlagModel: globalFlags,
Limit: limit,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, _ *inputModel, apiClient *runcommand.APIClient) runcommand.ApiListCommandTemplatesRequest {
req := apiClient.ListCommandTemplates(ctx)
return req
}
func outputResult(p *print.Printer, outputFormat string, templates []runcommand.CommandTemplate) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(templates, "", " ")
if err != nil {
return fmt.Errorf("marshal server command template list: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(templates, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal server command template list: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("NAME", "OS TYPE", "TITLE")
for i := range templates {
s := templates[i]
table.AddRow(*s.Name, strings.Join(*s.OsType, ","), *s.Title)
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package template
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/command/template/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/command/template/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: "template",
Short: "Provides functionality for Server Command Template",
Long: "Provides functionality for Server Command Template.",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, p)
return cmd
}
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(describe.NewCmd(p))
cmd.AddCommand(list.NewCmd(p))
}
package client
import (
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
"github.com/stackitcloud/stackit-cli/internal/pkg/config"
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/spf13/viper"
sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
)
func ConfigureClient(p *print.Printer) (*iaas.APIClient, error) {
var err error
var apiClient *iaas.APIClient
var cfgOptions []sdkConfig.ConfigurationOption
authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser)
if err != nil {
p.Debug(print.ErrorLevel, "configure authentication: %v", err)
return nil, &errors.AuthError{}
}
cfgOptions = append(cfgOptions, authCfgOption)
customEndpoint := viper.GetString(config.IaaSCustomEndpointKey)
if customEndpoint != "" {
cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint))
} else {
cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion("eu01"))
}
if p.IsVerbosityDebug() {
cfgOptions = append(cfgOptions,
sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)),
)
}
apiClient, err = iaas.NewAPIClient(cfgOptions...)
if err != nil {
p.Debug(print.ErrorLevel, "create new API client: %v", err)
return nil, &errors.AuthError{}
}
return apiClient, nil
}
package utils
import (
"context"
"fmt"
"reflect"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
)
type IaaSClientMocked struct {
GetNetworkFails bool
GetNetworkResp *iaas.Network
GetNetworkAreaFails bool
GetNetworkAreaResp *iaas.NetworkArea
GetAttachedProjectsFails bool
GetAttachedProjectsResp *iaas.ProjectListResponse
GetNetworkAreaRangeFails bool
GetNetworkAreaRangeResp *iaas.NetworkRange
}
func (m *IaaSClientMocked) GetNetworkExecute(_ context.Context, _, _ string) (*iaas.Network, error) {
if m.GetNetworkFails {
return nil, fmt.Errorf("could not get network")
}
return m.GetNetworkResp, nil
}
func (m *IaaSClientMocked) GetNetworkAreaExecute(_ context.Context, _, _ string) (*iaas.NetworkArea, error) {
if m.GetNetworkAreaFails {
return nil, fmt.Errorf("could not get network area")
}
return m.GetNetworkAreaResp, nil
}
func (m *IaaSClientMocked) ListNetworkAreaProjectsExecute(_ context.Context, _, _ string) (*iaas.ProjectListResponse, error) {
if m.GetAttachedProjectsFails {
return nil, fmt.Errorf("could not get attached projects")
}
return m.GetAttachedProjectsResp, nil
}
func (m *IaaSClientMocked) GetNetworkAreaRangeExecute(_ context.Context, _, _, _ string) (*iaas.NetworkRange, error) {
if m.GetNetworkAreaRangeFails {
return nil, fmt.Errorf("could not get network range")
}
return m.GetNetworkAreaRangeResp, nil
}
func TestGetNetworkName(t *testing.T) {
type args struct {
getInstanceFails bool
getInstanceResp *iaas.Network
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "base",
args: args{
getInstanceResp: &iaas.Network{
Name: utils.Ptr("test"),
},
},
want: "test",
},
{
name: "get network fails",
args: args{
getInstanceFails: true,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &IaaSClientMocked{
GetNetworkFails: tt.args.getInstanceFails,
GetNetworkResp: tt.args.getInstanceResp,
}
got, err := GetNetworkName(context.Background(), m, "", "")
if (err != nil) != tt.wantErr {
t.Errorf("GetNetworkName() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GetNetworkName() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetNetworkAreaName(t *testing.T) {
type args struct {
getInstanceFails bool
getInstanceResp *iaas.NetworkArea
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "base",
args: args{
getInstanceResp: &iaas.NetworkArea{
Name: utils.Ptr("test"),
},
},
want: "test",
},
{
name: "get network area fails",
args: args{
getInstanceFails: true,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &IaaSClientMocked{
GetNetworkAreaFails: tt.args.getInstanceFails,
GetNetworkAreaResp: tt.args.getInstanceResp,
}
got, err := GetNetworkAreaName(context.Background(), m, "", "")
if (err != nil) != tt.wantErr {
t.Errorf("GetNetworkAreaName() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GetNetworkAreaName() = %v, want %v", got, tt.want)
}
})
}
}
func TestListAttachedProjects(t *testing.T) {
type args struct {
getAttachedProjectsFails bool
getAttachedProjectsResp *iaas.ProjectListResponse
}
tests := []struct {
name string
args args
want []string
wantErr bool
}{
{
name: "base",
args: args{
getAttachedProjectsResp: &iaas.ProjectListResponse{
Items: &[]string{"test"},
},
},
want: []string{"test"},
},
{
name: "get attached projects fails",
args: args{
getAttachedProjectsFails: true,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &IaaSClientMocked{
GetAttachedProjectsFails: tt.args.getAttachedProjectsFails,
GetAttachedProjectsResp: tt.args.getAttachedProjectsResp,
}
got, err := ListAttachedProjects(context.Background(), m, "", "")
if (err != nil) != tt.wantErr {
t.Errorf("GetAttachedProjects() error = %v, wantErr %v", err, tt.wantErr)
return
}
if fmt.Sprintf("%v", got) != fmt.Sprintf("%v", tt.want) {
t.Errorf("GetAttachedProjects() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetNetworkRangePrefix(t *testing.T) {
type args struct {
getNetworkAreaRangeFails bool
getNetworkAreaRangeResp *iaas.NetworkRange
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "base",
args: args{
getNetworkAreaRangeResp: &iaas.NetworkRange{
Prefix: utils.Ptr("test"),
},
},
want: "test",
},
{
name: "get network area range fails",
args: args{
getNetworkAreaRangeFails: true,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &IaaSClientMocked{
GetNetworkAreaRangeFails: tt.args.getNetworkAreaRangeFails,
GetNetworkAreaRangeResp: tt.args.getNetworkAreaRangeResp,
}
got, err := GetNetworkRangePrefix(context.Background(), m, "", "", "")
if (err != nil) != tt.wantErr {
t.Errorf("GetNetworkRangePrefix() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GetNetworkRangePrefix() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetRouteFromAPIResponse(t *testing.T) {
type args struct {
prefix string
nexthop string
routes *[]iaas.Route
}
tests := []struct {
name string
args args
want iaas.Route
wantErr bool
}{
{
name: "base",
args: args{
prefix: "1.1.1.0/24",
nexthop: "1.1.1.1",
routes: &[]iaas.Route{
{
Prefix: utils.Ptr("1.1.1.0/24"),
Nexthop: utils.Ptr("1.1.1.1"),
},
{
Prefix: utils.Ptr("2.2.2.0/24"),
Nexthop: utils.Ptr("2.2.2.2"),
},
{
Prefix: utils.Ptr("3.3.3.0/24"),
Nexthop: utils.Ptr("3.3.3.3"),
},
},
},
want: iaas.Route{
Prefix: utils.Ptr("1.1.1.0/24"),
Nexthop: utils.Ptr("1.1.1.1"),
},
},
{
name: "not found",
args: args{
prefix: "1.1.1.0/24",
nexthop: "1.1.1.1",
routes: &[]iaas.Route{
{
Prefix: utils.Ptr("2.2.2.0/24"),
Nexthop: utils.Ptr("2.2.2.2"),
},
{
Prefix: utils.Ptr("3.3.3.0/24"),
Nexthop: utils.Ptr("3.3.3.3"),
},
},
},
wantErr: true,
},
{
name: "empty",
args: args{
prefix: "1.1.1.0/24",
nexthop: "1.1.1.1",
routes: &[]iaas.Route{},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetRouteFromAPIResponse(tt.args.prefix, tt.args.nexthop, tt.args.routes)
if (err != nil) != tt.wantErr {
t.Errorf("GetRouteFromAPIResponse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetRouteFromAPIResponse() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetNetworkRangeFromAPIResponse(t *testing.T) {
type args struct {
prefix string
networkRanges *[]iaas.NetworkRange
}
tests := []struct {
name string
args args
want iaas.NetworkRange
wantErr bool
}{
{
name: "base",
args: args{
prefix: "1.1.1.0/24",
networkRanges: &[]iaas.NetworkRange{
{
Prefix: utils.Ptr("1.1.1.0/24"),
},
{
Prefix: utils.Ptr("2.2.2.0/24"),
},
{
Prefix: utils.Ptr("3.3.3.0/24"),
},
},
},
want: iaas.NetworkRange{
Prefix: utils.Ptr("1.1.1.0/24"),
},
},
{
name: "not found",
args: args{
prefix: "1.1.1.0/24",
networkRanges: &[]iaas.NetworkRange{
{
Prefix: utils.Ptr("2.2.2.0/24"),
},
{
Prefix: utils.Ptr("3.3.3.0/24"),
},
},
},
wantErr: true,
},
{
name: "empty",
args: args{
prefix: "1.1.1.0/24",
networkRanges: &[]iaas.NetworkRange{},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetNetworkRangeFromAPIResponse(tt.args.prefix, tt.args.networkRanges)
if (err != nil) != tt.wantErr {
t.Errorf("GetNetworkRangeFromAPIResponse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetNetworkRangeFromAPIResponse() = %v, want %v", got, tt.want)
}
})
}
}
package utils
import (
"context"
"fmt"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
)
type IaaSClient interface {
GetNetworkExecute(ctx context.Context, projectId, networkId string) (*iaas.Network, error)
GetNetworkAreaExecute(ctx context.Context, organizationId, areaId string) (*iaas.NetworkArea, error)
ListNetworkAreaProjectsExecute(ctx context.Context, organizationId, areaId string) (*iaas.ProjectListResponse, error)
GetNetworkAreaRangeExecute(ctx context.Context, organizationId, areaId, networkRangeId string) (*iaas.NetworkRange, error)
}
func GetNetworkName(ctx context.Context, apiClient IaaSClient, projectId, networkId string) (string, error) {
resp, err := apiClient.GetNetworkExecute(ctx, projectId, networkId)
if err != nil {
return "", fmt.Errorf("get network: %w", err)
}
return *resp.Name, nil
}
func GetNetworkAreaName(ctx context.Context, apiClient IaaSClient, organizationId, areaId string) (string, error) {
resp, err := apiClient.GetNetworkAreaExecute(ctx, organizationId, areaId)
if err != nil {
return "", fmt.Errorf("get network area: %w", err)
}
return *resp.Name, nil
}
func ListAttachedProjects(ctx context.Context, apiClient IaaSClient, organizationId, areaId string) ([]string, error) {
resp, err := apiClient.ListNetworkAreaProjectsExecute(ctx, organizationId, areaId)
if err != nil {
return nil, fmt.Errorf("list network area attached projects: %w", err)
}
return *resp.Items, nil
}
func GetNetworkRangePrefix(ctx context.Context, apiClient IaaSClient, organizationId, areaId, networkRangeId string) (string, error) {
resp, err := apiClient.GetNetworkAreaRangeExecute(ctx, organizationId, areaId, networkRangeId)
if err != nil {
return "", fmt.Errorf("get network range: %w", err)
}
return *resp.Prefix, nil
}
// GetRouteFromAPIResponse returns the static route from the API response that matches the prefix and nexthop
// This works because static routes are unique by prefix and nexthop
func GetRouteFromAPIResponse(prefix, nexthop string, routes *[]iaas.Route) (iaas.Route, error) {
for _, route := range *routes {
if *route.Prefix == prefix && *route.Nexthop == nexthop {
return route, nil
}
}
return iaas.Route{}, fmt.Errorf("new static route not found in API response")
}
// GetNetworkRangeFromAPIResponse returns the network range from the API response that matches the given prefix
// This works because network range prefixes are unique in the same SNA
func GetNetworkRangeFromAPIResponse(prefix string, networkRanges *[]iaas.NetworkRange) (iaas.NetworkRange, error) {
for _, networkRange := range *networkRanges {
if *networkRange.Prefix == prefix {
return networkRange, nil
}
}
return iaas.NetworkRange{}, fmt.Errorf("new network range not found in API response")
}
package utils
import (
"context"
"fmt"
"testing"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
)
var (
testOrgId = uuid.NewString()
)
const (
testOrgName = "organization"
)
type resourceManagerClientMocked struct {
getOrganizationFails bool
getOrganizationResp *resourcemanager.OrganizationResponse
getProjectFails bool
getProjectResp *resourcemanager.GetProjectResponse
}
func (s *resourceManagerClientMocked) GetOrganizationExecute(_ context.Context, _ string) (*resourcemanager.OrganizationResponse, error) {
if s.getOrganizationFails {
return nil, fmt.Errorf("could not get organization")
}
return s.getOrganizationResp, nil
}
func (s *resourceManagerClientMocked) GetProjectExecute(_ context.Context, _ string) (*resourcemanager.GetProjectResponse, error) {
if s.getProjectFails {
return nil, fmt.Errorf("could not get project")
}
return s.getProjectResp, nil
}
func TestGetOrganizationName(t *testing.T) {
tests := []struct {
description string
getOrganizationFails bool
getOrganizationResp *resourcemanager.OrganizationResponse
isValid bool
expectedOutput string
}{
{
description: "base",
getOrganizationResp: &resourcemanager.OrganizationResponse{
Name: utils.Ptr(testOrgName),
},
isValid: true,
expectedOutput: testOrgName,
},
{
description: "get organization fails",
getOrganizationFails: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &resourceManagerClientMocked{
getOrganizationFails: tt.getOrganizationFails,
getOrganizationResp: tt.getOrganizationResp,
}
output, err := GetOrganizationName(context.Background(), client, testOrgId)
if tt.isValid && err != nil {
t.Errorf("failed on valid input")
}
if !tt.isValid && err == nil {
t.Errorf("did not fail on invalid input")
}
if !tt.isValid {
return
}
if output != tt.expectedOutput {
t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output)
}
})
}
}
func TestGetProjectName(t *testing.T) {
tests := []struct {
description string
getProjectFails bool
getProjectResp *resourcemanager.GetProjectResponse
isValid bool
expectedOutput string
}{
{
description: "base",
getProjectResp: &resourcemanager.GetProjectResponse{
Name: utils.Ptr("project"),
},
isValid: true,
expectedOutput: "project",
},
{
description: "get project fails",
getProjectFails: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &resourceManagerClientMocked{
getProjectFails: tt.getProjectFails,
getProjectResp: tt.getProjectResp,
}
output, err := GetProjectName(context.Background(), client, testOrgId)
if tt.isValid && err != nil {
t.Errorf("failed on valid input")
}
if !tt.isValid && err == nil {
t.Errorf("did not fail on invalid input")
}
if !tt.isValid {
return
}
if output != tt.expectedOutput {
t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output)
}
})
}
}
package utils
import (
"context"
"fmt"
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
)
type ResourceManagerClient interface {
GetOrganizationExecute(ctx context.Context, organizationId string) (*resourcemanager.OrganizationResponse, error)
GetProjectExecute(ctx context.Context, projectId string) (*resourcemanager.GetProjectResponse, error)
}
// GetOrganizationName returns the name of an organization by its ID.
func GetOrganizationName(ctx context.Context, apiClient ResourceManagerClient, orgId string) (string, error) {
resp, err := apiClient.GetOrganizationExecute(ctx, orgId)
if err != nil {
return "", fmt.Errorf("get organization details: %w", err)
}
return *resp.Name, nil
}
func GetProjectName(ctx context.Context, apiClient ResourceManagerClient, projectId string) (string, error) {
resp, err := apiClient.GetProjectExecute(ctx, projectId)
if err != nil {
return "", fmt.Errorf("get project details: %w", err)
}
return *resp.Name, nil
}
package client
import (
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
"github.com/stackitcloud/stackit-cli/internal/pkg/config"
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/spf13/viper"
sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/services/runcommand"
)
func ConfigureClient(p *print.Printer) (*runcommand.APIClient, error) {
var err error
var apiClient *runcommand.APIClient
var cfgOptions []sdkConfig.ConfigurationOption
authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser)
if err != nil {
p.Debug(print.ErrorLevel, "configure authentication: %v", err)
return nil, &errors.AuthError{}
}
cfgOptions = append(cfgOptions, authCfgOption)
customEndpoint := viper.GetString(config.RunCommandCustomEndpointKey)
if customEndpoint != "" {
cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint))
} else {
cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion("eu01"))
}
if p.IsVerbosityDebug() {
cfgOptions = append(cfgOptions,
sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)),
)
}
apiClient, err = runcommand.NewAPIClient(cfgOptions...)
if err != nil {
p.Debug(print.ErrorLevel, "create new API client: %v", err)
return nil, &errors.AuthError{}
}
return apiClient, nil
}
package utils
import (
"testing"
)
func TestParseScriptParams(t *testing.T) {
tests := []struct {
description string
input map[string]string
expectedOutput map[string]string
isValid bool
}{
{
"base-ok",
map[string]string{"script": "ls /"},
map[string]string{"script": "ls /"},
true,
},
{
"not-ok-nonexistant-file-specified-for-script",
map[string]string{"script": "@{/some/file/which/does/not/exist/and/thus/fails}"},
nil,
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output, err := ParseScriptParams(tt.input)
if tt.isValid && err != nil {
t.Errorf("failed on valid input")
}
if !tt.isValid && err == nil {
t.Errorf("did not fail on invalid input")
}
if !tt.isValid {
return
}
if output["script"] != tt.expectedOutput["script"] {
t.Errorf("expected output to be %s, got %s", tt.expectedOutput["script"], output["script"])
}
})
}
}
package utils
import (
"os"
"strings"
)
func ParseScriptParams(params map[string]string) (map[string]string, error) {
if params == nil {
return nil, nil
}
parsed := map[string]string{}
for k, v := range params {
parsed[k] = v
if k == "script" && strings.HasPrefix(v, "@{") && strings.HasSuffix(v, "}") {
// Check if a script file path was specified, like: --params script=@{/tmp/test.sh}
fileContents, err := os.ReadFile(v[2 : len(v)-1])
if err != nil {
return nil, err
}
parsed[k] = string(fileContents)
}
}
return parsed, nil
}
+1
-1

@@ -16,5 +16,5 @@ name: Renovate

- name: Self-hosted Renovate
uses: renovatebot/github-action@v40.2.3
uses: renovatebot/github-action@v40.2.6
with:
configurationFile: .github/renovate.json
token: ${{ secrets.RENOVATE_TOKEN }}

@@ -33,2 +33,3 @@ ## stackit beta server

* [stackit beta server backup](./stackit_beta_server_backup.md) - Provides functionality for Server Backup
* [stackit beta server command](./stackit_beta_server_command.md) - Provides functionality for Server Command

@@ -43,4 +43,6 @@ ## stackit beta

* [stackit](./stackit.md) - Manage STACKIT resources using the command line
* [stackit beta network](./stackit_beta_network.md) - Provides functionality for Network
* [stackit beta network-area](./stackit_beta_network-area.md) - Provides functionality for STACKIT Network Area (SNA)
* [stackit beta server](./stackit_beta_server.md) - Provides functionality for Server
* [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex

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

-h, --help Help for "stackit config set"
--iaas-custom-endpoint string IaaS API base URL, used in calls to this API
--identity-provider-custom-endpoint string Identity Provider base URL, used for user authentication

@@ -48,2 +49,3 @@ --load-balancer-custom-endpoint string Load Balancer API base URL, used in calls to this API

--resource-manager-custom-endpoint string Resource Manager API base URL, used in calls to this API
--runcommand-custom-endpoint string Run Command API base URL, used in calls to this API
--secrets-manager-custom-endpoint string Secrets Manager API base URL, used in calls to this API

@@ -50,0 +52,0 @@ --serverbackup-custom-endpoint string Server Backup API base URL, used in calls to this API

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

-h, --help Help for "stackit config unset"
--iaas-custom-endpoint IaaS API base URL. If unset, uses the default base URL
--identity-provider-custom-endpoint Identity Provider base URL. If unset, uses the default base URL

@@ -48,2 +49,3 @@ --load-balancer-custom-endpoint Load Balancer API base URL. If unset, uses the default base URL

--resource-manager-custom-endpoint Resource Manager API base URL. If unset, uses the default base URL
--runcommand-custom-endpoint Server Command base URL. If unset, uses the default base URL
--secrets-manager-custom-endpoint Secrets Manager API base URL. If unset, uses the default base URL

@@ -50,0 +52,0 @@ --serverbackup-custom-endpoint Server Backup base URL. If unset, uses the default base URL

@@ -8,3 +8,8 @@ ## stackit project create

Creates a STACKIT project.
You can associate a project with a STACKIT Network Area (SNA) by providing the ID of the SNA.
The STACKIT Network Area (SNA) allows projects within an organization to be connected to each other on a network level.
This makes it possible to connect various resources of the projects within an SNA and also simplifies the connection with on-prem environments (hybrid cloud).
The network type can no longer be changed after the project has been created. If you require a different network type, you must create a new project.
```

@@ -22,2 +27,5 @@ stackit project create [flags]

$ stackit project create --parent-id xxxx --name my-project --label key=value --label foo=bar
Create a STACKIT project with a network area
$ stackit project create --parent-id xxxx --name my-project --network-area-id yyyy
```

@@ -28,6 +36,7 @@

```
-h, --help Help for "stackit project create"
--label stringToString Labels are key-value string pairs which can be attached to a project. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels (default [])
--name string Project name
--parent-id string Parent resource identifier. Both container ID (user-friendly) and UUID are supported
-h, --help Help for "stackit project create"
--label stringToString Labels are key-value string pairs which can be attached to a project. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels (default [])
--name string Project name
--network-area-id string ID of a STACKIT Network Area (SNA) to associate with the project.
--parent-id string Parent resource identifier. Both container ID (user-friendly) and UUID are supported
```

@@ -34,0 +43,0 @@

+7
-5

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

github.com/stackitcloud/stackit-sdk-go/services/dns v0.10.0
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.6.0
github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.14.0

@@ -26,2 +27,3 @@ github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.16.0

github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.9.0
github.com/stackitcloud/stackit-sdk-go/services/runcommand v0.1.0
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.8.0

@@ -34,6 +36,6 @@ github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.1.0

github.com/zalando/go-keyring v0.2.5
golang.org/x/mod v0.19.0
golang.org/x/oauth2 v0.21.0
golang.org/x/term v0.22.0
golang.org/x/text v0.16.0
golang.org/x/mod v0.20.0
golang.org/x/oauth2 v0.22.0
golang.org/x/term v0.23.0
golang.org/x/text v0.17.0
k8s.io/apimachinery v0.29.2

@@ -91,3 +93,3 @@ k8s.io/client-go v0.29.2

golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/sys v0.23.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect

@@ -94,0 +96,0 @@ gopkg.in/yaml.v2 v2.4.0 // indirect

+14
-10

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

github.com/stackitcloud/stackit-sdk-go/services/dns v0.10.0/go.mod h1:MdZcRbs19s2NLeJmSLSoqTzm9IPIQhE1ZEMpo9gePq0=
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.6.0 h1:bOUmdso7RuVgswGaVgDRpNWXizSAwihDP77DcZHkkO8=
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.6.0/go.mod h1:XtJA9FMK/yJ0dj4HtRAogmZPRUsZiFcuwUSfHYNASjo=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.14.0 h1:/GwkGMD7ID5hSjdZs1l/Mj8waceCt7oj3TxHgBfEMDQ=

@@ -155,2 +157,4 @@ github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.14.0/go.mod h1:wsO3+vXe1XiKLeCIctWAptaHQZ07Un7kmLTQ+drbj7w=

github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.9.0/go.mod h1:p16qz/pAW8b1gEhqMpIgJfutRPeDPqQLlbVGyCo3f8o=
github.com/stackitcloud/stackit-sdk-go/services/runcommand v0.1.0 h1:4EApay9ceTTmIG7bI1XWN1zcevkAiCyAvHrS4sp7s5g=
github.com/stackitcloud/stackit-sdk-go/services/runcommand v0.1.0/go.mod h1:pSDRPhmRYgRmKoQKL+tzgOmnin90sXUjfnc9jaTir8c=
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.8.0 h1:pJBG455kmtbQFpCxcBfBK8wOuEnmsMv3h90LFcdj3q0=

@@ -196,4 +200,4 @@ github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.8.0/go.mod h1:LX0Mcyr7/QP77zf7e05fHCJO38RMuTxr7nEDUDZ3oPQ=

golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=

@@ -205,4 +209,4 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=

golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

@@ -216,10 +220,10 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=

@@ -226,0 +230,0 @@ golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=

@@ -6,2 +6,4 @@ package beta

"github.com/stackitcloud/stackit-cli/internal/cmd/beta/network"
networkArea "github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server"

@@ -42,2 +44,4 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex"

cmd.AddCommand(server.NewCmd(p))
cmd.AddCommand(networkArea.NewCmd(p))
cmd.AddCommand(network.NewCmd(p))
}

@@ -5,2 +5,3 @@ package server

"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/command"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"

@@ -27,2 +28,3 @@ "github.com/stackitcloud/stackit-cli/internal/pkg/print"

cmd.AddCommand(backup.NewCmd(p))
cmd.AddCommand(command.NewCmd(p))
}

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

serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint"
runCommandCustomEndpointFlag = "runcommand-custom-endpoint"
serviceAccountCustomEndpointFlag = "service-account-custom-endpoint"

@@ -43,2 +44,3 @@ serviceEnablementCustomEndpointFlag = "service-enablement-custom-endpoint"

sqlServerFlexCustomEndpointFlag = "sqlserverflex-custom-endpoint"
iaasCustomEndpointFlag = "iaas-custom-endpoint"
)

@@ -149,4 +151,6 @@

cmd.Flags().String(serverBackupCustomEndpointFlag, "", "Server Backup API base URL, used in calls to this API")
cmd.Flags().String(runCommandCustomEndpointFlag, "", "Run Command API base URL, used in calls to this API")
cmd.Flags().String(skeCustomEndpointFlag, "", "SKE API base URL, used in calls to this API")
cmd.Flags().String(sqlServerFlexCustomEndpointFlag, "", "SQLServer Flex API base URL, used in calls to this API")
cmd.Flags().String(iaasCustomEndpointFlag, "", "IaaS API base URL, used in calls to this API")

@@ -188,2 +192,4 @@ err := viper.BindPFlag(config.SessionTimeLimitKey, cmd.Flags().Lookup(sessionTimeLimitFlag))

cobra.CheckErr(err)
err = viper.BindPFlag(config.RunCommandCustomEndpointKey, cmd.Flags().Lookup(runCommandCustomEndpointFlag))
cobra.CheckErr(err)
err = viper.BindPFlag(config.ServiceAccountCustomEndpointKey, cmd.Flags().Lookup(serviceAccountCustomEndpointFlag))

@@ -197,2 +203,4 @@ cobra.CheckErr(err)

cobra.CheckErr(err)
err = viper.BindPFlag(config.IaaSCustomEndpointKey, cmd.Flags().Lookup(iaasCustomEndpointFlag))
cobra.CheckErr(err)
}

@@ -199,0 +207,0 @@

@@ -36,4 +36,6 @@ package unset

serverBackupCustomEndpointFlag: true,
runCommandCustomEndpointFlag: true,
skeCustomEndpointFlag: true,
sqlServerFlexCustomEndpointFlag: true,
iaasCustomEndpointFlag: true,
}

@@ -70,4 +72,6 @@ for _, mod := range mods {

ServerBackupCustomEndpoint: true,
RunCommandCustomEndpoint: true,
SKECustomEndpoint: true,
SQLServerFlexCustomEndpoint: true,
IaaSCustomEndpoint: true,
}

@@ -120,4 +124,6 @@ for _, mod := range mods {

model.ServerBackupCustomEndpoint = false
model.RunCommandCustomEndpoint = false
model.SKECustomEndpoint = false
model.SQLServerFlexCustomEndpoint = false
model.IaaSCustomEndpoint = false
}),

@@ -225,2 +231,12 @@ },

},
{
description: "runcommand custom endpoint empty",
flagValues: fixtureFlagValues(func(flagValues map[string]bool) {
flagValues[runCommandCustomEndpointFlag] = false
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.RunCommandCustomEndpoint = false
}),
},
}

@@ -227,0 +243,0 @@ for _, tt := range tests {

@@ -43,4 +43,6 @@ package unset

serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint"
runCommandCustomEndpointFlag = "runcommand-custom-endpoint"
skeCustomEndpointFlag = "ske-custom-endpoint"
sqlServerFlexCustomEndpointFlag = "sqlserverflex-custom-endpoint"
iaasCustomEndpointFlag = "iaas-custom-endpoint"
)

@@ -72,2 +74,3 @@

ServerBackupCustomEndpoint bool
RunCommandCustomEndpoint bool
ServiceAccountCustomEndpoint bool

@@ -77,2 +80,3 @@ ServiceEnablementCustomEndpoint bool

SQLServerFlexCustomEndpoint bool
IaaSCustomEndpoint bool
}

@@ -171,2 +175,5 @@

}
if model.RunCommandCustomEndpoint {
viper.Set(config.RunCommandCustomEndpointKey, "")
}
if model.SKECustomEndpoint {

@@ -178,2 +185,5 @@ viper.Set(config.SKECustomEndpointKey, "")

}
if model.IaaSCustomEndpoint {
viper.Set(config.IaaSCustomEndpointKey, "")
}

@@ -217,4 +227,6 @@ err := config.Write()

cmd.Flags().Bool(serverBackupCustomEndpointFlag, false, "Server Backup base URL. If unset, uses the default base URL")
cmd.Flags().Bool(runCommandCustomEndpointFlag, false, "Server Command base URL. If unset, uses the default base URL")
cmd.Flags().Bool(skeCustomEndpointFlag, false, "SKE API base URL. If unset, uses the default base URL")
cmd.Flags().Bool(sqlServerFlexCustomEndpointFlag, false, "SQLServer Flex API base URL. If unset, uses the default base URL")
cmd.Flags().Bool(iaasCustomEndpointFlag, false, "IaaS API base URL. If unset, uses the default base URL")
}

@@ -249,4 +261,6 @@

ServerBackupCustomEndpoint: flags.FlagToBoolValue(p, cmd, serverBackupCustomEndpointFlag),
RunCommandCustomEndpoint: flags.FlagToBoolValue(p, cmd, runCommandCustomEndpointFlag),
SKECustomEndpoint: flags.FlagToBoolValue(p, cmd, skeCustomEndpointFlag),
SQLServerFlexCustomEndpoint: flags.FlagToBoolValue(p, cmd, sqlServerFlexCustomEndpointFlag),
IaaSCustomEndpoint: flags.FlagToBoolValue(p, cmd, iaasCustomEndpointFlag),
}

@@ -253,0 +267,0 @@

@@ -24,2 +24,3 @@ package create

var testParentId = uuid.NewString()
var testNetworkAreaId = uuid.NewString()
var testEmail = "email"

@@ -29,5 +30,6 @@

flagValues := map[string]string{
parentIdFlag: testParentId,
nameFlag: "name",
labelFlag: "key=value",
parentIdFlag: testParentId,
nameFlag: "name",
labelFlag: "key=value",
networkAreaIdFlag: testNetworkAreaId,
}

@@ -48,2 +50,3 @@ for _, mod := range mods {

}),
NetworkAreaId: utils.Ptr(testNetworkAreaId),
}

@@ -56,9 +59,9 @@ for _, mod := range mods {

func fixtureRequest(mods ...func(request *resourcemanager.ApiCreateProjectRequest)) resourcemanager.ApiCreateProjectRequest {
request := testClient.CreateProject(testCtx)
request = request.CreateProjectPayload(resourcemanager.CreateProjectPayload{
func fixturePayload(mods ...func(payload *resourcemanager.CreateProjectPayload)) resourcemanager.CreateProjectPayload {
payload := resourcemanager.CreateProjectPayload{
ContainerParentId: utils.Ptr(testParentId),
Name: utils.Ptr(nameFlag),
Labels: utils.Ptr(map[string]string{
"key": "value",
"key": "value",
networkAreaLabel: testNetworkAreaId,
}),

@@ -71,4 +74,13 @@ Members: &[]resourcemanager.Member{

},
})
}
for _, mod := range mods {
mod(&payload)
}
return payload
}
func fixtureRequest(mods ...func(request *resourcemanager.ApiCreateProjectRequest)) resourcemanager.ApiCreateProjectRequest {
request := testClient.CreateProject(testCtx)
request = request.CreateProjectPayload(fixturePayload())
for _, mod := range mods {
mod(&request)

@@ -144,2 +156,27 @@ }

},
{
description: "network_area_id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, networkAreaIdFlag)
}),
isValid: true,
expectedModel: fixtureInputModel(
func(model *inputModel) {
model.NetworkAreaId = nil
}),
},
{
description: "network_area_id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = ""
}),
isValid: false,
},
{
description: "network_area_id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[networkAreaIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}

@@ -238,2 +275,47 @@

{
description: "missing_network_area_id sa_key",
model: fixtureInputModel(
func(model *inputModel) {
model.NetworkAreaId = nil
}),
authFlow: auth.AUTH_FLOW_SERVICE_ACCOUNT_KEY,
sa_email: utils.Ptr(testEmail),
expectedRequest: fixtureRequest().CreateProjectPayload(fixturePayload(
func(payload *resourcemanager.CreateProjectPayload) {
delete((*payload.Labels), networkAreaLabel)
}),
),
isValid: true,
},
{
description: "missing_network_area_id sa_token",
model: fixtureInputModel(
func(model *inputModel) {
model.NetworkAreaId = nil
}),
authFlow: auth.AUTH_FLOW_SERVICE_ACCOUNT_TOKEN,
sa_email: utils.Ptr(testEmail),
expectedRequest: fixtureRequest().CreateProjectPayload(fixturePayload(
func(payload *resourcemanager.CreateProjectPayload) {
delete((*payload.Labels), networkAreaLabel)
}),
),
isValid: true,
},
{
description: "missing_network_area_id user",
model: fixtureInputModel(
func(model *inputModel) {
model.NetworkAreaId = nil
}),
authFlow: auth.AUTH_FLOW_USER_TOKEN,
user_email: utils.Ptr(testEmail),
expectedRequest: fixtureRequest().CreateProjectPayload(fixturePayload(
func(payload *resourcemanager.CreateProjectPayload) {
delete((*payload.Labels), networkAreaLabel)
}),
),
isValid: true,
},
{
description: "missing_auth_flow",

@@ -240,0 +322,0 @@ model: fixtureInputModel(),

@@ -25,9 +25,11 @@ package create

const (
parentIdFlag = "parent-id"
nameFlag = "name"
labelFlag = "label"
parentIdFlag = "parent-id"
nameFlag = "name"
labelFlag = "label"
networkAreaIdFlag = "network-area-id"
ownerRole = "project.owner"
labelKeyRegex = `[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`
labelValueRegex = `^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`
ownerRole = "project.owner"
labelKeyRegex = `[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`
labelValueRegex = `^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`
networkAreaLabel = "networkArea"
)

@@ -37,5 +39,6 @@

*globalflags.GlobalFlagModel
ParentId *string
Name *string
Labels *map[string]string
ParentId *string
Name *string
Labels *map[string]string
NetworkAreaId *string
}

@@ -47,4 +50,10 @@

Short: "Creates a STACKIT project",
Long: "Creates a STACKIT project.",
Args: args.NoArgs,
Long: fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n",
"Creates a STACKIT project.",
"You can associate a project with a STACKIT Network Area (SNA) by providing the ID of the SNA.",
"The STACKIT Network Area (SNA) allows projects within an organization to be connected to each other on a network level.",
"This makes it possible to connect various resources of the projects within an SNA and also simplifies the connection with on-prem environments (hybrid cloud).",
"The network type can no longer be changed after the project has been created. If you require a different network type, you must create a new project.",
),
Args: args.NoArgs,
Example: examples.Build(

@@ -57,2 +66,5 @@ examples.NewExample(

"$ stackit project create --parent-id xxxx --name my-project --label key=value --label foo=bar"),
examples.NewExample(
`Create a STACKIT project with a network area`,
"$ stackit project create --parent-id xxxx --name my-project --network-area-id yyyy"),
),

@@ -101,2 +113,3 @@ RunE: func(cmd *cobra.Command, args []string) error {

cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a project. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels")
cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "ID of a STACKIT Network Area (SNA) to associate with the project.")

@@ -136,2 +149,3 @@ err := flags.MarkFlagsRequired(cmd, parentIdFlag, nameFlag)

Labels: labels,
NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag),
}

@@ -183,6 +197,15 @@

labels := model.Labels
if model.NetworkAreaId != nil {
if labels == nil {
labels = &map[string]string{}
}
(*labels)[networkAreaLabel] = *model.NetworkAreaId
}
req = req.CreateProjectPayload(resourcemanager.CreateProjectPayload{
ContainerParentId: model.ParentId,
Name: model.Name,
Labels: model.Labels,
Labels: labels,
Members: &[]resourcemanager.Member{

@@ -189,0 +212,0 @@ {

@@ -39,4 +39,6 @@ package config

ServerBackupCustomEndpointKey = "serverbackup_custom_endpoint"
RunCommandCustomEndpointKey = "runcommand_custom_endpoint"
SKECustomEndpointKey = "ske_custom_endpoint"
SQLServerFlexCustomEndpointKey = "sqlserverflex_custom_endpoint"
IaaSCustomEndpointKey = "iaas_custom_endpoint"

@@ -88,4 +90,6 @@ ProjectNameKey = "project_name"

ServerBackupCustomEndpointKey,
RunCommandCustomEndpointKey,
SKECustomEndpointKey,
SQLServerFlexCustomEndpointKey,
IaaSCustomEndpointKey,
}

@@ -161,4 +165,6 @@

viper.SetDefault(ServerBackupCustomEndpointKey, "")
viper.SetDefault(RunCommandCustomEndpointKey, "")
viper.SetDefault(SKECustomEndpointKey, "")
viper.SetDefault(SQLServerFlexCustomEndpointKey, "")
viper.SetDefault(IaaSCustomEndpointKey, "")
}

@@ -165,0 +171,0 @@

@@ -13,2 +13,3 @@ package projectname

"github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/utils"

@@ -37,8 +38,7 @@ "github.com/spf13/cobra"

}
req := apiClient.GetProject(ctx, projectId)
resp, err := req.Execute()
projectName, err := utils.GetProjectName(ctx, apiClient, projectId)
if err != nil {
return "", fmt.Errorf("read project details: %w", err)
return "", fmt.Errorf("get project name: %w", err)
}
projectName := *resp.Name

@@ -45,0 +45,0 @@ // If project ID is set in config, we store the project name in config

@@ -69,5 +69,5 @@ <div align="center">

| Service | CLI Commands | Status |
| ---------------------------------- | ------------------------- | ------------------------- |
|------------------------------------|---------------------------|---------------------------|
| Argus | `argus` | :white_check_mark: |
| Infrastructure as a Service (IaaS) | | Will be integrated soon |
| Infrastructure as a Service (IaaS) | `beta network-area` | :white_check_mark: (beta) |
| Authorization | `project`, `organization` | :white_check_mark: |

@@ -87,3 +87,4 @@ | DNS | `dns` | :white_check_mark: |

| Secrets Manager | `secrets-manager` | :white_check_mark: |
| Server Backup Management | `beta server backup` | :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: |

@@ -90,0 +91,0 @@ | SQLServer Flex | `beta sqlserverflex` | :white_check_mark: (beta) |