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

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

Package Overview
Dependencies
Versions
173
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.44.2-0.20251021072002-99f4d2d6bf24
to
v0.45.0
+62
docs/stackit_beta_kms_key_create.md
## stackit beta kms key create
Creates a KMS key
### Synopsis
Creates a KMS key.
```
stackit beta kms key create [flags]
```
### Examples
```
Create a symmetric AES key (AES-256) with the name "symm-aes-gcm" under the key ring "my-keyring-id"
$ stackit beta kms key create --keyring-id "my-keyring-id" --algorithm "aes_256_gcm" --name "symm-aes-gcm" --purpose "symmetric_encrypt_decrypt" --protection "software"
Create an asymmetric RSA encryption key (RSA-2048)
$ stackit beta kms key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256" --name "prod-orders-rsa" --purpose "asymmetric_encrypt_decrypt" --protection "software"
Create a message authentication key (HMAC-SHA512)
$ stackit beta kms key create --keyring-id "my-keyring-id" --algorithm "hmac_sha512" --name "api-mac-key" --purpose "message_authentication_code" --protection "software"
Create an ECDSA P-256 key for signing & verification
$ stackit beta kms key create --keyring-id "my-keyring-id" --algorithm "ecdsa_p256_sha256" --name "signing-ecdsa-p256" --purpose "asymmetric_sign_verify" --protection "software"
Create an import-only key (versions must be imported)
$ stackit beta kms key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256" --name "ext-managed-rsa" --purpose "asymmetric_encrypt_decrypt" --protection "software" --import-only
Create a key and print the result as YAML
$ stackit beta kms key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256" --name "yaml-output-rsa" --purpose "asymmetric_encrypt_decrypt" --protection "software" --output yaml
```
### Options
```
--algorithm string En-/Decryption / signing algorithm. Possible values: ["aes_256_gcm" "rsa_2048_oaep_sha256" "rsa_3072_oaep_sha256" "rsa_4096_oaep_sha256" "rsa_4096_oaep_sha512" "hmac_sha256" "hmac_sha384" "hmac_sha512" "ecdsa_p256_sha256" "ecdsa_p384_sha384" "ecdsa_p521_sha512"]
--description string Optional description of the key
-h, --help Help for "stackit beta kms key create"
--import-only States whether versions can be created or only imported
--keyring-id string ID of the KMS key ring
--name string The display name to distinguish multiple keys
--protection string The underlying system that is responsible for protecting the key material. Possible values: ["symmetric_encrypt_decrypt" "asymmetric_encrypt_decrypt" "message_authentication_code" "asymmetric_sign_verify"]
--purpose string Purpose of the key. Possible values: ["symmetric_encrypt_decrypt" "asymmetric_encrypt_decrypt" "message_authentication_code" "asymmetric_sign_verify"]
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms key](./stackit_beta_kms_key.md) - Manage KMS keys
## stackit beta kms key delete
Deletes a KMS key
### Synopsis
Deletes a KMS key inside a specific key ring.
```
stackit beta kms key delete KEY_ID [flags]
```
### Examples
```
Delete a KMS key "MY_KEY_ID" inside the key ring "my-keyring-id"
$ stackit beta kms key delete "MY_KEY_ID" --keyring-id "my-keyring-id"
```
### Options
```
-h, --help Help for "stackit beta kms key delete"
--keyring-id string ID of the KMS key ring where the key is stored
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms key](./stackit_beta_kms_key.md) - Manage KMS keys
## stackit beta kms key import
Import a KMS key
### Synopsis
After encrypting the secret with the wrapping key’s public key and Base64-encoding it, import it as a new version of the specified KMS key.
```
stackit beta kms key import KEY_ID [flags]
```
### Examples
```
Import a new version for the given KMS key "MY_KEY_ID" from literal value
$ stackit beta kms key import "MY_KEY_ID" --keyring-id "my-keyring-id" --wrapped-key "BASE64_VALUE" --wrapping-key-id "MY_WRAPPING_KEY_ID"
Import from a file
$ stackit beta kms key import "MY_KEY_ID" --keyring-id "my-keyring-id" --wrapped-key "@path/to/wrapped.key.b64" --wrapping-key-id "MY_WRAPPING_KEY_ID"
```
### Options
```
-h, --help Help for "stackit beta kms key import"
--keyring-id string ID of the KMS key ring
--wrapped-key string The wrapped key material to be imported. Base64-encoded. Pass the value directly or a file path (e.g. @path/to/wrapped.key.b64)
--wrapping-key-id string The unique id of the wrapping key the key material has been wrapped with
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms key](./stackit_beta_kms_key.md) - Manage KMS keys
## stackit beta kms key list
List all KMS keys
### Synopsis
List all KMS keys inside a key ring.
```
stackit beta kms key list [flags]
```
### Examples
```
List all KMS keys for the key ring "my-keyring-id"
$ stackit beta kms key list --keyring-id "my-keyring-id"
List all KMS keys in JSON format
$ stackit beta kms key list --keyring-id "my-keyring-id" --output-format json
```
### Options
```
-h, --help Help for "stackit beta kms key list"
--keyring-id string ID of the KMS key ring where the key is stored
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms key](./stackit_beta_kms_key.md) - Manage KMS keys
## stackit beta kms key restore
Restore a key
### Synopsis
Restores the given key from deletion.
```
stackit beta kms key restore KEY_ID [flags]
```
### Examples
```
Restore a KMS key "MY_KEY_ID" inside the key ring "my-keyring-id" that was scheduled for deletion.
$ stackit beta kms key restore "MY_KEY_ID" --keyring-id "my-keyring-id"
```
### Options
```
-h, --help Help for "stackit beta kms key restore"
--keyring-id string ID of the KMS key ring where the key is stored
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms key](./stackit_beta_kms_key.md) - Manage KMS keys
## stackit beta kms key rotate
Rotate a key
### Synopsis
Rotates the given key.
```
stackit beta kms key rotate KEY_ID [flags]
```
### Examples
```
Rotate a KMS key "MY_KEY_ID" and increase its version inside the key ring "my-keyring-id".
$ stackit beta kms key rotate "MY_KEY_ID" --keyring-id "my-keyring-id"
```
### Options
```
-h, --help Help for "stackit beta kms key rotate"
--keyring-id string ID of the KMS key ring where the key is stored
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms key](./stackit_beta_kms_key.md) - Manage KMS keys
## stackit beta kms key
Manage KMS keys
### Synopsis
Provides functionality for key operations inside the KMS
```
stackit beta kms key [flags]
```
### Options
```
-h, --help Help for "stackit beta kms key"
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS
* [stackit beta kms key create](./stackit_beta_kms_key_create.md) - Creates a KMS key
* [stackit beta kms key delete](./stackit_beta_kms_key_delete.md) - Deletes a KMS key
* [stackit beta kms key import](./stackit_beta_kms_key_import.md) - Import a KMS key
* [stackit beta kms key list](./stackit_beta_kms_key_list.md) - List all KMS keys
* [stackit beta kms key restore](./stackit_beta_kms_key_restore.md) - Restore a key
* [stackit beta kms key rotate](./stackit_beta_kms_key_rotate.md) - Rotate a key
## stackit beta kms keyring create
Creates a KMS key ring
### Synopsis
Creates a KMS key ring.
```
stackit beta kms keyring create [flags]
```
### Examples
```
Create a KMS key ring with name "my-keyring"
$ stackit beta kms keyring create --name my-keyring
Create a KMS key ring with a description
$ stackit beta kms keyring create --name my-keyring --description my-description
Create a KMS key ring and print the result as YAML
$ stackit beta kms keyring create --name my-keyring -o yaml
```
### Options
```
--description string Optional description of the key ring
-h, --help Help for "stackit beta kms keyring create"
--name string Name of the KMS key ring
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms keyring](./stackit_beta_kms_keyring.md) - Manage KMS key rings
## stackit beta kms keyring delete
Deletes a KMS key ring
### Synopsis
Deletes a KMS key ring.
```
stackit beta kms keyring delete KEYRING-ID [flags]
```
### Examples
```
Delete a KMS key ring with ID "MY_KEYRING_ID"
$ stackit beta kms keyring delete "MY_KEYRING_ID"
```
### Options
```
-h, --help Help for "stackit beta kms keyring delete"
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms keyring](./stackit_beta_kms_keyring.md) - Manage KMS key rings
## stackit beta kms keyring list
Lists all KMS key rings
### Synopsis
Lists all KMS key rings.
```
stackit beta kms keyring list [flags]
```
### Examples
```
List all KMS key rings
$ stackit beta kms keyring list
List all KMS key rings in JSON format
$ stackit beta kms keyring list --output-format json
```
### Options
```
-h, --help Help for "stackit beta kms keyring list"
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms keyring](./stackit_beta_kms_keyring.md) - Manage KMS key rings
## stackit beta kms keyring
Manage KMS key rings
### Synopsis
Provides functionality for key ring operations inside the KMS
```
stackit beta kms keyring [flags]
```
### Options
```
-h, --help Help for "stackit beta kms keyring"
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS
* [stackit beta kms keyring create](./stackit_beta_kms_keyring_create.md) - Creates a KMS key ring
* [stackit beta kms keyring delete](./stackit_beta_kms_keyring_delete.md) - Deletes a KMS key ring
* [stackit beta kms keyring list](./stackit_beta_kms_keyring_list.md) - Lists all KMS key rings
## stackit beta kms version destroy
Destroy a key version
### Synopsis
Removes the key material of a version.
```
stackit beta kms version destroy VERSION_NUMBER [flags]
```
### Examples
```
Destroy key version "42" for the key "my-key-id" inside the key ring "my-keyring-id"
$ stackit beta kms version destroy 42 --key-id "my-key-id" --keyring-id "my-keyring-id"
```
### Options
```
-h, --help Help for "stackit beta kms version destroy"
--key-id string ID of the key
--keyring-id string ID of the KMS key ring
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms version](./stackit_beta_kms_version.md) - Manage KMS key versions
## stackit beta kms version disable
Disable a key version
### Synopsis
Disable the given key version.
```
stackit beta kms version disable VERSION_NUMBER [flags]
```
### Examples
```
Disable key version "42" for the key "my-key-id" inside the key ring "my-keyring-id"
$ stackit beta kms version disable 42 --key-id "my-key-id" --keyring-id "my-keyring-id"
```
### Options
```
-h, --help Help for "stackit beta kms version disable"
--key-id string ID of the key
--keyring-id string ID of the KMS key ring
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms version](./stackit_beta_kms_version.md) - Manage KMS key versions
## stackit beta kms version enable
Enable a key version
### Synopsis
Enable the given key version.
```
stackit beta kms version enable VERSION_NUMBER [flags]
```
### Examples
```
Enable key version "42" for the key "my-key-id" inside the key ring "my-keyring-id"
$ stackit beta kms version enable 42 --key-id "my-key-id" --keyring-id "my-keyring-id"
```
### Options
```
-h, --help Help for "stackit beta kms version enable"
--key-id string ID of the key
--keyring-id string ID of the KMS key ring
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms version](./stackit_beta_kms_version.md) - Manage KMS key versions
## stackit beta kms version list
List all key versions
### Synopsis
List all versions of a given key.
```
stackit beta kms version list [flags]
```
### Examples
```
List all key versions for the key "my-key-id" inside the key ring "my-keyring-id"
$ stackit beta kms version list --key-id "my-key-id" --keyring-id "my-keyring-id"
List all key versions in JSON format
$ stackit beta kms version list --key-id "my-key-id" --keyring-id "my-keyring-id" -o json
```
### Options
```
-h, --help Help for "stackit beta kms version list"
--key-id string ID of the key
--keyring-id string ID of the KMS key ring
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms version](./stackit_beta_kms_version.md) - Manage KMS key versions
## stackit beta kms version restore
Restore a key version
### Synopsis
Restores the specified version of a key.
```
stackit beta kms version restore VERSION_NUMBER [flags]
```
### Examples
```
Restore key version "42" for the key "my-key-id" inside the key ring "my-keyring-id"
$ stackit beta kms version restore 42 --key-id "my-key-id" --keyring-id "my-keyring-id"
```
### Options
```
-h, --help Help for "stackit beta kms version restore"
--key-id string ID of the key
--keyring-id string ID of the KMS key ring
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms version](./stackit_beta_kms_version.md) - Manage KMS key versions
## stackit beta kms version
Manage KMS key versions
### Synopsis
Provides functionality for key version operations inside the KMS
```
stackit beta kms version [flags]
```
### Options
```
-h, --help Help for "stackit beta kms version"
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS
* [stackit beta kms version destroy](./stackit_beta_kms_version_destroy.md) - Destroy a key version
* [stackit beta kms version disable](./stackit_beta_kms_version_disable.md) - Disable a key version
* [stackit beta kms version enable](./stackit_beta_kms_version_enable.md) - Enable a key version
* [stackit beta kms version list](./stackit_beta_kms_version_list.md) - List all key versions
* [stackit beta kms version restore](./stackit_beta_kms_version_restore.md) - Restore a key version
## stackit beta kms wrapping-key create
Creates a KMS wrapping key
### Synopsis
Creates a KMS wrapping key.
```
stackit beta kms wrapping-key create [flags]
```
### Examples
```
Create a symmetric (RSA + AES) KMS wrapping key with name "my-wrapping-key-name" in key ring with ID "my-keyring-id"
$ stackit beta kms wrapping-key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256_aes_256_key_wrap" --name "my-wrapping-key-name" --purpose "wrap_symmetric_key" --protection "software"
Create an asymmetric (RSA) KMS wrapping key with name "my-wrapping-key-name" in key ring with ID "my-keyring-id"
$ stackit beta kms wrapping-key create --keyring-id "my-keyring-id" --algorithm "rsa_3072_oaep_sha256" --name "my-wrapping-key-name" --purpose "wrap_asymmetric_key" --protection "software"
```
### Options
```
--algorithm string En-/Decryption / signing algorithm. Possible values: ["rsa_2048_oaep_sha256" "rsa_3072_oaep_sha256" "rsa_4096_oaep_sha256" "rsa_4096_oaep_sha512" "rsa_2048_oaep_sha256_aes_256_key_wrap" "rsa_3072_oaep_sha256_aes_256_key_wrap" "rsa_4096_oaep_sha256_aes_256_key_wrap" "rsa_4096_oaep_sha512_aes_256_key_wrap"]
--description string Optional description of the wrapping key
-h, --help Help for "stackit beta kms wrapping-key create"
--keyring-id string ID of the KMS key ring
--name string The display name to distinguish multiple wrapping keys
--protection string The underlying system that is responsible for protecting the wrapping key material. Possible values: ["wrap_symmetric_key" "wrap_asymmetric_key"]
--purpose string Purpose of the wrapping key. Possible values: ["wrap_symmetric_key" "wrap_asymmetric_key"]
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms wrapping-key](./stackit_beta_kms_wrapping-key.md) - Manage KMS wrapping keys
## stackit beta kms wrapping-key delete
Deletes a KMS wrapping key
### Synopsis
Deletes a KMS wrapping key inside a specific key ring.
```
stackit beta kms wrapping-key delete WRAPPING_KEY_ID [flags]
```
### Examples
```
Delete a KMS wrapping key "MY_WRAPPING_KEY_ID" inside the key ring "my-keyring-id"
$ stackit beta kms wrapping-key delete "MY_WRAPPING_KEY_ID" --keyring-id "my-keyring-id"
```
### Options
```
-h, --help Help for "stackit beta kms wrapping-key delete"
--keyring-id string ID of the KMS key ring where the wrapping key is stored
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms wrapping-key](./stackit_beta_kms_wrapping-key.md) - Manage KMS wrapping keys
## stackit beta kms wrapping-key list
Lists all KMS wrapping keys
### Synopsis
Lists all KMS wrapping keys inside a key ring.
```
stackit beta kms wrapping-key list [flags]
```
### Examples
```
List all KMS wrapping keys for the key ring "my-keyring-id"
$ stackit beta kms wrapping-key list --keyring-id "my-keyring-id"
List all KMS wrapping keys in JSON format
$ stackit beta kms wrapping-key list --keyring-id "my-keyring-id" --output-format json
```
### Options
```
-h, --help Help for "stackit beta kms wrapping-key list"
--keyring-id string ID of the KMS key ring where the key is stored
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms wrapping-key](./stackit_beta_kms_wrapping-key.md) - Manage KMS wrapping keys
## stackit beta kms wrapping-key
Manage KMS wrapping keys
### Synopsis
Provides functionality for wrapping key operations inside the KMS
```
stackit beta kms wrapping-key [flags]
```
### Options
```
-h, --help Help for "stackit beta kms wrapping-key"
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS
* [stackit beta kms wrapping-key create](./stackit_beta_kms_wrapping-key_create.md) - Creates a KMS wrapping key
* [stackit beta kms wrapping-key delete](./stackit_beta_kms_wrapping-key_delete.md) - Deletes a KMS wrapping key
* [stackit beta kms wrapping-key list](./stackit_beta_kms_wrapping-key_list.md) - Lists all KMS wrapping keys
## stackit beta kms
Provides functionality for KMS
### Synopsis
Provides functionality for KMS.
```
stackit beta kms [flags]
```
### Options
```
-h, --help Help for "stackit beta kms"
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands
* [stackit beta kms key](./stackit_beta_kms_key.md) - Manage KMS keys
* [stackit beta kms keyring](./stackit_beta_kms_keyring.md) - Manage KMS key rings
* [stackit beta kms version](./stackit_beta_kms_version.md) - Manage KMS key versions
* [stackit beta kms wrapping-key](./stackit_beta_kms_wrapping-key.md) - Manage KMS wrapping keys
package create
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu01"
testAlgorithm = "rsa_2048_oaep_sha256"
testDisplayName = "my-key"
testPurpose = "asymmetric_encrypt_decrypt"
testDescription = "my key description"
testImportOnly = "true"
testProtection = "software"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
)
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
algorithmFlag: testAlgorithm,
displayNameFlag: testDisplayName,
purposeFlag: testPurpose,
descriptionFlag: testDescription,
importOnlyFlag: testImportOnly,
protectionFlag: testProtection,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
Algorithm: utils.Ptr(testAlgorithm),
Name: utils.Ptr(testDisplayName),
Purpose: utils.Ptr(testPurpose),
Description: utils.Ptr(testDescription),
ImportOnly: true, // Watch out: ImportOnly is not testImportOnly!
Protection: utils.Ptr(testProtection),
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiCreateKeyRequest)) kms.ApiCreateKeyRequest {
request := testClient.CreateKey(testCtx, testProjectId, testRegion, testKeyRingId)
request = request.CreateKeyPayload(kms.CreateKeyPayload{
Algorithm: kms.CreateKeyPayloadGetAlgorithmAttributeType(utils.Ptr(testAlgorithm)),
DisplayName: utils.Ptr(testDisplayName),
Purpose: kms.CreateKeyPayloadGetPurposeAttributeType(utils.Ptr(testPurpose)),
Description: utils.Ptr(testDescription),
ImportOnly: utils.Ptr(true),
Protection: kms.CreateKeyPayloadGetProtectionAttributeType(utils.Ptr(testProtection)),
})
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: "optional flags omitted",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, descriptionFlag)
delete(flagValues, importOnlyFlag)
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Description = nil
model.ImportOnly = false
}),
},
{
description: "no values provided",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key ring id missing (required)",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "key ring id invalid",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "algorithm missing (required)",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, algorithmFlag)
}),
isValid: false,
},
{
description: "protection missing (required)",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, protectionFlag)
}),
isValid: false,
},
{
description: "name missing (required)",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, displayNameFlag)
}),
isValid: false,
},
{
description: "purpose missing (required)",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, purposeFlag)
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
cmd := &cobra.Command{}
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
configureFlags(cmd)
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)
}
p := print.NewPrinter()
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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiCreateKeyRequest
}{
{
description: "base case",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
{
description: "no optional values",
model: fixtureInputModel(func(model *inputModel) {
model.Description = nil
model.ImportOnly = false
}),
expectedRequest: fixtureRequest().CreateKeyPayload(kms.CreateKeyPayload{
Algorithm: kms.CreateKeyPayloadGetAlgorithmAttributeType(utils.Ptr(testAlgorithm)),
DisplayName: utils.Ptr(testDisplayName),
Purpose: kms.CreateKeyPayloadGetPurposeAttributeType(utils.Ptr(testPurpose)),
Description: nil,
ImportOnly: utils.Ptr(false),
Protection: kms.CreateKeyPayloadGetProtectionAttributeType(utils.Ptr(testProtection)),
}),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request, err := buildRequest(testCtx, tt.model, testClient)
if err != nil {
t.Fatalf("error building request: %v", err)
}
diff := cmp.Diff(tt.expectedRequest, request,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
model *inputModel
key *kms.Key
wantErr bool
}{
{
description: "nil response",
key: nil,
wantErr: true,
},
{
description: "default output",
key: &kms.Key{},
model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}},
wantErr: false,
},
{
description: "json output",
key: &kms.Key{},
model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.JSONOutputFormat}},
wantErr: false,
},
{
description: "yaml output",
key: &kms.Key{},
model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.YAMLOutputFormat}},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.model, tt.key)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package create
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
"github.com/stackitcloud/stackit-sdk-go/services/kms/wait"
)
const (
keyRingIdFlag = "keyring-id"
algorithmFlag = "algorithm"
descriptionFlag = "description"
displayNameFlag = "name"
importOnlyFlag = "import-only"
purposeFlag = "purpose"
protectionFlag = "protection"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyRingId string
Algorithm *string
Description *string
Name *string
ImportOnly bool // Default false
Purpose *string
Protection *string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a KMS key",
Long: "Creates a KMS key.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a symmetric AES key (AES-256) with the name "symm-aes-gcm" under the key ring "my-keyring-id"`,
`$ stackit beta kms key create --keyring-id "my-keyring-id" --algorithm "aes_256_gcm" --name "symm-aes-gcm" --purpose "symmetric_encrypt_decrypt" --protection "software"`),
examples.NewExample(
`Create an asymmetric RSA encryption key (RSA-2048)`,
`$ stackit beta kms key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256" --name "prod-orders-rsa" --purpose "asymmetric_encrypt_decrypt" --protection "software"`),
examples.NewExample(
`Create a message authentication key (HMAC-SHA512)`,
`$ stackit beta kms key create --keyring-id "my-keyring-id" --algorithm "hmac_sha512" --name "api-mac-key" --purpose "message_authentication_code" --protection "software"`),
examples.NewExample(
`Create an ECDSA P-256 key for signing & verification`,
`$ stackit beta kms key create --keyring-id "my-keyring-id" --algorithm "ecdsa_p256_sha256" --name "signing-ecdsa-p256" --purpose "asymmetric_sign_verify" --protection "software"`),
examples.NewExample(
`Create an import-only key (versions must be imported)`,
`$ stackit beta kms key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256" --name "ext-managed-rsa" --purpose "asymmetric_encrypt_decrypt" --protection "software" --import-only`),
examples.NewExample(
`Create a key and print the result as YAML`,
`$ stackit beta kms key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256" --name "yaml-output-rsa" --purpose "asymmetric_encrypt_decrypt" --protection "software" --output yaml`),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
if !model.AssumeYes {
err = params.Printer.PromptForConfirmation("Are you sure you want to create a KMS Key?")
if err != nil {
return err
}
}
// Call API
req, _ := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("create KMS key: %w", err)
}
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(params.Printer)
s.Start("Creating key")
_, err = wait.CreateOrUpdateKeyWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.KeyRingId, *resp.Id).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for KMS key creation: %w", err)
}
s.Stop()
}
return outputResult(params.Printer, model, resp)
},
}
configureFlags(cmd)
return cmd
}
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,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
Algorithm: flags.FlagToStringPointer(p, cmd, algorithmFlag),
Name: flags.FlagToStringPointer(p, cmd, displayNameFlag),
Description: flags.FlagToStringPointer(p, cmd, descriptionFlag),
ImportOnly: flags.FlagToBoolValue(p, cmd, importOnlyFlag),
Purpose: flags.FlagToStringPointer(p, cmd, purposeFlag),
Protection: flags.FlagToStringPointer(p, cmd, protectionFlag),
}
p.DebugInputModel(model)
return &model, nil
}
type kmsKeyClient interface {
CreateKey(ctx context.Context, projectId string, regionId string, keyRingId string) kms.ApiCreateKeyRequest
}
func buildRequest(ctx context.Context, model *inputModel, apiClient kmsKeyClient) (kms.ApiCreateKeyRequest, error) {
req := apiClient.CreateKey(ctx, model.ProjectId, model.Region, model.KeyRingId)
req = req.CreateKeyPayload(kms.CreateKeyPayload{
DisplayName: model.Name,
Description: model.Description,
Algorithm: kms.CreateKeyPayloadGetAlgorithmAttributeType(model.Algorithm),
Purpose: kms.CreateKeyPayloadGetPurposeAttributeType(model.Purpose),
ImportOnly: &model.ImportOnly,
Protection: kms.CreateKeyPayloadGetProtectionAttributeType(model.Protection),
})
return req, nil
}
func outputResult(p *print.Printer, model *inputModel, resp *kms.Key) error {
if resp == nil {
return fmt.Errorf("response is nil")
}
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal KMS key: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal KMS key: %w", err)
}
p.Outputln(string(details))
default:
operationState := "Created"
if model.Async {
operationState = "Triggered creation of"
}
p.Outputf("%s the KMS key %q. Key ID: %s\n", operationState, utils.PtrString(resp.DisplayName), utils.PtrString(resp.Id))
}
return nil
}
func configureFlags(cmd *cobra.Command) {
// Algorithm
var algorithmFlagOptions []string
for _, val := range kms.AllowedAlgorithmEnumValues {
algorithmFlagOptions = append(algorithmFlagOptions, string(val))
}
cmd.Flags().Var(flags.EnumFlag(false, "", algorithmFlagOptions...), algorithmFlag, fmt.Sprintf("En-/Decryption / signing algorithm. Possible values: %q", algorithmFlagOptions))
// Purpose
var purposeFlagOptions []string
for _, val := range kms.AllowedPurposeEnumValues {
purposeFlagOptions = append(purposeFlagOptions, string(val))
}
cmd.Flags().Var(flags.EnumFlag(false, "", purposeFlagOptions...), purposeFlag, fmt.Sprintf("Purpose of the key. Possible values: %q", purposeFlagOptions))
// Protection
var protectionFlagOptions []string
for _, val := range kms.AllowedProtectionEnumValues {
protectionFlagOptions = append(protectionFlagOptions, string(val))
}
cmd.Flags().Var(flags.EnumFlag(false, "", protectionFlagOptions...), protectionFlag, fmt.Sprintf("The underlying system that is responsible for protecting the key material. Possible values: %q", purposeFlagOptions))
// All further non Enum Flags
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring")
cmd.Flags().String(displayNameFlag, "", "The display name to distinguish multiple keys")
cmd.Flags().String(descriptionFlag, "", "Optional description of the key")
cmd.Flags().Bool(importOnlyFlag, false, "States whether versions can be created or only imported")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, algorithmFlag, purposeFlag, displayNameFlag, protectionFlag)
cobra.CheckErr(err)
}
package delete
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu02"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
testKeyId = uuid.NewString()
)
// Args
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testKeyId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
KeyId: testKeyId,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiDeleteKeyRequest)) kms.ApiDeleteKeyRequest {
request := testClient.DeleteKey(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId)
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(),
expectedModel: fixtureInputModel(),
isValid: true,
},
{
description: "no args (keyId)",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key ring id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "key ring id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = ""
}),
isValid: false,
},
{
description: "key ring id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key id invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "key 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(&params.CmdParams{Printer: p})
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %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, 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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiDeleteKeyRequest
}{
{
description: "base case",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
wantErr bool
outputFormat string
resp *kms.Key
}{
{
description: "nil response",
resp: nil,
wantErr: true,
},
{
description: "default output",
resp: &kms.Key{},
wantErr: false,
},
{
description: "json output",
outputFormat: print.JSONOutputFormat,
resp: &kms.Key{},
wantErr: false,
},
{
description: "yaml output",
outputFormat: print.YAMLOutputFormat,
resp: &kms.Key{},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.outputFormat, tt.resp)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package delete
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"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"
kmsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
keyIdArg = "KEY_ID"
keyRingIdFlag = "keyring-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyId string
KeyRingId string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("delete %s", keyIdArg),
Short: "Deletes a KMS key",
Long: "Deletes a KMS key inside a specific key ring.",
Args: args.SingleArg(keyIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Delete a KMS key "MY_KEY_ID" inside the key ring "my-keyring-id"`,
`$ stackit beta kms key delete "MY_KEY_ID" --keyring-id "my-keyring-id"`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
keyName, err := kmsUtils.GetKeyName(ctx, apiClient, model.ProjectId, model.Region, model.KeyRingId, model.KeyId)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get key name: %v", err)
keyName = model.KeyId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete key %q? (This cannot be undone)", keyName)
err = params.Printer.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
err = req.Execute()
if err != nil {
return fmt.Errorf("delete KMS key: %w", err)
}
// Don't wait for a month until the deletion was performed.
// Just print the deletion date.
resp, err := apiClient.GetKeyExecute(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get key: %v", err)
}
return outputResult(params.Printer, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
keyId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
KeyId: keyId,
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiDeleteKeyRequest {
req := apiClient.DeleteKey(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId)
return req
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring where the key is stored")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag)
cobra.CheckErr(err)
}
func outputResult(p *print.Printer, outputFormat string, resp *kms.Key) error {
if resp == nil {
return fmt.Errorf("response is nil")
}
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal output to JSON: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal output to YAML: %w", err)
}
p.Outputln(string(details))
default:
p.Outputf("Deletion of KMS key %s scheduled successfully for the deletion date: %s\n", utils.PtrString(resp.DisplayName), utils.PtrString(resp.DeletionDate))
}
return nil
}
package importKey
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu01"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
testKeyId = uuid.NewString()
testWrappingKeyId = uuid.NewString()
testWrappedKey = "SnVzdCBzYXlpbmcgaGV5Oyk="
)
// Args
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testKeyId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
wrappedKeyFlag: testWrappedKey,
wrappingKeyIdFlag: testWrappingKeyId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
KeyId: testKeyId,
WrappedKey: &testWrappedKey,
WrappingKeyId: &testWrappingKeyId,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiImportKeyRequest)) kms.ApiImportKeyRequest {
request := testClient.ImportKey(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId)
request = request.ImportKeyPayload(kms.ImportKeyPayload{
WrappedKey: &testWrappedKey,
WrappingKeyId: &testWrappingKeyId,
})
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 args (keyId)",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no values provided",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key ring id missing (required)",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "key ring id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = ""
}),
isValid: false,
},
{
description: "key ring id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key id invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "key id invalid 2",
argValues: []string{"invalid-key"},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "wrapping key id missing (required)",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, wrappingKeyIdFlag)
}),
isValid: false,
},
{
description: "wrapping key id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[wrappingKeyIdFlag] = ""
}),
isValid: false,
},
{
description: "wrapping key id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[wrappingKeyIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "wrapped key missing (required)",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, wrappedKeyFlag)
}),
isValid: false,
},
{
description: "wrapped key invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[wrappedKeyFlag] = ""
}),
isValid: false,
},
{
description: "wrapped key invalid 2 - not base64",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[wrappedKeyFlag] = "Not Base 64"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(&params.CmdParams{Printer: p})
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %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, 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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiImportKeyRequest
}{
{
description: "base case",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request, err := buildRequest(testCtx, tt.model, testClient)
if err != nil {
t.Fatalf("error building request: %v", err)
}
diff := cmp.Diff(tt.expectedRequest, request,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
version *kms.Version
outputFormat string
keyRingName string
keyName string
wantErr bool
}{
{
description: "nil response",
version: nil,
wantErr: true,
},
{
description: "default output",
version: &kms.Version{},
keyRingName: "my-key-ring",
keyName: "my-key",
wantErr: false,
},
{
description: "json output",
version: &kms.Version{},
outputFormat: print.JSONOutputFormat,
keyRingName: "my-key-ring",
keyName: "my-key",
wantErr: false,
},
{
description: "yaml output",
version: &kms.Version{},
outputFormat: print.YAMLOutputFormat,
keyRingName: "my-key-ring",
keyName: "my-key",
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.outputFormat, tt.keyRingName, tt.keyName, tt.version)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package importKey
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client"
kmsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"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-sdk-go/services/kms"
)
const (
keyIdArg = "KEY_ID"
keyRingIdFlag = "keyring-id"
wrappedKeyFlag = "wrapped-key"
wrappingKeyIdFlag = "wrapping-key-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyRingId string
KeyId string
WrappedKey *string
WrappingKeyId *string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("import %s", keyIdArg),
Short: "Import a KMS key",
Long: "After encrypting the secret with the wrapping key’s public key and Base64-encoding it, import it as a new version of the specified KMS key.",
Args: args.SingleArg(keyIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Import a new version for the given KMS key "MY_KEY_ID" from literal value`,
`$ stackit beta kms key import "MY_KEY_ID" --keyring-id "my-keyring-id" --wrapped-key "BASE64_VALUE" --wrapping-key-id "MY_WRAPPING_KEY_ID"`),
examples.NewExample(
`Import from a file`,
`$ stackit beta kms key import "MY_KEY_ID" --keyring-id "my-keyring-id" --wrapped-key "@path/to/wrapped.key.b64" --wrapping-key-id "MY_WRAPPING_KEY_ID"`,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
keyName, err := kmsUtils.GetKeyName(ctx, apiClient, model.ProjectId, model.Region, model.KeyRingId, model.KeyId)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get key name: %v", err)
keyName = model.KeyId
}
keyRingName, err := kmsUtils.GetKeyRingName(ctx, apiClient, model.ProjectId, model.KeyRingId, model.Region)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get key ring name: %v", err)
keyRingName = model.KeyRingId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to import a new version for the KMS Key %q inside the key ring %q?", keyName, keyRingName)
err = params.Printer.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req, _ := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("import KMS key: %w", err)
}
return outputResult(params.Printer, model.OutputFormat, keyRingName, keyName, resp)
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
keyId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &cliErr.ProjectIdError{}
}
// WrappedKey needs to be base64 encoded
var wrappedKey *string = flags.FlagToStringPointer(p, cmd, wrappedKeyFlag)
_, err := base64.StdEncoding.DecodeString(*wrappedKey)
if err != nil || *wrappedKey == "" {
return nil, &cliErr.FlagValidationError{
Flag: wrappedKeyFlag,
Details: "The 'wrappedKey' argument is required and needs to be base64 encoded (whether provided inline or via file).",
}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyId: keyId,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
WrappedKey: wrappedKey,
WrappingKeyId: flags.FlagToStringPointer(p, cmd, wrappingKeyIdFlag),
}
p.DebugInputModel(model)
return &model, nil
}
type kmsKeyClient interface {
ImportKey(ctx context.Context, projectId string, regionId string, keyRingId string, keyId string) kms.ApiImportKeyRequest
}
func buildRequest(ctx context.Context, model *inputModel, apiClient kmsKeyClient) (kms.ApiImportKeyRequest, error) {
req := apiClient.ImportKey(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId)
req = req.ImportKeyPayload(kms.ImportKeyPayload{
WrappedKey: model.WrappedKey,
WrappingKeyId: model.WrappingKeyId,
})
return req, nil
}
func outputResult(p *print.Printer, outputFormat, keyRingName, keyName string, resp *kms.Version) error {
if resp == nil {
return fmt.Errorf("response is nil")
}
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal KMS key: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal KMS key: %w", err)
}
p.Outputln(string(details))
default:
p.Outputf("Imported a new version for the key %q inside the key ring %q\n", keyName, keyRingName)
}
return nil
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring")
cmd.Flags().Var(flags.ReadFromFileFlag(), wrappedKeyFlag, "The wrapped key material to be imported. Base64-encoded. Pass the value directly or a file path (e.g. @path/to/wrapped.key.b64)")
cmd.Flags().Var(flags.UUIDFlag(), wrappingKeyIdFlag, "The unique id of the wrapping key the key material has been wrapped with")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, wrappedKeyFlag, wrappingKeyIdFlag)
cobra.CheckErr(err)
}
package key
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/importKey"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/list"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/restore"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/rotate"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
)
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "key",
Short: "Manage KMS keys",
Long: "Provides functionality for key operations inside the KMS",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, params)
return cmd
}
func addSubcommands(cmd *cobra.Command, params *params.CmdParams) {
cmd.AddCommand(create.NewCmd(params))
cmd.AddCommand(delete.NewCmd(params))
cmd.AddCommand(importKey.NewCmd(params))
cmd.AddCommand(list.NewCmd(params))
cmd.AddCommand(restore.NewCmd(params))
cmd.AddCommand(rotate.NewCmd(params))
}
package list
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu01"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
)
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiListKeysRequest)) kms.ApiListKeysRequest {
request := testClient.ListKeys(testCtx, testProjectId, testRegion, testKeyRingId)
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: "missing keyRingId",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "invalid keyRingId 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = ""
}),
isValid: false,
},
{
description: "invalid keyRingId 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "Not a valid uuid"
}),
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
cmd := &cobra.Command{}
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
configureFlags(cmd)
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)
}
p := print.NewPrinter()
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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiListKeysRequest
}{
{
description: "base case",
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(tt.expectedRequest, request,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
resp *kms.KeyList
projectId string
keyRingId string
outputFormat string
wantErr bool
}{
{
description: "nil response",
resp: nil,
projectId: uuid.NewString(),
keyRingId: uuid.NewString(),
wantErr: true,
},
{
description: "empty response",
resp: &kms.KeyList{},
projectId: uuid.NewString(),
keyRingId: uuid.NewString(),
wantErr: true,
},
{
description: "default output",
resp: &kms.KeyList{Keys: &[]kms.Key{}},
projectId: uuid.NewString(),
keyRingId: uuid.NewString(),
wantErr: false,
},
{
description: "json output",
resp: &kms.KeyList{Keys: &[]kms.Key{}},
projectId: uuid.NewString(),
keyRingId: uuid.NewString(),
outputFormat: print.JSONOutputFormat,
wantErr: false,
},
{
description: "yaml output",
resp: &kms.KeyList{Keys: &[]kms.Key{}},
projectId: uuid.NewString(),
keyRingId: uuid.NewString(),
outputFormat: print.YAMLOutputFormat,
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.outputFormat, tt.projectId, tt.keyRingId, tt.resp)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package list
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"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/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
keyRingIdFlag = "keyring-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyRingId string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List all KMS keys",
Long: "List all KMS keys inside a key ring.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all KMS keys for the key ring "my-keyring-id"`,
`$ stackit beta kms key list --keyring-id "my-keyring-id"`),
examples.NewExample(
`List all KMS keys in JSON format`,
`$ stackit beta kms key list --keyring-id "my-keyring-id" --output-format json`),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("get KMS Keys: %w", err)
}
return outputResult(params.Printer, model.OutputFormat, model.ProjectId, model.KeyRingId, resp)
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiListKeysRequest {
req := apiClient.ListKeys(ctx, model.ProjectId, model.Region, model.KeyRingId)
return req
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring where the key is stored")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag)
cobra.CheckErr(err)
}
func outputResult(p *print.Printer, outputFormat, projectId, keyRingId string, resp *kms.KeyList) error {
if resp == nil || resp.Keys == nil {
return fmt.Errorf("response was nil / empty")
}
keys := *resp.Keys
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(keys, "", " ")
if err != nil {
return fmt.Errorf("marshal KMS Keys list: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(keys, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal KMS Keys list: %w", err)
}
p.Outputln(string(details))
default:
if len(keys) == 0 {
p.Outputf("No keys found for project %q under the key ring %q\n", projectId, keyRingId)
return nil
}
table := tables.NewTable()
table.SetHeader("ID", "NAME", "SCOPE", "ALGORITHM", "DELETION DATE", "STATUS")
for _, key := range keys {
table.AddRow(
utils.PtrString(key.Id),
utils.PtrString(key.DisplayName),
utils.PtrString(key.Purpose),
utils.PtrString(key.Algorithm),
utils.PtrString(key.DeletionDate),
utils.PtrString(key.State),
)
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
}
return nil
}
package restore
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu02"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
testKeyId = uuid.NewString()
)
// Args
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testKeyId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
KeyId: testKeyId,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiRestoreKeyRequest)) kms.ApiRestoreKeyRequest {
request := testClient.RestoreKey(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId)
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(),
expectedModel: fixtureInputModel(),
isValid: true,
},
{
description: "no args (keyId)",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key ring id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "key ring id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = ""
}),
isValid: false,
},
{
description: "key ring id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key id invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "key 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(&params.CmdParams{Printer: p})
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %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, 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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiRestoreKeyRequest
}{
{
description: "base case",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
wantErr bool
outputFormat string
resp *kms.Key
}{
{
description: "nil response",
resp: nil,
wantErr: true,
},
{
description: "default output",
resp: &kms.Key{},
wantErr: false,
},
{
description: "json output",
outputFormat: print.JSONOutputFormat,
resp: &kms.Key{},
wantErr: false,
},
{
description: "yaml output",
outputFormat: print.YAMLOutputFormat,
resp: &kms.Key{},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.outputFormat, tt.resp)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package restore
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"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"
kmsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
keyIdArg = "KEY_ID"
keyRingIdFlag = "keyring-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyId string
KeyRingId string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("restore %s", keyIdArg),
Short: "Restore a key",
Long: "Restores the given key from deletion.",
Args: args.SingleArg(keyIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Restore a KMS key "MY_KEY_ID" inside the key ring "my-keyring-id" that was scheduled for deletion.`,
`$ stackit beta kms key restore "MY_KEY_ID" --keyring-id "my-keyring-id"`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
keyName, err := kmsUtils.GetKeyName(ctx, apiClient, model.ProjectId, model.Region, model.KeyRingId, model.KeyId)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get key name: %v", err)
keyName = model.KeyId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to restore key %q? (This cannot be undone)", keyName)
err = params.Printer.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
err = req.Execute()
if err != nil {
return fmt.Errorf("restore KMS key: %w", err)
}
// Grab the key after the restore was applied to display the new state to the user.
resp, err := apiClient.GetKeyExecute(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get key: %v", err)
}
return outputResult(params.Printer, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
keyId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
KeyId: keyId,
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiRestoreKeyRequest {
req := apiClient.RestoreKey(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId)
return req
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring where the key is stored")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag)
cobra.CheckErr(err)
}
func outputResult(p *print.Printer, outputFormat string, resp *kms.Key) error {
if resp == nil {
return fmt.Errorf("response is nil")
}
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal output to JSON: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal output to YAML: %w", err)
}
p.Outputln(string(details))
default:
p.Outputf("Successfully restored KMS key %q\n", utils.PtrString(resp.DisplayName))
}
return nil
}
package rotate
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu02"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
testKeyId = uuid.NewString()
)
// Args
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testKeyId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
KeyId: testKeyId,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiRotateKeyRequest)) kms.ApiRotateKeyRequest {
request := testClient.RotateKey(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId)
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(),
expectedModel: fixtureInputModel(),
isValid: true,
},
{
description: "no args (keyId)",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key ring id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "key ring id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = ""
}),
isValid: false,
},
{
description: "key ring id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key id invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "key 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(&params.CmdParams{Printer: p})
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %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, 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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiRotateKeyRequest
}{
{
description: "base case",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
resp *kms.Version
outputFormat string
wantErr bool
}{
{
description: "nil response",
resp: nil,
wantErr: true,
},
{
description: "default output",
resp: &kms.Version{},
wantErr: false,
},
{
description: "json output",
resp: &kms.Version{},
outputFormat: print.JSONOutputFormat,
wantErr: false,
},
{
description: "yaml output",
resp: &kms.Version{},
outputFormat: print.YAMLOutputFormat,
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.outputFormat, tt.resp)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package rotate
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"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"
kmsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
keyIdArg = "KEY_ID"
keyRingIdFlag = "keyring-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyId string
KeyRingId string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("rotate %s", keyIdArg),
Short: "Rotate a key",
Long: "Rotates the given key.",
Args: args.SingleArg(keyIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Rotate a KMS key "MY_KEY_ID" and increase its version inside the key ring "my-keyring-id".`,
`$ stackit beta kms key rotate "MY_KEY_ID" --keyring-id "my-keyring-id"`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
keyName, err := kmsUtils.GetKeyName(ctx, apiClient, model.ProjectId, model.Region, model.KeyRingId, model.KeyId)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get key name: %v", err)
keyName = model.KeyId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to rotate the key %q? (this cannot be undone)", keyName)
err = params.Printer.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("rotate KMS key: %w", err)
}
return outputResult(params.Printer, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
keyId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
KeyId: keyId,
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiRotateKeyRequest {
req := apiClient.RotateKey(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId)
return req
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring where the key is stored")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag)
cobra.CheckErr(err)
}
func outputResult(p *print.Printer, outputFormat string, resp *kms.Version) error {
if resp == nil {
return fmt.Errorf("response is nil")
}
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal KMS key version: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal KMS key version: %w", err)
}
p.Outputln(string(details))
default:
p.Outputf("Rotated key %s\n", utils.PtrString(resp.KeyId))
}
return nil
}
package create
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu01"
testKeyRingName = "my-key-ring"
testDescription = "my-description"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
)
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingNameFlag: testKeyRingName,
descriptionFlag: testDescription,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyringName: testKeyRingName,
Description: testDescription,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiCreateKeyRingRequest)) kms.ApiCreateKeyRingRequest {
request := testClient.CreateKeyRing(testCtx, testProjectId, testRegion)
request = request.CreateKeyRingPayload(kms.CreateKeyRingPayload{
DisplayName: utils.Ptr(testKeyRingName),
Description: utils.Ptr(testDescription),
})
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: "optional flags omitted",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, descriptionFlag)
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Description = ""
}),
},
{
description: "no values provided",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
cmd := &cobra.Command{}
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
configureFlags(cmd)
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)
}
p := print.NewPrinter()
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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiCreateKeyRingRequest
}{
{
description: "base case",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request, err := buildRequest(testCtx, tt.model, testClient)
if err != nil {
t.Fatalf("error building request: %v", err)
}
diff := cmp.Diff(tt.expectedRequest, request,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
model *inputModel
description string
keyRing *kms.KeyRing
wantErr bool
}{
{
description: "nil response",
keyRing: nil,
wantErr: true,
},
{
description: "default output",
model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}},
keyRing: &kms.KeyRing{},
wantErr: false,
},
{
description: "json output",
model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.JSONOutputFormat}},
keyRing: &kms.KeyRing{},
wantErr: false,
},
{
description: "yaml output",
model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.YAMLOutputFormat}},
keyRing: &kms.KeyRing{},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.model, tt.keyRing)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package create
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
"github.com/stackitcloud/stackit-sdk-go/services/kms/wait"
)
const (
keyRingNameFlag = "name"
descriptionFlag = "description"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyringName string
Description string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a KMS key ring",
Long: "Creates a KMS key ring.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a KMS key ring with name "my-keyring"`,
"$ stackit beta kms keyring create --name my-keyring"),
examples.NewExample(
`Create a KMS key ring with a description`,
"$ stackit beta kms keyring create --name my-keyring --description my-description"),
examples.NewExample(
`Create a KMS key ring and print the result as YAML`,
"$ stackit beta kms keyring create --name my-keyring -o yaml"),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
if !model.AssumeYes {
err = params.Printer.PromptForConfirmation("Are you sure you want to create a KMS key ring?")
if err != nil {
return err
}
}
// Call API
req, _ := buildRequest(ctx, model, apiClient)
keyRing, err := req.Execute()
if err != nil {
return fmt.Errorf("create KMS key ring: %w", err)
}
// Prevent potential nil pointer dereference
if keyRing == nil || keyRing.Id == nil {
return fmt.Errorf("API call succeeded but returned an invalid response (missing key ring ID)")
}
keyRingId := *keyRing.Id
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(params.Printer)
s.Start("Creating key ring")
_, err = wait.CreateKeyRingWaitHandler(ctx, apiClient, model.ProjectId, model.Region, keyRingId).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for KMS key ring creation: %w", err)
}
s.Stop()
}
return outputResult(params.Printer, model, keyRing)
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &cliErr.ProjectIdError{}
}
keyringName := flags.FlagToStringValue(p, cmd, keyRingNameFlag)
if keyringName == "" {
return nil, &cliErr.DSAInputPlanError{
Cmd: cmd,
}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyringName: keyringName,
Description: flags.FlagToStringValue(p, cmd, descriptionFlag),
}
p.DebugInputModel(model)
return &model, nil
}
type kmsKeyringClient interface {
CreateKeyRing(ctx context.Context, projectId string, regionId string) kms.ApiCreateKeyRingRequest
}
func buildRequest(ctx context.Context, model *inputModel, apiClient kmsKeyringClient) (kms.ApiCreateKeyRingRequest, error) {
req := apiClient.CreateKeyRing(ctx, model.ProjectId, model.Region)
req = req.CreateKeyRingPayload(kms.CreateKeyRingPayload{
DisplayName: &model.KeyringName,
// Description should be empty by default and only be overwritten with the descriptionFlag if it was passed.
Description: &model.Description,
})
return req, nil
}
func outputResult(p *print.Printer, model *inputModel, resp *kms.KeyRing) error {
if resp == nil {
return fmt.Errorf("response is nil")
}
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal KMS key ring: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal KMS key ring: %w", err)
}
p.Outputln(string(details))
default:
operationState := "Created"
if model.Async {
operationState = "Triggered creation of"
}
p.Outputf("%s key ring. KMS key ring ID: %s\n", operationState, utils.PtrString(resp.Id))
}
return nil
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().String(keyRingNameFlag, "", "Name of the KMS key ring")
cmd.Flags().String(descriptionFlag, "", "Optional description of the key ring")
err := flags.MarkFlagsRequired(cmd, keyRingNameFlag)
cobra.CheckErr(err)
}
package delete
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu01"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
)
// Args
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testKeyRingId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiDeleteKeyRingRequest)) kms.ApiDeleteKeyRingRequest {
request := testClient.DeleteKeyRing(testCtx, testProjectId, testRegion, testKeyRingId)
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 args (keyRingId)",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "invalid keyRingId",
argValues: fixtureArgValues(func(argValues []string) {
argValues[0] = "Not an uuid"
}),
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(&params.CmdParams{Printer: 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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiDeleteKeyRingRequest
}{
{
description: "base case",
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(tt.expectedRequest, request,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package delete
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"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"
kmsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
keyRingIdArg = "KEYRING-ID"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyRingId string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("delete %s", keyRingIdArg),
Short: "Deletes a KMS key ring",
Long: "Deletes a KMS key ring.",
Args: args.SingleArg(keyRingIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Delete a KMS key ring with ID "MY_KEYRING_ID"`,
`$ stackit beta kms keyring delete "MY_KEYRING_ID"`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
keyRingLabel, err := kmsUtils.GetKeyRingName(ctx, apiClient, model.ProjectId, model.KeyRingId, model.Region)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get key ring name: %v", err)
keyRingLabel = model.KeyRingId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete key ring %q? (this cannot be undone)", keyRingLabel)
err = params.Printer.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
err = req.Execute()
if err != nil {
return fmt.Errorf("delete KMS key ring: %w", err)
}
// No async wait required; key ring deletion is synchronous.
// Don't output anything. It's a deletion.
params.Printer.Info("Deleted the key ring %q\n", keyRingLabel)
return nil
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
keyRingId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyRingId: keyRingId,
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiDeleteKeyRingRequest {
req := apiClient.DeleteKeyRing(ctx, model.ProjectId, model.Region, model.KeyRingId)
return req
}
package keyring
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/list"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
)
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "keyring",
Short: "Manage KMS key rings",
Long: "Provides functionality for key ring operations inside the KMS",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, params)
return cmd
}
func addSubcommands(cmd *cobra.Command, params *params.CmdParams) {
cmd.AddCommand(list.NewCmd(params))
cmd.AddCommand(delete.NewCmd(params))
cmd.AddCommand(create.NewCmd(params))
}
package list
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu01"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
)
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiListKeyRingsRequest)) kms.ApiListKeyRingsRequest {
request := testClient.ListKeyRings(testCtx, testProjectId, testRegion)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values provided",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
cmd := &cobra.Command{}
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)
}
p := print.NewPrinter()
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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiListKeyRingsRequest
}{
{
description: "base case",
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(tt.expectedRequest, request,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
projectId string
resp *kms.KeyRingList
outputFormat string
projectLabel string
wantErr bool
}{
{
description: "nil response",
resp: nil,
projectId: uuid.NewString(),
projectLabel: "my-project",
wantErr: true,
},
{
description: "empty response",
resp: &kms.KeyRingList{},
projectId: uuid.NewString(),
projectLabel: "my-project",
wantErr: true,
},
{
description: "default output",
projectId: uuid.NewString(),
resp: &kms.KeyRingList{KeyRings: &[]kms.KeyRing{}},
projectLabel: "my-project",
wantErr: false,
},
{
description: "json output",
projectId: uuid.NewString(),
resp: &kms.KeyRingList{KeyRings: &[]kms.KeyRing{}},
outputFormat: print.JSONOutputFormat,
wantErr: false,
},
{
description: "yaml output",
projectId: uuid.NewString(),
resp: &kms.KeyRingList{KeyRings: &[]kms.KeyRing{}},
outputFormat: print.YAMLOutputFormat,
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.outputFormat, tt.projectId, tt.resp)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package list
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"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/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
type inputModel struct {
*globalflags.GlobalFlagModel
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all KMS key rings",
Long: "Lists all KMS key rings.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all KMS key rings`,
"$ stackit beta kms keyring list"),
examples.NewExample(
`List all KMS key rings in JSON format`,
"$ stackit beta kms keyring list --output-format json"),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("get KMS key rings: %w", err)
}
return outputResult(params.Printer, model.OutputFormat, model.ProjectId, resp)
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiListKeyRingsRequest {
req := apiClient.ListKeyRings(ctx, model.ProjectId, model.Region)
return req
}
func outputResult(p *print.Printer, outputFormat, projectId string, resp *kms.KeyRingList) error {
if resp == nil || resp.KeyRings == nil {
return fmt.Errorf("response was nil / empty")
}
keyRings := *resp.KeyRings
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(keyRings, "", " ")
if err != nil {
return fmt.Errorf("marshal KMS key rings list: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(keyRings, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal KMS key rings list: %w", err)
}
p.Outputln(string(details))
default:
if len(keyRings) == 0 {
p.Outputf("No key rings found for project %q\n", projectId)
return nil
}
table := tables.NewTable()
table.SetHeader("ID", "NAME", "STATUS")
for i := range keyRings {
keyRing := keyRings[i]
table.AddRow(
utils.PtrString(keyRing.Id),
utils.PtrString(keyRing.DisplayName),
utils.PtrString(keyRing.State),
)
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
}
return nil
}
package kms
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/version"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
)
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "kms",
Short: "Provides functionality for KMS",
Long: "Provides functionality for KMS.",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, params)
return cmd
}
func addSubcommands(cmd *cobra.Command, params *params.CmdParams) {
cmd.AddCommand(keyring.NewCmd(params))
cmd.AddCommand(wrappingkey.NewCmd(params))
cmd.AddCommand(key.NewCmd(params))
cmd.AddCommand(version.NewCmd(params))
}
package destroy
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu02"
testVersionNumber = int64(1)
testVersionNumberString = "1"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
testKeyId = uuid.NewString()
)
// Args
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testVersionNumberString,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
keyIdFlag: testKeyId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
KeyId: testKeyId,
VersionNumber: testVersionNumber,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiDestroyVersionRequest)) kms.ApiDestroyVersionRequest {
request := testClient.DestroyVersion(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId, testVersionNumber)
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(),
expectedModel: fixtureInputModel(),
isValid: true,
},
{
description: "no args (versionNumber)",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key ring id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "key ring id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = ""
}),
isValid: false,
},
{
description: "key ring id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyIdFlag)
}),
isValid: false,
},
{
description: "key id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyIdFlag] = ""
}),
isValid: false,
},
{
description: "key id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "version number invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "version number invalid 2",
argValues: []string{"Not a Number!"},
flagValues: fixtureFlagValues(),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(&params.CmdParams{Printer: p})
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %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, 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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiDestroyVersionRequest
}{
{
description: "base case",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
wantErr bool
outputFormat string
resp *kms.Version
}{
{
description: "nil response",
resp: nil,
wantErr: true,
},
{
description: "default output",
resp: &kms.Version{},
wantErr: false,
},
{
description: "json output",
resp: &kms.Version{},
wantErr: false,
},
{
description: "yaml output",
resp: &kms.Version{},
outputFormat: print.YAMLOutputFormat,
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.outputFormat, tt.resp)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package destroy
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"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/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
versionNumberArg = "VERSION_NUMBER"
keyRingIdFlag = "keyring-id"
keyIdFlag = "key-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyRingId string
KeyId string
VersionNumber int64
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("destroy %s", versionNumberArg),
Short: "Destroy a key version",
Long: "Removes the key material of a version.",
Args: args.SingleArg(versionNumberArg, nil),
Example: examples.Build(
examples.NewExample(
`Destroy key version "42" for the key "my-key-id" inside the key ring "my-keyring-id"`,
`$ stackit beta kms version destroy 42 --key-id "my-key-id" --keyring-id "my-keyring-id"`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
// This operation can be undone. Don't ask for confirmation!
// Call API
req := buildRequest(ctx, model, apiClient)
err = req.Execute()
if err != nil {
return fmt.Errorf("destroy key Version: %w", err)
}
// Get the key version in its state afterwards
resp, err := apiClient.GetVersionExecute(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get key version: %v", err)
}
return outputResult(params.Printer, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
versionStr := inputArgs[0]
versionNumber, err := strconv.ParseInt(versionStr, 10, 64)
if err != nil || versionNumber < 0 {
return nil, &errors.ArgValidationError{
Arg: versionNumberArg,
Details: fmt.Sprintf("invalid value %q: must be a positive integer", versionStr),
}
}
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
KeyId: flags.FlagToStringValue(p, cmd, keyIdFlag),
VersionNumber: versionNumber,
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiDestroyVersionRequest {
return apiClient.DestroyVersion(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber)
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring")
cmd.Flags().Var(flags.UUIDFlag(), keyIdFlag, "ID of the key")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, keyIdFlag)
cobra.CheckErr(err)
}
func outputResult(p *print.Printer, outputFormat string, resp *kms.Version) error {
if resp == nil {
return fmt.Errorf("response is nil")
}
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal KMS key: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal KMS key: %w", err)
}
p.Outputln(string(details))
default:
p.Outputf("Destroyed version %d of the key %q\n", utils.PtrValue(resp.Number), utils.PtrValue(resp.KeyId))
}
return nil
}
package disable
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu02"
testVersionNumber = int64(1)
testVersionNumberString = "1"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
testKeyId = uuid.NewString()
)
// Args
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testVersionNumberString,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
keyIdFlag: testKeyId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
KeyId: testKeyId,
VersionNumber: testVersionNumber,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiDisableVersionRequest)) kms.ApiDisableVersionRequest {
request := testClient.DisableVersion(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId, testVersionNumber)
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(),
expectedModel: fixtureInputModel(),
isValid: true,
},
{
description: "no args (versionNumber)",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key ring id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "key ring id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = ""
}),
isValid: false,
},
{
description: "key ring id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyIdFlag)
}),
isValid: false,
},
{
description: "key id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyIdFlag] = ""
}),
isValid: false,
},
{
description: "key id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "version number invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "version number invalid 2",
argValues: []string{"Not a Number!"},
flagValues: fixtureFlagValues(),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(&params.CmdParams{Printer: p})
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %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, 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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiDisableVersionRequest
}{
{
description: "base case",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
wantErr bool
outputFormat string
resp *kms.Version
}{
{
description: "nil response",
resp: nil,
wantErr: true,
},
{
description: "default output",
resp: &kms.Version{},
wantErr: false,
},
{
description: "json output",
outputFormat: print.JSONOutputFormat,
resp: &kms.Version{},
wantErr: false,
},
{
description: "yaml output",
outputFormat: print.YAMLOutputFormat,
resp: &kms.Version{},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.outputFormat, tt.resp)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package disable
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"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/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
versionNumberArg = "VERSION_NUMBER"
keyRingIdFlag = "keyring-id"
keyIdFlag = "key-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyRingId string
KeyId string
VersionNumber int64
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("disable %s", versionNumberArg),
Short: "Disable a key version",
Long: "Disable the given key version.",
Args: args.SingleArg(versionNumberArg, nil),
Example: examples.Build(
examples.NewExample(
`Disable key version "42" for the key "my-key-id" inside the key ring "my-keyring-id"`,
`$ stackit beta kms version disable 42 --key-id "my-key-id" --keyring-id "my-keyring-id"`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
// This operation can be undone. Don't ask for confirmation!
// Call API
req := buildRequest(ctx, model, apiClient)
err = req.Execute()
if err != nil {
return fmt.Errorf("disable key version: %w", err)
}
// kms v1.0.0 has a waiter, but it get's stuck even though the disable api call was already successfully completed.
// Get the key version in its state afterwards
resp, err := apiClient.GetVersionExecute(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get key version: %v", err)
}
return outputResult(params.Printer, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
versionStr := inputArgs[0]
versionNumber, err := strconv.ParseInt(versionStr, 10, 64)
if err != nil || versionNumber < 0 {
return nil, &errors.ArgValidationError{
Arg: versionNumberArg,
Details: fmt.Sprintf("invalid value %q: must be a positive integer", versionStr),
}
}
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
KeyId: flags.FlagToStringValue(p, cmd, keyIdFlag),
VersionNumber: versionNumber,
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiDisableVersionRequest {
return apiClient.DisableVersion(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber)
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring")
cmd.Flags().Var(flags.UUIDFlag(), keyIdFlag, "ID of the key")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, keyIdFlag)
cobra.CheckErr(err)
}
func outputResult(p *print.Printer, outputFormat string, resp *kms.Version) error {
if resp == nil {
return fmt.Errorf("response is nil")
}
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal KMS key: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal KMS key: %w", err)
}
p.Outputln(string(details))
default:
p.Outputf("Disabled version %d of the key %q\n", utils.PtrValue(resp.Number), utils.PtrValue(resp.KeyId))
}
return nil
}
package enable
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu02"
testVersionNumber = int64(1)
testVersionNumberString = "1"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
testKeyId = uuid.NewString()
)
// Args
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testVersionNumberString,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
keyIdFlag: testKeyId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
KeyId: testKeyId,
VersionNumber: testVersionNumber,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiEnableVersionRequest)) kms.ApiEnableVersionRequest {
request := testClient.EnableVersion(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId, testVersionNumber)
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(),
expectedModel: fixtureInputModel(),
isValid: true,
},
{
description: "no args (versionNumber)",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key ring id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "key ring id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = ""
}),
isValid: false,
},
{
description: "key ring id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyIdFlag)
}),
isValid: false,
},
{
description: "key id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyIdFlag] = ""
}),
isValid: false,
},
{
description: "key id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "version number invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "version number invalid 2",
argValues: []string{"Not a Number!"},
flagValues: fixtureFlagValues(),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(&params.CmdParams{Printer: p})
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %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, 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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiEnableVersionRequest
}{
{
description: "base case",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
wantErr bool
outputFormat string
resp *kms.Version
}{
{
description: "nil response",
resp: nil,
wantErr: true,
},
{
description: "default output",
resp: &kms.Version{},
wantErr: false,
},
{
description: "json output",
resp: &kms.Version{},
outputFormat: print.JSONOutputFormat,
wantErr: false,
},
{
description: "yaml output",
resp: &kms.Version{},
outputFormat: print.YAMLOutputFormat,
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.outputFormat, tt.resp)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package enable
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"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/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
"github.com/stackitcloud/stackit-sdk-go/services/kms/wait"
)
const (
versionNumberArg = "VERSION_NUMBER"
keyRingIdFlag = "keyring-id"
keyIdFlag = "key-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyRingId string
KeyId string
VersionNumber int64
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("enable %s", versionNumberArg),
Short: "Enable a key version",
Long: "Enable the given key version.",
Args: args.SingleArg(versionNumberArg, nil),
Example: examples.Build(
examples.NewExample(
`Enable key version "42" for the key "my-key-id" inside the key ring "my-keyring-id"`,
`$ stackit beta kms version enable 42 --key-id "my-key-id" --keyring-id "my-keyring-id"`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
// This operation can be undone. Don't ask for confirmation!
// Call API
req := buildRequest(ctx, model, apiClient)
err = req.Execute()
if err != nil {
return fmt.Errorf("enable key version: %w", err)
}
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(params.Printer)
s.Start("Enabling key version")
_, err = wait.EnableKeyVersionWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for key version to be enabled: %w", err)
}
s.Stop()
}
// Get the key version in its state afterwards
resp, err := apiClient.GetVersionExecute(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get key version: %v", err)
}
return outputResult(params.Printer, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
versionStr := inputArgs[0]
versionNumber, err := strconv.ParseInt(versionStr, 10, 64)
if err != nil || versionNumber < 0 {
return nil, &errors.ArgValidationError{
Arg: versionNumberArg,
Details: fmt.Sprintf("invalid value %q: must be a positive integer", versionStr),
}
}
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
KeyId: flags.FlagToStringValue(p, cmd, keyIdFlag),
VersionNumber: versionNumber,
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiEnableVersionRequest {
return apiClient.EnableVersion(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber)
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring")
cmd.Flags().Var(flags.UUIDFlag(), keyIdFlag, "ID of the key")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, keyIdFlag)
cobra.CheckErr(err)
}
func outputResult(p *print.Printer, outputFormat string, resp *kms.Version) error {
if resp == nil {
return fmt.Errorf("response is nil")
}
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal KMS key: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal KMS key: %w", err)
}
p.Outputln(string(details))
default:
p.Outputf("Enabled version %d of the key %q\n", utils.PtrValue(resp.Number), utils.PtrValue(resp.KeyId))
}
return nil
}
package list
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu02"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
testKeyId = uuid.NewString()
)
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
keyIdFlag: testKeyId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
KeyId: testKeyId,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiListVersionsRequest)) kms.ApiListVersionsRequest {
request := testClient.ListVersions(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId)
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(),
expectedModel: fixtureInputModel(),
isValid: true,
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key ring id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "key ring id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = ""
}),
isValid: false,
},
{
description: "key ring id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyIdFlag)
}),
isValid: false,
},
{
description: "key id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyIdFlag] = ""
}),
isValid: false,
},
{
description: "key id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
cmd := &cobra.Command{}
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
configureFlags(cmd)
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)
}
p := print.NewPrinter()
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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiListVersionsRequest
}{
{
description: "base case",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
projectId string
keyId string
resp *kms.VersionList
outputFormat string
projectLabel string
wantErr bool
}{
{
description: "nil response",
resp: nil,
projectLabel: "my-project",
wantErr: true,
},
{
description: "empty default",
resp: &kms.VersionList{},
projectLabel: "my-project",
wantErr: true,
},
{
description: "default output",
resp: &kms.VersionList{Versions: &[]kms.Version{}},
projectLabel: "my-project",
wantErr: false,
},
{
description: "json output",
resp: &kms.VersionList{Versions: &[]kms.Version{}},
outputFormat: print.JSONOutputFormat,
wantErr: false,
},
{
description: "yaml output",
resp: &kms.VersionList{Versions: &[]kms.Version{}},
outputFormat: print.YAMLOutputFormat,
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.outputFormat, tt.projectId, tt.keyId, tt.resp)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package list
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"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/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
keyRingIdFlag = "keyring-id"
keyIdFlag = "key-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyRingId string
KeyId string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List all key versions",
Long: "List all versions of a given key.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all key versions for the key "my-key-id" inside the key ring "my-keyring-id"`,
`$ stackit beta kms version list --key-id "my-key-id" --keyring-id "my-keyring-id"`),
examples.NewExample(
`List all key versions in JSON format`,
`$ stackit beta kms version list --key-id "my-key-id" --keyring-id "my-keyring-id" -o json`),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("get key version: %w", err)
}
return outputResult(params.Printer, model.OutputFormat, model.ProjectId, model.KeyId, resp)
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
KeyId: flags.FlagToStringValue(p, cmd, keyIdFlag),
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiListVersionsRequest {
return apiClient.ListVersions(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId)
}
func outputResult(p *print.Printer, outputFormat, projectId, keyId string, resp *kms.VersionList) error {
if resp == nil || resp.Versions == nil {
return fmt.Errorf("response is nil / empty")
}
versions := *resp.Versions
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(versions, "", " ")
if err != nil {
return fmt.Errorf("marshal key versions list: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(versions, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal key versions list: %w", err)
}
p.Outputln(string(details))
default:
if len(versions) == 0 {
p.Outputf("No key versions found for project %q for the key %q\n", projectId, keyId)
return nil
}
table := tables.NewTable()
table.SetHeader("ID", "NUMBER", "CREATED AT", "DESTROY DATE", "STATUS")
for _, version := range versions {
table.AddRow(
utils.PtrString(version.KeyId),
utils.PtrString(version.Number),
utils.PtrString(version.CreatedAt),
utils.PtrString(version.DestroyDate),
utils.PtrString(version.State),
)
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
}
return nil
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring")
cmd.Flags().Var(flags.UUIDFlag(), keyIdFlag, "ID of the key")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, keyIdFlag)
cobra.CheckErr(err)
}
package restore
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu02"
testVersionNumber = int64(1)
testVersionNumberString = "1"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
testKeyId = uuid.NewString()
)
// Args
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testVersionNumberString,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
keyIdFlag: testKeyId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
KeyId: testKeyId,
VersionNumber: testVersionNumber,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiRestoreVersionRequest)) kms.ApiRestoreVersionRequest {
request := testClient.RestoreVersion(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId, testVersionNumber)
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(),
expectedModel: fixtureInputModel(),
isValid: true,
},
{
description: "no args (versionNumber)",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key ring id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "key ring id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = ""
}),
isValid: false,
},
{
description: "key ring id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyIdFlag)
}),
isValid: false,
},
{
description: "key id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyIdFlag] = ""
}),
isValid: false,
},
{
description: "key id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "version number invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "version number invalid 2",
argValues: []string{"Not a Number!"},
flagValues: fixtureFlagValues(),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(&params.CmdParams{Printer: p})
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %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, 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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiRestoreVersionRequest
}{
{
description: "base case",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
wantErr bool
outputFormat string
resp *kms.Version
}{
{
description: "nil response",
resp: nil,
wantErr: true,
},
{
description: "default output",
resp: &kms.Version{},
wantErr: false,
},
{
description: "json output",
outputFormat: print.JSONOutputFormat,
resp: &kms.Version{},
wantErr: false,
},
{
description: "yaml output",
outputFormat: print.YAMLOutputFormat,
resp: &kms.Version{},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.outputFormat, tt.resp)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package restore
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"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/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
versionNumberArg = "VERSION_NUMBER"
keyRingIdFlag = "keyring-id"
keyIdFlag = "key-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyRingId string
KeyId string
VersionNumber int64
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("restore %s", versionNumberArg),
Short: "Restore a key version",
Long: "Restores the specified version of a key.",
Args: args.SingleArg(versionNumberArg, nil),
Example: examples.Build(
examples.NewExample(
`Restore key version "42" for the key "my-key-id" inside the key ring "my-keyring-id"`,
`$ stackit beta kms version restore 42 --key-id "my-key-id" --keyring-id "my-keyring-id"`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
// This operation can be undone. Don't ask for confirmation!
// Call API
req := buildRequest(ctx, model, apiClient)
err = req.Execute()
if err != nil {
return fmt.Errorf("restore key Version: %w", err)
}
// Grab the key after the restore was applied to display the new state to the user.
resp, err := apiClient.GetVersionExecute(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get key version: %v", err)
}
return outputResult(params.Printer, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
versionStr := inputArgs[0]
versionNumber, err := strconv.ParseInt(versionStr, 10, 64)
if err != nil || versionNumber < 0 {
return nil, &errors.ArgValidationError{
Arg: versionNumberArg,
Details: fmt.Sprintf("invalid value %q: must be a positive integer", versionStr),
}
}
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
KeyId: flags.FlagToStringValue(p, cmd, keyIdFlag),
VersionNumber: versionNumber,
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiRestoreVersionRequest {
return apiClient.RestoreVersion(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber)
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring")
cmd.Flags().Var(flags.UUIDFlag(), keyIdFlag, "ID of the key")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, keyIdFlag)
cobra.CheckErr(err)
}
func outputResult(p *print.Printer, outputFormat string, resp *kms.Version) error {
if resp == nil {
return fmt.Errorf("response is nil / empty")
}
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal output to JSON: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal output to YAML: %w", err)
}
p.Outputln(string(details))
default:
p.Outputf("Restored version %d of the key %q\n", utils.PtrValue(resp.Number), utils.PtrValue(resp.KeyId))
}
return nil
}
package version
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/version/destroy"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/version/disable"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/version/enable"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/version/list"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/version/restore"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
)
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "version",
Short: "Manage KMS key versions",
Long: "Provides functionality for key version operations inside the KMS",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, params)
return cmd
}
func addSubcommands(cmd *cobra.Command, params *params.CmdParams) {
cmd.AddCommand(destroy.NewCmd(params))
cmd.AddCommand(disable.NewCmd(params))
cmd.AddCommand(enable.NewCmd(params))
cmd.AddCommand(list.NewCmd(params))
cmd.AddCommand(restore.NewCmd(params))
}
package create
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu01"
testAlgorithm = "rsa_2048_oaep_sha256"
testDisplayName = "my-key"
testPurpose = "wrap_asymmetric_key"
testDescription = "my key description"
testProtection = "software"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
)
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
algorithmFlag: testAlgorithm,
displayNameFlag: testDisplayName,
purposeFlag: testPurpose,
descriptionFlag: testDescription,
protectionFlag: testProtection,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
Algorithm: utils.Ptr(testAlgorithm),
Name: utils.Ptr(testDisplayName),
Purpose: utils.Ptr(testPurpose),
Description: utils.Ptr(testDescription),
Protection: utils.Ptr(testProtection),
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiCreateWrappingKeyRequest)) kms.ApiCreateWrappingKeyRequest {
request := testClient.CreateWrappingKey(testCtx, testProjectId, testRegion, testKeyRingId)
request = request.CreateWrappingKeyPayload(kms.CreateWrappingKeyPayload{
Algorithm: kms.CreateWrappingKeyPayloadGetAlgorithmAttributeType(utils.Ptr(testAlgorithm)),
DisplayName: utils.Ptr(testDisplayName),
Purpose: kms.CreateWrappingKeyPayloadGetPurposeAttributeType(utils.Ptr(testPurpose)),
Description: utils.Ptr(testDescription),
Protection: kms.CreateWrappingKeyPayloadGetProtectionAttributeType(utils.Ptr(testProtection)),
})
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: "optional flags omitted",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, descriptionFlag)
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Description = nil
}),
},
{
description: "no values provided",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key ring id missing (required)",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "key ring id invalid",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "algorithm missing (required)",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, algorithmFlag)
}),
isValid: false,
},
{
description: "name missing (required)",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, displayNameFlag)
}),
isValid: false,
},
{
description: "purpose missing (required)",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, purposeFlag)
}),
isValid: false,
},
{
description: "protection missing (required)",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, protectionFlag)
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
cmd := &cobra.Command{}
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
configureFlags(cmd)
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)
}
p := print.NewPrinter()
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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiCreateWrappingKeyRequest
}{
{
description: "base case",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
{
description: "no optional values",
model: fixtureInputModel(func(model *inputModel) {
model.Description = nil
}),
expectedRequest: fixtureRequest().CreateWrappingKeyPayload(kms.CreateWrappingKeyPayload{
Algorithm: kms.CreateWrappingKeyPayloadGetAlgorithmAttributeType(utils.Ptr(testAlgorithm)),
DisplayName: utils.Ptr(testDisplayName),
Purpose: kms.CreateWrappingKeyPayloadGetPurposeAttributeType(utils.Ptr(testPurpose)),
Protection: kms.CreateWrappingKeyPayloadGetProtectionAttributeType(utils.Ptr(testProtection)),
}),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request, err := buildRequest(testCtx, tt.model, testClient)
if err != nil {
t.Fatalf("error building request: %v", err)
}
diff := cmp.Diff(tt.expectedRequest, request,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
model *inputModel
wrappingKey *kms.WrappingKey
wantErr bool
}{
{
description: "nil response",
wrappingKey: nil,
wantErr: true,
},
{
description: "default output",
model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}},
wrappingKey: &kms.WrappingKey{},
wantErr: false,
},
{
description: "json output",
model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.JSONOutputFormat}},
wrappingKey: &kms.WrappingKey{},
wantErr: false,
},
{
description: "yaml output",
model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.YAMLOutputFormat}},
wrappingKey: &kms.WrappingKey{},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.model, tt.wrappingKey)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package create
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
"github.com/stackitcloud/stackit-sdk-go/services/kms/wait"
)
const (
keyRingIdFlag = "keyring-id"
algorithmFlag = "algorithm"
descriptionFlag = "description"
displayNameFlag = "name"
purposeFlag = "purpose"
protectionFlag = "protection"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyRingId string
Algorithm *string
Description *string
Name *string
Purpose *string
Protection *string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a KMS wrapping key",
Long: "Creates a KMS wrapping key.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a symmetric (RSA + AES) KMS wrapping key with name "my-wrapping-key-name" in key ring with ID "my-keyring-id"`,
`$ stackit beta kms wrapping-key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256_aes_256_key_wrap" --name "my-wrapping-key-name" --purpose "wrap_symmetric_key" --protection "software"`),
examples.NewExample(
`Create an asymmetric (RSA) KMS wrapping key with name "my-wrapping-key-name" in key ring with ID "my-keyring-id"`,
`$ stackit beta kms wrapping-key create --keyring-id "my-keyring-id" --algorithm "rsa_3072_oaep_sha256" --name "my-wrapping-key-name" --purpose "wrap_asymmetric_key" --protection "software"`),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
if !model.AssumeYes {
err = params.Printer.PromptForConfirmation("Are you sure you want to create a KMS wrapping key?")
if err != nil {
return err
}
}
// Call API
req, _ := buildRequest(ctx, model, apiClient)
wrappingKey, err := req.Execute()
if err != nil {
return fmt.Errorf("create KMS wrapping key: %w", err)
}
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(params.Printer)
s.Start("Creating wrapping key")
_, err = wait.CreateWrappingKeyWaitHandler(ctx, apiClient, model.ProjectId, model.Region, *wrappingKey.KeyRingId, *wrappingKey.Id).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for KMS wrapping key creation: %w", err)
}
s.Stop()
}
return outputResult(params.Printer, model, wrappingKey)
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &cliErr.ProjectIdError{}
}
// All values are mandatory strings. No additional type check required.
model := inputModel{
GlobalFlagModel: globalFlags,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
Algorithm: flags.FlagToStringPointer(p, cmd, algorithmFlag),
Name: flags.FlagToStringPointer(p, cmd, displayNameFlag),
Description: flags.FlagToStringPointer(p, cmd, descriptionFlag),
Purpose: flags.FlagToStringPointer(p, cmd, purposeFlag),
Protection: flags.FlagToStringPointer(p, cmd, protectionFlag),
}
p.DebugInputModel(model)
return &model, nil
}
type kmsWrappingKeyClient interface {
CreateWrappingKey(ctx context.Context, projectId string, regionId string, keyRingId string) kms.ApiCreateWrappingKeyRequest
}
func buildRequest(ctx context.Context, model *inputModel, apiClient kmsWrappingKeyClient) (kms.ApiCreateWrappingKeyRequest, error) {
req := apiClient.CreateWrappingKey(ctx, model.ProjectId, model.Region, model.KeyRingId)
req = req.CreateWrappingKeyPayload(kms.CreateWrappingKeyPayload{
DisplayName: model.Name,
Description: model.Description,
Algorithm: kms.CreateWrappingKeyPayloadGetAlgorithmAttributeType(model.Algorithm),
Purpose: kms.CreateWrappingKeyPayloadGetPurposeAttributeType(model.Purpose),
Protection: kms.CreateWrappingKeyPayloadGetProtectionAttributeType(model.Protection),
})
return req, nil
}
func outputResult(p *print.Printer, model *inputModel, resp *kms.WrappingKey) error {
if resp == nil {
return fmt.Errorf("response is nil")
}
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal KMS wrapping key: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal KMS wrapping key: %w", err)
}
p.Outputln(string(details))
default:
operationState := "Created"
if model.Async {
operationState = "Triggered creation of"
}
p.Outputf("%s wrapping key. Wrapping key ID: %s\n", operationState, utils.PtrString(resp.Id))
}
return nil
}
func configureFlags(cmd *cobra.Command) {
// Algorithm
var algorithmFlagOptions []string
for _, val := range kms.AllowedWrappingAlgorithmEnumValues {
algorithmFlagOptions = append(algorithmFlagOptions, string(val))
}
cmd.Flags().Var(flags.EnumFlag(false, "", algorithmFlagOptions...), algorithmFlag, fmt.Sprintf("En-/Decryption / signing algorithm. Possible values: %q", algorithmFlagOptions))
// Purpose
var purposeFlagOptions []string
for _, val := range kms.AllowedWrappingPurposeEnumValues {
purposeFlagOptions = append(purposeFlagOptions, string(val))
}
cmd.Flags().Var(flags.EnumFlag(false, "", purposeFlagOptions...), purposeFlag, fmt.Sprintf("Purpose of the wrapping key. Possible values: %q", purposeFlagOptions))
// Protection
// backend was deprectaed in /v1beta, but protection is a required attribute with value "software"
var protectionFlagOptions []string
for _, val := range kms.AllowedProtectionEnumValues {
protectionFlagOptions = append(protectionFlagOptions, string(val))
}
cmd.Flags().Var(flags.EnumFlag(false, "", protectionFlagOptions...), protectionFlag, fmt.Sprintf("The underlying system that is responsible for protecting the wrapping key material. Possible values: %q", purposeFlagOptions))
// All further non Enum Flags
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring")
cmd.Flags().String(displayNameFlag, "", "The display name to distinguish multiple wrapping keys")
cmd.Flags().String(descriptionFlag, "", "Optional description of the wrapping key")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, algorithmFlag, purposeFlag, displayNameFlag, protectionFlag)
cobra.CheckErr(err)
}
package delete
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu01"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
testWrappingKeyId = uuid.NewString()
)
// Args
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testWrappingKeyId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
WrappingKeyId: testWrappingKeyId,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiDeleteWrappingKeyRequest)) kms.ApiDeleteWrappingKeyRequest {
request := testClient.DeleteWrappingKey(testCtx, testProjectId, testRegion, testKeyRingId, testWrappingKeyId)
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 args (wrappingKeyId)",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no values provided",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "key ring id missing (required)",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "key ring id invalid",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "wrapping key id invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "wrapping key 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(&params.CmdParams{Printer: p})
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %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, 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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiDeleteWrappingKeyRequest
}{
{
description: "base case",
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(tt.expectedRequest, request,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package delete
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"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"
kmsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
wrappingKeyIdArg = "WRAPPING_KEY_ID"
keyRingIdFlag = "keyring-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
WrappingKeyId string
KeyRingId string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("delete %s", wrappingKeyIdArg),
Short: "Deletes a KMS wrapping key",
Long: "Deletes a KMS wrapping key inside a specific key ring.",
Args: args.SingleArg(wrappingKeyIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Delete a KMS wrapping key "MY_WRAPPING_KEY_ID" inside the key ring "my-keyring-id"`,
`$ stackit beta kms wrapping-key delete "MY_WRAPPING_KEY_ID" --keyring-id "my-keyring-id"`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
wrappingKeyName, err := kmsUtils.GetWrappingKeyName(ctx, apiClient, model.ProjectId, model.Region, model.KeyRingId, model.WrappingKeyId)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get wrapping key name: %v", err)
wrappingKeyName = model.WrappingKeyId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete the wrapping key %q? (this cannot be undone)", wrappingKeyName)
err = params.Printer.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
err = req.Execute()
if err != nil {
return fmt.Errorf("delete KMS wrapping key: %w", err)
}
// Wait for async operation not relevant. Wrapping key deletion is synchronous
// Don't output anything. It's a deletion.
params.Printer.Info("Deleted wrapping key %q\n", wrappingKeyName)
return nil
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
wrappingKeyId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
WrappingKeyId: wrappingKeyId,
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiDeleteWrappingKeyRequest {
req := apiClient.DeleteWrappingKey(ctx, model.ProjectId, model.Region, model.KeyRingId, model.WrappingKeyId)
return req
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring where the wrapping key is stored")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag)
cobra.CheckErr(err)
}
package list
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
testRegion = "eu02"
)
type testCtxKey struct{}
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &kms.APIClient{}
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
)
// Flags
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
globalflags.ProjectIdFlag: testProjectId,
globalflags.RegionFlag: testRegion,
keyRingIdFlag: testKeyRingId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
// Input Model
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: testRegion,
Verbosity: globalflags.VerbosityDefault,
},
KeyRingId: testKeyRingId,
}
for _, mod := range mods {
mod(model)
}
return model
}
// Request
func fixtureRequest(mods ...func(request *kms.ApiListWrappingKeysRequest)) kms.ApiListWrappingKeysRequest {
request := testClient.ListWrappingKeys(testCtx, testProjectId, testRegion, testKeyRingId)
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(),
expectedModel: fixtureInputModel(),
isValid: true,
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "missing keyRingId",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, keyRingIdFlag)
}),
isValid: false,
},
{
description: "invalid keyRingId 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = ""
}),
isValid: false,
},
{
description: "invalid keyRingId 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[keyRingIdFlag] = "Not an uuid"
}),
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, globalflags.ProjectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
cmd := &cobra.Command{}
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
configureFlags(cmd)
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)
}
p := print.NewPrinter()
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(tt.expectedModel, model)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest kms.ApiListWrappingKeysRequest
}{
{
description: "base case",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestOutputResult(t *testing.T) {
tests := []struct {
description string
keyRingId string
resp *kms.WrappingKeyList
outputFormat string
projectLabel string
wantErr bool
}{
{
description: "nil response",
resp: nil,
projectLabel: "my-project",
wantErr: true,
},
{
description: "default output",
resp: &kms.WrappingKeyList{WrappingKeys: &[]kms.WrappingKey{}},
projectLabel: "my-project",
wantErr: false,
},
{
description: "json output",
resp: &kms.WrappingKeyList{WrappingKeys: &[]kms.WrappingKey{}},
outputFormat: print.JSONOutputFormat,
wantErr: false,
},
{
description: "yaml output",
resp: &kms.WrappingKeyList{WrappingKeys: &[]kms.WrappingKey{}},
outputFormat: print.YAMLOutputFormat,
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := outputResult(p, tt.outputFormat, tt.keyRingId, tt.resp)
if (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
package list
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"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/kms/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
const (
keyRingIdFlag = "keyring-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
KeyRingId string
}
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all KMS wrapping keys",
Long: "Lists all KMS wrapping keys inside a key ring.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all KMS wrapping keys for the key ring "my-keyring-id"`,
`$ stackit beta kms wrapping-key list --keyring-id "my-keyring-id"`),
examples.NewExample(
`List all KMS wrapping keys in JSON format`,
`$ stackit beta kms wrapping-key list --keyring-id "my-keyring-id" --output-format json`),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("get KMS wrapping keys: %w", err)
}
return outputResult(params.Printer, model.OutputFormat, model.KeyRingId, resp)
},
}
configureFlags(cmd)
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag),
}
p.DebugInputModel(model)
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiListWrappingKeysRequest {
req := apiClient.ListWrappingKeys(ctx, model.ProjectId, model.Region, model.KeyRingId)
return req
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring where the key is stored")
err := flags.MarkFlagsRequired(cmd, keyRingIdFlag)
cobra.CheckErr(err)
}
func outputResult(p *print.Printer, outputFormat, keyRingId string, resp *kms.WrappingKeyList) error {
if resp == nil || resp.WrappingKeys == nil {
return fmt.Errorf("response is nil / empty")
}
wrappingKeys := *resp.WrappingKeys
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(wrappingKeys, "", " ")
if err != nil {
return fmt.Errorf("marshal KMS wrapping keys list: %w", err)
}
p.Outputln(string(details))
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(wrappingKeys, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
if err != nil {
return fmt.Errorf("marshal KMS wrapping keys list: %w", err)
}
p.Outputln(string(details))
default:
if len(wrappingKeys) == 0 {
p.Outputf("No wrapping keys found under the key ring %q\n", keyRingId)
return nil
}
table := tables.NewTable()
table.SetHeader("ID", "NAME", "SCOPE", "ALGORITHM", "EXPIRES AT", "STATUS")
for i := range wrappingKeys {
wrappingKey := wrappingKeys[i]
table.AddRow(
utils.PtrString(wrappingKey.Id),
utils.PtrString(wrappingKey.DisplayName),
utils.PtrString(wrappingKey.Purpose),
utils.PtrString(wrappingKey.Algorithm),
utils.PtrString(wrappingKey.ExpiresAt),
utils.PtrString(wrappingKey.State),
)
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
}
return nil
}
package wrappingkey
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/list"
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
)
func NewCmd(params *params.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "wrapping-key",
Short: "Manage KMS wrapping keys",
Long: "Provides functionality for wrapping key operations inside the KMS",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, params)
return cmd
}
func addSubcommands(cmd *cobra.Command, params *params.CmdParams) {
cmd.AddCommand(list.NewCmd(params))
cmd.AddCommand(delete.NewCmd(params))
cmd.AddCommand(create.NewCmd(params))
}
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/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/viper"
sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
func ConfigureClient(p *print.Printer, cliVersion string) (*kms.APIClient, error) {
authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser)
if err != nil {
p.Debug(print.ErrorLevel, "configure authentication: %v", err)
return nil, &errors.AuthError{}
}
cfgOptions := []sdkConfig.ConfigurationOption{
utils.UserAgentConfigOption(cliVersion),
authCfgOption,
}
customEndpoint := viper.GetString(config.KMSCustomEndpointKey)
if customEndpoint != "" {
cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint))
}
if p.IsVerbosityDebug() {
cfgOptions = append(cfgOptions,
sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)),
)
}
apiClient, err := kms.NewAPIClient(cfgOptions...)
if err != nil {
p.Debug(print.ErrorLevel, "create new API client: %v", err)
return nil, &errors.AuthError{}
}
return apiClient, nil
}
package utils
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
var (
testProjectId = uuid.NewString()
testKeyRingId = uuid.NewString()
testKeyId = uuid.NewString()
testWrappingKeyId = uuid.NewString()
)
const (
testRegion = "eu01"
testKeyName = "my-test-key"
testKeyRingName = "my-key-ring"
testWrappingKeyName = "my-wrapping-key"
)
type kmsClientMocked struct {
getKeyFails bool
getKeyResp *kms.Key
getKeyRingFails bool
getKeyRingResp *kms.KeyRing
getWrappingKeyFails bool
getWrappingKeyResp *kms.WrappingKey
}
// Implement the KMSClient interface methods for the mock.
func (m *kmsClientMocked) GetKeyExecute(_ context.Context, _, _, _, _ string) (*kms.Key, error) {
if m.getKeyFails {
return nil, fmt.Errorf("could not get key")
}
return m.getKeyResp, nil
}
func (m *kmsClientMocked) GetKeyRingExecute(_ context.Context, _, _, _ string) (*kms.KeyRing, error) {
if m.getKeyRingFails {
return nil, fmt.Errorf("could not get key ring")
}
return m.getKeyRingResp, nil
}
func (m *kmsClientMocked) GetWrappingKeyExecute(_ context.Context, _, _, _, _ string) (*kms.WrappingKey, error) {
if m.getWrappingKeyFails {
return nil, fmt.Errorf("could not get wrapping key")
}
return m.getWrappingKeyResp, nil
}
func TestGetKeyName(t *testing.T) {
keyName := testKeyName
tests := []struct {
description string
getKeyFails bool
getKeyResp *kms.Key
isValid bool
expectedOutput string
}{
{
description: "base",
getKeyResp: &kms.Key{
DisplayName: &keyName,
},
isValid: true,
expectedOutput: testKeyName,
},
{
description: "get key fails",
getKeyFails: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &kmsClientMocked{
getKeyFails: tt.getKeyFails,
getKeyResp: tt.getKeyResp,
}
output, err := GetKeyName(context.Background(), client, testProjectId, testRegion, testKeyRingId, testKeyId)
if tt.isValid && err != nil {
t.Errorf("failed on valid input: %v", err)
}
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 %q, got %q", tt.expectedOutput, output)
}
})
}
}
// TestGetKeyDeletionDate tests the GetKeyDeletionDate function.
func TestGetKeyDeletionDate(t *testing.T) {
mockTime := time.Date(2025, 8, 20, 0, 0, 0, 0, time.UTC)
tests := []struct {
description string
getKeyFails bool
getKeyResp *kms.Key
isValid bool
expectedOutput time.Time
}{
{
description: "base",
getKeyResp: &kms.Key{
DeletionDate: &mockTime,
},
isValid: true,
expectedOutput: mockTime,
},
{
description: "get key fails",
getKeyFails: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &kmsClientMocked{
getKeyFails: tt.getKeyFails,
getKeyResp: tt.getKeyResp,
}
output, err := GetKeyDeletionDate(context.Background(), client, testProjectId, testRegion, testKeyRingId, testKeyId)
if tt.isValid && err != nil {
t.Errorf("failed on valid input: %v", err)
}
if !tt.isValid && err == nil {
t.Errorf("did not fail on invalid input")
}
if !tt.isValid {
return
}
if !output.Equal(tt.expectedOutput) {
t.Errorf("expected output to be %v, got %v", tt.expectedOutput, output)
}
})
}
}
// TestGetKeyRingName tests the GetKeyRingName function.
func TestGetKeyRingName(t *testing.T) {
keyRingName := testKeyRingName
tests := []struct {
description string
getKeyRingFails bool
getKeyRingResp *kms.KeyRing
isValid bool
expectedOutput string
}{
{
description: "base",
getKeyRingResp: &kms.KeyRing{
DisplayName: &keyRingName,
},
isValid: true,
expectedOutput: testKeyRingName,
},
{
description: "get key ring fails",
getKeyRingFails: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &kmsClientMocked{
getKeyRingFails: tt.getKeyRingFails,
getKeyRingResp: tt.getKeyRingResp,
}
output, err := GetKeyRingName(context.Background(), client, testProjectId, testKeyRingId, testRegion)
if tt.isValid && err != nil {
t.Errorf("failed on valid input: %v", err)
}
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 %q, got %q", tt.expectedOutput, output)
}
})
}
}
func TestGetWrappingKeyName(t *testing.T) {
wrappingKeyName := testWrappingKeyName
tests := []struct {
description string
getWrappingKeyFails bool
getWrappingKeyResp *kms.WrappingKey
isValid bool
expectedOutput string
}{
{
description: "base",
getWrappingKeyResp: &kms.WrappingKey{
DisplayName: &wrappingKeyName,
},
isValid: true,
expectedOutput: testWrappingKeyName,
},
{
description: "get wrapping key fails",
getWrappingKeyFails: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &kmsClientMocked{
getWrappingKeyFails: tt.getWrappingKeyFails,
getWrappingKeyResp: tt.getWrappingKeyResp,
}
output, err := GetWrappingKeyName(context.Background(), client, testProjectId, testRegion, testKeyRingId, testWrappingKeyId)
if tt.isValid && err != nil {
t.Errorf("failed on valid input: %v", err)
}
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 %q, got %q", tt.expectedOutput, output)
}
})
}
}
package utils
import (
"context"
"fmt"
"time"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
type KMSClient interface {
GetKeyExecute(ctx context.Context, projectId string, regionId string, keyRingId string, keyId string) (*kms.Key, error)
GetKeyRingExecute(ctx context.Context, projectId string, regionId string, keyRingId string) (*kms.KeyRing, error)
GetWrappingKeyExecute(ctx context.Context, projectId string, regionId string, keyRingId string, wrappingKeyId string) (*kms.WrappingKey, error)
}
func GetKeyName(ctx context.Context, apiClient KMSClient, projectId, region, keyRingId, keyId string) (string, error) {
resp, err := apiClient.GetKeyExecute(ctx, projectId, region, keyRingId, keyId)
if err != nil {
return "", fmt.Errorf("get KMS Key: %w", err)
}
if resp == nil || resp.DisplayName == nil {
return "", fmt.Errorf("response is nil / empty")
}
return *resp.DisplayName, nil
}
func GetKeyDeletionDate(ctx context.Context, apiClient KMSClient, projectId, region, keyRingId, keyId string) (time.Time, error) {
resp, err := apiClient.GetKeyExecute(ctx, projectId, region, keyRingId, keyId)
if err != nil {
return time.Now(), fmt.Errorf("get KMS Key: %w", err)
}
if resp == nil || resp.DeletionDate == nil {
return time.Time{}, fmt.Errorf("response is nil / empty")
}
return *resp.DeletionDate, nil
}
func GetKeyRingName(ctx context.Context, apiClient KMSClient, projectId, id, region string) (string, error) {
resp, err := apiClient.GetKeyRingExecute(ctx, projectId, region, id)
if err != nil {
return "", fmt.Errorf("get KMS key ring: %w", err)
}
if resp == nil || resp.DisplayName == nil {
return "", fmt.Errorf("response is nil / empty")
}
return *resp.DisplayName, nil
}
func GetWrappingKeyName(ctx context.Context, apiClient KMSClient, projectId, region, keyRingId, wrappingKeyId string) (string, error) {
resp, err := apiClient.GetWrappingKeyExecute(ctx, projectId, region, keyRingId, wrappingKeyId)
if err != nil {
return "", fmt.Errorf("get KMS Wrapping Key: %w", err)
}
if resp == nil || resp.DisplayName == nil {
return "", fmt.Errorf("response is nil / empty")
}
return *resp.DisplayName, nil
}
+1
-1

@@ -24,3 +24,3 @@ ## stackit beta sqlserverflex instance create

Create a SQLServer Flex instance with name "my-instance", specify flavor by CPU and RAM, set storage size to 20 GB, and restrict access to a specific range of IP addresses. Other parameters are set to default values
$ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4 --storage-size 20 --acl 1.2.3.0/24
$ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4 --storage-size 20 --acl 1.2.3.0/24
```

@@ -27,0 +27,0 @@

@@ -45,3 +45,4 @@ ## stackit beta

* [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers
* [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS
* [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex

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

--identity-provider-custom-well-known-configuration string Identity Provider well-known OpenID configuration URL, used for user authentication
--kms-custom-endpoint string KMS API base URL, used in calls to this API
--load-balancer-custom-endpoint string Load Balancer API base URL, used in calls to this API

@@ -41,0 +42,0 @@ --logme-custom-endpoint string LogMe API base URL, used in calls to this API

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

--identity-provider-custom-well-known-configuration Identity Provider well-known OpenID configuration URL. If unset, uses the default identity provider
--kms-custom-endpoint KMS API base URL. If unset, uses the default base URL
--load-balancer-custom-endpoint Load Balancer API base URL. If unset, uses the default base URL

@@ -39,0 +40,0 @@ --logme-custom-endpoint LogMe API base URL. If unset, uses the default base URL

+1
-0

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

github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stackitcloud/stackit-sdk-go/services/kms v1.0.0
github.com/sagikazarmark/locafero v0.11.0 // indirect

@@ -243,0 +244,0 @@ github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect

@@ -7,2 +7,3 @@ package beta

"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex"

@@ -42,2 +43,3 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/params"

cmd.AddCommand(alb.NewCmd(params))
cmd.AddCommand(kms.NewCmd(params))
}

@@ -83,3 +83,3 @@ package create

`Create a SQLServer Flex instance with name "my-instance", specify flavor by CPU and RAM, set storage size to 20 GB, and restrict access to a specific range of IP addresses. Other parameters are set to default values`,
`$ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4 --storage-size 20 --acl 1.2.3.0/24`),
`$ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4 --storage-size 20 --acl 1.2.3.0/24`),
),

@@ -86,0 +86,0 @@ RunE: func(cmd *cobra.Command, args []string) error {

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

secretsManagerCustomEndpointFlag = "secrets-manager-custom-endpoint"
kmsCustomEndpointFlag = "kms-custom-endpoint"
serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint"

@@ -154,2 +155,3 @@ serverOsUpdateCustomEndpointFlag = "server-osupdate-custom-endpoint"

cmd.Flags().String(secretsManagerCustomEndpointFlag, "", "Secrets Manager API base URL, used in calls to this API")
cmd.Flags().String(kmsCustomEndpointFlag, "", "KMS API base URL, used in calls to this API")
cmd.Flags().String(serviceAccountCustomEndpointFlag, "", "Service Account API base URL, used in calls to this API")

@@ -202,2 +204,4 @@ cmd.Flags().String(serviceEnablementCustomEndpointFlag, "", "Service Enablement API base URL, used in calls to this API")

cobra.CheckErr(err)
err = viper.BindPFlag(config.KMSCustomEndpointKey, cmd.Flags().Lookup(kmsCustomEndpointFlag))
cobra.CheckErr(err)
err = viper.BindPFlag(config.ServerBackupCustomEndpointKey, cmd.Flags().Lookup(serverBackupCustomEndpointFlag))

@@ -204,0 +208,0 @@ cobra.CheckErr(err)

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

secretsManagerCustomEndpointFlag: true,
kmsCustomEndpointFlag: true,
serviceAccountCustomEndpointFlag: true,

@@ -78,2 +79,3 @@ serverBackupCustomEndpointFlag: true,

SecretsManagerCustomEndpoint: true,
KMSCustomEndpoint: true,
ServiceAccountCustomEndpoint: true,

@@ -134,2 +136,3 @@ ServerBackupCustomEndpoint: true,

model.SecretsManagerCustomEndpoint = false
model.KMSCustomEndpoint = false
model.ServiceAccountCustomEndpoint = false

@@ -226,2 +229,12 @@ model.ServerBackupCustomEndpoint = false

{
description: "kms custom endpoint empty",
flagValues: fixtureFlagValues(func(flagValues map[string]bool) {
flagValues[kmsCustomEndpointFlag] = false
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.KMSCustomEndpoint = false
}),
},
{
description: "service account custom endpoint empty",

@@ -228,0 +241,0 @@ flagValues: fixtureFlagValues(func(flagValues map[string]bool) {

@@ -44,2 +44,3 @@ package unset

secretsManagerCustomEndpointFlag = "secrets-manager-custom-endpoint"
kmsCustomEndpointFlag = "kms-custom-endpoint"
serviceAccountCustomEndpointFlag = "service-account-custom-endpoint"

@@ -82,2 +83,3 @@ serviceEnablementCustomEndpointFlag = "service-enablement-custom-endpoint"

SecretsManagerCustomEndpoint bool
KMSCustomEndpoint bool
ServerBackupCustomEndpoint bool

@@ -185,2 +187,5 @@ ServerOsUpdateCustomEndpoint bool

}
if model.KMSCustomEndpoint {
viper.Set(config.KMSCustomEndpointKey, "")
}
if model.ServiceAccountCustomEndpoint {

@@ -251,2 +256,3 @@ viper.Set(config.ServiceAccountCustomEndpointKey, "")

cmd.Flags().Bool(secretsManagerCustomEndpointFlag, false, "Secrets Manager API base URL. If unset, uses the default base URL")
cmd.Flags().Bool(kmsCustomEndpointFlag, false, "KMS API base URL. If unset, uses the default base URL")
cmd.Flags().Bool(serviceAccountCustomEndpointFlag, false, "Service Account API base URL. If unset, uses the default base URL")

@@ -290,2 +296,3 @@ cmd.Flags().Bool(serviceEnablementCustomEndpointFlag, false, "Service Enablement API base URL. If unset, uses the default base URL")

SecretsManagerCustomEndpoint: flags.FlagToBoolValue(p, cmd, secretsManagerCustomEndpointFlag),
KMSCustomEndpoint: flags.FlagToBoolValue(p, cmd, kmsCustomEndpointFlag),
ServiceAccountCustomEndpoint: flags.FlagToBoolValue(p, cmd, serviceAccountCustomEndpointFlag),

@@ -292,0 +299,0 @@ ServiceEnablementCustomEndpoint: flags.FlagToBoolValue(p, cmd, serviceEnablementCustomEndpointFlag),

@@ -39,2 +39,3 @@ package config

SecretsManagerCustomEndpointKey = "secrets_manager_custom_endpoint"
KMSCustomEndpointKey = "kms_custom_endpoint"
ServiceAccountCustomEndpointKey = "service_account_custom_endpoint"

@@ -99,2 +100,3 @@ ServiceEnablementCustomEndpointKey = "service_enablement_custom_endpoint"

SecretsManagerCustomEndpointKey,
KMSCustomEndpointKey,
ServiceAccountCustomEndpointKey,

@@ -185,2 +187,3 @@ ServiceEnablementCustomEndpointKey,

viper.SetDefault(SecretsManagerCustomEndpointKey, "")
viper.SetDefault(KMSCustomEndpointKey, "")
viper.SetDefault(ServiceAccountCustomEndpointKey, "")

@@ -187,0 +190,0 @@ viper.SetDefault(ServiceEnablementCustomEndpointKey, "")

Sorry, the diff of this file is too big to display