fix: sort user roles to prevent state change

This commit is contained in:
Marcel S. Henselin 2026-02-16 09:55:11 +01:00
parent 38d650e1c7
commit 01d4ca8c5e
10 changed files with 190 additions and 46 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,
@ -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

@ -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.