mygithub.libinneed.workers.dev/stackitcloud/stackit-cli
Advanced tools
| ## 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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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 | ||
| } |
@@ -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