feat(kms): add key resource and datasource (#1055)

relates to STACKITTPR-411
This commit is contained in:
Ruben Hönle 2025-11-17 11:58:11 +01:00 committed by GitHub
parent b5f82e7de9
commit 5e8c7a7369
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1369 additions and 3 deletions

View file

@ -0,0 +1,45 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_kms_key Data Source - stackit"
subcategory: ""
description: |-
KMS Key datasource schema. Uses the default_region specified in the provider configuration as a fallback in case no region is defined on datasource level.
---
# stackit_kms_key (Data Source)
KMS Key datasource schema. Uses the `default_region` specified in the provider configuration as a fallback in case no `region` is defined on datasource level.
## Example Usage
```terraform
data "stackit_kms_key" "key" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
keyring_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
key_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `key_id` (String) The ID of the key
- `keyring_id` (String) The ID of the associated key ring
- `project_id` (String) STACKIT project ID to which the key is associated.
### Optional
- `region` (String) The resource region. If not defined, the provider region is used.
### Read-Only
- `access_scope` (String) The access scope of the key. Default is `PUBLIC`. Possible values are: `PUBLIC`, `SNA`.
- `algorithm` (String) The encryption algorithm that the key will use to encrypt data. Possible values are: `aes_256_gcm`, `rsa_2048_oaep_sha256`, `rsa_3072_oaep_sha256`, `rsa_4096_oaep_sha256`, `rsa_4096_oaep_sha512`, `hmac_sha256`, `hmac_sha384`, `hmac_sha512`, `ecdsa_p256_sha256`, `ecdsa_p384_sha384`, `ecdsa_p521_sha512`.
- `description` (String) A user chosen description to distinguish multiple keys
- `display_name` (String) The display name to distinguish multiple keys
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`keyring_id`,`key_id`".
- `import_only` (Boolean) States whether versions can be created or only imported.
- `protection` (String) The underlying system that is responsible for protecting the key material. Possible values are: `software`.
- `purpose` (String) The purpose for which the key will be used. Possible values are: `symmetric_encrypt_decrypt`, `asymmetric_encrypt_decrypt`, `message_authentication_code`, `asymmetric_sign_verify`.

51
docs/resources/kms_key.md Normal file
View file

@ -0,0 +1,51 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_kms_key Resource - stackit"
subcategory: ""
description: |-
KMS Key resource schema. Uses the default_region specified in the provider configuration as a fallback in case no region is defined on resource level.
~> Keys will not be instantly destroyed by terraform during a terraform destroy. They will just be scheduled for deletion via the API and thrown out of the Terraform state afterwards. This way we can ensure no key setups are deleted by accident and it gives you the option to recover your keys within the grace period.
---
# stackit_kms_key (Resource)
KMS Key resource schema. Uses the `default_region` specified in the provider configuration as a fallback in case no `region` is defined on resource level.
~> Keys will **not** be instantly destroyed by terraform during a `terraform destroy`. They will just be scheduled for deletion via the API and thrown out of the Terraform state afterwards. **This way we can ensure no key setups are deleted by accident and it gives you the option to recover your keys within the grace period.**
## Example Usage
```terraform
resource "stackit_kms_key" "key" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
keyring_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
display_name = "key-01"
protection = "software"
algorithm = "aes_256_gcm"
purpose = "symmetric_encrypt_decrypt"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `algorithm` (String) The encryption algorithm that the key will use to encrypt data. Possible values are: `aes_256_gcm`, `rsa_2048_oaep_sha256`, `rsa_3072_oaep_sha256`, `rsa_4096_oaep_sha256`, `rsa_4096_oaep_sha512`, `hmac_sha256`, `hmac_sha384`, `hmac_sha512`, `ecdsa_p256_sha256`, `ecdsa_p384_sha384`, `ecdsa_p521_sha512`.
- `display_name` (String) The display name to distinguish multiple keys
- `keyring_id` (String) The ID of the associated keyring
- `project_id` (String) STACKIT project ID to which the key is associated.
- `protection` (String) The underlying system that is responsible for protecting the key material. Possible values are: `software`.
- `purpose` (String) The purpose for which the key will be used. Possible values are: `symmetric_encrypt_decrypt`, `asymmetric_encrypt_decrypt`, `message_authentication_code`, `asymmetric_sign_verify`.
### Optional
- `access_scope` (String) The access scope of the key. Default is `PUBLIC`. Possible values are: `PUBLIC`, `SNA`.
- `description` (String) A user chosen description to distinguish multiple keys
- `import_only` (Boolean) States whether versions can be created or only imported.
- `region` (String) The resource region. If not defined, the provider region is used.
### Read-Only
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`keyring_id`,`key_id`".
- `key_id` (String) The ID of the key

View file

@ -0,0 +1,5 @@
data "stackit_kms_key" "key" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
keyring_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
key_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

View file

@ -0,0 +1,8 @@
resource "stackit_kms_key" "key" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
keyring_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
display_name = "key-01"
protection = "software"
algorithm = "aes_256_gcm"
purpose = "symmetric_encrypt_decrypt"
}

2
go.mod
View file

@ -11,7 +11,7 @@ require (
github.com/hashicorp/terraform-plugin-go v0.29.0
github.com/hashicorp/terraform-plugin-log v0.10.0
github.com/hashicorp/terraform-plugin-testing v1.13.3
github.com/stackitcloud/stackit-sdk-go/core v0.17.3
github.com/stackitcloud/stackit-sdk-go/core v0.19.0
github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0
github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1
github.com/stackitcloud/stackit-sdk-go/services/git v0.8.0

4
go.sum
View file

@ -152,8 +152,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stackitcloud/stackit-sdk-go/core v0.17.3 h1:GsZGmRRc/3GJLmCUnsZswirr5wfLRrwavbnL/renOqg=
github.com/stackitcloud/stackit-sdk-go/core v0.17.3/go.mod h1:HBCXJGPgdRulplDzhrmwC+Dak9B/x0nzNtmOpu+1Ahg=
github.com/stackitcloud/stackit-sdk-go/core v0.19.0 h1:dtJcs6/TTCzzb2RKI7HJugDrbCkaFEDmn1pOeFe8qnI=
github.com/stackitcloud/stackit-sdk-go/core v0.19.0/go.mod h1:fqto7M82ynGhEnpZU6VkQKYWYoFG5goC076JWXTUPRQ=
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 h1:7ZKd3b+E/R4TEVShLTXxx5FrsuDuJBOyuVOuKTMa4mo=
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0/go.mod h1:/FoXa6hF77Gv8brrvLBCKa5ie1Xy9xn39yfHwaln9Tw=
github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0 h1:Q+qIdejeMsYMkbtVoI9BpGlKGdSVFRBhH/zj44SP8TM=

View file

@ -0,0 +1,187 @@
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"
sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
kmsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
var (
_ datasource.DataSource = &keyDataSource{}
)
func NewKeyDataSource() datasource.DataSource {
return &keyDataSource{}
}
type keyDataSource struct {
client *kms.APIClient
providerData core.ProviderData
}
func (k *keyDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_kms_key"
}
func (k *keyDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
var ok bool
k.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
k.client = kmsUtils.ConfigureClient(ctx, &k.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "KMS client configured")
}
func (k *keyDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: fmt.Sprintf("KMS Key datasource schema. %s", core.DatasourceRegionFallbackDocstring),
Attributes: map[string]schema.Attribute{
"access_scope": schema.StringAttribute{
Description: fmt.Sprintf("The access scope of the key. Default is `%s`. %s", string(kms.ACCESSSCOPE_PUBLIC), utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedAccessScopeEnumValues)...)),
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"algorithm": schema.StringAttribute{
Description: fmt.Sprintf("The encryption algorithm that the key will use to encrypt data. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedAlgorithmEnumValues)...)),
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"description": schema.StringAttribute{
Description: "A user chosen description to distinguish multiple keys",
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"display_name": schema.StringAttribute{
Description: "The display name to distinguish multiple keys",
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`keyring_id`,`key_id`\".",
Computed: true,
},
"import_only": schema.BoolAttribute{
Description: "States whether versions can be created or only imported.",
Computed: true,
},
"key_id": schema.StringAttribute{
Description: "The ID of the key",
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"keyring_id": schema.StringAttribute{
Description: "The ID of the associated key ring",
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"protection": schema.StringAttribute{
Description: fmt.Sprintf("The underlying system that is responsible for protecting the key material. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedProtectionEnumValues)...)),
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"purpose": schema.StringAttribute{
Description: fmt.Sprintf("The purpose for which the key will be used. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedPurposeEnumValues)...)),
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"project_id": schema.StringAttribute{
Description: "STACKIT project ID to which the key is associated.",
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
Description: "The resource region. If not defined, the provider region is used.",
},
},
}
}
func (k *keyDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.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, "keyring_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,
&resp.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),
},
)
resp.State.RemoveResource(ctx)
return
}
err = mapFields(keyResponse, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Key read")
}

View file

@ -0,0 +1,443 @@
package kms
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/stackitcloud/stackit-sdk-go/services/kms/wait"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
kmsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
const (
deletionWarning = "Keys will **not** be instantly destroyed by terraform during a `terraform destroy`. They will just be scheduled for deletion via the API and thrown out of the Terraform state afterwards. **This way we can ensure no key setups are deleted by accident and it gives you the option to recover your keys within the grace period.**"
)
var (
_ resource.Resource = &keyResource{}
_ resource.ResourceWithConfigure = &keyResource{}
_ resource.ResourceWithImportState = &keyResource{}
_ resource.ResourceWithModifyPlan = &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:"keyring_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 (r *keyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_kms_key"
}
func (r *keyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
r.client = kmsUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "KMS client configured")
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *keyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
func (r *keyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
description := fmt.Sprintf("KMS Key resource schema. %s", core.ResourceRegionFallbackDocstring)
resp.Schema = schema.Schema{
Description: description,
MarkdownDescription: fmt.Sprintf("%s\n\n ~> %s", description, deletionWarning),
Attributes: map[string]schema.Attribute{
"access_scope": schema.StringAttribute{
Description: fmt.Sprintf("The access scope of the key. Default is `%s`. %s", string(kms.ACCESSSCOPE_PUBLIC), utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedAccessScopeEnumValues)...)),
Optional: true,
Computed: true, // must be computed because of default value
Default: stringdefault.StaticString(string(kms.ACCESSSCOPE_PUBLIC)),
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"algorithm": schema.StringAttribute{
Description: fmt.Sprintf("The encryption algorithm that the key will use to encrypt data. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedAlgorithmEnumValues)...)),
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"description": schema.StringAttribute{
Description: "A user chosen description to distinguish multiple keys",
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"display_name": schema.StringAttribute{
Description: "The display name to distinguish multiple keys",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`keyring_id`,`key_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"import_only": schema.BoolAttribute{
Description: "States whether versions can be created or only imported.",
Computed: true,
Optional: true,
},
"key_id": schema.StringAttribute{
Description: "The ID of the key",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"keyring_id": schema.StringAttribute{
Description: "The ID of the associated keyring",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"protection": schema.StringAttribute{
Description: fmt.Sprintf("The underlying system that is responsible for protecting the key material. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedProtectionEnumValues)...)),
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"purpose": schema.StringAttribute{
Description: fmt.Sprintf("The purpose for which the key will be used. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedPurposeEnumValues)...)),
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"project_id": schema.StringAttribute{
Description: "STACKIT project ID to which the key is associated.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
Description: "The resource region. If not defined, the provider region is used.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
},
}
}
func (r *keyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
keyRingId := model.KeyRingId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "keyring_id", keyRingId)
payload, err := toCreatePayload(&model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key", fmt.Sprintf("Creating API payload: %v", err))
return
}
createResponse, err := r.client.CreateKey(ctx, projectId, region, keyRingId).CreateKeyPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key", fmt.Sprintf("Calling API: %v", err))
return
}
if createResponse == nil || createResponse.Id == nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key", "API returned empty response")
return
}
keyId := *createResponse.Id
// Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": projectId,
"region": region,
"keyring_id": keyRingId,
"key_id": keyId,
})
waitHandlerResp, err := wait.CreateOrUpdateKeyWaitHandler(ctx, r.client, projectId, region, keyRingId, keyId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error waiting for key creation", fmt.Sprintf("Calling API: %v", err))
return
}
err = mapFields(waitHandlerResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Key created")
}
func (r *keyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
keyRingId := model.KeyRingId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
keyId := model.KeyId.ValueString()
ctx = tflog.SetField(ctx, "keyring_id", keyRingId)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "key_id", keyId)
keyResponse, err := r.client.GetKey(ctx, projectId, region, keyRingId, keyId).Execute()
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if ok && oapiErr.StatusCode == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key", fmt.Sprintf("Calling API: %v", err))
return
}
err = mapFields(keyResponse, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Key read")
}
func (r *keyResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
// keys cannot be updated, so we log an error.
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating key", "Keys can't be updated")
}
func (r *keyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
keyRingId := model.KeyRingId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
keyId := model.KeyId.ValueString()
err := r.client.DeleteKey(ctx, projectId, region, keyRingId, keyId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting key", fmt.Sprintf("Calling API: %v", err))
}
// The keys can't be deleted instantly by Terraform, they can only be scheduled for deletion via the API.
core.LogAndAddWarning(ctx, &resp.Diagnostics, "Key scheduled for deletion on API side", deletionWarning)
tflog.Info(ctx, "key deleted")
}
func (r *keyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing key",
fmt.Sprintf("Exptected import identifier with format: [project_id],[region],[keyring_id],[key_id], got :%q", req.ID),
)
return
}
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"region": idParts[1],
"keyring_id": idParts[2],
"key_id": idParts[3],
})
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(), region, model.KeyRingId.ValueString(), keyId)
model.KeyId = types.StringValue(keyId)
model.DisplayName = types.StringPointerValue(key.DisplayName)
model.Region = types.StringValue(region)
model.ImportOnly = types.BoolPointerValue(key.ImportOnly)
model.AccessScope = types.StringValue(string(key.GetAccessScope()))
model.Algorithm = types.StringValue(string(key.GetAlgorithm()))
model.Purpose = types.StringValue(string(key.GetPurpose()))
model.Protection = types.StringValue(string(key.GetProtection()))
// TODO: workaround - remove once STACKITKMS-377 is resolved (just write the return value from the API to the state then)
if !(model.Description.IsNull() && key.Description != nil && *key.Description == "") {
model.Description = types.StringPointerValue(key.Description)
} else {
model.Description = types.StringNull()
}
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
}

View file

@ -0,0 +1,216 @@
package kms
import (
"fmt"
"testing"
"github.com/google/uuid"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)
var (
keyId = uuid.NewString()
keyRingId = uuid.NewString()
projectId = uuid.NewString()
)
func TestMapFields(t *testing.T) {
type args struct {
state Model
input *kms.Key
region string
}
tests := []struct {
description string
args args
expected Model
isValid bool
}{
{
description: "default values",
args: args{
state: Model{
KeyId: types.StringValue(keyId),
KeyRingId: types.StringValue(keyRingId),
ProjectId: types.StringValue(projectId),
},
input: &kms.Key{
Id: utils.Ptr(keyId),
Protection: utils.Ptr(kms.PROTECTION_SOFTWARE),
Algorithm: utils.Ptr(kms.ALGORITHM_ECDSA_P256_SHA256),
Purpose: utils.Ptr(kms.PURPOSE_ASYMMETRIC_SIGN_VERIFY),
AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC),
},
region: "eu01",
},
expected: Model{
Description: types.StringNull(),
DisplayName: types.StringNull(),
KeyRingId: types.StringValue(keyRingId),
KeyId: types.StringValue(keyId),
Id: types.StringValue(fmt.Sprintf("%s,eu01,%s,%s", projectId, keyRingId, keyId)),
ProjectId: types.StringValue(projectId),
Region: types.StringValue("eu01"),
Protection: types.StringValue(string(kms.PROTECTION_SOFTWARE)),
Algorithm: types.StringValue(string(kms.ALGORITHM_ECDSA_P256_SHA256)),
Purpose: types.StringValue(string(kms.PURPOSE_ASYMMETRIC_SIGN_VERIFY)),
AccessScope: types.StringValue(string(kms.ACCESSSCOPE_PUBLIC)),
},
isValid: true,
},
{
description: "values_ok",
args: args{
state: Model{
KeyId: types.StringValue(keyId),
KeyRingId: types.StringValue(keyRingId),
ProjectId: types.StringValue(projectId),
},
input: &kms.Key{
Id: utils.Ptr(keyId),
Description: utils.Ptr("descr"),
DisplayName: utils.Ptr("name"),
ImportOnly: utils.Ptr(true),
Protection: utils.Ptr(kms.PROTECTION_SOFTWARE),
Algorithm: utils.Ptr(kms.ALGORITHM_AES_256_GCM),
Purpose: utils.Ptr(kms.PURPOSE_MESSAGE_AUTHENTICATION_CODE),
AccessScope: utils.Ptr(kms.ACCESSSCOPE_SNA),
},
region: "eu01",
},
expected: Model{
Description: types.StringValue("descr"),
DisplayName: types.StringValue("name"),
KeyId: types.StringValue(keyId),
KeyRingId: types.StringValue(keyRingId),
Id: types.StringValue(fmt.Sprintf("%s,eu01,%s,%s", projectId, keyRingId, keyId)),
ProjectId: types.StringValue(projectId),
Region: types.StringValue("eu01"),
ImportOnly: types.BoolValue(true),
Protection: types.StringValue(string(kms.PROTECTION_SOFTWARE)),
Algorithm: types.StringValue(string(kms.ALGORITHM_AES_256_GCM)),
Purpose: types.StringValue(string(kms.PURPOSE_MESSAGE_AUTHENTICATION_CODE)),
AccessScope: types.StringValue(string(kms.ACCESSSCOPE_SNA)),
},
isValid: true,
},
{
description: "nil_response_field",
args: args{
state: Model{},
input: &kms.Key{
Id: nil,
},
},
expected: Model{},
isValid: false,
},
{
description: "nil_response",
args: args{
state: Model{},
input: nil,
},
expected: Model{},
isValid: false,
},
{
description: "no_resource_id",
args: args{
state: Model{
Region: types.StringValue("eu01"),
ProjectId: types.StringValue(projectId),
},
input: &kms.Key{},
},
expected: Model{},
isValid: 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.args.input, state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(state, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
expected *kms.CreateKeyPayload
isValid bool
}{
{
description: "default_values",
input: &Model{},
expected: &kms.CreateKeyPayload{},
isValid: true,
},
{
description: "simple_values",
input: &Model{
DisplayName: types.StringValue("name"),
},
expected: &kms.CreateKeyPayload{
DisplayName: utils.Ptr("name"),
},
isValid: true,
},
{
description: "null_fields",
input: &Model{
DisplayName: types.StringValue(""),
Description: types.StringValue(""),
},
expected: &kms.CreateKeyPayload{
DisplayName: utils.Ptr(""),
Description: utils.Ptr(""),
},
isValid: true,
},
{
description: "nil_model",
input: nil,
expected: nil,
isValid: 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)
}
}
})
}
}

View file

@ -30,8 +30,16 @@ var (
//go:embed testdata/keyring-max.tf
resourceKeyRingMaxConfig string
//go:embed testdata/key-min.tf
resourceKeyMinConfig string
//go:embed testdata/key-max.tf
resourceKeyMaxConfig string
)
// KEY RING - MIN
var testConfigKeyRingVarsMin = config.Variables{
"project_id": config.StringVariable(testutil.ProjectId),
"display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)),
@ -44,6 +52,8 @@ var testConfigKeyRingVarsMinUpdated = func() config.Variables {
return updatedConfig
}
// KEY RING - MAX
var testConfigKeyRingVarsMax = config.Variables{
"project_id": config.StringVariable(testutil.ProjectId),
"description": config.StringVariable("description"),
@ -58,6 +68,51 @@ var testConfigKeyRingVarsMaxUpdated = func() config.Variables {
return updatedConfig
}
// KEY - MIN
var testConfigKeyVarsMin = config.Variables{
"project_id": config.StringVariable(testutil.ProjectId),
"keyring_display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)),
"display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)),
"algorithm": config.StringVariable(string(kms.ALGORITHM_AES_256_GCM)),
"protection": config.StringVariable("software"),
"purpose": config.StringVariable(string(kms.PURPOSE_SYMMETRIC_ENCRYPT_DECRYPT)),
}
var testConfigKeyVarsMinUpdated = func() config.Variables {
updatedConfig := config.Variables{}
maps.Copy(updatedConfig, testConfigKeyVarsMin)
updatedConfig["display_name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["display_name"])))
updatedConfig["algorithm"] = config.StringVariable(string(kms.ALGORITHM_RSA_3072_OAEP_SHA256))
updatedConfig["purpose"] = config.StringVariable(string(kms.PURPOSE_ASYMMETRIC_ENCRYPT_DECRYPT))
return updatedConfig
}
// KEY - MAX
var testConfigKeyVarsMax = config.Variables{
"project_id": config.StringVariable(testutil.ProjectId),
"keyring_display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)),
"display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)),
"algorithm": config.StringVariable(string(kms.ALGORITHM_AES_256_GCM)),
"protection": config.StringVariable("software"),
"purpose": config.StringVariable(string(kms.PURPOSE_SYMMETRIC_ENCRYPT_DECRYPT)),
"access_scope": config.StringVariable(string(kms.ACCESSSCOPE_PUBLIC)),
"import_only": config.BoolVariable(true),
"description": config.StringVariable("kms-key-description"),
}
var testConfigKeyVarsMaxUpdated = func() config.Variables {
updatedConfig := config.Variables{}
maps.Copy(updatedConfig, testConfigKeyVarsMax)
updatedConfig["display_name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["display_name"])))
updatedConfig["algorithm"] = config.StringVariable(string(kms.ALGORITHM_RSA_3072_OAEP_SHA256))
updatedConfig["purpose"] = config.StringVariable(string(kms.PURPOSE_ASYMMETRIC_ENCRYPT_DECRYPT))
updatedConfig["import_only"] = config.BoolVariable(true)
updatedConfig["description"] = config.StringVariable("kms-key-description-updated")
return updatedConfig
}
func TestAccKeyRingMin(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
@ -246,8 +301,269 @@ func TestAccKeyRingMax(t *testing.T) {
})
}
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),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionCreate),
plancheck.ExpectResourceAction("stackit_kms_key.key", plancheck.ResourceActionCreate),
},
},
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("stackit_kms_key.key", "project_id", testutil.ProjectId),
resource.TestCheckResourceAttr("stackit_kms_key.key", "region", testutil.Region),
resource.TestCheckResourceAttrPair(
"stackit_kms_keyring.keyring", "keyring_id",
"stackit_kms_key.key", "keyring_id",
),
resource.TestCheckResourceAttrSet("stackit_kms_key.key", "key_id"),
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"])),
resource.TestCheckNoResourceAttr("stackit_kms_key.key", "description"),
resource.TestCheckResourceAttr("stackit_kms_key.key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)),
resource.TestCheckResourceAttr("stackit_kms_key.key", "import_only", "false"),
),
},
// Data Source
{
ConfigVariables: testConfigKeyVarsMin,
Config: fmt.Sprintf(`
%s
%s
data "stackit_kms_key" "key" {
project_id = stackit_kms_key.key.project_id
keyring_id = stackit_kms_key.key.keyring_id
key_id = stackit_kms_key.key.key_id
}
`,
testutil.KMSProviderConfig(), resourceKeyMinConfig,
),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop),
plancheck.ExpectResourceAction("stackit_kms_key.key", plancheck.ResourceActionNoop),
},
},
Check: resource.ComposeAggregateTestCheckFunc(
resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "project_id", testutil.ProjectId),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "region", testutil.Region),
resource.TestCheckResourceAttrPair(
"stackit_kms_keyring.keyring", "keyring_id",
"data.stackit_kms_key.key", "keyring_id",
),
resource.TestCheckResourceAttrPair(
"stackit_kms_key.key", "key_id",
"data.stackit_kms_key.key", "key_id",
),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "algorithm", testutil.ConvertConfigVariable(testConfigKeyVarsMin["algorithm"])),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "display_name", testutil.ConvertConfigVariable(testConfigKeyVarsMin["display_name"])),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "purpose", testutil.ConvertConfigVariable(testConfigKeyVarsMin["purpose"])),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "protection", testutil.ConvertConfigVariable(testConfigKeyVarsMin["protection"])),
resource.TestCheckNoResourceAttr("data.stackit_kms_key.key", "description"),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "import_only", "false"),
),
),
},
// Import
{
ConfigVariables: testConfigKeyVarsMin,
ResourceName: "stackit_kms_key.key",
ImportStateIdFunc: func(s *terraform.State) (string, error) {
r, ok := s.RootModule().Resources["stackit_kms_key.key"]
if !ok {
return "", fmt.Errorf("couldn't find resource stackit_kms_key.key")
}
keyRingId, ok := r.Primary.Attributes["keyring_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute keyring_id")
}
keyId, ok := r.Primary.Attributes["key_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute key_id")
}
return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, keyRingId, keyId), nil
},
ImportState: true,
ImportStateVerify: true,
},
// Update
{
ConfigVariables: testConfigKeyVarsMinUpdated(),
Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyMinConfig),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop),
plancheck.ExpectResourceAction("stackit_kms_key.key", plancheck.ResourceActionReplace),
},
},
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("stackit_kms_key.key", "project_id", testutil.ProjectId),
resource.TestCheckResourceAttr("stackit_kms_key.key", "region", testutil.Region),
resource.TestCheckResourceAttrPair(
"stackit_kms_keyring.keyring", "keyring_id",
"stackit_kms_key.key", "keyring_id",
),
resource.TestCheckResourceAttrSet("stackit_kms_key.key", "key_id"),
resource.TestCheckResourceAttr("stackit_kms_key.key", "algorithm", testutil.ConvertConfigVariable(testConfigKeyVarsMinUpdated()["algorithm"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "display_name", testutil.ConvertConfigVariable(testConfigKeyVarsMinUpdated()["display_name"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "purpose", testutil.ConvertConfigVariable(testConfigKeyVarsMinUpdated()["purpose"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "protection", testutil.ConvertConfigVariable(testConfigKeyVarsMinUpdated()["protection"])),
resource.TestCheckNoResourceAttr("stackit_kms_key.key", "description"),
resource.TestCheckResourceAttr("stackit_kms_key.key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)),
resource.TestCheckResourceAttr("stackit_kms_key.key", "import_only", "false"),
),
},
// Deletion is done by the framework implicitly
},
})
}
func TestAccKeyMax(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckDestroy,
Steps: []resource.TestStep{
// Creation
{
ConfigVariables: testConfigKeyVarsMax,
Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyMaxConfig),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionCreate),
plancheck.ExpectResourceAction("stackit_kms_key.key", plancheck.ResourceActionCreate),
},
},
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("stackit_kms_key.key", "project_id", testutil.ProjectId),
resource.TestCheckResourceAttr("stackit_kms_key.key", "region", testutil.Region),
resource.TestCheckResourceAttrPair(
"stackit_kms_keyring.keyring", "keyring_id",
"stackit_kms_key.key", "keyring_id",
),
resource.TestCheckResourceAttrSet("stackit_kms_key.key", "key_id"),
resource.TestCheckResourceAttr("stackit_kms_key.key", "algorithm", testutil.ConvertConfigVariable(testConfigKeyVarsMax["algorithm"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "display_name", testutil.ConvertConfigVariable(testConfigKeyVarsMax["display_name"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "purpose", testutil.ConvertConfigVariable(testConfigKeyVarsMax["purpose"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "protection", testutil.ConvertConfigVariable(testConfigKeyVarsMax["protection"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "description", testutil.ConvertConfigVariable(testConfigKeyVarsMax["description"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "access_scope", testutil.ConvertConfigVariable(testConfigKeyVarsMax["access_scope"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "import_only", testutil.ConvertConfigVariable(testConfigKeyVarsMax["import_only"])),
),
},
// Data Source
{
ConfigVariables: testConfigKeyVarsMax,
Config: fmt.Sprintf(`
%s
%s
data "stackit_kms_key" "key" {
project_id = stackit_kms_key.key.project_id
keyring_id = stackit_kms_key.key.keyring_id
key_id = stackit_kms_key.key.key_id
}
`,
testutil.KMSProviderConfig(), resourceKeyMaxConfig,
),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop),
plancheck.ExpectResourceAction("stackit_kms_key.key", plancheck.ResourceActionNoop),
},
},
Check: resource.ComposeAggregateTestCheckFunc(
resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "project_id", testutil.ProjectId),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "region", testutil.Region),
resource.TestCheckResourceAttrPair(
"stackit_kms_keyring.keyring", "keyring_id",
"data.stackit_kms_key.key", "keyring_id",
),
resource.TestCheckResourceAttrPair(
"stackit_kms_key.key", "key_id",
"data.stackit_kms_key.key", "key_id",
),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "algorithm", testutil.ConvertConfigVariable(testConfigKeyVarsMax["algorithm"])),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "display_name", testutil.ConvertConfigVariable(testConfigKeyVarsMax["display_name"])),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "purpose", testutil.ConvertConfigVariable(testConfigKeyVarsMax["purpose"])),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "protection", testutil.ConvertConfigVariable(testConfigKeyVarsMax["protection"])),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "description", testutil.ConvertConfigVariable(testConfigKeyVarsMax["description"])),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "access_scope", testutil.ConvertConfigVariable(testConfigKeyVarsMax["access_scope"])),
resource.TestCheckResourceAttr("data.stackit_kms_key.key", "import_only", testutil.ConvertConfigVariable(testConfigKeyVarsMax["import_only"])),
),
),
},
// Import
{
ConfigVariables: testConfigKeyVarsMax,
ResourceName: "stackit_kms_key.key",
ImportStateIdFunc: func(s *terraform.State) (string, error) {
r, ok := s.RootModule().Resources["stackit_kms_key.key"]
if !ok {
return "", fmt.Errorf("couldn't find resource stackit_kms_key.key")
}
keyRingId, ok := r.Primary.Attributes["keyring_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute keyring_id")
}
keyId, ok := r.Primary.Attributes["key_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute key_id")
}
return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, keyRingId, keyId), nil
},
ImportState: true,
ImportStateVerify: true,
},
// Update
{
ConfigVariables: testConfigKeyVarsMaxUpdated(),
Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyMaxConfig),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop),
plancheck.ExpectResourceAction("stackit_kms_key.key", plancheck.ResourceActionReplace),
},
},
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("stackit_kms_key.key", "project_id", testutil.ProjectId),
resource.TestCheckResourceAttr("stackit_kms_key.key", "region", testutil.Region),
resource.TestCheckResourceAttrPair(
"stackit_kms_keyring.keyring", "keyring_id",
"stackit_kms_key.key", "keyring_id",
),
resource.TestCheckResourceAttrSet("stackit_kms_key.key", "key_id"),
resource.TestCheckResourceAttr("stackit_kms_key.key", "algorithm", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["algorithm"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "display_name", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["display_name"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "purpose", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["purpose"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "protection", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["protection"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "description", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["description"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "access_scope", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["access_scope"])),
resource.TestCheckResourceAttr("stackit_kms_key.key", "import_only", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["import_only"])),
),
},
// Deletion is done by the framework implicitly
},
})
}
func testAccCheckDestroy(s *terraform.State) error {
checkFunctions := []func(s *terraform.State) error{
testAccCheckKeyDestroy,
testAccCheckKeyRingDestroy,
}
@ -313,3 +629,47 @@ func testAccCheckKeyRingDestroy(s *terraform.State) error {
return errors.Join(errs...)
}
func testAccCheckKeyDestroy(s *terraform.State) error {
ctx := context.Background()
var client *kms.APIClient
var err error
if testutil.KMSCustomEndpoint == "" {
client, err = kms.NewAPIClient()
} else {
client, err = kms.NewAPIClient(
coreConfig.WithEndpoint(testutil.KMSCustomEndpoint),
)
}
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
var errs []error
for _, rs := range s.RootModule().Resources {
if rs.Type != "stackit_kms_key" {
continue
}
keyRingId := strings.Split(rs.Primary.ID, core.Separator)[2]
keyId := strings.Split(rs.Primary.ID, core.Separator)[3]
err := client.DeleteKeyExecute(ctx, testutil.ProjectId, testutil.Region, keyRingId, keyId)
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
if errors.As(err, &oapiErr) {
if oapiErr.StatusCode == http.StatusNotFound {
continue
}
// workaround: when the delete endpoint is called a second time for a key which is already scheduled
// for deletion, one will get an HTTP 400 error which we have to ignore here
if oapiErr.StatusCode == http.StatusBadRequest {
continue
}
}
errs = append(errs, fmt.Errorf("cannot trigger key deletion %q: %w", keyRingId, err))
}
}
return errors.Join(errs...)
}

View file

@ -0,0 +1,27 @@
variable "project_id" {}
variable "keyring_display_name" {}
variable "display_name" {}
variable "description" {}
variable "access_scope" {}
variable "import_only" {}
variable "protection" {}
variable "algorithm" {}
variable "purpose" {}
resource "stackit_kms_keyring" "keyring" {
project_id = var.project_id
display_name = var.keyring_display_name
}
resource "stackit_kms_key" "key" {
project_id = var.project_id
keyring_id = stackit_kms_keyring.keyring.keyring_id
protection = var.protection
algorithm = var.algorithm
display_name = var.display_name
purpose = var.purpose
description = var.description
access_scope = var.access_scope
import_only = var.import_only
}

View file

@ -0,0 +1,21 @@
variable "project_id" {}
variable "keyring_display_name" {}
variable "display_name" {}
variable "protection" {}
variable "algorithm" {}
variable "purpose" {}
resource "stackit_kms_keyring" "keyring" {
project_id = var.project_id
display_name = var.keyring_display_name
}
resource "stackit_kms_key" "key" {
project_id = var.project_id
keyring_id = stackit_kms_keyring.keyring.keyring_id
protection = var.protection
algorithm = var.algorithm
display_name = var.display_name
purpose = var.purpose
}

View file

@ -48,6 +48,7 @@ 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/keyring"
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"
@ -494,6 +495,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
iaasalphaRoutingTables.NewRoutingTablesDataSource,
iaasalphaRoutingTableRoutes.NewRoutingTableRoutesDataSource,
iaasSecurityGroupRule.NewSecurityGroupRuleDataSource,
kmsKey.NewKeyDataSource,
kmsKeyRing.NewKeyRingDataSource,
loadBalancer.NewLoadBalancerDataSource,
logMeInstance.NewInstanceDataSource,
@ -563,6 +565,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
iaasSecurityGroupRule.NewSecurityGroupRuleResource,
iaasalphaRoutingTable.NewRoutingTableResource,
iaasalphaRoutingTableRoute.NewRoutingTableRouteResource,
kmsKey.NewKeyResource,
kmsKeyRing.NewKeyRingResource,
loadBalancer.NewLoadBalancerResource,
loadBalancerObservabilityCredential.NewObservabilityCredentialResource,