fix: #63 sort user roles to prevent state change (#65)

fix: include recent api changes
Reviewed-on: #65
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 S. Henselin 2026-02-16 09:04:16 +00:00 committed by Marcel_Henselin
parent 452f73877f
commit 43223f5d1f
Signed by: tf-provider.git.onstackit.cloud
GPG key ID: 6D7E8A1ED8955A9C
13 changed files with 202 additions and 63 deletions

View file

@ -5,6 +5,7 @@ import (
_ "embed"
"fmt"
"math"
"slices"
"strconv"
"strings"
"time"
@ -29,11 +30,12 @@ import (
var (
// Ensure the implementation satisfies the expected interfaces.
_ resource.Resource = &userResource{}
_ resource.ResourceWithConfigure = &userResource{}
_ resource.ResourceWithImportState = &userResource{}
_ resource.ResourceWithModifyPlan = &userResource{}
_ resource.ResourceWithIdentity = &userResource{}
_ resource.Resource = &userResource{}
_ resource.ResourceWithConfigure = &userResource{}
_ resource.ResourceWithImportState = &userResource{}
_ resource.ResourceWithModifyPlan = &userResource{}
_ resource.ResourceWithIdentity = &userResource{}
_ resource.ResourceWithValidateConfig = &userResource{}
// Error message constants
extractErrorSummary = "extracting failed"
@ -138,6 +140,39 @@ func (r *userResource) Schema(ctx context.Context, _ resource.SchemaRequest, res
resp.Schema = s
}
func (r *userResource) ValidateConfig(
ctx context.Context,
req resource.ValidateConfigRequest,
resp *resource.ValidateConfigResponse,
) {
var data resourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
var roles []string
diags := data.Roles.ElementsAs(ctx, &roles, false)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
var resRoles []string
for _, role := range roles {
if slices.Contains(resRoles, role) {
resp.Diagnostics.AddAttributeError(
path.Root("roles"),
"Attribute Configuration Error",
"defined roles MUST NOT contain duplicates",
)
return
}
resRoles = append(resRoles, role)
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *userResource) Create(
ctx context.Context,
@ -217,7 +252,7 @@ func (r *userResource) Create(
model.UserId = types.Int64Value(id)
model.Password = types.StringValue(userResp.GetPassword())
model.Status = types.StringValue(userResp.GetStatus())
model.ConnectionString = types.StringValue(userResp.GetConnectionString())
//model.ConnectionString = types.StringValue(userResp.GetConnectionString())
waitResp, err := postgresflexalphaWait.GetUserByIdWaitHandler(
ctx,
@ -712,5 +747,6 @@ func (r *userResource) expandRoles(ctx context.Context, rolesSet types.List, dia
}
var roles []string
diags.Append(rolesSet.ElementsAs(ctx, &roles, false)...)
slices.Sort(roles)
return roles
}

View file

@ -14,11 +14,6 @@ import (
func UserResourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"connection_string": schema.StringAttribute{
Computed: true,
Description: "The connection string for the user to the instance.",
MarkdownDescription: "The connection string for the user to the instance.",
},
"id": schema.Int64Attribute{
Computed: true,
Description: "The ID of the user.",
@ -80,14 +75,13 @@ func UserResourceSchema(ctx context.Context) schema.Schema {
}
type UserModel struct {
ConnectionString types.String `tfsdk:"connection_string"`
Id types.Int64 `tfsdk:"id"`
InstanceId types.String `tfsdk:"instance_id"`
Name types.String `tfsdk:"name"`
Password types.String `tfsdk:"password"`
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
Roles types.List `tfsdk:"roles"`
Status types.String `tfsdk:"status"`
UserId types.Int64 `tfsdk:"user_id"`
Id types.Int64 `tfsdk:"id"`
InstanceId types.String `tfsdk:"instance_id"`
Name types.String `tfsdk:"name"`
Password types.String `tfsdk:"password"`
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
Roles types.List `tfsdk:"roles"`
Status types.String `tfsdk:"status"`
UserId types.Int64 `tfsdk:"user_id"`
}

View file

@ -2,6 +2,7 @@ package sqlserverflexalpha
import (
"fmt"
"slices"
"strconv"
"github.com/hashicorp/terraform-plugin-framework/attr"
@ -44,8 +45,11 @@ func mapDataSourceFields(userResp *sqlserverflexalpha.GetUserResponse, model *da
if user.Roles == nil {
model.Roles = types.List(types.SetNull(types.StringType))
} else {
resRoles := *user.Roles
slices.Sort(resRoles)
var roles []attr.Value
for _, role := range *user.Roles {
for _, role := range resRoles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)
@ -92,8 +96,11 @@ func mapFields(userResp *sqlserverflexalpha.GetUserResponse, model *resourceMode
// Map roles
if user.Roles != nil {
resRoles := *user.Roles
slices.Sort(resRoles)
var roles []attr.Value
for _, role := range *user.Roles {
for _, role := range resRoles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)
@ -139,8 +146,11 @@ func mapFieldsCreate(userResp *sqlserverflexalpha.CreateUserResponse, model *res
model.Password = types.StringValue(*user.Password)
if user.Roles != nil {
resRoles := *user.Roles
slices.Sort(resRoles)
var roles []attr.Value
for _, role := range *user.Roles {
for _, role := range resRoles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)

View file

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"time"
@ -30,11 +31,12 @@ import (
)
var (
_ resource.Resource = &userResource{}
_ resource.ResourceWithConfigure = &userResource{}
_ resource.ResourceWithImportState = &userResource{}
_ resource.ResourceWithModifyPlan = &userResource{}
_ resource.ResourceWithIdentity = &userResource{}
_ resource.Resource = &userResource{}
_ resource.ResourceWithConfigure = &userResource{}
_ resource.ResourceWithImportState = &userResource{}
_ resource.ResourceWithModifyPlan = &userResource{}
_ resource.ResourceWithIdentity = &userResource{}
_ resource.ResourceWithValidateConfig = &userResource{}
)
func NewUserResource() resource.Resource {
@ -156,6 +158,39 @@ func (r *userResource) IdentitySchema(
}
}
func (r *userResource) ValidateConfig(
ctx context.Context,
req resource.ValidateConfigRequest,
resp *resource.ValidateConfigResponse,
) {
var data resourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
var roles []string
diags := data.Roles.ElementsAs(ctx, &roles, false)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
var resRoles []string
for _, role := range roles {
if slices.Contains(resRoles, role) {
resp.Diagnostics.AddAttributeError(
path.Root("roles"),
"Attribute Configuration Error",
"defined roles MUST NOT contain duplicates",
)
return
}
resRoles = append(resRoles, role)
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *userResource) Create(
ctx context.Context,
@ -186,6 +221,8 @@ func (r *userResource) Create(
if resp.Diagnostics.HasError() {
return
}
slices.Sort(roles)
}
// Generate API request body from model

View file

@ -2,6 +2,7 @@ package sqlserverflexbeta
import (
"fmt"
"slices"
"strconv"
"github.com/hashicorp/terraform-plugin-framework/attr"
@ -92,10 +93,14 @@ func mapFields(userResp *sqlserverflexbeta.GetUserResponse, model *resourceModel
// Map roles
if user.Roles != nil {
resRoles := *user.Roles
slices.Sort(resRoles)
var roles []attr.Value
for _, role := range *user.Roles {
for _, role := range resRoles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)
if diags.HasError() {
return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags))
@ -139,8 +144,11 @@ func mapFieldsCreate(userResp *sqlserverflexbeta.CreateUserResponse, model *reso
model.Password = types.StringValue(*user.Password)
if user.Roles != nil {
resRoles := *user.Roles
slices.Sort(resRoles)
var roles []attr.Value
for _, role := range *user.Roles {
for _, role := range resRoles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)

View file

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"time"
@ -30,11 +31,12 @@ import (
)
var (
_ resource.Resource = &userResource{}
_ resource.ResourceWithConfigure = &userResource{}
_ resource.ResourceWithImportState = &userResource{}
_ resource.ResourceWithModifyPlan = &userResource{}
_ resource.ResourceWithIdentity = &userResource{}
_ resource.Resource = &userResource{}
_ resource.ResourceWithConfigure = &userResource{}
_ resource.ResourceWithImportState = &userResource{}
_ resource.ResourceWithModifyPlan = &userResource{}
_ resource.ResourceWithIdentity = &userResource{}
_ resource.ResourceWithValidateConfig = &userResource{}
)
func NewUserResource() resource.Resource {
@ -156,6 +158,39 @@ func (r *userResource) IdentitySchema(
}
}
func (r *userResource) ValidateConfig(
ctx context.Context,
req resource.ValidateConfigRequest,
resp *resource.ValidateConfigResponse,
) {
var data resourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
var roles []string
diags := data.Roles.ElementsAs(ctx, &roles, false)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
var resRoles []string
for _, role := range roles {
if slices.Contains(resRoles, role) {
resp.Diagnostics.AddAttributeError(
path.Root("roles"),
"Attribute Configuration Error",
"defined roles MUST NOT contain duplicates",
)
return
}
resRoles = append(resRoles, role)
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *userResource) Create(
ctx context.Context,
@ -186,6 +221,7 @@ func (r *userResource) Create(
if resp.Diagnostics.HasError() {
return
}
slices.Sort(roles)
}
// Generate API request body from model
@ -379,7 +415,7 @@ func (r *userResource) Update(
resp *resource.UpdateResponse,
) { // nolint:gocritic // function signature required by Terraform
// Update shouldn't be called
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", "User can't be updated")
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", "an SQL server user can not be updated, only created")
}
// Delete deletes the resource and removes the Terraform state on success.