Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a7259ff
first steps for kms key-ring resource and datasource
ruslan-18 Jul 1, 2025
6f6d063
define model, implement Metadata, Configure, Schema and Create method…
ruslan-18 Jul 11, 2025
5b59eb6
Merge branch 'main' into kms-integration
ruslan-18 Jul 11, 2025
185fed5
Merge branch 'main' into kms-integration
ruslan-18 Jul 16, 2025
6bcc14a
PR comments, fix region logic, add example, add datasource, add examp…
ruslan-18 Jul 28, 2025
d320073
Merge remote-tracking branch 'origin/kms-integration' into kms-integr…
ruslan-18 Jul 28, 2025
bcd0528
first steps for kms key-ring resource and datasource
ruslan-18 Jul 1, 2025
99e2b7f
define model, implement Metadata, Configure, Schema and Create method…
ruslan-18 Jul 11, 2025
8c654ad
PR comments, fix region logic, add example, add datasource, add examp…
ruslan-18 Jul 28, 2025
219adac
Merge remote-tracking branch 'origin/kms-integration' into kms-integr…
ruslan-18 Jul 28, 2025
1186cee
PR comments, fix region logic, add example, add datasource, add examp…
ruslan-18 Jul 28, 2025
cb23a4e
PR comments, fix region logic, add example, add datasource, add examp…
ruslan-18 Jul 28, 2025
8d0da16
Merge remote-tracking branch 'origin/kms-integration' into kms-integr…
ruslan-18 Jul 28, 2025
2732c4a
add missing resources and unit tests
ruslan-18 Aug 8, 2025
b483c13
Merge branch 'main' into kms-integration
ruslan-18 Aug 8, 2025
248748e
add missing examples and docs
ruslan-18 Aug 8, 2025
d41ad9d
Merge remote-tracking branch 'origin/kms-integration' into kms-integr…
ruslan-18 Aug 8, 2025
77a623e
fix linter findings
ruslan-18 Aug 8, 2025
7df0307
Merge branch 'main' into kms-integration
ruslan-18 Aug 8, 2025
d87d60f
update docs
ruslan-18 Aug 8, 2025
438da14
Merge remote-tracking branch 'origin/kms-integration' into kms-integr…
ruslan-18 Aug 8, 2025
889b222
update kms client config
ruslan-18 Aug 8, 2025
0ce560c
(wip) some PR comment fixes, Key Ring Acceptance Test
ruslan-18 Sep 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/stackitcloud/stackit-sdk-go/services/git v0.6.0
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.26.0
github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha
github.com/stackitcloud/stackit-sdk-go/services/kms v0.3.0
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.4.0
github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.0
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ github.com/stackitcloud/stackit-sdk-go/services/iaas v0.26.0 h1:7qm/Tft79wFlHomP
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.26.0/go.mod h1:lUGkcbyMkd4nRBDFmKohIwlgtOZqQo4Ek5S5ajw90Xg=
github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha h1:m1jq6a8dbUe+suFuUNdHmM/cSehpGLUtDbK1CqLqydg=
github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha/go.mod h1:Nu1b5Phsv8plgZ51+fkxPVsU91ZJ5Ayz+cthilxdmQ8=
github.com/stackitcloud/stackit-sdk-go/services/kms v0.3.0 h1:Fo1XCnfW47yW9zNfQgJk6/e9Z7YVoD4fay/PxYnSf4c=
github.com/stackitcloud/stackit-sdk-go/services/kms v0.3.0/go.mod h1:I3UOleO9ZlTYQDd5iTpomfjx7l1J7JAIj9VGmAjTMjk=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.4.0 h1:Ef4SyTBjIkfwaws4mssa6AoK+OokHFtr7ZIflUpoXVE=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.4.0/go.mod h1:FiVhDlw9+yuTiUmnyGLn2qpsLW26w9OC4TS1y78czvg=
github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.0 h1:QKOfaB7EcuJmBCxpFXN2K7g2ih0gQM6cyZ1VhTmtQfI=
Expand Down
1 change: 1 addition & 0 deletions stackit/internal/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type ProviderData struct {
DnsCustomEndpoint string
GitCustomEndpoint string
IaaSCustomEndpoint string
KMSCustomEndpoint string
LoadBalancerCustomEndpoint string
LogMeCustomEndpoint string
MariaDBCustomEndpoint string
Expand Down
34 changes: 34 additions & 0 deletions stackit/internal/services/kms/key-ring/datasource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kms

import (
"context"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/stackitcloud/stackit-sdk-go/services/kms"
)

var (
_ datasource.DataSource = &keyRingDataSource{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only a nitpick and most of our datasources are also missing this. But this ensures that the Configure method is implemented correct

Suggested change
_ datasource.DataSource = &keyRingDataSource{}
_ datasource.DataSource = &keyRingDataSource{}
_ datasource.DataSourceWithConfigure = &instanceDataSource{}

)

func NewKeyRingDataSource() datasource.DataSource {
return &keyRingDataSource{}
}

type keyRingDataSource struct {
client *kms.APIClient
}

func (k keyRingDataSource) Metadata(ctx context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) {
//TODO implement me
panic("implement me")
}

func (k keyRingDataSource) Schema(ctx context.Context, request datasource.SchemaRequest, response *datasource.SchemaResponse) {
//TODO implement me
panic("implement me")
}

func (k keyRingDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) {
//TODO implement me
panic("implement me")
}
212 changes: 212 additions & 0 deletions stackit/internal/services/kms/key-ring/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package kms

import (
"context"
"fmt"
"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/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"
)

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"`
RegionId types.String `tfsdk:"region_id"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
RegionId types.String `tfsdk:"region_id"`
Region types.String `tfsdk:"region"` # small adjustment to stick with the naming conventions across the codebase

According to https://docs.api.stackit.cloud/documentation/kms/version/v1beta , the KMS API already uses the new multi-region concept.

See e.g. the stackit_routing_table resource how to implement the multi-region concept. The resource has a region field which is marked as optional and will use the default_region configured in the provider as a fallback: https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/routing_table

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated region logic

}

func NewKeyRingResource() resource.Resource {
return &keyRingResource{}
}

type keyRingResource struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't know if you are aware of this, please don't forget to implement the import.

You can ensure this by adding the code below:

// Ensure the implementation satisfies the expected interfaces.
var (
	_ resource.Resource                 = &keyRingResource{}
	_ resource.ResourceWithConfigure    = &keyRingResource{}
	_ resource.ResourceWithImportState  = &keyRingResource{}
)

See the git instance resource how to do it:

// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &gitResource{}
_ resource.ResourceWithConfigure = &gitResource{}
_ resource.ResourceWithImportState = &gitResource{}
)

Consider doing the same for the datasource, see the git instance datasource for example:

https://github.com/stackitcloud/terraform-provider-stackit/blob/main/stackit/internal/services/git/instance/datasource.go#L24-L27

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

client *kms.APIClient
}

func (k *keyRingResource) Metadata(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) {
response.TypeName = request.ProviderTypeName + "kms_key_ring"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
response.TypeName = request.ProviderTypeName + "kms_key_ring"
response.TypeName = request.ProviderTypeName + "_kms_key_ring"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

}

func (k *keyRingResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics)
if !ok {
return
}

apiClient := kmsUtils.ConfigureClient(ctx, &providerData, &response.Diagnostics)
if response.Diagnostics.HasError() {
return
}
k.client = apiClient
}

func (k *keyRingResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) {
descriptions := map[string]string{
"main": "KMS Key Ring resource schema.",
"description": "A user chosen description to distinguish multiple key rings.",
"display_name": "The display name to distinguish multiple key rings.",
"key_ring_id": "An auto generated unique id which identifies the key ring.",
"id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".",
"project_id": "STACKIT project ID to which the key ring is associated.",
"region_id": "The STACKIT region name the key ring is located in.",
}

response.Schema = schema.Schema{
Description: descriptions["main"],
Attributes: map[string]schema.Attribute{
"description": schema.StringAttribute{
Description: descriptions["description"],
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"display_name": schema.StringAttribute{
Description: descriptions["description"],
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"key_ring_id": schema.StringAttribute{
Description: descriptions["key_ring_id"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"id": schema.StringAttribute{
Description: descriptions["id"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
},
}

}

func (k *keyRingResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
var model Model
diags := request.Plan.Get(ctx, &model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
regionId := model.RegionId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region_id", regionId)

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, regionId).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)

err = mapFields(createResponse, &model)
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) {
//TODO implement me
panic("implement me")
}

func (k *keyRingResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
//TODO implement me
panic("implement me")
}

func (k *keyRingResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
//TODO implement me
panic("implement me")
}

func toCreatePayload(model *Model) (*kms.CreateKeyRingPayload, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider implementing a unit test for this func.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added unit test

if model == nil {
return nil, fmt.Errorf("nil model")
}
return &kms.CreateKeyRingPayload{
Description: conversion.StringValueToPointer(model.Description),
DisplayName: conversion.StringValueToPointer(model.DisplayName),
}, nil
}

func mapFields(keyRing *kms.KeyRing, model *Model) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added unit test

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)

return nil
}
29 changes: 29 additions & 0 deletions stackit/internal/services/kms/utils/util.go
Original file line number Diff line number Diff line change
@@ -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))
} else {
apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion()))
apiClientConfigOptions = append(apiClientConfigOptions))

Since the KMS API already implemented the new multi-region concept, you don't need to set the region here (or better: the SDK should throw an error if you do 😄 ).

Apart from that, consider adding a unit tests for this func, see e.g. https://github.com/stackitcloud/terraform-provider-stackit/blob/8e776757ea2280d1222afe50c3024945b4d99eed/stackit/internal/services/git/utils/util_test.go

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

}
apiClient, err := kms.NewAPIClient(apiClientConfigOptions...)
if err != nil {
core.LogAndAddError(ctx, diags, "Error configurin API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
}

return apiClient
}
14 changes: 14 additions & 0 deletions stackit/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,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"
kms "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/key-ring"
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"
Expand Down Expand Up @@ -124,6 +125,7 @@ type providerModel struct {
DNSCustomEndpoint types.String `tfsdk:"dns_custom_endpoint"`
GitCustomEndpoint types.String `tfsdk:"git_custom_endpoint"`
IaaSCustomEndpoint types.String `tfsdk:"iaas_custom_endpoint"`
KMSCustomEndpoint types.List `tfsdk:"kms_custom_endpoint"`
PostgresFlexCustomEndpoint types.String `tfsdk:"postgresflex_custom_endpoint"`
MongoDBFlexCustomEndpoint types.String `tfsdk:"mongodbflex_custom_endpoint"`
ModelServingCustomEndpoint types.String `tfsdk:"modelserving_custom_endpoint"`
Expand Down Expand Up @@ -165,6 +167,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",
Expand Down Expand Up @@ -252,6 +255,11 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro
Optional: true,
Description: descriptions["iaas_custom_endpoint"],
},
"kms_custom_endpoint": schema.ListAttribute{
ElementType: types.StringType,
Optional: true,
Description: descriptions["kms_custom_endpoint"],
},
"postgresflex_custom_endpoint": schema.StringAttribute{
Optional: true,
Description: descriptions["postgresflex_custom_endpoint"],
Expand Down Expand Up @@ -417,6 +425,10 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest,
setStringField(providerConfig.ServiceEnablementCustomEndpoint, func(v string) { providerData.ServiceEnablementCustomEndpoint = v })
setBoolField(providerConfig.EnableBetaResources, func(v bool) { providerData.EnableBetaResources = v })

if !(providerConfig.KMSCustomEndpoint.IsUnknown() || providerConfig.KMSCustomEndpoint.IsNull()) {
providerData.KMSCustomEndpoint = providerConfig.KMSCustomEndpoint.String()
}

if !(providerConfig.Experiments.IsUnknown() || providerConfig.Experiments.IsNull()) {
var experimentValues []string
diags := providerConfig.Experiments.ElementsAs(ctx, &experimentValues, false)
Expand Down Expand Up @@ -467,6 +479,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
iaasalphaRoutingTables.NewRoutingTablesDataSource,
iaasalphaRoutingTableRoutes.NewRoutingTableRoutesDataSource,
iaasSecurityGroupRule.NewSecurityGroupRuleDataSource,
kms.NewKeyRingDataSource,
loadBalancer.NewLoadBalancerDataSource,
logMeInstance.NewInstanceDataSource,
logMeCredential.NewCredentialDataSource,
Expand Down Expand Up @@ -530,6 +543,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
iaasSecurityGroupRule.NewSecurityGroupRuleResource,
iaasalphaRoutingTable.NewRoutingTableResource,
iaasalphaRoutingTableRoute.NewRoutingTableRouteResource,
kms.NewKeyRingResource,
loadBalancer.NewLoadBalancerResource,
loadBalancerObservabilityCredential.NewObservabilityCredentialResource,
logMeInstance.NewInstanceResource,
Expand Down
Loading