From 89c440b849f102e57e28f74b319c9856ffec06c3 Mon Sep 17 00:00:00 2001 From: Ruben Hoenle Date: Tue, 4 Nov 2025 12:49:27 +0100 Subject: [PATCH 1/2] feat(kms): add wrapping key resource and datasource relates to STACKITTPR-416 --- docs/data-sources/kms_wrapping_key.md | 45 ++ docs/resources/kms_keyring.md | 2 +- docs/resources/kms_wrapping_key.md | 48 ++ .../stackit_kms_wrapping_key/data-source.tf | 5 + .../resources/stackit_kms_keyring/resource.tf | 2 +- .../stackit_kms_wrapping_key/resource.tf | 8 + stackit/internal/services/kms/kms_acc_test.go | 348 ++++++++++++++ .../services/kms/testdata/wrapping-key-max.tf | 23 + .../services/kms/testdata/wrapping-key-min.tf | 21 + .../services/kms/wrapping-key/datasource.go | 168 +++++++ .../services/kms/wrapping-key/resource.go | 430 ++++++++++++++++++ .../kms/wrapping-key/resource_test.go | 244 ++++++++++ stackit/provider.go | 3 + 13 files changed, 1345 insertions(+), 2 deletions(-) create mode 100644 docs/data-sources/kms_wrapping_key.md create mode 100644 docs/resources/kms_wrapping_key.md create mode 100644 examples/data-sources/stackit_kms_wrapping_key/data-source.tf create mode 100644 examples/resources/stackit_kms_wrapping_key/resource.tf create mode 100644 stackit/internal/services/kms/testdata/wrapping-key-max.tf create mode 100644 stackit/internal/services/kms/testdata/wrapping-key-min.tf create mode 100644 stackit/internal/services/kms/wrapping-key/datasource.go create mode 100644 stackit/internal/services/kms/wrapping-key/resource.go create mode 100644 stackit/internal/services/kms/wrapping-key/resource_test.go diff --git a/docs/data-sources/kms_wrapping_key.md b/docs/data-sources/kms_wrapping_key.md new file mode 100644 index 000000000..c942ff1d5 --- /dev/null +++ b/docs/data-sources/kms_wrapping_key.md @@ -0,0 +1,45 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_kms_wrapping_key Data Source - stackit" +subcategory: "" +description: |- + KMS wrapping key datasource schema. +--- + +# stackit_kms_wrapping_key (Data Source) + +KMS wrapping key datasource schema. + +## Example Usage + +```terraform +data "stackit_kms_wrapping_key" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + keyring_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + wrapping_key_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `keyring_id` (String) The ID of the associated keyring +- `project_id` (String) STACKIT project ID to which the keyring is associated. +- `wrapping_key_id` (String) The ID of the wrapping key + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `access_scope` (String) The access scope of the key. Default is `PUBLIC`. Possible values are: `PUBLIC`, `SNA`. +- `algorithm` (String) The wrapping algorithm used to wrap the key to import. Possible values are: `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) A user chosen description to distinguish multiple wrapping keys. +- `display_name` (String) The display name to distinguish multiple wrapping keys. +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`keyring_id`,`wrapping_key_id`". +- `protection` (String) The underlying system that is responsible for protecting the key material. Possible values are: `software`. +- `public_key` (String) The public key of the wrapping key. +- `purpose` (String) The purpose for which the key will be used. Possible values are: `wrap_symmetric_key`, `wrap_asymmetric_key`. diff --git a/docs/resources/kms_keyring.md b/docs/resources/kms_keyring.md index 1d8561372..272e83297 100644 --- a/docs/resources/kms_keyring.md +++ b/docs/resources/kms_keyring.md @@ -18,7 +18,7 @@ KMS Keyring resource schema. Uses the `default_region` specified in the provider ```terraform resource "stackit_kms_keyring" "example" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - display_name = "example name" + display_name = "example-name" description = "example description" } ``` diff --git a/docs/resources/kms_wrapping_key.md b/docs/resources/kms_wrapping_key.md new file mode 100644 index 000000000..373a53652 --- /dev/null +++ b/docs/resources/kms_wrapping_key.md @@ -0,0 +1,48 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_kms_wrapping_key Resource - stackit" +subcategory: "" +description: |- + KMS wrapping key resource schema. +--- + +# stackit_kms_wrapping_key (Resource) + +KMS wrapping key resource schema. + +## Example Usage + +```terraform +resource "stackit_kms_wrapping_key" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + keyring_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + display_name = "example-name" + protection = "software" + algorithm = "rsa_2048_oaep_sha256" + purpose = "wrap_symmetric_key" +} +``` + + +## Schema + +### Required + +- `algorithm` (String) The wrapping algorithm used to wrap the key to import. Possible values are: `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`. +- `display_name` (String) The display name to distinguish multiple wrapping keys. +- `keyring_id` (String) The ID of the associated keyring +- `project_id` (String) STACKIT project ID to which the keyring is associated. +- `protection` (String) The underlying system that is responsible for protecting the key material. Possible values are: `software`. +- `purpose` (String) The purpose for which the key will be used. Possible values are: `wrap_symmetric_key`, `wrap_asymmetric_key`. + +### Optional + +- `access_scope` (String) The access scope of the key. Default is `PUBLIC`. Possible values are: `PUBLIC`, `SNA`. +- `description` (String) A user chosen description to distinguish multiple wrapping keys. +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`keyring_id`,`wrapping_key_id`". +- `public_key` (String) The public key of the wrapping key. +- `wrapping_key_id` (String) The ID of the wrapping key diff --git a/examples/data-sources/stackit_kms_wrapping_key/data-source.tf b/examples/data-sources/stackit_kms_wrapping_key/data-source.tf new file mode 100644 index 000000000..bb12e4986 --- /dev/null +++ b/examples/data-sources/stackit_kms_wrapping_key/data-source.tf @@ -0,0 +1,5 @@ +data "stackit_kms_wrapping_key" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + keyring_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + wrapping_key_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} diff --git a/examples/resources/stackit_kms_keyring/resource.tf b/examples/resources/stackit_kms_keyring/resource.tf index 15f2d5e45..1efc90fa8 100644 --- a/examples/resources/stackit_kms_keyring/resource.tf +++ b/examples/resources/stackit_kms_keyring/resource.tf @@ -1,5 +1,5 @@ resource "stackit_kms_keyring" "example" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - display_name = "example name" + display_name = "example-name" description = "example description" } diff --git a/examples/resources/stackit_kms_wrapping_key/resource.tf b/examples/resources/stackit_kms_wrapping_key/resource.tf new file mode 100644 index 000000000..3850b8e19 --- /dev/null +++ b/examples/resources/stackit_kms_wrapping_key/resource.tf @@ -0,0 +1,8 @@ +resource "stackit_kms_wrapping_key" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + keyring_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + display_name = "example-name" + protection = "software" + algorithm = "rsa_2048_oaep_sha256" + purpose = "wrap_symmetric_key" +} diff --git a/stackit/internal/services/kms/kms_acc_test.go b/stackit/internal/services/kms/kms_acc_test.go index b6bd03e99..78adc7e10 100644 --- a/stackit/internal/services/kms/kms_acc_test.go +++ b/stackit/internal/services/kms/kms_acc_test.go @@ -36,6 +36,12 @@ var ( //go:embed testdata/key-max.tf resourceKeyMaxConfig string + + //go:embed testdata/wrapping-key-min.tf + resourceWrappingKeyMinConfig string + + //go:embed testdata/wrapping-key-max.tf + resourceWrappingKeyMaxConfig string ) // KEY RING - MIN @@ -113,6 +119,49 @@ var testConfigKeyVarsMaxUpdated = func() config.Variables { return updatedConfig } +// WRAPPING KEY - MIN + +var testConfigWrappingKeyVarsMin = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "keyring_display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "algorithm": config.StringVariable(string(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256)), + "protection": config.StringVariable(string(kms.PROTECTION_SOFTWARE)), + "purpose": config.StringVariable(string(kms.WRAPPINGPURPOSE_SYMMETRIC_KEY)), +} + +var testConfigWrappingKeyVarsMinUpdated = func() config.Variables { + updatedConfig := config.Variables{} + maps.Copy(updatedConfig, testConfigWrappingKeyVarsMin) + updatedConfig["display_name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["display_name"]))) + updatedConfig["algorithm"] = config.StringVariable(string(kms.WRAPPINGALGORITHM__4096_OAEP_SHA256_AES_256_KEY_WRAP)) + updatedConfig["purpose"] = config.StringVariable(string(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY)) + return updatedConfig +} + +// WRAPPING KEY - MAX + +var testConfigWrappingKeyVarsMax = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "keyring_display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "algorithm": config.StringVariable(string(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256)), + "protection": config.StringVariable(string(kms.PROTECTION_SOFTWARE)), + "purpose": config.StringVariable(string(kms.WRAPPINGPURPOSE_SYMMETRIC_KEY)), + "description": config.StringVariable("kms-wrapping-key-description"), + "access_scope": config.StringVariable(string(kms.ACCESSSCOPE_PUBLIC)), +} + +var testConfigWrappingKeyVarsMaxUpdated = func() config.Variables { + updatedConfig := config.Variables{} + maps.Copy(updatedConfig, testConfigWrappingKeyVarsMax) + updatedConfig["display_name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["display_name"]))) + updatedConfig["algorithm"] = config.StringVariable(string(kms.WRAPPINGALGORITHM__4096_OAEP_SHA256_AES_256_KEY_WRAP)) + updatedConfig["purpose"] = config.StringVariable(string(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY)) + updatedConfig["description"] = config.StringVariable("kms-wrapping-key-description-updated") + return updatedConfig +} + func TestAccKeyRingMin(t *testing.T) { resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, @@ -561,9 +610,270 @@ func TestAccKeyMax(t *testing.T) { }) } +func TestAccWrappingKeyMin(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigWrappingKeyVarsMin, + Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceWrappingKeyMinConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("stackit_kms_wrapping_key.wrapping_key", plancheck.ResourceActionCreate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "region", testutil.Region), + resource.TestCheckResourceAttrPair( + "stackit_kms_keyring.keyring", "keyring_id", + "stackit_kms_wrapping_key.wrapping_key", "keyring_id", + ), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id"), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "algorithm", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["algorithm"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "display_name", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["display_name"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "purpose", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["purpose"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "protection", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["protection"])), + resource.TestCheckNoResourceAttr("stackit_kms_wrapping_key.wrapping_key", "description"), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "public_key"), + ), + }, + // Data Source + { + ConfigVariables: testConfigWrappingKeyVarsMin, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_kms_wrapping_key" "wrapping_key" { + project_id = stackit_kms_wrapping_key.wrapping_key.project_id + keyring_id = stackit_kms_wrapping_key.wrapping_key.keyring_id + wrapping_key_id = stackit_kms_wrapping_key.wrapping_key.wrapping_key_id + } + `, + testutil.KMSProviderConfig(), resourceWrappingKeyMinConfig, + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), + plancheck.ExpectResourceAction("stackit_kms_wrapping_key.wrapping_key", plancheck.ResourceActionNoop), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "region", testutil.Region), + resource.TestCheckResourceAttrPair( + "stackit_kms_keyring.keyring", "keyring_id", + "data.stackit_kms_wrapping_key.wrapping_key", "keyring_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id", + "data.stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id", + ), + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "algorithm", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["algorithm"])), + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "display_name", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["display_name"])), + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "purpose", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["purpose"])), + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "protection", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["protection"])), + resource.TestCheckNoResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "description"), + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)), + resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "public_key"), + ), + ), + }, + // Import + { + ConfigVariables: testConfigWrappingKeyVarsMin, + ResourceName: "stackit_kms_wrapping_key.wrapping_key", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_kms_wrapping_key.wrapping_key"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_kms_wrapping_key.wrapping_key") + } + keyRingId, ok := r.Primary.Attributes["keyring_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute keyring_id") + } + wrappingKeyId, ok := r.Primary.Attributes["wrapping_key_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute wrapping_key_id") + } + + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, keyRingId, wrappingKeyId), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigWrappingKeyVarsMinUpdated(), + Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceWrappingKeyMinConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), + plancheck.ExpectResourceAction("stackit_kms_wrapping_key.wrapping_key", plancheck.ResourceActionReplace), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "region", testutil.Region), + resource.TestCheckResourceAttrPair( + "stackit_kms_keyring.keyring", "keyring_id", + "stackit_kms_wrapping_key.wrapping_key", "keyring_id", + ), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id"), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "algorithm", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMinUpdated()["algorithm"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "display_name", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMinUpdated()["display_name"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "purpose", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMinUpdated()["purpose"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "protection", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMinUpdated()["protection"])), + resource.TestCheckNoResourceAttr("stackit_kms_wrapping_key.wrapping_key", "description"), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "public_key"), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + +func TestAccWrappingKeyMax(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigWrappingKeyVarsMax, + Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceWrappingKeyMaxConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("stackit_kms_wrapping_key.wrapping_key", plancheck.ResourceActionCreate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "region", testutil.Region), + resource.TestCheckResourceAttrPair( + "stackit_kms_keyring.keyring", "keyring_id", + "stackit_kms_wrapping_key.wrapping_key", "keyring_id", + ), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id"), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "algorithm", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["algorithm"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "display_name", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["display_name"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "purpose", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["purpose"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "protection", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["protection"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "description", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["description"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "access_scope", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["access_scope"])), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "public_key"), + ), + }, + // Data Source + { + ConfigVariables: testConfigWrappingKeyVarsMax, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_kms_wrapping_key" "wrapping_key" { + project_id = stackit_kms_wrapping_key.wrapping_key.project_id + keyring_id = stackit_kms_wrapping_key.wrapping_key.keyring_id + wrapping_key_id = stackit_kms_wrapping_key.wrapping_key.wrapping_key_id + } + `, + testutil.KMSProviderConfig(), resourceWrappingKeyMaxConfig, + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), + plancheck.ExpectResourceAction("stackit_kms_wrapping_key.wrapping_key", plancheck.ResourceActionNoop), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "region", testutil.Region), + resource.TestCheckResourceAttrPair( + "stackit_kms_keyring.keyring", "keyring_id", + "data.stackit_kms_wrapping_key.wrapping_key", "keyring_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id", + "data.stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id", + ), + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "algorithm", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["algorithm"])), + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "display_name", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["display_name"])), + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "purpose", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["purpose"])), + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "protection", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["protection"])), + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "description", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["description"])), + resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "access_scope", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["access_scope"])), + resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "public_key"), + ), + ), + }, + // Import + { + ConfigVariables: testConfigWrappingKeyVarsMax, + ResourceName: "stackit_kms_wrapping_key.wrapping_key", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_kms_wrapping_key.wrapping_key"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_kms_wrapping_key.wrapping_key") + } + keyRingId, ok := r.Primary.Attributes["keyring_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute keyring_id") + } + wrappingKeyId, ok := r.Primary.Attributes["wrapping_key_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute wrapping_key_id") + } + + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, keyRingId, wrappingKeyId), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigWrappingKeyVarsMaxUpdated(), + Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceWrappingKeyMaxConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), + plancheck.ExpectResourceAction("stackit_kms_wrapping_key.wrapping_key", plancheck.ResourceActionReplace), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "region", testutil.Region), + resource.TestCheckResourceAttrPair( + "stackit_kms_keyring.keyring", "keyring_id", + "stackit_kms_wrapping_key.wrapping_key", "keyring_id", + ), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id"), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "algorithm", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["algorithm"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "display_name", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["display_name"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "purpose", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["purpose"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "protection", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["protection"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "description", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["description"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "access_scope", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["access_scope"])), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "public_key"), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + func testAccCheckDestroy(s *terraform.State) error { checkFunctions := []func(s *terraform.State) error{ testAccCheckKeyDestroy, + testAccCheckWrappingKeyDestroy, testAccCheckKeyRingDestroy, } @@ -673,3 +983,41 @@ func testAccCheckKeyDestroy(s *terraform.State) error { return errors.Join(errs...) } + +func testAccCheckWrappingKeyDestroy(s *terraform.State) error { + ctx := context.Background() + var client *kms.APIClient + var err error + if testutil.KMSCustomEndpoint == "" { + client, err = kms.NewAPIClient() + } else { + client, err = kms.NewAPIClient( + coreConfig.WithEndpoint(testutil.KMSCustomEndpoint), + ) + } + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + var errs []error + + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_kms_wrapping_key" { + continue + } + keyRingId := strings.Split(rs.Primary.ID, core.Separator)[2] + wrappingKeyId := strings.Split(rs.Primary.ID, core.Separator)[3] + err := client.DeleteWrappingKeyExecute(ctx, testutil.ProjectId, testutil.Region, keyRingId, wrappingKeyId) + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) { + if oapiErr.StatusCode == http.StatusNotFound { + continue + } + } + errs = append(errs, fmt.Errorf("cannot trigger wrapping key deletion %q: %w", keyRingId, err)) + } + } + + return errors.Join(errs...) +} diff --git a/stackit/internal/services/kms/testdata/wrapping-key-max.tf b/stackit/internal/services/kms/testdata/wrapping-key-max.tf new file mode 100644 index 000000000..ec4ccfd02 --- /dev/null +++ b/stackit/internal/services/kms/testdata/wrapping-key-max.tf @@ -0,0 +1,23 @@ +variable "project_id" {} + +variable "keyring_display_name" {} +variable "display_name" {} +variable "protection" {} +variable "algorithm" {} +variable "purpose" {} +variable "description" {} + +resource "stackit_kms_keyring" "keyring" { + project_id = var.project_id + display_name = var.keyring_display_name +} + +resource "stackit_kms_wrapping_key" "wrapping_key" { + project_id = var.project_id + keyring_id = stackit_kms_keyring.keyring.keyring_id + protection = var.protection + algorithm = var.algorithm + display_name = var.display_name + purpose = var.purpose + description = var.description +} diff --git a/stackit/internal/services/kms/testdata/wrapping-key-min.tf b/stackit/internal/services/kms/testdata/wrapping-key-min.tf new file mode 100644 index 000000000..3a5df17f5 --- /dev/null +++ b/stackit/internal/services/kms/testdata/wrapping-key-min.tf @@ -0,0 +1,21 @@ +variable "project_id" {} + +variable "keyring_display_name" {} +variable "display_name" {} +variable "protection" {} +variable "algorithm" {} +variable "purpose" {} + +resource "stackit_kms_keyring" "keyring" { + project_id = var.project_id + display_name = var.keyring_display_name +} + +resource "stackit_kms_wrapping_key" "wrapping_key" { + project_id = var.project_id + keyring_id = stackit_kms_keyring.keyring.keyring_id + protection = var.protection + algorithm = var.algorithm + display_name = var.display_name + purpose = var.purpose +} diff --git a/stackit/internal/services/kms/wrapping-key/datasource.go b/stackit/internal/services/kms/wrapping-key/datasource.go new file mode 100644 index 000000000..4c5efe63d --- /dev/null +++ b/stackit/internal/services/kms/wrapping-key/datasource.go @@ -0,0 +1,168 @@ +package kms + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-log/tflog" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + kmsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ datasource.DataSource = &wrappingKeyDataSource{} +) + +func NewWrappingKeyDataSource() datasource.DataSource { + return &wrappingKeyDataSource{} +} + +type wrappingKeyDataSource struct { + client *kms.APIClient + providerData core.ProviderData +} + +func (w *wrappingKeyDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_kms_wrapping_key" +} + +func (w *wrappingKeyDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { + var ok bool + w.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) + if !ok { + return + } + + apiClient := kmsUtils.ConfigureClient(ctx, &w.providerData, &response.Diagnostics) + if response.Diagnostics.HasError() { + return + } + + w.client = apiClient + tflog.Info(ctx, "Wrapping key configured") +} + +func (w *wrappingKeyDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { + response.Schema = schema.Schema{ + Description: "KMS wrapping key datasource schema.", + Attributes: map[string]schema.Attribute{ + "access_scope": schema.StringAttribute{ + Description: fmt.Sprintf("The access scope of the key. Default is `%s`. %s", string(kms.ACCESSSCOPE_PUBLIC), utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedAccessScopeEnumValues)...)), + Computed: true, + }, + "algorithm": schema.StringAttribute{ + Description: fmt.Sprintf("The wrapping algorithm used to wrap the key to import. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedWrappingAlgorithmEnumValues)...)), + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "A user chosen description to distinguish multiple wrapping keys.", + Computed: true, + }, + "display_name": schema.StringAttribute{ + Description: "The display name to distinguish multiple wrapping keys.", + Computed: true, + }, + "id": schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`keyring_id`,`wrapping_key_id`\".", + Computed: true, + }, + "keyring_id": schema.StringAttribute{ + Description: "The ID of the associated keyring", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "protection": schema.StringAttribute{ + Description: fmt.Sprintf("The underlying system that is responsible for protecting the key material. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedProtectionEnumValues)...)), + Computed: true, + }, + "purpose": schema.StringAttribute{ + Description: fmt.Sprintf("The purpose for which the key will be used. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedWrappingPurposeEnumValues)...)), + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: "STACKIT project ID to which the keyring is associated.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The resource region. If not defined, the provider region is used.", + }, + "wrapping_key_id": schema.StringAttribute{ + Description: "The ID of the wrapping key", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "public_key": schema.StringAttribute{ + Description: "The public key of the wrapping key.", + Computed: true, + }, + }, + } +} + +func (w *wrappingKeyDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := request.Config.Get(ctx, &model) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + keyRingId := model.KeyRingId.ValueString() + region := w.providerData.GetRegionWithOverride(model.Region) + wrappingKeyId := model.WrappingKeyId.ValueString() + + ctx = tflog.SetField(ctx, "keyring_id", keyRingId) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "wrapping_key_id", wrappingKeyId) + + wrappingKeyResponse, err := w.client.GetWrappingKey(ctx, projectId, region, keyRingId, wrappingKeyId).Execute() + if err != nil { + utils.LogError( + ctx, + &response.Diagnostics, + err, + "Reading wrapping key", + fmt.Sprintf("Wrapping key with ID %q does not exist in project %q.", wrappingKeyId, projectId), + map[int]string{ + http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), + }, + ) + response.State.RemoveResource(ctx) + return + } + + err = mapFields(wrappingKeyResponse, &model, region) + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error reading wrapping key", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = response.State.Set(ctx, model) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Wrapping key read") +} diff --git a/stackit/internal/services/kms/wrapping-key/resource.go b/stackit/internal/services/kms/wrapping-key/resource.go new file mode 100644 index 000000000..e5043241d --- /dev/null +++ b/stackit/internal/services/kms/wrapping-key/resource.go @@ -0,0 +1,430 @@ +package kms + +import ( + "context" + "fmt" + "net/http" + "strings" + + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/stackitcloud/stackit-sdk-go/services/kms/wait" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/kms" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + kmsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ resource.Resource = &wrappingKeyResource{} + _ resource.ResourceWithConfigure = &wrappingKeyResource{} + _ resource.ResourceWithImportState = &wrappingKeyResource{} + _ resource.ResourceWithModifyPlan = &wrappingKeyResource{} +) + +type Model struct { + AccessScope types.String `tfsdk:"access_scope"` + Algorithm types.String `tfsdk:"algorithm"` + Description types.String `tfsdk:"description"` + DisplayName types.String `tfsdk:"display_name"` + Id types.String `tfsdk:"id"` // needed by TF + KeyRingId types.String `tfsdk:"keyring_id"` + Protection types.String `tfsdk:"protection"` + Purpose types.String `tfsdk:"purpose"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + WrappingKeyId types.String `tfsdk:"wrapping_key_id"` + PublicKey types.String `tfsdk:"public_key"` +} + +func NewWrappingKeyResource() resource.Resource { + return &wrappingKeyResource{} +} + +type wrappingKeyResource struct { + client *kms.APIClient + providerData core.ProviderData +} + +func (r *wrappingKeyResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_kms_wrapping_key" +} + +func (r *wrappingKeyResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) + if !ok { + return + } + apiClient := kmsUtils.ConfigureClient(ctx, &r.providerData, &response.Diagnostics) + if response.Diagnostics.HasError() { + return + } + r.client = apiClient +} + +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *wrappingKeyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *wrappingKeyResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Description: "KMS wrapping key resource schema.", + Attributes: map[string]schema.Attribute{ + "access_scope": schema.StringAttribute{ + Description: fmt.Sprintf("The access scope of the key. Default is `%s`. %s", string(kms.ACCESSSCOPE_PUBLIC), utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedAccessScopeEnumValues)...)), + Optional: true, + Computed: true, + Default: stringdefault.StaticString(string(kms.ACCESSSCOPE_PUBLIC)), + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "algorithm": schema.StringAttribute{ + Description: fmt.Sprintf("The wrapping algorithm used to wrap the key to import. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedWrappingAlgorithmEnumValues)...)), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "description": schema.StringAttribute{ + Description: "A user chosen description to distinguish multiple wrapping keys.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "display_name": schema.StringAttribute{ + Description: "The display name to distinguish multiple wrapping keys.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "id": schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`keyring_id`,`wrapping_key_id`\".", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "keyring_id": schema.StringAttribute{ + Description: "The ID of the associated keyring", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "protection": schema.StringAttribute{ + Description: fmt.Sprintf("The underlying system that is responsible for protecting the key material. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedProtectionEnumValues)...)), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "purpose": schema.StringAttribute{ + Description: fmt.Sprintf("The purpose for which the key will be used. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedWrappingPurposeEnumValues)...)), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "project_id": schema.StringAttribute{ + Description: "STACKIT project ID to which the keyring is associated.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + Description: "The resource region. If not defined, the provider region is used.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "wrapping_key_id": schema.StringAttribute{ + Description: "The ID of the wrapping key", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "public_key": schema.StringAttribute{ + Description: "The public key of the wrapping key.", + Computed: true, + }, + }, + } +} + +func (r *wrappingKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + keyRingId := model.KeyRingId.ValueString() + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "keyring_id", keyRingId) + + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating wrapping key", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + createWrappingKeyResp, err := r.client.CreateWrappingKey(ctx, projectId, region, keyRingId).CreateWrappingKeyPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating wrapping key", fmt.Sprintf("Calling API: %v", err)) + return + } + + if createWrappingKeyResp == nil || createWrappingKeyResp.Id == nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating wrapping key", "API returned empty response") + return + } + + wrappingKeyId := *createWrappingKeyResp.Id + + // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{ + "project_id": projectId, + "region": region, + "keyring_id": keyRingId, + "wrapping_key_id": wrappingKeyId, + }) + + wrappingKey, err := wait.CreateWrappingKeyWaitHandler(ctx, r.client, projectId, region, keyRingId, wrappingKeyId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error waiting for wrapping key creation", fmt.Sprintf("Calling API: %v", err)) + return + } + + err = mapFields(wrappingKey, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating wrapping key", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Key created") +} + +func (r *wrappingKeyResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := request.State.Get(ctx, &model) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + keyRingId := model.KeyRingId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + wrappingKeyId := model.WrappingKeyId.ValueString() + + ctx = tflog.SetField(ctx, "keyring_id", keyRingId) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "wrapping_key_id", wrappingKeyId) + + wrappingKeyResponse, err := r.client.GetWrappingKey(ctx, projectId, region, keyRingId, wrappingKeyId).Execute() + if err != nil { + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + if ok && oapiErr.StatusCode == http.StatusNotFound { + response.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &response.Diagnostics, "Error reading wrapping key", fmt.Sprintf("Calling API: %v", err)) + return + } + + err = mapFields(wrappingKeyResponse, &model, region) + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error reading wrapping key", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = response.State.Set(ctx, model) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Wrapping key read") +} + +func (r *wrappingKeyResource) Update(ctx context.Context, _ resource.UpdateRequest, response *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + // wrapping keys cannot be updated, so we log an error. + core.LogAndAddError(ctx, &response.Diagnostics, "Error updating wrapping key", "Keys can't be updated") +} + +func (r *wrappingKeyResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := request.State.Get(ctx, &model) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + keyRingId := model.KeyRingId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + wrappingKeyId := model.WrappingKeyId.ValueString() + + err := r.client.DeleteWrappingKey(ctx, projectId, region, keyRingId, wrappingKeyId).Execute() + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error deleting wrapping key", fmt.Sprintf("Calling API: %v", err)) + } + + tflog.Info(ctx, "wrapping key deleted") +} + +func (r *wrappingKeyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing wrapping key", + fmt.Sprintf("Exptected import identifier with format: [project_id],[region],[keyring_id],[wrapping_key_id], got :%q", req.ID), + ) + return + } + + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "keyring_id": idParts[2], + "wrapping_key_id": idParts[3], + }) + + tflog.Info(ctx, "wrapping key state imported") +} + +func mapFields(wrappingKey *kms.WrappingKey, model *Model, region string) error { + if wrappingKey == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var wrappingKeyId string + if model.WrappingKeyId.ValueString() != "" { + wrappingKeyId = model.WrappingKeyId.ValueString() + } else if wrappingKey.Id != nil { + wrappingKeyId = *wrappingKey.Id + } else { + return fmt.Errorf("key id not present") + } + + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.KeyRingId.ValueString(), wrappingKeyId) + model.Region = types.StringValue(region) + model.WrappingKeyId = types.StringValue(wrappingKeyId) + model.DisplayName = types.StringPointerValue(wrappingKey.DisplayName) + model.PublicKey = types.StringPointerValue(wrappingKey.PublicKey) + model.AccessScope = types.StringValue(string(wrappingKey.GetAccessScope())) + model.Algorithm = types.StringValue(string(wrappingKey.GetAlgorithm())) + model.Purpose = types.StringValue(string(wrappingKey.GetPurpose())) + model.Protection = types.StringValue(string(wrappingKey.GetProtection())) + + // TODO: workaround - remove once STACKITKMS-377 is resolved (just write the return value from the API to the state then) + if !(model.Description.IsNull() && wrappingKey.Description != nil && *wrappingKey.Description == "") { + model.Description = types.StringPointerValue(wrappingKey.Description) + } else { + model.Description = types.StringNull() + } + + return nil +} + +func toCreatePayload(model *Model) (*kms.CreateWrappingKeyPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + return &kms.CreateWrappingKeyPayload{ + AccessScope: kms.CreateKeyPayloadGetAccessScopeAttributeType(conversion.StringValueToPointer(model.AccessScope)), + Algorithm: kms.CreateWrappingKeyPayloadGetAlgorithmAttributeType(conversion.StringValueToPointer(model.Algorithm)), + Description: conversion.StringValueToPointer(model.Description), + DisplayName: conversion.StringValueToPointer(model.DisplayName), + Protection: kms.CreateKeyPayloadGetProtectionAttributeType(conversion.StringValueToPointer(model.Protection)), + Purpose: kms.CreateWrappingKeyPayloadGetPurposeAttributeType(conversion.StringValueToPointer(model.Purpose)), + }, nil +} diff --git a/stackit/internal/services/kms/wrapping-key/resource_test.go b/stackit/internal/services/kms/wrapping-key/resource_test.go new file mode 100644 index 000000000..fcc189e03 --- /dev/null +++ b/stackit/internal/services/kms/wrapping-key/resource_test.go @@ -0,0 +1,244 @@ +package kms + +import ( + "fmt" + "testing" + + "github.com/google/uuid" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" +) + +var ( + projectId = uuid.NewString() + keyRingId = uuid.NewString() + wrappingKeyId = uuid.NewString() +) + +func TestMapFields(t *testing.T) { + type args struct { + state *Model + input *kms.WrappingKey + region string + } + tests := []struct { + description string + args args + expected *Model + isValid bool + }{ + { + description: "default values", + args: args{ + state: &Model{ + KeyRingId: types.StringValue(keyRingId), + ProjectId: types.StringValue(projectId), + WrappingKeyId: types.StringValue(wrappingKeyId), + }, + input: &kms.WrappingKey{ + Id: utils.Ptr("wid"), + AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC), + Algorithm: utils.Ptr(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256), + Purpose: utils.Ptr(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY), + Protection: utils.Ptr(kms.PROTECTION_SOFTWARE), + }, + region: "eu01", + }, + expected: &Model{ + Description: types.StringNull(), + DisplayName: types.StringNull(), + KeyRingId: types.StringValue(keyRingId), + Id: types.StringValue(fmt.Sprintf("%s,eu01,%s,%s", projectId, keyRingId, wrappingKeyId)), + ProjectId: types.StringValue(projectId), + Region: types.StringValue("eu01"), + WrappingKeyId: types.StringValue(wrappingKeyId), + AccessScope: types.StringValue(string(kms.ACCESSSCOPE_PUBLIC)), + Algorithm: types.StringValue(string(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256)), + Purpose: types.StringValue(string(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY)), + Protection: types.StringValue(string(kms.PROTECTION_SOFTWARE)), + }, + isValid: true, + }, + { + description: "values_ok", + args: args{ + state: &Model{ + KeyRingId: types.StringValue(keyRingId), + ProjectId: types.StringValue(projectId), + WrappingKeyId: types.StringValue(wrappingKeyId), + }, + input: &kms.WrappingKey{ + Description: utils.Ptr("descr"), + DisplayName: utils.Ptr("name"), + Id: utils.Ptr(wrappingKeyId), + AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC), + Algorithm: utils.Ptr(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256), + Purpose: utils.Ptr(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY), + Protection: utils.Ptr(kms.PROTECTION_SOFTWARE), + }, + region: "eu02", + }, + expected: &Model{ + Description: types.StringValue("descr"), + DisplayName: types.StringValue("name"), + KeyRingId: types.StringValue(keyRingId), + Id: types.StringValue(fmt.Sprintf("%s,eu02,%s,%s", projectId, keyRingId, wrappingKeyId)), + ProjectId: types.StringValue(projectId), + Region: types.StringValue("eu02"), + WrappingKeyId: types.StringValue(wrappingKeyId), + AccessScope: types.StringValue(string(kms.ACCESSSCOPE_PUBLIC)), + Algorithm: types.StringValue(string(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256)), + Purpose: types.StringValue(string(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY)), + Protection: types.StringValue(string(kms.PROTECTION_SOFTWARE)), + }, + isValid: true, + }, + { + description: "nil_response_field", + args: args{ + state: &Model{}, + input: &kms.WrappingKey{ + Id: nil, + }, + }, + expected: &Model{}, + isValid: false, + }, + { + description: "nil_response", + args: args{ + state: &Model{}, + input: nil, + }, + expected: &Model{}, + isValid: false, + }, + { + description: "no_resource_id", + args: args{ + state: &Model{ + Region: types.StringValue("eu01"), + ProjectId: types.StringValue("pid"), + }, + input: &kms.WrappingKey{}, + }, + expected: &Model{}, + isValid: false, + }, + { + // TODO: test for workaround - remove once STACKITKMS-377 is resolved + description: "empty description string", + args: args{ + state: &Model{ + KeyRingId: types.StringValue(keyRingId), + ProjectId: types.StringValue(projectId), + WrappingKeyId: types.StringValue(wrappingKeyId), + }, + input: &kms.WrappingKey{ + Description: utils.Ptr(""), + Id: utils.Ptr(wrappingKeyId), + AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC), + Algorithm: utils.Ptr(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256), + Purpose: utils.Ptr(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY), + Protection: utils.Ptr(kms.PROTECTION_SOFTWARE), + }, + region: "eu02", + }, + expected: &Model{ + Description: types.StringNull(), + KeyRingId: types.StringValue(keyRingId), + Id: types.StringValue(fmt.Sprintf("%s,eu02,%s,%s", projectId, keyRingId, wrappingKeyId)), + ProjectId: types.StringValue(projectId), + Region: types.StringValue("eu02"), + WrappingKeyId: types.StringValue(wrappingKeyId), + AccessScope: types.StringValue(string(kms.ACCESSSCOPE_PUBLIC)), + Algorithm: types.StringValue(string(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256)), + Purpose: types.StringValue(string(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY)), + Protection: types.StringValue(string(kms.PROTECTION_SOFTWARE)), + }, + isValid: true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapFields(tt.args.input, tt.args.state, tt.args.region) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + + if tt.isValid { + diff := cmp.Diff(tt.args.state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} + +func TestToCreatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + expected *kms.CreateWrappingKeyPayload + isValid bool + }{ + { + "default_values", + &Model{}, + &kms.CreateWrappingKeyPayload{}, + true, + }, + { + "simple_values", + &Model{ + DisplayName: types.StringValue("name"), + }, + &kms.CreateWrappingKeyPayload{ + DisplayName: utils.Ptr("name"), + }, + true, + }, + { + "null_fields", + &Model{ + DisplayName: types.StringValue(""), + Description: types.StringValue(""), + }, + &kms.CreateWrappingKeyPayload{ + DisplayName: utils.Ptr(""), + Description: utils.Ptr(""), + }, + true, + }, + { + "nil_model", + nil, + nil, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output, err := toCreatePayload(tt.input) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(output, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/provider.go b/stackit/provider.go index 750d8f53b..fbc149f25 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -50,6 +50,7 @@ import ( iaasalphaRoutingTables "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/tables" kmsKey "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/key" kmsKeyRing "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/keyring" + kmsWrappingKey "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/wrapping-key" loadBalancer "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/loadbalancer" loadBalancerObservabilityCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/observability-credential" logMeCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/credential" @@ -497,6 +498,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource iaasSecurityGroupRule.NewSecurityGroupRuleDataSource, kmsKey.NewKeyDataSource, kmsKeyRing.NewKeyRingDataSource, + kmsWrappingKey.NewWrappingKeyDataSource, loadBalancer.NewLoadBalancerDataSource, logMeInstance.NewInstanceDataSource, logMeCredential.NewCredentialDataSource, @@ -567,6 +569,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { iaasalphaRoutingTableRoute.NewRoutingTableRouteResource, kmsKey.NewKeyResource, kmsKeyRing.NewKeyRingResource, + kmsWrappingKey.NewWrappingKeyResource, loadBalancer.NewLoadBalancerResource, loadBalancerObservabilityCredential.NewObservabilityCredentialResource, logMeInstance.NewInstanceResource, From ab107e799a0aab95c8151f2fa03ff45953a7bfbc Mon Sep 17 00:00:00 2001 From: Ruben Hoenle Date: Tue, 18 Nov 2025 16:44:43 +0100 Subject: [PATCH 2/2] PR review findings --- docs/data-sources/kms_wrapping_key.md | 2 ++ docs/resources/kms_wrapping_key.md | 2 ++ stackit/internal/services/kms/kms_acc_test.go | 12 +++++++ .../services/kms/testdata/wrapping-key-max.tf | 2 ++ .../services/kms/wrapping-key/datasource.go | 13 ++++++-- .../services/kms/wrapping-key/resource.go | 31 +++++++++++++++++-- 6 files changed, 56 insertions(+), 6 deletions(-) diff --git a/docs/data-sources/kms_wrapping_key.md b/docs/data-sources/kms_wrapping_key.md index c942ff1d5..83e66a938 100644 --- a/docs/data-sources/kms_wrapping_key.md +++ b/docs/data-sources/kms_wrapping_key.md @@ -37,8 +37,10 @@ data "stackit_kms_wrapping_key" "example" { - `access_scope` (String) The access scope of the key. Default is `PUBLIC`. Possible values are: `PUBLIC`, `SNA`. - `algorithm` (String) The wrapping algorithm used to wrap the key to import. Possible values are: `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`. +- `created_at` (String) The date and time the creation of the wrapping key was triggered. - `description` (String) A user chosen description to distinguish multiple wrapping keys. - `display_name` (String) The display name to distinguish multiple wrapping keys. +- `expires_at` (String) The date and time the wrapping key will expire. - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`keyring_id`,`wrapping_key_id`". - `protection` (String) The underlying system that is responsible for protecting the key material. Possible values are: `software`. - `public_key` (String) The public key of the wrapping key. diff --git a/docs/resources/kms_wrapping_key.md b/docs/resources/kms_wrapping_key.md index 373a53652..392c35db7 100644 --- a/docs/resources/kms_wrapping_key.md +++ b/docs/resources/kms_wrapping_key.md @@ -43,6 +43,8 @@ resource "stackit_kms_wrapping_key" "example" { ### Read-Only +- `created_at` (String) The date and time the creation of the wrapping key was triggered. +- `expires_at` (String) The date and time the wrapping key will expire. - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`keyring_id`,`wrapping_key_id`". - `public_key` (String) The public key of the wrapping key. - `wrapping_key_id` (String) The ID of the wrapping key diff --git a/stackit/internal/services/kms/kms_acc_test.go b/stackit/internal/services/kms/kms_acc_test.go index 78adc7e10..bea3e4523 100644 --- a/stackit/internal/services/kms/kms_acc_test.go +++ b/stackit/internal/services/kms/kms_acc_test.go @@ -640,6 +640,8 @@ func TestAccWrappingKeyMin(t *testing.T) { resource.TestCheckNoResourceAttr("stackit_kms_wrapping_key.wrapping_key", "description"), resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)), resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "public_key"), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "expires_at"), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "created_at"), ), }, // Data Source @@ -682,6 +684,8 @@ func TestAccWrappingKeyMin(t *testing.T) { resource.TestCheckNoResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "description"), resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)), resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "public_key"), + resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "expires_at"), + resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "created_at"), ), ), }, @@ -733,6 +737,8 @@ func TestAccWrappingKeyMin(t *testing.T) { resource.TestCheckNoResourceAttr("stackit_kms_wrapping_key.wrapping_key", "description"), resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)), resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "public_key"), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "expires_at"), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "created_at"), ), }, // Deletion is done by the framework implicitly @@ -770,6 +776,8 @@ func TestAccWrappingKeyMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "description", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["description"])), resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "access_scope", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["access_scope"])), resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "public_key"), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "expires_at"), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "created_at"), ), }, // Data Source @@ -812,6 +820,8 @@ func TestAccWrappingKeyMax(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "description", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["description"])), resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "access_scope", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["access_scope"])), resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "public_key"), + resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "expires_at"), + resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "created_at"), ), ), }, @@ -863,6 +873,8 @@ func TestAccWrappingKeyMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "description", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["description"])), resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "access_scope", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["access_scope"])), resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "public_key"), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "expires_at"), + resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "created_at"), ), }, // Deletion is done by the framework implicitly diff --git a/stackit/internal/services/kms/testdata/wrapping-key-max.tf b/stackit/internal/services/kms/testdata/wrapping-key-max.tf index ec4ccfd02..0b461ed06 100644 --- a/stackit/internal/services/kms/testdata/wrapping-key-max.tf +++ b/stackit/internal/services/kms/testdata/wrapping-key-max.tf @@ -6,6 +6,7 @@ variable "protection" {} variable "algorithm" {} variable "purpose" {} variable "description" {} +variable "access_scope" {} resource "stackit_kms_keyring" "keyring" { project_id = var.project_id @@ -20,4 +21,5 @@ resource "stackit_kms_wrapping_key" "wrapping_key" { display_name = var.display_name purpose = var.purpose description = var.description + access_scope = var.access_scope } diff --git a/stackit/internal/services/kms/wrapping-key/datasource.go b/stackit/internal/services/kms/wrapping-key/datasource.go index 4c5efe63d..cca9a2558 100644 --- a/stackit/internal/services/kms/wrapping-key/datasource.go +++ b/stackit/internal/services/kms/wrapping-key/datasource.go @@ -42,13 +42,12 @@ func (w *wrappingKeyDataSource) Configure(ctx context.Context, request datasourc return } - apiClient := kmsUtils.ConfigureClient(ctx, &w.providerData, &response.Diagnostics) + w.client = kmsUtils.ConfigureClient(ctx, &w.providerData, &response.Diagnostics) if response.Diagnostics.HasError() { return } - w.client = apiClient - tflog.Info(ctx, "Wrapping key configured") + tflog.Info(ctx, "KMS client configured") } func (w *wrappingKeyDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { @@ -116,6 +115,14 @@ func (w *wrappingKeyDataSource) Schema(_ context.Context, _ datasource.SchemaReq Description: "The public key of the wrapping key.", Computed: true, }, + "expires_at": schema.StringAttribute{ + Description: "The date and time the wrapping key will expire.", + Computed: true, + }, + "created_at": schema.StringAttribute{ + Description: "The date and time the creation of the wrapping key was triggered.", + Computed: true, + }, }, } } diff --git a/stackit/internal/services/kms/wrapping-key/resource.go b/stackit/internal/services/kms/wrapping-key/resource.go index e5043241d..021997bfa 100644 --- a/stackit/internal/services/kms/wrapping-key/resource.go +++ b/stackit/internal/services/kms/wrapping-key/resource.go @@ -2,9 +2,11 @@ package kms import ( "context" + "errors" "fmt" "net/http" "strings" + "time" sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" @@ -48,6 +50,8 @@ type Model struct { Region types.String `tfsdk:"region"` WrappingKeyId types.String `tfsdk:"wrapping_key_id"` PublicKey types.String `tfsdk:"public_key"` + ExpiresAt types.String `tfsdk:"expires_at"` + CreatedAt types.String `tfsdk:"created_at"` } func NewWrappingKeyResource() resource.Resource { @@ -69,11 +73,13 @@ func (r *wrappingKeyResource) Configure(ctx context.Context, request resource.Co if !ok { return } - apiClient := kmsUtils.ConfigureClient(ctx, &r.providerData, &response.Diagnostics) + + r.client = kmsUtils.ConfigureClient(ctx, &r.providerData, &response.Diagnostics) if response.Diagnostics.HasError() { return } - r.client = apiClient + + tflog.Info(ctx, "KMS client configured") } // ModifyPlan implements resource.ResourceWithModifyPlan. @@ -222,6 +228,14 @@ func (r *wrappingKeyResource) Schema(_ context.Context, _ resource.SchemaRequest Description: "The public key of the wrapping key.", Computed: true, }, + "expires_at": schema.StringAttribute{ + Description: "The date and time the wrapping key will expire.", + Computed: true, + }, + "created_at": schema.StringAttribute{ + Description: "The date and time the creation of the wrapping key was triggered.", + Computed: true, + }, }, } } @@ -309,7 +323,8 @@ func (r *wrappingKeyResource) Read(ctx context.Context, request resource.ReadReq wrappingKeyResponse, err := r.client.GetWrappingKey(ctx, projectId, region, keyRingId, wrappingKeyId).Execute() if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) if ok && oapiErr.StatusCode == http.StatusNotFound { response.State.RemoveResource(ctx) return @@ -405,6 +420,16 @@ func mapFields(wrappingKey *kms.WrappingKey, model *Model, region string) error model.Purpose = types.StringValue(string(wrappingKey.GetPurpose())) model.Protection = types.StringValue(string(wrappingKey.GetProtection())) + model.CreatedAt = types.StringNull() + if wrappingKey.CreatedAt != nil { + model.CreatedAt = types.StringValue(wrappingKey.CreatedAt.Format(time.RFC3339)) + } + + model.ExpiresAt = types.StringNull() + if wrappingKey.ExpiresAt != nil { + model.ExpiresAt = types.StringValue(wrappingKey.ExpiresAt.Format(time.RFC3339)) + } + // TODO: workaround - remove once STACKITKMS-377 is resolved (just write the return value from the API to the state then) if !(model.Description.IsNull() && wrappingKey.Description != nil && *wrappingKey.Description == "") { model.Description = types.StringPointerValue(wrappingKey.Description)