feat: enhance user resource with identity data extraction and logging

This commit is contained in:
Andre_Harms 2026-02-05 23:29:46 +01:00
parent 5ec7e54d41
commit d5d0caf5c7

View file

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"math" "math"
"net/http" "net/http"
"strconv"
"strings" "strings"
"github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/diag"
@ -26,13 +27,17 @@ import (
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils"
) )
// Ensure the implementation satisfies the expected interfaces.
var ( var (
// Ensure the implementation satisfies the expected interfaces.
_ resource.Resource = &userResource{} _ resource.Resource = &userResource{}
_ resource.ResourceWithConfigure = &userResource{} _ resource.ResourceWithConfigure = &userResource{}
_ resource.ResourceWithImportState = &userResource{} _ resource.ResourceWithImportState = &userResource{}
_ resource.ResourceWithModifyPlan = &userResource{} _ resource.ResourceWithModifyPlan = &userResource{}
_ resource.ResourceWithIdentity = &userResource{} _ resource.ResourceWithIdentity = &userResource{}
// Error message constants
extractErrorSummary = "extracting failed"
extractErrorMessage = "Extracting identity data: %v"
) )
// ResourceModel represents the Terraform resource state for a PostgreSQL Flex user. // ResourceModel represents the Terraform resource state for a PostgreSQL Flex user.
@ -41,6 +46,14 @@ type ResourceModel struct {
TerraformID types.String `tfsdk:"id"` TerraformID types.String `tfsdk:"id"`
} }
// UserResourceIdentityModel describes the resource's identity attributes.
type UserResourceIdentityModel struct {
ProjectID types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
InstanceID types.String `tfsdk:"instance_id"`
UserID types.Int64 `tfsdk:"database_id"`
}
// NewUserResource is a helper function to simplify the provider implementation. // NewUserResource is a helper function to simplify the provider implementation.
func NewUserResource() resource.Resource { func NewUserResource() resource.Resource {
return &userResource{} return &userResource{}
@ -146,9 +159,26 @@ func (r *userResource) Create(
return return
} }
// Read identity data
var identityData UserResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx) ctx = core.InitProviderContext(ctx)
ctx = r.setTFLogFields(ctx, &model)
arg := r.getClientArg(&model) arg, errExt := r.extractIdentityData(model, identityData)
if errExt != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
extractErrorSummary,
fmt.Sprintf(extractErrorMessage, errExt),
)
}
ctx = r.setTFLogFields(ctx, arg)
var roles = r.expandRoles(ctx, model.Roles, &resp.Diagnostics) var roles = r.expandRoles(ctx, model.Roles, &resp.Diagnostics)
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {
@ -192,7 +222,7 @@ func (r *userResource) Create(
ctx = core.LogResponse(ctx) ctx = core.LogResponse(ctx)
// Verify creation // Verify creation
exists, err := r.getUserResource(ctx, &model) exists, err := r.getUserResource(ctx, &model, arg)
if err != nil { if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err))
@ -228,10 +258,31 @@ func (r *userResource) Read(
return return
} }
// Read identity data
var identityData UserResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
arg, errExt := r.extractIdentityData(model, identityData)
if errExt != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
extractErrorSummary,
fmt.Sprintf(extractErrorMessage, errExt),
)
}
ctx = r.setTFLogFields(ctx, arg)
ctx = core.InitProviderContext(ctx) ctx = core.InitProviderContext(ctx)
// Read resource state // Read resource state
exists, err := r.getUserResource(ctx, &model) exists, err := r.getUserResource(ctx, &model, arg)
if err != nil { if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err))
@ -267,9 +318,27 @@ func (r *userResource) Update(
return return
} }
// Read identity data
var identityData UserResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
arg, errExt := r.extractIdentityData(model, identityData)
if errExt != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
extractErrorSummary,
fmt.Sprintf(extractErrorMessage, errExt),
)
}
ctx = r.setTFLogFields(ctx, arg)
ctx = core.InitProviderContext(ctx) ctx = core.InitProviderContext(ctx)
ctx = r.setTFLogFields(ctx, &model)
arg := r.getClientArg(&model)
// Retrieve values from state // Retrieve values from state
var stateModel ResourceModel var stateModel ResourceModel
@ -314,7 +383,7 @@ func (r *userResource) Update(
ctx = core.LogResponse(ctx) ctx = core.LogResponse(ctx)
// Verify update // Verify update
exists, err := r.getUserResource(ctx, &stateModel) exists, err := r.getUserResource(ctx, &stateModel, arg)
if err != nil { if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Calling API: %v", err)) core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Calling API: %v", err))
@ -350,10 +419,27 @@ func (r *userResource) Delete(
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {
return return
} }
// Read identity data
var identityData UserResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx) ctx = core.InitProviderContext(ctx)
ctx = r.setTFLogFields(ctx, &model)
arg := r.getClientArg(&model) arg, errExt := r.extractIdentityData(model, identityData)
if errExt != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
extractErrorSummary,
fmt.Sprintf(extractErrorMessage, errExt),
)
}
ctx = r.setTFLogFields(ctx, arg)
ctx = core.InitProviderContext(ctx)
userId64 := arg.userId userId64 := arg.userId
if userId64 > math.MaxInt32 { if userId64 > math.MaxInt32 {
@ -371,7 +457,7 @@ func (r *userResource) Delete(
ctx = core.LogResponse(ctx) ctx = core.LogResponse(ctx)
// Verify deletion // Verify deletion
exists, err := r.getUserResource(ctx, &model) exists, err := r.getUserResource(ctx, &model, arg)
if err != nil { if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err))
return return
@ -433,16 +519,42 @@ func (r *userResource) ImportState(
return return
} }
userId, err := strconv.ParseInt(idParts[3], 10, 64)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error importing user",
fmt.Sprintf("Invalid userId format: %q. It must be a valid integer.", idParts[3]),
)
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) 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("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("instance_id"), idParts[2])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), idParts[3])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), userId)...)
core.LogAndAddWarning( core.LogAndAddWarning(
ctx, ctx,
&resp.Diagnostics, &resp.Diagnostics,
"postgresflexalpha user imported with empty password and empty uri", "postgresflexalpha user imported with empty password and empty uri",
"The user password and uri are not imported as they are only available upon creation of a new user. The password and uri fields will be empty.", "The user password and uri are not imported as they are only available upon creation of a new user. The password and uri fields will be empty.",
) )
var identityData UserResourceIdentityModel
identityData.ProjectID = types.StringValue(idParts[0])
identityData.Region = types.StringValue(idParts[1])
identityData.InstanceID = types.StringValue(idParts[2])
identityData.UserID = types.Int64Value(userId)
resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Postgres Flex instance state imported")
tflog.Info(ctx, "postgresflexalpha user state imported") tflog.Info(ctx, "postgresflexalpha user state imported")
} }
@ -489,15 +601,12 @@ func mapFields(userResp *postgresflex.GetUserResponse, model *Model, region stri
// getUserResource refreshes the resource state by calling the API and mapping the response to the model. // getUserResource refreshes the resource state by calling the API and mapping the response to the model.
// Returns true if the resource state was successfully refreshed, false if the resource does not exist. // Returns true if the resource state was successfully refreshed, false if the resource does not exist.
func (r *userResource) getUserResource(ctx context.Context, model *ResourceModel) (bool, error) { func (r *userResource) getUserResource(ctx context.Context, model *ResourceModel, arg *clientArg) (bool, error) {
ctx = r.setTFLogFields(ctx, model)
arg := r.getClientArg(model)
userId64 := arg.userId if arg.userId > math.MaxInt32 {
if userId64 > math.MaxInt32 {
return false, errors.New("error in type conversion: int value too large (userId)") return false, errors.New("error in type conversion: int value too large (userId)")
} }
userId := int32(userId64) userId := int32(arg.userId)
// API Call // API Call
userResp, err := r.client.GetUserRequest(ctx, arg.projectId, arg.region, arg.instanceId, userId).Execute() userResp, err := r.client.GetUserRequest(ctx, arg.projectId, arg.region, arg.instanceId, userId).Execute()
@ -526,24 +635,64 @@ type clientArg struct {
userId int64 userId int64
} }
// getClientArg constructs client arguments from the model. // extractIdentityData extracts essential identifiers from the resource model, falling back to the identity model.
func (r *userResource) getClientArg(model *ResourceModel) *clientArg { func (r *userResource) extractIdentityData(
return &clientArg{ model ResourceModel,
projectId: model.ProjectId.ValueString(), identity UserResourceIdentityModel,
instanceId: model.InstanceId.ValueString(), ) (*clientArg, error) {
region: r.providerData.GetRegionWithOverride(model.Region),
userId: model.UserId.ValueInt64(), var projectId, region, instanceId string
var userId int64
if !model.UserId.IsNull() && !model.UserId.IsUnknown() {
userId = model.UserId.ValueInt64()
} else {
if identity.UserID.IsNull() || identity.UserID.IsUnknown() {
return nil, fmt.Errorf("user_id not found in config")
}
userId = identity.UserID.ValueInt64()
} }
if !model.ProjectId.IsNull() && !model.ProjectId.IsUnknown() {
projectId = model.ProjectId.ValueString()
} else {
if identity.ProjectID.IsNull() || identity.ProjectID.IsUnknown() {
return nil, fmt.Errorf("project_id not found in config")
}
projectId = identity.ProjectID.ValueString()
}
if !model.Region.IsNull() && !model.Region.IsUnknown() {
region = r.providerData.GetRegionWithOverride(model.Region)
} else {
if identity.Region.IsNull() || identity.Region.IsUnknown() {
return nil, fmt.Errorf("region not found in config")
}
region = r.providerData.GetRegionWithOverride(identity.Region)
}
if !model.InstanceId.IsNull() && !model.InstanceId.IsUnknown() {
instanceId = model.InstanceId.ValueString()
} else {
if identity.InstanceID.IsNull() || identity.InstanceID.IsUnknown() {
return nil, fmt.Errorf("instance_id not found in config")
}
instanceId = identity.InstanceID.ValueString()
}
return &clientArg{
projectId: projectId,
instanceId: instanceId,
region: region,
userId: userId,
}, nil
} }
// setTFLogFields adds relevant fields to the context for terraform logging purposes. // setTFLogFields adds relevant fields to the context for terraform logging purposes.
func (r *userResource) setTFLogFields(ctx context.Context, model *ResourceModel) context.Context { func (r *userResource) setTFLogFields(ctx context.Context, arg *clientArg) context.Context {
usrCtx := r.getClientArg(model) ctx = tflog.SetField(ctx, "project_id", arg.projectId)
ctx = tflog.SetField(ctx, "instance_id", arg.instanceId)
ctx = tflog.SetField(ctx, "project_id", usrCtx.projectId) ctx = tflog.SetField(ctx, "region", arg.region)
ctx = tflog.SetField(ctx, "instance_id", usrCtx.instanceId) ctx = tflog.SetField(ctx, "user_id", arg.userId)
ctx = tflog.SetField(ctx, "region", usrCtx.region)
ctx = tflog.SetField(ctx, "user_id", usrCtx.userId)
return ctx return ctx
} }