fix: sqlserver_beta (#33)

## Description

<!-- **Please link some issue here describing what you are trying to achieve.**

In case there is no issue present for your PR, please consider creating one.
At least please give us some description what you are trying to achieve and why your change is needed. -->

relates to #1234

## Checklist

- [ ] Issue was linked above
- [ ] Code format was applied: `make fmt`
- [ ] Examples were added / adjusted (see `examples/` directory)
- [x] Docs are up-to-date: `make generate-docs` (will be checked by CI)
- [ ] Unit tests got implemented or updated
- [ ] Acceptance tests got implemented or updated (see e.g. [here](f5f99d1709/stackit/internal/services/dns/dns_acc_test.go))
- [x] Unit tests are passing: `make test` (will be checked by CI)
- [x] No linter issues: `make lint` (will be checked by CI)

Reviewed-on: #33
Reviewed-by: Andre_Harms <andre.harms@stackit.cloud>
Co-authored-by: Marcel S. Henselin <marcel.henselin@stackit.cloud>
Co-committed-by: Marcel S. Henselin <marcel.henselin@stackit.cloud>
This commit is contained in:
Marcel_Henselin 2026-02-05 15:11:41 +00:00 committed by Marcel_Henselin
parent 581e45eb9c
commit c22e758b2c
Signed by: tf-provider.git.onstackit.cloud
GPG key ID: 6D7E8A1ED8955A9C
14 changed files with 456 additions and 102 deletions

View file

@ -11,6 +11,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha"
sqlserverflexalphaUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/utils"
sqlserverflexalphaWait "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/wait/sqlserverflexalpha"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-log/tflog"
@ -405,9 +406,14 @@ func (r *userResource) Delete(
ctx = tflog.SetField(ctx, "region", region)
// Delete existing record set
err := r.client.DeleteUserRequest(ctx, projectId, region, instanceId, userId).Execute()
// err := r.client.DeleteUserRequest(ctx, projectId, region, instanceId, userId).Execute()
// Delete existing record set
_, err := sqlserverflexalphaWait.DeleteUserWaitHandler(ctx, r.client, projectId, region, instanceId, userId).
WaitWithContext(ctx)
//err := r.client.DeleteUserRequest(ctx, arg.projectId, arg.region, arg.instanceId, userId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err))
core.LogAndAddError(ctx, &resp.Diagnostics, "User Delete Error", fmt.Sprintf("Calling API: %v", err))
return
}

View file

@ -6,6 +6,7 @@ import (
"net/http"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/config"
@ -30,12 +31,22 @@ type databaseDataSource struct {
providerData core.ProviderData
}
type dsModel struct {
sqlserverflexbetaGen.DatabaseModel
TfId types.String `tfsdk:"id"`
}
func (d *databaseDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_database"
}
func (d *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = sqlserverflexbetaGen.DatabaseDataSourceSchema(ctx)
resp.Schema.Attributes["id"] = schema.StringAttribute{
Computed: true,
Description: "The terraform internal identifier.",
MarkdownDescription: "The terraform internal identifier.",
}
}
// Configure adds the provider configured client to the data source.
@ -81,7 +92,7 @@ func (d *databaseDataSource) Configure(
}
func (d *databaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data sqlserverflexbetaGen.DatabaseModel
var data dsModel
readErr := "Read DB error"
// Read Terraform configuration data into the model
@ -131,19 +142,21 @@ func (d *databaseDataSource) Read(ctx context.Context, req datasource.ReadReques
)
return
}
data.Id = types.Int64Value(dbId)
owner, ok := databaseResp.GetOwnerOk()
if !ok {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
readErr,
"Database creation waiting: returned owner is nil",
)
return
}
data.Owner = types.StringValue(owner)
data.Id = types.Int64Value(dbId)
data.TfId = utils.BuildInternalTerraformId(projectId, region, instanceId, databaseName)
data.Owner = types.StringValue(databaseResp.GetOwner())
// TODO: fill remaining fields
// data.CollationName = types.Sometype(apiResponse.GetCollationName())
// data.CompatibilityLevel = types.Sometype(apiResponse.GetCompatibilityLevel())
// data.DatabaseName = types.Sometype(apiResponse.GetDatabaseName())
// data.Id = types.Sometype(apiResponse.GetId())
// data.InstanceId = types.Sometype(apiResponse.GetInstanceId())
// data.Name = types.Sometype(apiResponse.GetName())
// data.Owner = types.Sometype(apiResponse.GetOwner())
// data.ProjectId = types.Sometype(apiResponse.GetProjectId())
// data.Region = types.Sometype(apiResponse.GetRegion())
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)

View file

@ -29,7 +29,7 @@ func DatabaseDataSourceSchema(ctx context.Context) schema.Schema {
Description: "The name of the database.",
MarkdownDescription: "The name of the database.",
},
"id": schema.Int64Attribute{
"tf_original_api_id": schema.Int64Attribute{
Computed: true,
Description: "The id of the database.",
MarkdownDescription: "The id of the database.",
@ -55,8 +55,7 @@ func DatabaseDataSourceSchema(ctx context.Context) schema.Schema {
MarkdownDescription: "The STACKIT project ID.",
},
"region": schema.StringAttribute{
Optional: true,
Computed: true,
Required: true,
Description: "The region which should be addressed",
MarkdownDescription: "The region which should be addressed",
Validators: []validator.String{
@ -73,7 +72,7 @@ type DatabaseModel struct {
CollationName types.String `tfsdk:"collation_name"`
CompatibilityLevel types.Int64 `tfsdk:"compatibility_level"`
DatabaseName types.String `tfsdk:"database_name"`
Id types.Int64 `tfsdk:"id"`
Id types.Int64 `tfsdk:"tf_original_api_id"`
InstanceId types.String `tfsdk:"instance_id"`
Name types.String `tfsdk:"name"`
Owner types.String `tfsdk:"owner"`

View file

@ -244,6 +244,7 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques
func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data sqlserverflexbetaResGen.DatabaseModel
readErr := "[Database Read]"
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
@ -265,22 +266,34 @@ func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, r
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
// Todo: Read API call logic
instanceId := identityData.InstanceID.ValueString()
ctx = tflog.SetField(ctx, "instance_id", instanceId)
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
dbName := identityData.DatabaseName.ValueString()
ctx = tflog.SetField(ctx, "database_name", dbName)
// TODO: Set data returned by API in identity
identity := DatabaseResourceIdentityModel{
ProjectID: types.StringValue(projectId),
Region: types.StringValue(region),
// InstanceID: types.StringValue(instanceId),
}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
if resp.Diagnostics.HasError() {
getResp, err := r.client.GetDatabaseRequest(ctx, projectId, region, instanceId, dbName).Execute()
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
readErr,
fmt.Sprintf("Calling API: %v", err),
)
return
}
ctx = core.LogResponse(ctx)
data.Id = types.Int64Value(getResp.GetId())
data.Owner = types.StringValue(getResp.GetOwner())
data.Name = types.StringValue(getResp.GetName())
data.DatabaseName = types.StringValue(getResp.GetName())
data.CollationName = types.StringValue(getResp.GetCollationName())
data.CompatibilityLevel = types.Int64Value(getResp.GetCompatibilityLevel())
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
tflog.Info(ctx, "sqlserverflexbeta.Database read")
}
@ -350,11 +363,13 @@ func (r *databaseResource) ModifyPlan(
req resource.ModifyPlanRequest,
resp *resource.ModifyPlanResponse,
) { // nolint:gocritic // function signature required by Terraform
var configModel sqlserverflexbetaResGen.DatabaseModel
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
var configModel sqlserverflexbetaResGen.DatabaseModel
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
@ -374,10 +389,14 @@ func (r *databaseResource) ModifyPlan(
var identityModel DatabaseResourceIdentityModel
identityModel.ProjectID = planModel.ProjectId
identityModel.Region = planModel.Region
// TODO: complete
//if !planModel.InstanceId.IsNull() && !planModel.InstanceId.IsUnknown() {
// identityModel.InstanceID = planModel.InstanceId
//}
if !planModel.InstanceId.IsNull() && !planModel.InstanceId.IsUnknown() {
identityModel.InstanceID = planModel.InstanceId
}
if !planModel.Name.IsNull() && !planModel.Name.IsUnknown() {
identityModel.DatabaseName = planModel.Name
}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identityModel)...)
if resp.Diagnostics.HasError() {
@ -397,30 +416,54 @@ func (r *databaseResource) ImportState(
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
idParts := strings.Split(req.ID, core.Separator)
ctx = core.InitProviderContext(ctx)
// Todo: Import logic
if len(idParts) < 2 || idParts[0] == "" || idParts[1] == "" {
core.LogAndAddError(
ctx, &resp.Diagnostics,
"Error importing database",
fmt.Sprintf(
"Expected import identifier with format [project_id],[region],..., got %q",
req.ID,
),
)
if req.ID != "" {
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 database",
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[instance_id],[database_name] Got: %q", req.ID),
)
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_name"), idParts[3])...)
var identityData DatabaseResourceIdentityModel
identityData.ProjectID = types.StringValue(idParts[0])
identityData.Region = types.StringValue(idParts[1])
identityData.InstanceID = types.StringValue(idParts[2])
identityData.DatabaseName = types.StringValue(idParts[3])
resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Sqlserverflexbeta database state imported")
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...)
// ... more ...
var identityData DatabaseResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), identityData.ProjectID.ValueString())...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), identityData.Region.ValueString())...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), identityData.InstanceID.ValueString())...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_name"), identityData.DatabaseName.ValueString())...)
resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
core.LogAndAddWarning(
ctx,
&resp.Diagnostics,
"Sqlserverflexbeta database imported with empty password",
"The database password is not imported as it is only available upon creation of a new database. The password field will be empty.",
)
tflog.Info(ctx, "Sqlserverflexbeta database state imported")
}

View file

@ -92,17 +92,47 @@ func (r *flavorDataSource) Configure(ctx context.Context, req datasource.Configu
return
}
r.client = apiClient
tflog.Info(ctx, "Postgres Flex instance client configured")
tflog.Info(ctx, "SQL Server Flex instance client configured")
}
func (r *flavorDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"project_id": schema.StringAttribute{
Required: true,
Description: "The project ID of the flavor.",
MarkdownDescription: "The project ID of the flavor.",
},
"region": schema.StringAttribute{
Required: true,
Description: "The region of the flavor.",
MarkdownDescription: "The region of the flavor.",
},
"cpu": schema.Int64Attribute{
Computed: true,
Required: true,
Description: "The cpu count of the instance.",
MarkdownDescription: "The cpu count of the instance.",
},
"ram": schema.Int64Attribute{
Required: true,
Description: "The memory of the instance in Gibibyte.",
MarkdownDescription: "The memory of the instance in Gibibyte.",
},
"storage_class": schema.StringAttribute{
Required: true,
Description: "The memory of the instance in Gibibyte.",
MarkdownDescription: "The memory of the instance in Gibibyte.",
},
"node_type": schema.StringAttribute{
Required: true,
Description: "defines the nodeType it can be either single or HA",
MarkdownDescription: "defines the nodeType it can be either single or HA",
},
"flavor_id": schema.StringAttribute{
Computed: true,
Description: "The id of the instance flavor.",
MarkdownDescription: "The id of the instance flavor.",
},
"description": schema.StringAttribute{
Computed: true,
Description: "The flavor description.",
@ -118,21 +148,11 @@ func (r *flavorDataSource) Schema(ctx context.Context, _ datasource.SchemaReques
Description: "maximum storage which can be ordered for the flavor in Gigabyte.",
MarkdownDescription: "maximum storage which can be ordered for the flavor in Gigabyte.",
},
"memory": schema.Int64Attribute{
Computed: true,
Description: "The memory of the instance in Gibibyte.",
MarkdownDescription: "The memory of the instance in Gibibyte.",
},
"min_gb": schema.Int64Attribute{
Computed: true,
Description: "minimum storage which is required to order in Gigabyte.",
MarkdownDescription: "minimum storage which is required to order in Gigabyte.",
},
"node_type": schema.StringAttribute{
Computed: true,
Description: "defines the nodeType it can be either single or HA",
MarkdownDescription: "defines the nodeType it can be either single or HA",
},
"storage_classes": schema.ListNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
@ -331,5 +351,5 @@ func (r *flavorDataSource) Read(ctx context.Context, req datasource.ReadRequest,
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Postgres Flex flavors read")
tflog.Info(ctx, "SQL Server Flex flavors read")
}

View file

@ -32,6 +32,11 @@ type APIClientInstanceInterface interface {
GetInstanceRequestExecute(ctx context.Context, projectId, region, instanceId string) (*sqlserverflex.GetInstanceResponse, error)
}
// APIClientUserInterface Interface needed for tests
type APIClientUserInterface interface {
DeleteUserRequestExecute(ctx context.Context, projectId string, region string, instanceId string, userId int64) error
}
// CreateInstanceWaitHandler will wait for instance creation
func CreateInstanceWaitHandler(ctx context.Context, a APIClientInstanceInterface, projectId, instanceId, region string) *wait.AsyncActionHandler[sqlserverflex.GetInstanceResponse] {
handler := wait.New(func() (waitFinished bool, response *sqlserverflex.GetInstanceResponse, err error) {
@ -131,3 +136,36 @@ func DeleteInstanceWaitHandler(ctx context.Context, a APIClientInstanceInterface
handler.SetTimeout(15 * time.Minute)
return handler
}
// DeleteUserWaitHandler will wait for instance deletion
func DeleteUserWaitHandler(
ctx context.Context,
a APIClientUserInterface,
projectId, instanceId, region string,
userId int64,
) *wait.AsyncActionHandler[struct{}] {
handler := wait.New(func() (waitFinished bool, response *struct{}, err error) {
err = a.DeleteUserRequestExecute(ctx, projectId, region, instanceId, userId)
if err == nil {
return false, nil, nil
}
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if !ok {
return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError")
}
switch oapiErr.StatusCode {
case http.StatusNotFound:
return true, nil, nil
case http.StatusInternalServerError:
tflog.Warn(ctx, "Wait handler got error 500")
return false, nil, nil
default:
return false, nil, err
}
})
handler.SetTimeout(15 * time.Minute)
handler.SetSleepBeforeWait(15 * time.Second)
return handler
}