diff --git a/docs/data-sources/kms_key.md b/docs/data-sources/kms_key.md new file mode 100644 index 000000000..be4e35778 --- /dev/null +++ b/docs/data-sources/kms_key.md @@ -0,0 +1,36 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_kms_key Data Source - stackit" +subcategory: "" +description: |- + KMS Key resource schema. Must have a region specified in the provider configuration. +--- + +# stackit_kms_key (Data Source) + +KMS Key resource schema. Must have a `region` specified in the provider configuration. + + + + +## Schema + +### Required + +- `algorithm` (String) The encryption algorithm that the key will use to encrypt data +- `backend` (String) The backend that is used for KMS. Right now, only software is accepted. +- `display_name` (String) The display name to distinguish multiple keys +- `import_only` (Boolean) Terraform's internal resource ID. It is structured as "`project_id`,`instance_id`". +- `key_id` (String) +- `key_ring_id` (String) The ID of the associated key ring +- `project_id` (String) STACKIT project ID to which the key ring is associated. +- `purpose` (String) The purpose for which the key will be used + +### Optional + +- `description` (String) A user chosen description to distinguish multiple 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`,`instance_id`". diff --git a/docs/data-sources/kms_key_ring.md b/docs/data-sources/kms_key_ring.md new file mode 100644 index 000000000..55f05f207 --- /dev/null +++ b/docs/data-sources/kms_key_ring.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_kms_key_ring Data Source - stackit" +subcategory: "" +description: |- + KMS Key Ring resource schema. +--- + +# stackit_kms_key_ring (Data Source) + +KMS Key Ring resource schema. + + + + +## Schema + +### Required + +- `display_name` (String) A user chosen description to distinguish multiple key rings. +- `key_ring_id` (String) An auto generated unique id which identifies the key ring. +- `project_id` (String) STACKIT project ID to which the key ring is associated. + +### Optional + +- `description` (String) A user chosen description to distinguish multiple key rings. +- `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`,`instance_id`". diff --git a/docs/data-sources/kms_wrapping_key.md b/docs/data-sources/kms_wrapping_key.md new file mode 100644 index 000000000..ea2952a3f --- /dev/null +++ b/docs/data-sources/kms_wrapping_key.md @@ -0,0 +1,35 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_kms_wrapping_key Data Source - stackit" +subcategory: "" +description: |- + KMS Key resource schema. Must have a region specified in the provider configuration. +--- + +# stackit_kms_wrapping_key (Data Source) + +KMS Key resource schema. Must have a `region` specified in the provider configuration. + + + + +## Schema + +### Required + +- `algorithm` (String) The encryption algorithm that the key will use to encrypt data +- `backend` (String) The backend that is used for KMS. Right now, only software is accepted. +- `display_name` (String) The display name to distinguish multiple keys +- `key_ring_id` (String) The ID of the associated key ring +- `project_id` (String) STACKIT project ID to which the key ring is associated. +- `purpose` (String) The purpose for which the key will be used +- `wrapping_key_id` (String) + +### Optional + +- `description` (String) A user chosen description to distinguish multiple 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`,`instance_id`". diff --git a/docs/index.md b/docs/index.md index 55fd20abb..a0a705112 100644 --- a/docs/index.md +++ b/docs/index.md @@ -162,6 +162,7 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de - `experiments` (List of String) Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: iam, routing-tables, network - `git_custom_endpoint` (String) Custom endpoint for the Git service - `iaas_custom_endpoint` (String) Custom endpoint for the IaaS service +- `kms_custom_endpoint` (List of String) Custom endpoint for the KMS service - `loadbalancer_custom_endpoint` (String) Custom endpoint for the Load Balancer service - `logme_custom_endpoint` (String) Custom endpoint for the LogMe service - `mariadb_custom_endpoint` (String) Custom endpoint for the MariaDB service diff --git a/docs/resources/kms_key.md b/docs/resources/kms_key.md new file mode 100644 index 000000000..7d73d973b --- /dev/null +++ b/docs/resources/kms_key.md @@ -0,0 +1,49 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_kms_key Resource - stackit" +subcategory: "" +description: |- + KMS Key resource schema. Must have a region specified in the provider configuration. +--- + +# stackit_kms_key (Resource) + +KMS Key resource schema. Must have a `region` specified in the provider configuration. + +## Example Usage + +```terraform +resource "stackit_kms_key" "name" { + algorithm = "example algorithm" + backend = "software" + description = "new descr" + display_name = "example name" + import_only = false + key_ring_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + purpose = "example purpose" +} +``` + + +## Schema + +### Required + +- `algorithm` (String) The encryption algorithm that the key will use to encrypt data +- `backend` (String) The backend that is used for KMS. Right now, only software is accepted. +- `display_name` (String) The display name to distinguish multiple keys +- `import_only` (Boolean) Terraform's internal resource ID. It is structured as "`project_id`,`instance_id`". +- `key_ring_id` (String) The ID of the associated key ring +- `project_id` (String) STACKIT project ID to which the key ring is associated. +- `purpose` (String) The purpose for which the key will be used + +### Optional + +- `description` (String) A user chosen description to distinguish multiple 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`,`instance_id`". +- `key_id` (String) The ID of the key diff --git a/docs/resources/kms_key_ring.md b/docs/resources/kms_key_ring.md new file mode 100644 index 000000000..f0cd73514 --- /dev/null +++ b/docs/resources/kms_key_ring.md @@ -0,0 +1,40 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_kms_key_ring Resource - stackit" +subcategory: "" +description: |- + KMS Key Ring resource schema. Must have a region specified in the provider configuration. +--- + +# stackit_kms_key_ring (Resource) + +KMS Key Ring resource schema. Must have a `region` specified in the provider configuration. + +## Example Usage + +```terraform +resource "stackit_kms_key_ring" "example" { + description = "example description" + display_name = "example name" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + region_id = "eu01" +} +``` + + +## Schema + +### Required + +- `display_name` (String) A user chosen description to distinguish multiple key rings. +- `project_id` (String) STACKIT project ID to which the key ring is associated. + +### Optional + +- `description` (String) A user chosen description to distinguish multiple key rings. +- `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`,`instance_id`". +- `key_ring_id` (String) An auto generated unique id which identifies the key ring. diff --git a/docs/resources/kms_wrapping_key.md b/docs/resources/kms_wrapping_key.md new file mode 100644 index 000000000..aeb39f2f9 --- /dev/null +++ b/docs/resources/kms_wrapping_key.md @@ -0,0 +1,47 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_kms_wrapping_key Resource - stackit" +subcategory: "" +description: |- + KMS Key resource schema. Must have a region specified in the provider configuration. +--- + +# stackit_kms_wrapping_key (Resource) + +KMS Key resource schema. Must have a `region` specified in the provider configuration. + +## Example Usage + +```terraform +resource "stackit_kms_wrapping_key" "name" { + algorithm = "example algorithm" + backend = "software" + description = "new descr" + display_name = "example name" + key_ring_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + purpose = "example purpose" +} +``` + + +## Schema + +### Required + +- `algorithm` (String) The encryption algorithm that the key will use to encrypt data +- `backend` (String) The backend that is used for KMS. Right now, only software is accepted. +- `display_name` (String) The display name to distinguish multiple keys +- `key_ring_id` (String) The ID of the associated key ring +- `project_id` (String) STACKIT project ID to which the key ring is associated. +- `purpose` (String) The purpose for which the key will be used + +### Optional + +- `description` (String) A user chosen description to distinguish multiple 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`,`instance_id`". +- `wrapping_key_id` (String) The ID of the wrapping key diff --git a/examples/resources/stackit_kms_key/resource.tf b/examples/resources/stackit_kms_key/resource.tf new file mode 100644 index 000000000..1431c9045 --- /dev/null +++ b/examples/resources/stackit_kms_key/resource.tf @@ -0,0 +1,10 @@ +resource "stackit_kms_key" "name" { + algorithm = "example algorithm" + backend = "software" + description = "new descr" + display_name = "example name" + import_only = false + key_ring_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + purpose = "example purpose" +} diff --git a/examples/resources/stackit_kms_key_ring/resource.tf b/examples/resources/stackit_kms_key_ring/resource.tf new file mode 100644 index 000000000..a1a6e232e --- /dev/null +++ b/examples/resources/stackit_kms_key_ring/resource.tf @@ -0,0 +1,6 @@ +resource "stackit_kms_key_ring" "example" { + description = "example description" + display_name = "example name" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + region_id = "eu01" +} 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..3fb55692b --- /dev/null +++ b/examples/resources/stackit_kms_wrapping_key/resource.tf @@ -0,0 +1,9 @@ +resource "stackit_kms_wrapping_key" "name" { + algorithm = "example algorithm" + backend = "software" + description = "new descr" + display_name = "example name" + key_ring_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + purpose = "example purpose" +} diff --git a/go.mod b/go.mod index 9fcd384d9..7eebc460a 100644 --- a/go.mod +++ b/go.mod @@ -17,13 +17,14 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/git v0.7.1 github.com/stackitcloud/stackit-sdk-go/services/iaas v0.28.0 github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha + github.com/stackitcloud/stackit-sdk-go/services/kms v0.5.0 github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.5.1 github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.1 github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1 github.com/stackitcloud/stackit-sdk-go/services/modelserving v0.5.1 - github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.1 + github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.2 github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.3.1 - github.com/stackitcloud/stackit-sdk-go/services/observability v0.9.1 + github.com/stackitcloud/stackit-sdk-go/services/observability v0.10.0 github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.1 github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1 github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.1 @@ -37,7 +38,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/ske v1.3.0 github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.1 github.com/teambition/rrule-go v1.8.2 - golang.org/x/mod v0.26.0 + golang.org/x/mod v0.27.0 ) require github.com/hashicorp/go-retryablehttp v0.7.7 // indirect @@ -65,7 +66,7 @@ require ( github.com/hashicorp/terraform-exec v0.23.0 // indirect github.com/hashicorp/terraform-json v0.25.0 // indirect github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 // indirect - github.com/hashicorp/terraform-registry-address v0.2.5 // indirect + github.com/hashicorp/terraform-registry-address v0.3.0 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/kr/pretty v0.3.1 // indirect @@ -76,7 +77,7 @@ require ( github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/oklog/run v1.1.0 // indirect + github.com/oklog/run v1.2.0 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/stackitcloud/stackit-sdk-go/services/authorization v0.8.1 github.com/stretchr/testify v1.8.4 // indirect @@ -84,16 +85,16 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.16.3 // indirect - golang.org/x/crypto v0.40.0 // indirect - golang.org/x/net v0.42.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.27.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect golang.org/x/tools v0.35.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.73.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/grpc v1.74.2 // indirect + google.golang.org/protobuf v1.36.7 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 1cb3562b7..1de6cef16 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= @@ -98,8 +98,8 @@ github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 h1:NFPMacTrY/IdcIcnUB+7hsor github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0/go.mod h1:QYmYnLfsosrxjCnGY1p9c7Zj6n9thnEE+7RObeYs3fA= github.com/hashicorp/terraform-plugin-testing v1.13.2 h1:mSotG4Odl020vRjIenA3rggwo6Kg6XCKIwtRhYgp+/M= github.com/hashicorp/terraform-plugin-testing v1.13.2/go.mod h1:WHQ9FDdiLoneey2/QHpGM/6SAYf4A7AZazVg7230pLE= -github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwDkRe4pcBDq0uuK5VJngU1M= -github.com/hashicorp/terraform-registry-address v0.2.5/go.mod h1:PpzXWINwB5kuVS5CA7m1+eO2f1jKb5ZDIxrOPfpnGkg= +github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo= +github.com/hashicorp/terraform-registry-address v0.3.0/go.mod h1:jRGCMiLaY9zii3GLC7hqpSnwhfnCN5yzvY0hh4iCGbM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= @@ -136,8 +136,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= -github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= +github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -164,6 +164,8 @@ github.com/stackitcloud/stackit-sdk-go/services/iaas v0.28.0 h1:ggnr5AD62QiP+Us+ github.com/stackitcloud/stackit-sdk-go/services/iaas v0.28.0/go.mod h1:b/jgJf7QHdRzU2fmZeJJtu5j0TAevDRghzcn5MyRmOI= github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha h1:m1jq6a8dbUe+suFuUNdHmM/cSehpGLUtDbK1CqLqydg= github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha/go.mod h1:Nu1b5Phsv8plgZ51+fkxPVsU91ZJ5Ayz+cthilxdmQ8= +github.com/stackitcloud/stackit-sdk-go/services/kms v0.5.0 h1:pKxX9XcCfTqAM86xD66/iQVWdXSkAT+9AGNYD/195jA= +github.com/stackitcloud/stackit-sdk-go/services/kms v0.5.0/go.mod h1:KEPVoO21pC4bjy5l0nyhjUJ0+uVwVWb+k2TYrzJ8xYw= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.5.1 h1:OdJEs8eOfrzn9tCBDLxIyP8hX50zPfcXNYnRoQX+chs= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.5.1/go.mod h1:11uzaOPCF9SeDHXEGOPMlHDD3J5r2TnvCGUwW9Igq9c= github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.1 h1:hv5WrRU9rN6Jx4OwdOGJRyaQrfA9p1tzEoQK6/CDyoA= @@ -172,12 +174,12 @@ github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1 h1:Db/ebOL2vbpIe github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1/go.mod h1:8jdN4v2euK3f9gfdzbRi8e4nBJ8g/Q5YF9aPB4M4fCQ= github.com/stackitcloud/stackit-sdk-go/services/modelserving v0.5.1 h1:Fc+iVo5de5Z1XI6nXGcuswZYkCiryr2h/Z9v2JmNk0w= github.com/stackitcloud/stackit-sdk-go/services/modelserving v0.5.1/go.mod h1:84Gfz5wimt0gNyOXaAfTwVk/RFovgBxq3AOG2cdZx4Q= -github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.1 h1:XOpikSY2IXfBJPzUdgBk69iJXFC99xzfYtY1h4bZ5vM= -github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.1/go.mod h1:G7S/hGa6EyX5Avxxw/PIdbdtbFeiXL/T1vUkPOJ120w= +github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.2 h1:BQ+qAkVS/aGHepE/+gVsvSg1sRkPOyIUI/jkCyUOrWg= +github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.2/go.mod h1:oc8Mpwl7O6EZwG0YxfhOzNCJwNQBWK5rFh764OtxoMY= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.3.1 h1:4jsFLbDVEosYTgQz6lPds1E9KDOiHwjuhWqcG+lo5B4= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.3.1/go.mod h1:j1SHAS5lN8F9b/iPUOfjAl9QAA9tOT7NKOiDEzcM2zc= -github.com/stackitcloud/stackit-sdk-go/services/observability v0.9.1 h1:eSiUKN61FJ4x42vgIvhVU7bgmrPbj05xR4y0nnRV5Yg= -github.com/stackitcloud/stackit-sdk-go/services/observability v0.9.1/go.mod h1:oJku0heeBwsy4IToqhvSdPJI++GUNkBSESxOjiLWRVQ= +github.com/stackitcloud/stackit-sdk-go/services/observability v0.10.0 h1:SIctDqGprEoZArWaTds7hzQyh8Pqaz95Nmuj/1QuDEQ= +github.com/stackitcloud/stackit-sdk-go/services/observability v0.10.0/go.mod h1:tJEOi6L0le4yQZPGwalup/PZ13gqs1aCQDqlUs2cYW0= github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.1 h1:50n87uZn0EvSP9hJGLqd3Wm2hfqbyh7BMGGCk7axgqA= github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.1/go.mod h1:jfguuSPa56Z5Bzs/Xg/CI37XzPo5Zn5lzC5LhfuT8Qc= github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1 h1:K8vXele3U6b5urcSIpq21EkVblWfPDY3eMPSuQ48TkI= @@ -224,29 +226,29 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -263,18 +265,18 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -285,14 +287,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index e477c9064..12aacb555 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -33,6 +33,7 @@ type ProviderData struct { DnsCustomEndpoint string GitCustomEndpoint string IaaSCustomEndpoint string + KMSCustomEndpoint string LoadBalancerCustomEndpoint string LogMeCustomEndpoint string MariaDBCustomEndpoint string diff --git a/stackit/internal/services/kms/key-ring/datasource.go b/stackit/internal/services/kms/key-ring/datasource.go new file mode 100644 index 000000000..4877f10c4 --- /dev/null +++ b/stackit/internal/services/kms/key-ring/datasource.go @@ -0,0 +1,156 @@ +package kms + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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" + "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 = &keyRingDataSource{} +) + +func NewKeyRingDataSource() datasource.DataSource { + return &keyRingDataSource{} +} + +type keyRingDataSource struct { + client *kms.APIClient + providerData core.ProviderData +} + +func (k *keyRingDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_kms_key_ring" +} + +func (k *keyRingDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { + var ok bool + k.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) + if !ok { + return + } + + apiClient := kmsUtils.ConfigureClient(ctx, &k.providerData, &response.Diagnostics) + if response.Diagnostics.HasError() { + return + } + + k.client = apiClient + tflog.Info(ctx, "Key ring configured") +} + +func (k *keyRingDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { + descriptions := map[string]string{ + "main": "KMS Key Ring resource schema.", + "description": "A user chosen description to distinguish multiple key rings.", + "display_name": "The display name to distinguish multiple key rings.", + "key_ring_id": "An auto generated unique id which identifies the key ring.", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", + "project_id": "STACKIT project ID to which the key ring is associated.", + "region_id": "The STACKIT region name the key ring is located in.", + } + + response.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "description": schema.StringAttribute{ + Description: descriptions["description"], + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "display_name": schema.StringAttribute{ + Description: descriptions["description"], + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "key_ring_id": schema.StringAttribute{ + Description: descriptions["key_ring_id"], + Computed: false, + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + 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.", + }, + }, + } +} + +func (k *keyRingDataSource) 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 := k.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "key_ring_id", keyRingId) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + + keyRingResponse, err := k.client.GetKeyRing(ctx, projectId, region, keyRingId).Execute() + if err != nil { + utils.LogError( + ctx, + &response.Diagnostics, + err, + "Reading key ring", + fmt.Sprintf("Key ring with ID %q does not exist in project %q.", keyRingId, 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(keyRingResponse, &model, region) + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error reading key ring", 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, "Key ring read") +} diff --git a/stackit/internal/services/kms/key-ring/resource.go b/stackit/internal/services/kms/key-ring/resource.go new file mode 100644 index 000000000..179cf1387 --- /dev/null +++ b/stackit/internal/services/kms/key-ring/resource.go @@ -0,0 +1,313 @@ +package kms + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "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/stackit-sdk-go/services/kms/wait" + "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 = &keyRingResource{} + _ resource.ResourceWithConfigure = &keyRingResource{} + _ resource.ResourceWithImportState = &keyRingResource{} +) + +type Model struct { + Description types.String `tfsdk:"description"` + DisplayName types.String `tfsdk:"display_name"` + KeyRingId types.String `tfsdk:"key_ring_id"` + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` +} + +func NewKeyRingResource() resource.Resource { + return &keyRingResource{} +} + +type keyRingResource struct { + client *kms.APIClient + providerData core.ProviderData +} + +func (k *keyRingResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_kms_key_ring" +} + +func (k *keyRingResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + var ok bool + k.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) + if !ok { + return + } + + apiClient := kmsUtils.ConfigureClient(ctx, &k.providerData, &response.Diagnostics) + if response.Diagnostics.HasError() { + return + } + k.client = apiClient +} + +func (k *keyRingResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { + descriptions := map[string]string{ + "main": "KMS Key Ring resource schema. Must have a `region` specified in the provider configuration.", + "description": "A user chosen description to distinguish multiple key rings.", + "display_name": "The display name to distinguish multiple key rings.", + "key_ring_id": "An auto generated unique id which identifies the key ring.", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", + "project_id": "STACKIT project ID to which the key ring is associated.", + "region_id": "The STACKIT region name the key ring is located in.", + } + + response.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "description": schema.StringAttribute{ + Description: descriptions["description"], + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "display_name": schema.StringAttribute{ + Description: descriptions["description"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "key_ring_id": schema.StringAttribute{ + Description: descriptions["key_ring_id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + 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.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (k *keyRingResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := request.Plan.Get(ctx, &model) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + region := k.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error creating key ring", fmt.Sprintf("Creating API payload: %v", err)) + return + } + createResponse, err := k.client.CreateKeyRing(ctx, projectId, region).CreateKeyRingPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error creating key ring", fmt.Sprintf("Calling API: %v", err)) + return + } + + keyRingId := *createResponse.Id + ctx = tflog.SetField(ctx, "key_ring_id", keyRingId) + + waitResp, err := wait.CreateKeyRingWaitHandler(ctx, k.client, projectId, region, keyRingId).SetSleepBeforeWait(5 * time.Second).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error creating key ring", fmt.Sprintf("Key Ring creation waiting: %v", err)) + return + } + + err = mapFields(waitResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error creating key ring", 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, "Key Ring created") +} + +func (k *keyRingResource) 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 := k.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "key_ring_id", keyRingId) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + + keyRingResponse, err := k.client.GetKeyRing(ctx, projectId, region, keyRingId).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 key ring", fmt.Sprintf("Calling API: %v", err)) + return + } + + err = mapFields(keyRingResponse, &model, region) + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error reading key ring", 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, "Key ring read") +} + +func (k *keyRingResource) Update(ctx context.Context, _ resource.UpdateRequest, response *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + // key rings cannot be updated, so we log an error. + core.LogAndAddError(ctx, &response.Diagnostics, "Error updating key ring", "Key rings can't be updated") +} + +func (k *keyRingResource) 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 := k.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "key_ring_id", keyRingId) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + + err := k.client.DeleteKeyRing(ctx, projectId, region, keyRingId).Execute() + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error deleting key ring", fmt.Sprintf("Calling API: %v", err)) + } + + tflog.Info(ctx, "key ring deleted") +} + +func (k *keyRingResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + idParts := strings.Split(request.ID, core.Separator) + + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + core.LogAndAddError(ctx, &response.Diagnostics, + "Error importing key ring", + fmt.Sprintf("Exptected import identifier with format: [proejct_id],[instance_id], got :%q", request.ID), + ) + return + } + + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("key_ring_id"), idParts[1])...) + tflog.Info(ctx, "key ring state imported") +} + +func mapFields(keyRing *kms.KeyRing, model *Model, region string) error { + if keyRing == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var keyRingId string + if model.KeyRingId.ValueString() != "" { + keyRingId = model.KeyRingId.ValueString() + } else if keyRing.Id != nil { + keyRingId = *keyRing.Id + } else { + return fmt.Errorf("keyring id not present") + } + + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), keyRingId) + model.KeyRingId = types.StringValue(keyRingId) + model.DisplayName = types.StringPointerValue(keyRing.DisplayName) + model.Description = types.StringPointerValue(keyRing.Description) + model.Region = types.StringValue(region) + + return nil +} + +func toCreatePayload(model *Model) (*kms.CreateKeyRingPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + return &kms.CreateKeyRingPayload{ + Description: conversion.StringValueToPointer(model.Description), + DisplayName: conversion.StringValueToPointer(model.DisplayName), + }, nil +} diff --git a/stackit/internal/services/kms/key-ring/resource_test.go b/stackit/internal/services/kms/key-ring/resource_test.go new file mode 100644 index 000000000..e6866b2c2 --- /dev/null +++ b/stackit/internal/services/kms/key-ring/resource_test.go @@ -0,0 +1,172 @@ +package kms + +import ( + "fmt" + "testing" + + "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" +) + +func TestMapFields(t *testing.T) { + const testRegion = "eu01" + tests := []struct { + description string + state Model + input *kms.KeyRing + expected Model + isValid bool + }{ + { + "default values", + Model{ + KeyRingId: types.StringValue("krid"), + ProjectId: types.StringValue("pid"), + }, + &kms.KeyRing{ + Id: utils.Ptr("krid"), + }, + Model{ + Description: types.StringNull(), + DisplayName: types.StringNull(), + KeyRingId: types.StringValue("krid"), + Id: types.StringValue("pid,krid"), + ProjectId: types.StringValue("pid"), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "values_ok", + Model{ + KeyRingId: types.StringValue("krid"), + ProjectId: types.StringValue("pid"), + }, + &kms.KeyRing{ + Description: utils.Ptr("descr"), + DisplayName: utils.Ptr("name"), + Id: utils.Ptr("krid"), + }, + Model{ + Description: types.StringValue("descr"), + DisplayName: types.StringValue("name"), + KeyRingId: types.StringValue("krid"), + Id: types.StringValue("pid,krid"), + ProjectId: types.StringValue("pid"), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "nil_response_field", + Model{}, + &kms.KeyRing{ + Id: nil, + }, + Model{}, + false, + }, + { + "nil_response", + Model{}, + nil, + Model{}, + false, + }, + { + "no_resource_id", + Model{ + Region: types.StringValue(testRegion), + ProjectId: types.StringValue("pid"), + }, + &kms.KeyRing{}, + Model{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + state := &Model{ + ProjectId: tt.expected.ProjectId, + KeyRingId: tt.expected.KeyRingId, + } + err := mapFields(tt.input, state, testRegion) + 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(state, &tt.expected) + if diff != "" { + fmt.Println("state: ", state, " expected: ", tt.expected) + t.Fatalf("Data does not match") + } + } + }) + } +} + +func TestToCreatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + expected *kms.CreateKeyRingPayload + isValid bool + }{ + { + "default_values", + &Model{}, + &kms.CreateKeyRingPayload{}, + true, + }, + { + "simple_values", + &Model{ + DisplayName: types.StringValue("name"), + }, + &kms.CreateKeyRingPayload{ + DisplayName: utils.Ptr("name"), + }, + true, + }, + { + "null_fields", + &Model{ + DisplayName: types.StringValue(""), + Description: types.StringValue(""), + }, + &kms.CreateKeyRingPayload{ + 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/internal/services/kms/key/datasource.go b/stackit/internal/services/kms/key/datasource.go new file mode 100644 index 000000000..ccf1afc4e --- /dev/null +++ b/stackit/internal/services/kms/key/datasource.go @@ -0,0 +1,195 @@ +package kms + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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" + "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 = &keyDataSource{} +) + +func NewKeyDataSource() datasource.DataSource { + return &keyDataSource{} +} + +type keyDataSource struct { + client *kms.APIClient + providerData core.ProviderData +} + +func (k *keyDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_kms_key" +} + +func (k *keyDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { + var ok bool + k.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) + if !ok { + return + } + + apiClient := kmsUtils.ConfigureClient(ctx, &k.providerData, &response.Diagnostics) + if response.Diagnostics.HasError() { + return + } + + k.client = apiClient + tflog.Info(ctx, "Key configured") +} + +func (k *keyDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { + descriptions := map[string]string{ + "main": "KMS Key resource schema. Must have a `region` specified in the provider configuration.", + "backend": "The backend that is used for KMS. Right now, only software is accepted.", + "algorithm": "The encryption algorithm that the key will use to encrypt data", + "description": "A user chosen description to distinguish multiple keys", + "display_name": "The display name to distinguish multiple keys", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", + "import_only": "Specifies if the the key should be import_only", + "key_ring_id": "The ID of the associated key ring", + "purpose": "The purpose for which the key will be used", + "project_id": "STACKIT project ID to which the key ring is associated.", + "region_id": "The STACKIT region name the key ring is located in.", + } + + response.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "algorithm": schema.StringAttribute{ + Description: descriptions["algorithm"], + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "backend": schema.StringAttribute{ + Description: descriptions["backend"], + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "description": schema.StringAttribute{ + Description: descriptions["description"], + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "display_name": schema.StringAttribute{ + Description: descriptions["display_name"], + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "import_only": schema.BoolAttribute{ + Description: descriptions["id"], + Computed: false, + Required: true, + }, + "key_id": schema.StringAttribute{ + Description: descriptions["key_id"], + Computed: false, + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "key_ring_id": schema.StringAttribute{ + Description: descriptions["key_ring_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "purpose": schema.StringAttribute{ + Description: descriptions["purpose"], + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + 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.", + }, + }, + } +} + +func (k *keyDataSource) 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 := k.providerData.GetRegionWithOverride(model.Region) + keyId := model.KeyId.ValueString() + + ctx = tflog.SetField(ctx, "key_ring_id", keyRingId) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "key_id", keyId) + + keyResponse, err := k.client.GetKey(ctx, projectId, region, keyRingId, keyId).Execute() + if err != nil { + utils.LogError( + ctx, + &response.Diagnostics, + err, + "Reading key", + fmt.Sprintf("Key with ID %q does not exist in project %q.", keyId, 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(keyResponse, &model, region) + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error reading 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, "Key read") +} diff --git a/stackit/internal/services/kms/key/resource.go b/stackit/internal/services/kms/key/resource.go new file mode 100644 index 000000000..23ab22734 --- /dev/null +++ b/stackit/internal/services/kms/key/resource.go @@ -0,0 +1,366 @@ +package kms + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "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 = &keyResource{} + _ resource.ResourceWithConfigure = &keyResource{} + _ resource.ResourceWithImportState = &keyResource{} +) + +type Model struct { + Algorithm types.String `tfsdk:"algorithm"` + Backend types.String `tfsdk:"backend"` + Description types.String `tfsdk:"description"` + DisplayName types.String `tfsdk:"display_name"` + Id types.String `tfsdk:"id"` // needed by TF + ImportOnly types.Bool `tfsdk:"import_only"` + KeyId types.String `tfsdk:"key_id"` + KeyRingId types.String `tfsdk:"key_ring_id"` + Purpose types.String `tfsdk:"purpose"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` +} + +func NewKeyResource() resource.Resource { + return &keyResource{} +} + +type keyResource struct { + client *kms.APIClient + providerData core.ProviderData +} + +func (k *keyResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_kms_key" +} + +func (k *keyResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + var ok bool + k.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) + if !ok { + return + } + apiClient := kmsUtils.ConfigureClient(ctx, &k.providerData, &response.Diagnostics) + if response.Diagnostics.HasError() { + return + } + k.client = apiClient +} + +func (k *keyResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { + descriptions := map[string]string{ + "main": "KMS Key resource schema. Must have a `region` specified in the provider configuration.", + "backend": "The backend that is used for KMS. Right now, only software is accepted.", + "algorithm": "The encryption algorithm that the key will use to encrypt data", + "description": "A user chosen description to distinguish multiple keys", + "display_name": "The display name to distinguish multiple keys", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", + "import_only": "Specifies if the the key should be import_only", + "key_id": "The ID of the key", + "key_ring_id": "The ID of the associated key ring", + "purpose": "The purpose for which the key will be used", + "project_id": "STACKIT project ID to which the key ring is associated.", + "region_id": "The STACKIT region name the key ring is located in.", + } + + response.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "algorithm": schema.StringAttribute{ + Description: descriptions["algorithm"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "backend": schema.StringAttribute{ + Description: descriptions["backend"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "description": schema.StringAttribute{ + Description: descriptions["description"], + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "display_name": schema.StringAttribute{ + Description: descriptions["display_name"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "import_only": schema.BoolAttribute{ + Description: descriptions["id"], + Computed: false, + Required: true, + }, + "key_id": schema.StringAttribute{ + Description: descriptions["key_id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "key_ring_id": schema.StringAttribute{ + Description: descriptions["key_ring_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "purpose": schema.StringAttribute{ + Description: descriptions["purpose"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + 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.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (k *keyResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := request.Plan.Get(ctx, &model) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + region := k.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, "key_ring_id", keyRingId) + + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error creating key", fmt.Sprintf("Creating API payload: %v", err)) + return + } + createResponse, err := k.client.CreateKey(ctx, projectId, region, keyRingId).CreateKeyPayload(*payload).Execute() + + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error creating key", fmt.Sprintf("Calling API: %v", err)) + return + } + + keyId := *createResponse.Id + ctx = tflog.SetField(ctx, "key_id", keyId) + + err = mapFields(createResponse, &model, region) + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error creating 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, "Key created") +} + +func (k *keyResource) 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 := k.providerData.GetRegionWithOverride(model.Region) + keyId := model.KeyId.ValueString() + + ctx = tflog.SetField(ctx, "key_ring_id", keyRingId) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "key_id", keyId) + + keyResponse, err := k.client.GetKey(ctx, projectId, region, keyRingId, keyId).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 key", fmt.Sprintf("Calling API: %v", err)) + return + } + + err = mapFields(keyResponse, &model, region) + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error reading 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, "Key read") +} + +func (k *keyResource) Update(ctx context.Context, _ resource.UpdateRequest, response *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + // keys cannot be updated, so we log an error. + core.LogAndAddError(ctx, &response.Diagnostics, "Error updating key", "Keys can't be updated") +} + +func (k *keyResource) 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 := k.providerData.GetRegionWithOverride(model.Region) + keyId := model.KeyId.ValueString() + + err := k.client.DeleteKey(ctx, projectId, region, keyRingId, keyId).Execute() + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error deleting key", fmt.Sprintf("Calling API: %v", err)) + } + + tflog.Info(ctx, "key deleted") +} + +func (k *keyResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + idParts := strings.Split(request.ID, core.Separator) + + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + core.LogAndAddError(ctx, &response.Diagnostics, + "Error importing key", + fmt.Sprintf("Exptected import identifier with format: [proejct_id],[instance_id], got :%q", request.ID), + ) + return + } + + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("key_id"), idParts[1])...) + tflog.Info(ctx, "key state imported") +} + +func mapFields(key *kms.Key, model *Model, region string) error { + if key == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var keyId string + if model.KeyId.ValueString() != "" { + keyId = model.KeyId.ValueString() + } else if key.Id != nil { + keyId = *key.Id + } else { + return fmt.Errorf("key id not present") + } + + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), keyId) + model.KeyId = types.StringValue(keyId) + model.DisplayName = types.StringPointerValue(key.DisplayName) + model.Description = types.StringPointerValue(key.Description) + model.Region = types.StringValue(region) + + return nil +} + +func toCreatePayload(model *Model) (*kms.CreateKeyPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + return &kms.CreateKeyPayload{ + Algorithm: kms.CreateKeyPayloadGetAlgorithmAttributeType(conversion.StringValueToPointer(model.Algorithm)), + Backend: kms.CreateKeyPayloadGetBackendAttributeType(conversion.StringValueToPointer(model.Backend)), + Description: conversion.StringValueToPointer(model.Description), + DisplayName: conversion.StringValueToPointer(model.DisplayName), + ImportOnly: conversion.BoolValueToPointer(model.ImportOnly), + Purpose: kms.CreateKeyPayloadGetPurposeAttributeType(conversion.StringValueToPointer(model.Purpose)), + }, nil +} diff --git a/stackit/internal/services/kms/key/resource_test.go b/stackit/internal/services/kms/key/resource_test.go new file mode 100644 index 000000000..667939b45 --- /dev/null +++ b/stackit/internal/services/kms/key/resource_test.go @@ -0,0 +1,176 @@ +package kms + +import ( + "fmt" + "testing" + + "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" +) + +func TestMapFields(t *testing.T) { + const testRegion = "eu01" + tests := []struct { + description string + state Model + input *kms.Key + expected Model + isValid bool + }{ + { + "default values", + Model{ + KeyId: types.StringValue("kid"), + KeyRingId: types.StringValue("krid"), + ProjectId: types.StringValue("pid"), + }, + &kms.Key{ + Id: utils.Ptr("kid"), + }, + Model{ + Description: types.StringNull(), + DisplayName: types.StringNull(), + KeyRingId: types.StringValue("krid"), + KeyId: types.StringValue("kid"), + Id: types.StringValue("pid,kid"), + ProjectId: types.StringValue("pid"), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "values_ok", + Model{ + KeyId: types.StringValue("kid"), + KeyRingId: types.StringValue("krid"), + ProjectId: types.StringValue("pid"), + }, + &kms.Key{ + Description: utils.Ptr("descr"), + DisplayName: utils.Ptr("name"), + Id: utils.Ptr("kid"), + }, + Model{ + Description: types.StringValue("descr"), + DisplayName: types.StringValue("name"), + KeyId: types.StringValue("kid"), + KeyRingId: types.StringValue("krid"), + Id: types.StringValue("pid,kid"), + ProjectId: types.StringValue("pid"), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "nil_response_field", + Model{}, + &kms.Key{ + Id: nil, + }, + Model{}, + false, + }, + { + "nil_response", + Model{}, + nil, + Model{}, + false, + }, + { + "no_resource_id", + Model{ + Region: types.StringValue(testRegion), + ProjectId: types.StringValue("pid"), + }, + &kms.Key{}, + Model{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + state := &Model{ + ProjectId: tt.expected.ProjectId, + KeyRingId: tt.expected.KeyRingId, + } + err := mapFields(tt.input, state, testRegion) + 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(state, &tt.expected) + if diff != "" { + fmt.Println("state: ", state, " expected: ", tt.expected) + t.Fatalf("Data does not match") + } + } + }) + } +} + +func TestToCreatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + expected *kms.CreateKeyPayload + isValid bool + }{ + { + "default_values", + &Model{}, + &kms.CreateKeyPayload{}, + true, + }, + { + "simple_values", + &Model{ + DisplayName: types.StringValue("name"), + }, + &kms.CreateKeyPayload{ + DisplayName: utils.Ptr("name"), + }, + true, + }, + { + "null_fields", + &Model{ + DisplayName: types.StringValue(""), + Description: types.StringValue(""), + }, + &kms.CreateKeyPayload{ + 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/internal/services/kms/utils/util.go b/stackit/internal/services/kms/utils/util.go new file mode 100644 index 000000000..9f6f64d81 --- /dev/null +++ b/stackit/internal/services/kms/utils/util.go @@ -0,0 +1,29 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/kms" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *kms.APIClient { + apiClientConfigOptions := []config.ConfigurationOption{ + config.WithCustomAuth(providerData.RoundTripper), + utils.UserAgentConfigOption(providerData.Version), + } + if providerData.KMSCustomEndpoint != "" { + apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.KMSCustomEndpoint)) + } + apiClient, err := kms.NewAPIClient(apiClientConfigOptions...) + if err != nil { + core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) + return nil + } + + return apiClient +} 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..a301852f9 --- /dev/null +++ b/stackit/internal/services/kms/wrapping-key/datasource.go @@ -0,0 +1,190 @@ +package kms + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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" + "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) { + descriptions := map[string]string{ + "main": "KMS Key resource schema. Must have a `region` specified in the provider configuration.", + "backend": "The backend that is used for KMS. Right now, only software is accepted.", + "algorithm": "The encryption algorithm that the key will use to encrypt data", + "description": "A user chosen description to distinguish multiple keys", + "display_name": "The display name to distinguish multiple keys", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", + "import_only": "Specifies if the the key should be import_only", + "key_ring_id": "The ID of the associated key ring", + "purpose": "The purpose for which the key will be used", + "project_id": "STACKIT project ID to which the key ring is associated.", + "region_id": "The STACKIT region name the key ring is located in.", + } + + response.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "algorithm": schema.StringAttribute{ + Description: descriptions["algorithm"], + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "backend": schema.StringAttribute{ + Description: descriptions["backend"], + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "description": schema.StringAttribute{ + Description: descriptions["description"], + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "display_name": schema.StringAttribute{ + Description: descriptions["display_name"], + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "key_ring_id": schema.StringAttribute{ + Description: descriptions["key_ring_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "purpose": schema.StringAttribute{ + Description: descriptions["purpose"], + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + 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: descriptions["wrapping_key_id"], + Computed: false, + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + }, + } +} + +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, "key_ring_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, "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..471326a5e --- /dev/null +++ b/stackit/internal/services/kms/wrapping-key/resource.go @@ -0,0 +1,360 @@ +package kms + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "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{} +) + +type Model struct { + Algorithm types.String `tfsdk:"algorithm"` + Backend types.String `tfsdk:"backend"` + Description types.String `tfsdk:"description"` + DisplayName types.String `tfsdk:"display_name"` + Id types.String `tfsdk:"id"` // needed by TF + KeyRingId types.String `tfsdk:"key_ring_id"` + Purpose types.String `tfsdk:"purpose"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + WrappingKeyId types.String `tfsdk:"wrapping_key_id"` +} + +func NewWrappingKeyResource() resource.Resource { + return &wrappingKeyResource{} +} + +type wrappingKeyResource struct { + client *kms.APIClient + providerData core.ProviderData +} + +func (w *wrappingKeyResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_kms_wrapping_key" +} + +func (w *wrappingKeyResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.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 +} + +func (w *wrappingKeyResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { + descriptions := map[string]string{ + "main": "KMS Key resource schema. Must have a `region` specified in the provider configuration.", + "algorithm": "The encryption algorithm that the key will use to encrypt data", + "backend": "The backend that is used for KMS. Right now, only software is accepted.", + "description": "A user chosen description to distinguish multiple keys", + "display_name": "The display name to distinguish multiple keys", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", + "key_ring_id": "The ID of the associated key ring", + "purpose": "The purpose for which the key will be used", + "project_id": "STACKIT project ID to which the key ring is associated.", + "region_id": "The STACKIT region name the key ring is located in.", + "wrapping_key_id": "The ID of the wrapping key", + } + + response.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "algorithm": schema.StringAttribute{ + Description: descriptions["algorithm"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "backend": schema.StringAttribute{ + Description: descriptions["backend"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "description": schema.StringAttribute{ + Description: descriptions["description"], + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "display_name": schema.StringAttribute{ + Description: descriptions["display_name"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "key_ring_id": schema.StringAttribute{ + Description: descriptions["key_ring_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "purpose": schema.StringAttribute{ + Description: descriptions["purpose"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + 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.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "wrapping_key_id": schema.StringAttribute{ + Description: descriptions["wrapping_key_id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + }, + } +} + +func (w *wrappingKeyResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + + diags := request.Plan.Get(ctx, &model) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + region := w.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, "key_ring_id", keyRingId) + + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error creating wrapping key", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + createResponse, err := w.client.CreateWrappingKey(ctx, projectId, region, keyRingId).CreateWrappingKeyPayload(*payload).Execute() + + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error creating wrapping key", fmt.Sprintf("Calling API: %v", err)) + return + } + + wrappingKeyId := *createResponse.Id + ctx = tflog.SetField(ctx, "wrapping_key_id", wrappingKeyId) + + err = mapFields(createResponse, &model, region) + if err != nil { + core.LogAndAddError(ctx, &response.Diagnostics, "Error creating 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, "Key created") +} + +func (w *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 := w.providerData.GetRegionWithOverride(model.Region) + wrappingKeyId := model.WrappingKeyId.ValueString() + + ctx = tflog.SetField(ctx, "key_ring_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 { + 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 (w *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 (w *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 := w.providerData.GetRegionWithOverride(model.Region) + wrappingKeyId := model.WrappingKeyId.ValueString() + + err := w.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 (w *wrappingKeyResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + idParts := strings.Split(request.ID, core.Separator) + + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + core.LogAndAddError(ctx, &response.Diagnostics, + "Error importing wrapping key", + fmt.Sprintf("Exptected import identifier with format: [proejct_id],[instance_id], got :%q", request.ID), + ) + return + } + + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("wrapping_key_id"), idParts[1])...) + 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(), wrappingKeyId) + model.WrappingKeyId = types.StringValue(wrappingKeyId) + model.DisplayName = types.StringPointerValue(wrappingKey.DisplayName) + model.Description = types.StringPointerValue(wrappingKey.Description) + model.Region = types.StringValue(region) + + return nil +} + +func toCreatePayload(model *Model) (*kms.CreateWrappingKeyPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + return &kms.CreateWrappingKeyPayload{ + Algorithm: kms.CreateWrappingKeyPayloadGetAlgorithmAttributeType(conversion.StringValueToPointer(model.Algorithm)), + Backend: kms.CreateKeyPayloadGetBackendAttributeType(conversion.StringValueToPointer(model.Backend)), + Description: conversion.StringValueToPointer(model.Description), + DisplayName: conversion.StringValueToPointer(model.DisplayName), + 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..ec46dfc84 --- /dev/null +++ b/stackit/internal/services/kms/wrapping-key/resource_test.go @@ -0,0 +1,176 @@ +package kms + +import ( + "fmt" + "testing" + + "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" +) + +func TestMapFields(t *testing.T) { + const testRegion = "eu01" + tests := []struct { + description string + state Model + input *kms.WrappingKey + expected Model + isValid bool + }{ + { + "default values", + Model{ + KeyRingId: types.StringValue("krid"), + ProjectId: types.StringValue("pid"), + WrappingKeyId: types.StringValue("wid"), + }, + &kms.WrappingKey{ + Id: utils.Ptr("wid"), + }, + Model{ + Description: types.StringNull(), + DisplayName: types.StringNull(), + KeyRingId: types.StringValue("krid"), + Id: types.StringValue("pid,wid"), + ProjectId: types.StringValue("pid"), + Region: types.StringValue(testRegion), + WrappingKeyId: types.StringValue("wid"), + }, + true, + }, + { + "values_ok", + Model{ + KeyRingId: types.StringValue("krid"), + ProjectId: types.StringValue("pid"), + WrappingKeyId: types.StringValue("wid"), + }, + &kms.WrappingKey{ + Description: utils.Ptr("descr"), + DisplayName: utils.Ptr("name"), + Id: utils.Ptr("wid"), + }, + Model{ + Description: types.StringValue("descr"), + DisplayName: types.StringValue("name"), + KeyRingId: types.StringValue("krid"), + Id: types.StringValue("pid,wid"), + ProjectId: types.StringValue("pid"), + Region: types.StringValue(testRegion), + WrappingKeyId: types.StringValue("wid"), + }, + true, + }, + { + "nil_response_field", + Model{}, + &kms.WrappingKey{ + Id: nil, + }, + Model{}, + false, + }, + { + "nil_response", + Model{}, + nil, + Model{}, + false, + }, + { + "no_resource_id", + Model{ + Region: types.StringValue(testRegion), + ProjectId: types.StringValue("pid"), + }, + &kms.WrappingKey{}, + Model{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + state := &Model{ + ProjectId: tt.expected.ProjectId, + KeyRingId: tt.expected.KeyRingId, + } + err := mapFields(tt.input, state, testRegion) + 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(state, &tt.expected) + if diff != "" { + fmt.Println("state: ", state, " expected: ", tt.expected) + t.Fatalf("Data does not match") + } + } + }) + } +} + +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 c7d0953bc..0b8af6e09 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -45,6 +45,9 @@ import ( iaasalphaRoutingTableRoutes "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/routes" iaasalphaRoutingTable "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/table" 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/key-ring" + 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" @@ -124,6 +127,7 @@ type providerModel struct { DNSCustomEndpoint types.String `tfsdk:"dns_custom_endpoint"` GitCustomEndpoint types.String `tfsdk:"git_custom_endpoint"` IaaSCustomEndpoint types.String `tfsdk:"iaas_custom_endpoint"` + KMSCustomEndpoint types.List `tfsdk:"kms_custom_endpoint"` PostgresFlexCustomEndpoint types.String `tfsdk:"postgresflex_custom_endpoint"` MongoDBFlexCustomEndpoint types.String `tfsdk:"mongodbflex_custom_endpoint"` ModelServingCustomEndpoint types.String `tfsdk:"modelserving_custom_endpoint"` @@ -165,6 +169,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "dns_custom_endpoint": "Custom endpoint for the DNS service", "git_custom_endpoint": "Custom endpoint for the Git service", "iaas_custom_endpoint": "Custom endpoint for the IaaS service", + "kms_custom_endpoint": "Custom endpoint for the KMS service", "mongodbflex_custom_endpoint": "Custom endpoint for the MongoDB Flex service", "modelserving_custom_endpoint": "Custom endpoint for the AI Model Serving service", "loadbalancer_custom_endpoint": "Custom endpoint for the Load Balancer service", @@ -255,6 +260,11 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Optional: true, Description: descriptions["iaas_custom_endpoint"], }, + "kms_custom_endpoint": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + Description: descriptions["kms_custom_endpoint"], + }, "postgresflex_custom_endpoint": schema.StringAttribute{ Optional: true, Description: descriptions["postgresflex_custom_endpoint"], @@ -420,6 +430,10 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, setStringField(providerConfig.ServiceEnablementCustomEndpoint, func(v string) { providerData.ServiceEnablementCustomEndpoint = v }) setBoolField(providerConfig.EnableBetaResources, func(v bool) { providerData.EnableBetaResources = v }) + if !(providerConfig.KMSCustomEndpoint.IsUnknown() || providerConfig.KMSCustomEndpoint.IsNull()) { + providerData.KMSCustomEndpoint = providerConfig.KMSCustomEndpoint.String() + } + if !(providerConfig.Experiments.IsUnknown() || providerConfig.Experiments.IsNull()) { var experimentValues []string diags := providerConfig.Experiments.ElementsAs(ctx, &experimentValues, false) @@ -470,6 +484,9 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource iaasalphaRoutingTables.NewRoutingTablesDataSource, iaasalphaRoutingTableRoutes.NewRoutingTableRoutesDataSource, iaasSecurityGroupRule.NewSecurityGroupRuleDataSource, + kmsKey.NewKeyDataSource, + kmsKeyRing.NewKeyRingDataSource, + kmsWrappingKey.NewWrappingKeyDataSource, loadBalancer.NewLoadBalancerDataSource, logMeInstance.NewInstanceDataSource, logMeCredential.NewCredentialDataSource, @@ -533,6 +550,9 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { iaasSecurityGroupRule.NewSecurityGroupRuleResource, iaasalphaRoutingTable.NewRoutingTableResource, iaasalphaRoutingTableRoute.NewRoutingTableRouteResource, + kmsKey.NewKeyResource, + kmsKeyRing.NewKeyRingResource, + kmsWrappingKey.NewWrappingKeyResource, loadBalancer.NewLoadBalancerResource, loadBalancerObservabilityCredential.NewObservabilityCredentialResource, logMeInstance.NewInstanceResource,