From d5d0caf5c7ce78ce742a680ce0db01df59771cca Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Thu, 5 Feb 2026 23:29:46 +0100 Subject: [PATCH] feat: enhance user resource with identity data extraction and logging --- .../postgresflexalpha/user/resource.go | 213 +++++++++++++++--- 1 file changed, 181 insertions(+), 32 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index c0934786..e55c5dee 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -7,6 +7,7 @@ import ( "fmt" "math" "net/http" + "strconv" "strings" "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" ) -// Ensure the implementation satisfies the expected interfaces. var ( + // Ensure the implementation satisfies the expected interfaces. _ resource.Resource = &userResource{} _ resource.ResourceWithConfigure = &userResource{} _ resource.ResourceWithImportState = &userResource{} _ resource.ResourceWithModifyPlan = &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. @@ -41,6 +46,14 @@ type ResourceModel struct { 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. func NewUserResource() resource.Resource { return &userResource{} @@ -146,9 +159,26 @@ func (r *userResource) Create( 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 = 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) if resp.Diagnostics.HasError() { @@ -192,7 +222,7 @@ func (r *userResource) Create( ctx = core.LogResponse(ctx) // Verify creation - exists, err := r.getUserResource(ctx, &model) + exists, err := r.getUserResource(ctx, &model, arg) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) @@ -228,10 +258,31 @@ func (r *userResource) Read( 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) // Read resource state - exists, err := r.getUserResource(ctx, &model) + exists, err := r.getUserResource(ctx, &model, arg) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) @@ -267,9 +318,27 @@ func (r *userResource) Update( 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 = r.setTFLogFields(ctx, &model) - arg := r.getClientArg(&model) // Retrieve values from state var stateModel ResourceModel @@ -314,7 +383,7 @@ func (r *userResource) Update( ctx = core.LogResponse(ctx) // Verify update - exists, err := r.getUserResource(ctx, &stateModel) + exists, err := r.getUserResource(ctx, &stateModel, arg) if err != nil { 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() { 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 = 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 if userId64 > math.MaxInt32 { @@ -371,7 +457,7 @@ func (r *userResource) Delete( ctx = core.LogResponse(ctx) // Verify deletion - exists, err := r.getUserResource(ctx, &model) + exists, err := r.getUserResource(ctx, &model, arg) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) return @@ -433,16 +519,42 @@ func (r *userResource) ImportState( 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("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("user_id"), idParts[3])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), userId)...) + core.LogAndAddWarning( ctx, &resp.Diagnostics, "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.", ) + + 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") } @@ -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. // 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) { - ctx = r.setTFLogFields(ctx, model) - arg := r.getClientArg(model) +func (r *userResource) getUserResource(ctx context.Context, model *ResourceModel, arg *clientArg) (bool, error) { - userId64 := arg.userId - if userId64 > math.MaxInt32 { + if arg.userId > math.MaxInt32 { return false, errors.New("error in type conversion: int value too large (userId)") } - userId := int32(userId64) + userId := int32(arg.userId) // API Call userResp, err := r.client.GetUserRequest(ctx, arg.projectId, arg.region, arg.instanceId, userId).Execute() @@ -526,24 +635,64 @@ type clientArg struct { userId int64 } -// getClientArg constructs client arguments from the model. -func (r *userResource) getClientArg(model *ResourceModel) *clientArg { - return &clientArg{ - projectId: model.ProjectId.ValueString(), - instanceId: model.InstanceId.ValueString(), - region: r.providerData.GetRegionWithOverride(model.Region), - userId: model.UserId.ValueInt64(), +// extractIdentityData extracts essential identifiers from the resource model, falling back to the identity model. +func (r *userResource) extractIdentityData( + model ResourceModel, + identity UserResourceIdentityModel, +) (*clientArg, error) { + + 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. -func (r *userResource) setTFLogFields(ctx context.Context, model *ResourceModel) context.Context { - usrCtx := r.getClientArg(model) - - ctx = tflog.SetField(ctx, "project_id", usrCtx.projectId) - ctx = tflog.SetField(ctx, "instance_id", usrCtx.instanceId) - ctx = tflog.SetField(ctx, "region", usrCtx.region) - ctx = tflog.SetField(ctx, "user_id", usrCtx.userId) +func (r *userResource) setTFLogFields(ctx context.Context, arg *clientArg) context.Context { + ctx = tflog.SetField(ctx, "project_id", arg.projectId) + ctx = tflog.SetField(ctx, "instance_id", arg.instanceId) + ctx = tflog.SetField(ctx, "region", arg.region) + ctx = tflog.SetField(ctx, "user_id", arg.userId) return ctx }