diff --git a/docs/data-sources/kms_key.md b/docs/data-sources/kms_key.md new file mode 100644 index 000000000..360babe28 --- /dev/null +++ b/docs/data-sources/kms_key.md @@ -0,0 +1,37 @@ +--- +# 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 +- `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. +- `protection` (String) The underlying system that is responsible for protecting the key material. Currently only software is accepted. +- `purpose` (String) The purpose for which the key will be used + +### Optional + +- `access_scope` (String) The access scope of the key. Default is PUBLIC. +- `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..ad809b983 --- /dev/null +++ b/docs/data-sources/kms_key_ring.md @@ -0,0 +1,28 @@ +--- +# 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 + +- `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. + +### Read-Only + +- `description` (String) A user chosen description to distinguish multiple key rings. +- `display_name` (String) A user chosen description to distinguish multiple key rings. +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`instance_id`". +- `region` (String) The resource region. If not defined, the provider region is used. diff --git a/docs/data-sources/kms_wrapping_key.md b/docs/data-sources/kms_wrapping_key.md new file mode 100644 index 000000000..e9c35a3a0 --- /dev/null +++ b/docs/data-sources/kms_wrapping_key.md @@ -0,0 +1,36 @@ +--- +# 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 +- `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. +- `protection` (String) The underlying system that is responsible for protecting the key material. Currently only software is accepted. +- `purpose` (String) The purpose for which the key will be used +- `wrapping_key_id` (String) + +### Optional + +- `access_scope` (String) The access scope of the key. Default is PUBLIC. +- `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 efc6ca4f8..4e9d3e2b2 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` (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..3ea566f16 --- /dev/null +++ b/docs/resources/kms_key.md @@ -0,0 +1,50 @@ +--- +# 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 +- `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. +- `protection` (String) The underlying system that is responsible for protecting the key material. Currently only software is accepted. +- `purpose` (String) The purpose for which the key will be used + +### Optional + +- `access_scope` (String) The access scope of the key. Default is PUBLIC. +- `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`". +- `import_only` (Boolean) Specifies if the the key should be import_only +- `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..f6aab4272 --- /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`,`key_ring_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..eec5986c8 --- /dev/null +++ b/docs/resources/kms_wrapping_key.md @@ -0,0 +1,49 @@ +--- +# 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 +- `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. +- `protection` (String) The underlying system that is responsible for protecting the key material. Currently only software is accepted. +- `purpose` (String) The purpose for which the key will be used + +### Optional + +- `access_scope` (String) The access scope of the key. Default is PUBLIC. +- `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`". +- `import_only` (Boolean) +- `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 9fc1c7a74..46a5c9540 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/git v0.8.0 github.com/stackitcloud/stackit-sdk-go/services/iaas v0.31.0 github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha + github.com/stackitcloud/stackit-sdk-go/services/kms v1.0.0 github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0 github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.1 github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1 @@ -80,7 +81,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.9.0 github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect diff --git a/go.sum b/go.sum index 878f69591..c0013cdf0 100644 --- a/go.sum +++ b/go.sum @@ -137,8 +137,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= @@ -166,6 +166,8 @@ github.com/stackitcloud/stackit-sdk-go/services/iaas v0.31.0 h1:dnEjyapuv8WwRN5v github.com/stackitcloud/stackit-sdk-go/services/iaas v0.31.0/go.mod h1:854gnLR92NvAbJAA1xZEumrtNh1DoBP1FXTMvhwYA6w= 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 v1.0.0 h1:zxoOv7Fu+FmdsvTKiKkbmLItrMKfL+QoVtz9ReEF30E= +github.com/stackitcloud/stackit-sdk-go/services/kms v1.0.0/go.mod h1:KEPVoO21pC4bjy5l0nyhjUJ0+uVwVWb+k2TYrzJ8xYw= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0 h1:q33ZaCBVEBUsnMDxYyuJKtJvGcE5nKgvuPed3s8zXNI= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0/go.mod h1:20QOZ3rBC9wTGgzXzLz9M6YheX0VaxWE0/JI+s8On7k= github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.1 h1:hv5WrRU9rN6Jx4OwdOGJRyaQrfA9p1tzEoQK6/CDyoA= diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index d993cddb3..6748da6e8 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..b1911f02b --- /dev/null +++ b/stackit/internal/services/kms/key-ring/datasource.go @@ -0,0 +1,148 @@ +package kms + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-log/tflog" + "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": "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"], + Computed: true, + }, + "display_name": schema.StringAttribute{ + Description: descriptions["description"], + Computed: true, + }, + "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{ + 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..938bba914 --- /dev/null +++ b/stackit/internal/services/kms/key-ring/resource.go @@ -0,0 +1,308 @@ +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`,`key_ring_id`\".", + "project_id": "STACKIT project ID to which the key ring is associated.", + "region": "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, + Computed: true, + }, + "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..95e8bfb46 --- /dev/null +++ b/stackit/internal/services/kms/key/datasource.go @@ -0,0 +1,204 @@ +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.", + "access_scope": "The access scope of the key. Default is PUBLIC.", + "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", + "protection": "The underlying system that is responsible for protecting the key material. Currently only software is accepted.", + "purpose": "The purpose for which the key will be used", + "project_id": "STACKIT project ID to which the key ring is associated.", + "region": "The STACKIT region name the key ring is located in.", + } + + response.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "access_scope": schema.StringAttribute{ + Description: descriptions["access_scope"], + Optional: true, + Required: false, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "algorithm": schema.StringAttribute{ + Description: descriptions["algorithm"], + 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(), + }, + }, + "protection": schema.StringAttribute{ + Description: descriptions["protection"], + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "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..56800bf95 --- /dev/null +++ b/stackit/internal/services/kms/key/resource.go @@ -0,0 +1,377 @@ +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 { + AccessScope types.String `tfsdk:"access_scope"` + Algorithm types.String `tfsdk:"algorithm"` + Description types.String `tfsdk:"description"` + DisplayName types.String `tfsdk:"display_name"` + Id types.String `tfsdk:"id"` // needed by TF + ImportOnly types.Bool `tfsdk:"import_only"` + KeyId types.String `tfsdk:"key_id"` + KeyRingId types.String `tfsdk:"key_ring_id"` + Protection types.String `tfsdk:"protection"` + 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.", + "access_scope": "The access scope of the key. Default is PUBLIC.", + "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", + "protection": "The underlying system that is responsible for protecting the key material. Currently only software is accepted.", + "purpose": "The purpose for which the key will be used", + "project_id": "STACKIT project ID to which the key ring is associated.", + "region": "The STACKIT region name the key ring is located in.", + } + + response.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "access_scope": schema.StringAttribute{ + Description: descriptions["access_scope"], + Optional: true, + Required: false, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "algorithm": schema.StringAttribute{ + Description: descriptions["algorithm"], + 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["import_only"], + Computed: true, + Required: false, + }, + "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(), + }, + }, + "protection": schema.StringAttribute{ + Description: descriptions["protection"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "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{ + AccessScope: kms.CreateKeyPayloadGetAccessScopeAttributeType(conversion.StringValueToPointer(model.AccessScope)), + Algorithm: kms.CreateKeyPayloadGetAlgorithmAttributeType(conversion.StringValueToPointer(model.Algorithm)), + Protection: kms.CreateKeyPayloadGetProtectionAttributeType(conversion.StringValueToPointer(model.Protection)), + 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/kms_acc_test.go b/stackit/internal/services/kms/kms_acc_test.go new file mode 100644 index 000000000..1bafa6587 --- /dev/null +++ b/stackit/internal/services/kms/kms_acc_test.go @@ -0,0 +1,269 @@ +package kms_test + +import ( + "context" + _ "embed" + "errors" + "fmt" + "maps" + "net/http" + "strings" + "sync" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + core_config "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "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/testutil" +) + +var ( + //go:embed testdata/resource-key-ring-min.tf + resourceKeyRingMinConfig string + + //go:embed testdata/resource-key-ring-max.tf + resourceKeyRingMaxConfig string + + //go:embed testdata/resource-key-min.tf + resourceKeyMinConfig string + + //go:embed testdata/resource-wrapping-key-min.tf + resourceWrappingKeyMinConfig string +) + +var testConfigKeyRingVarsMin = config.Variables{ + "display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "project_id": config.StringVariable(testutil.ProjectId), +} + +var testConfigKeyRingVarsMinUpdated = func() config.Variables { + updatedConfig := config.Variables{} + maps.Copy(updatedConfig, testConfigKeyRingVarsMin) + updatedConfig["display_name"] = config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)) + return updatedConfig +} + +var testConfigKeyRingVarsMax = config.Variables{ + "description": config.StringVariable("description"), + "display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "project_id": config.StringVariable(testutil.ProjectId), + "region": config.StringVariable(testutil.Region), +} + +var testConfigKeyRingVarsMaxUpdated = func() config.Variables { + updatedConfig := config.Variables{} + for k, v := range testConfigKeyRingVarsMax { + updatedConfig[k] = v + } + updatedConfig["description"] = config.StringVariable("updated description") + updatedConfig["display_name"] = config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)) + return updatedConfig +} + +var testConfigKeyVarsMin = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "algorithm": config.StringVariable("aes_256_gcm"), + "display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "protection": config.StringVariable("software"), + "purpose": config.StringVariable("symmetric_encrypt_decrypt"), +} + +var testConfigWrappingKeyVarsMin = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "algorithm": config.StringVariable("rsa_2048_oaep_sha256"), + "display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "protection": config.StringVariable("software"), + "purpose": config.StringVariable("wrap_symmetric_key"), +} + +func TestAccKeyRingMin(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + //CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigKeyRingVarsMin, + Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyRingMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_kms_key_ring.key_ring", "project_id", testutil.ConvertConfigVariable(testConfigKeyRingVarsMin["project_id"])), + resource.TestCheckResourceAttr("stackit_kms_key_ring.key_ring", "display_name", testutil.ConvertConfigVariable(testConfigKeyRingVarsMin["display_name"])), + resource.TestCheckResourceAttrSet("stackit_kms_key_ring.key_ring", "key_ring_id"), + resource.TestCheckResourceAttrSet("stackit_kms_key_ring.key_ring", "region"), + ), + }, + // Data source + { + ConfigVariables: testConfigKeyRingVarsMin, + Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyRingMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_kms_key_ring.key_ring", "project_id", testutil.ConvertConfigVariable(testConfigKeyRingVarsMin["project_id"])), + resource.TestCheckResourceAttrPair( + "stackit_kms_key_ring.key_ring", "key_ring_id", + "data.stackit_kms_key_ring.key_ring", "key_ring_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_kms_key_ring.key_ring", "region", + "data.stackit_kms_key_ring.key_ring", "region", + ), + resource.TestCheckResourceAttrPair( + "stackit_kms_key_ring.key_ring", "project_id", + "data.stackit_kms_key_ring.key_ring", "project_id", + ), + resource.TestCheckResourceAttr("data.stackit_kms_key_ring.key_ring", "display_name", testutil.ConvertConfigVariable(testConfigKeyRingVarsMin["display_name"])), + ), + }, + }, + }) +} + +func TestAccKeyRingMax(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + //CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + //Creation + { + ConfigVariables: testConfigKeyRingVarsMax, + Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyRingMaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_kms_key_ring.key_ring", "project_id", testutil.ConvertConfigVariable(testConfigKeyRingVarsMax["project_id"])), + resource.TestCheckResourceAttr("stackit_kms_key_ring.key_ring", "description", testutil.ConvertConfigVariable(testConfigKeyRingVarsMax["description"])), + resource.TestCheckResourceAttrSet("stackit_kms_key_ring.key_ring", "key_ring_id"), + resource.TestCheckResourceAttr("stackit_kms_key_ring.key_ring", "display_name", testutil.ConvertConfigVariable(testConfigKeyRingVarsMax["display_name"])), + resource.TestCheckResourceAttrSet("stackit_kms_key_ring.key_ring", "region"), + ), + }, + // Data Source + { + ConfigVariables: testConfigKeyRingVarsMax, + Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyRingMaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_kms_key_ring.key_ring", "project_id", testutil.ConvertConfigVariable(testConfigKeyRingVarsMax["project_id"])), + resource.TestCheckResourceAttrPair( + "stackit_kms_key_ring.key_ring", "key_ring_id", + "data.stackit_kms_key_ring.key_ring", "key_ring_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_kms_key_ring.key_ring", "region", + "data.stackit_kms_key_ring.key_ring", "region", + ), + resource.TestCheckResourceAttrPair( + "stackit_kms_key_ring.key_ring", "project_id", + "data.stackit_kms_key_ring.key_ring", "project_id", + ), + resource.TestCheckResourceAttr("data.stackit_kms_key_ring.key_ring", "description", testutil.ConvertConfigVariable(testConfigKeyRingVarsMax["description"])), + resource.TestCheckResourceAttr("data.stackit_kms_key_ring.key_ring", "display_name", testutil.ConvertConfigVariable(testConfigKeyRingVarsMax["display_name"])), + ), + ), + }, + }, + }) +} + +func TestAccKeyMin(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + //CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigKeyVarsMin, + Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_kms_key.key", "algorithm", testutil.ConvertConfigVariable(testConfigKeyVarsMin["algorithm"])), + resource.TestCheckResourceAttr("stackit_kms_key.key", "display_name", testutil.ConvertConfigVariable(testConfigKeyVarsMin["display_name"])), + resource.TestCheckResourceAttr("stackit_kms_key.key", "purpose", testutil.ConvertConfigVariable(testConfigKeyVarsMin["purpose"])), + resource.TestCheckResourceAttr("stackit_kms_key.key", "protection", testutil.ConvertConfigVariable(testConfigKeyVarsMin["protection"])), + ), + }, + }, + }) +} + +func TestAccWrappingKeyMin(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + //CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + { + ConfigVariables: testConfigWrappingKeyVarsMin, + Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceWrappingKeyMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "algorithm", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["algorithm"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "display_name", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["display_name"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "purpose", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["purpose"])), + resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "protection", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["protection"])), + ), + }, + }, + }) +} + +func testAccCheckDestroy(s *terraform.State) error { + checkFunctions := []func(s *terraform.State) error{ + testAccCheckKeyRingDestroy, + } + + var errs []error + + wg := sync.WaitGroup{} + wg.Add(len(checkFunctions)) + + for _, f := range checkFunctions { + go func() { + err := f(s) + if err != nil { + errs = append(errs, err) + } + wg.Done() + }() + } + wg.Wait() + return errors.Join(errs...) +} + +func testAccCheckKeyRingDestroy(s *terraform.State) error { + ctx := context.Background() + var client *kms.APIClient + var err error + if testutil.KMSCustomEndpoint == "" { + client, err = kms.NewAPIClient( + core_config.WithRegion("eu01"), + ) + } else { + client, err = kms.NewAPIClient( + core_config.WithEndpoint(testutil.KMSCustomEndpoint), + ) + } + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + var errs []error + + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_kms_key_ring" { + continue + } + keyRingId := strings.Split(rs.Primary.ID, core.Separator)[1] + err := client.DeleteKeyRingExecute(ctx, testutil.ProjectId, testutil.Region, keyRingId) + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) { + if oapiErr.StatusCode == http.StatusNotFound { + continue + } + } + errs = append(errs, fmt.Errorf("cannot trigger key ring deletion %q: %w", keyRingId, err)) + } + } + + return errors.Join(errs...) +} diff --git a/stackit/internal/services/kms/testdata/resource-key-min.tf b/stackit/internal/services/kms/testdata/resource-key-min.tf new file mode 100644 index 000000000..3274bc9e1 --- /dev/null +++ b/stackit/internal/services/kms/testdata/resource-key-min.tf @@ -0,0 +1,19 @@ +variable "project_id" {} +variable "algorithm" {} +variable "display_name" {} +variable "protection" {} +variable "purpose" {} + +resource "stackit_kms_key_ring" "key_ring" { + project_id = var.project_id + display_name = var.display_name +} + +resource "stackit_kms_key" "key" { + algorithm = var.algorithm + display_name = var.display_name + key_ring_id = stackit_kms_key_ring.key_ring.key_ring_id + project_id = var.project_id + protection = var.protection + purpose = var.purpose +} \ No newline at end of file diff --git a/stackit/internal/services/kms/testdata/resource-key-ring-max.tf b/stackit/internal/services/kms/testdata/resource-key-ring-max.tf new file mode 100644 index 000000000..3ff3e74af --- /dev/null +++ b/stackit/internal/services/kms/testdata/resource-key-ring-max.tf @@ -0,0 +1,16 @@ +variable "project_id" {} +variable "description" {} +variable "display_name" {} +variable "region" {} + +resource "stackit_kms_key_ring" "key_ring" { + project_id = var.project_id + description = var.description + display_name = var.display_name + region = var.region +} + +data "stackit_kms_key_ring" "key_ring" { + project_id = var.project_id + key_ring_id = stackit_kms_key_ring.key_ring.key_ring_id +} \ No newline at end of file diff --git a/stackit/internal/services/kms/testdata/resource-key-ring-min.tf b/stackit/internal/services/kms/testdata/resource-key-ring-min.tf new file mode 100644 index 000000000..03ea2fbaa --- /dev/null +++ b/stackit/internal/services/kms/testdata/resource-key-ring-min.tf @@ -0,0 +1,13 @@ +variable "project_id" {} + +variable "display_name" {} + +resource "stackit_kms_key_ring" "key_ring" { + project_id = var.project_id + display_name = var.display_name +} + +data "stackit_kms_key_ring" "key_ring" { + project_id = var.project_id + key_ring_id = stackit_kms_key_ring.key_ring.key_ring_id +} \ No newline at end of file diff --git a/stackit/internal/services/kms/testdata/resource-wrapping-key-min.tf b/stackit/internal/services/kms/testdata/resource-wrapping-key-min.tf new file mode 100644 index 000000000..b0b70b099 --- /dev/null +++ b/stackit/internal/services/kms/testdata/resource-wrapping-key-min.tf @@ -0,0 +1,19 @@ +variable "project_id" {} +variable "algorithm" {} +variable "display_name" {} +variable "protection" {} +variable "purpose" {} + +resource "stackit_kms_key_ring" "key_ring" { + project_id = var.project_id + display_name = var.display_name +} + +resource "stackit_kms_wrapping_key" "wrapping_key" { + algorithm = var.algorithm + display_name = var.display_name + key_ring_id = stackit_kms_key_ring.key_ring.key_ring_id + project_id = var.project_id + protection = var.protection + purpose = var.purpose +} \ No newline at end of file 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..c6d4d79ee --- /dev/null +++ b/stackit/internal/services/kms/wrapping-key/datasource.go @@ -0,0 +1,199 @@ +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.", + "access_scope": "The access scope of the key. Default is PUBLIC.", + "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", + "protection": "The underlying system that is responsible for protecting the key material. Currently only software is accepted.", + "purpose": "The purpose for which the key will be used", + "project_id": "STACKIT project ID to which the key ring is associated.", + "region": "The STACKIT region name the key ring is located in.", + } + + response.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "access_scope": schema.StringAttribute{ + Description: descriptions["access_scope"], + Optional: true, + Required: false, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "algorithm": schema.StringAttribute{ + Description: descriptions["algorithm"], + 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(), + }, + }, + "protection": schema.StringAttribute{ + Description: descriptions["protection"], + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "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, "Wrapping key read") +} diff --git a/stackit/internal/services/kms/wrapping-key/resource.go b/stackit/internal/services/kms/wrapping-key/resource.go new file mode 100644 index 000000000..6bf1d9342 --- /dev/null +++ b/stackit/internal/services/kms/wrapping-key/resource.go @@ -0,0 +1,377 @@ +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 { + AccessScope types.String `tfsdk:"access_scope"` + Algorithm types.String `tfsdk:"algorithm"` + Description types.String `tfsdk:"description"` + DisplayName types.String `tfsdk:"display_name"` + Id types.String `tfsdk:"id"` // needed by TF + ImportOnly types.Bool `tfsdk:"import_only"` + KeyRingId types.String `tfsdk:"key_ring_id"` + Protection types.String `tfsdk:"protection"` + Purpose types.String `tfsdk:"purpose"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + WrappingKeyId types.String `tfsdk:"wrapping_key_id"` +} + +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.", + "access_scope": "The access scope of the key. Default is PUBLIC.", + "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`\".", + "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.", + "protection": "The underlying system that is responsible for protecting the key material. Currently only software is accepted.", + "region": "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{ + "access_scope": schema.StringAttribute{ + Description: descriptions["access_scope"], + Optional: true, + Required: false, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "algorithm": schema.StringAttribute{ + Description: descriptions["algorithm"], + 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["import_only"], + Computed: true, + Required: false, + }, + "key_ring_id": schema.StringAttribute{ + Description: descriptions["key_ring_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "protection": schema.StringAttribute{ + Description: descriptions["protection"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "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{ + AccessScope: kms.CreateKeyPayloadGetAccessScopeAttributeType(conversion.StringValueToPointer(model.AccessScope)), + Algorithm: kms.CreateWrappingKeyPayloadGetAlgorithmAttributeType(conversion.StringValueToPointer(model.Algorithm)), + Description: conversion.StringValueToPointer(model.Description), + DisplayName: conversion.StringValueToPointer(model.DisplayName), + Protection: kms.CreateKeyPayloadGetProtectionAttributeType(conversion.StringValueToPointer(model.Protection)), + Purpose: kms.CreateWrappingKeyPayloadGetPurposeAttributeType(conversion.StringValueToPointer(model.Purpose)), + }, nil +} diff --git a/stackit/internal/services/kms/wrapping-key/resource_test.go b/stackit/internal/services/kms/wrapping-key/resource_test.go new file mode 100644 index 000000000..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/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index a2651db28..77d37fa5c 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -55,6 +55,7 @@ var ( DnsCustomEndpoint = os.Getenv("TF_ACC_DNS_CUSTOM_ENDPOINT") GitCustomEndpoint = os.Getenv("TF_ACC_GIT_CUSTOM_ENDPOINT") IaaSCustomEndpoint = os.Getenv("TF_ACC_IAAS_CUSTOM_ENDPOINT") + KMSCustomEndpoint = os.Getenv("TF_ACC_KMS_CUSTOM_ENDPOINT") LoadBalancerCustomEndpoint = os.Getenv("TF_ACC_LOADBALANCER_CUSTOM_ENDPOINT") LogMeCustomEndpoint = os.Getenv("TF_ACC_LOGME_CUSTOM_ENDPOINT") MariaDBCustomEndpoint = os.Getenv("TF_ACC_MARIADB_CUSTOM_ENDPOINT") @@ -169,6 +170,21 @@ func IaaSProviderConfigWithExperiments() string { ) } +func KMSProviderConfig() string { + if KMSCustomEndpoint == "" { + return ` + provider "stackit" { + default_region = "eu01" + }` + } + return fmt.Sprintf(` + provider "stackit" { + kms_custom_endpoint = "%s" + }`, + KMSCustomEndpoint, + ) +} + func LoadBalancerProviderConfig() string { if LoadBalancerCustomEndpoint == "" { return ` diff --git a/stackit/provider.go b/stackit/provider.go index 5f51fc33c..3bc2578b7 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -48,6 +48,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" @@ -131,6 +134,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.String `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"` @@ -173,6 +177,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", @@ -264,6 +269,10 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Optional: true, Description: descriptions["iaas_custom_endpoint"], }, + "kms_custom_endpoint": schema.StringAttribute{ + Optional: true, + Description: descriptions["kms_custom_endpoint"], + }, "postgresflex_custom_endpoint": schema.StringAttribute{ Optional: true, Description: descriptions["postgresflex_custom_endpoint"], @@ -414,6 +423,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, setStringField(providerConfig.GitCustomEndpoint, func(v string) { providerData.GitCustomEndpoint = v }) setStringField(providerConfig.IaaSCustomEndpoint, func(v string) { providerData.IaaSCustomEndpoint = v }) setStringField(providerConfig.PostgresFlexCustomEndpoint, func(v string) { providerData.PostgresFlexCustomEndpoint = v }) + setStringField(providerConfig.KMSCustomEndpoint, func(v string) { providerData.KMSCustomEndpoint = v }) setStringField(providerConfig.ModelServingCustomEndpoint, func(v string) { providerData.ModelServingCustomEndpoint = v }) setStringField(providerConfig.MongoDBFlexCustomEndpoint, func(v string) { providerData.MongoDBFlexCustomEndpoint = v }) setStringField(providerConfig.LoadBalancerCustomEndpoint, func(v string) { providerData.LoadBalancerCustomEndpoint = v }) @@ -434,6 +444,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.ValueString() + } + if !(providerConfig.Experiments.IsUnknown() || providerConfig.Experiments.IsNull()) { var experimentValues []string diags := providerConfig.Experiments.ElementsAs(ctx, &experimentValues, false) @@ -486,6 +500,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, @@ -554,6 +571,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,