feat: enhance user resource with identity data extraction and logging
This commit is contained in:
parent
5ec7e54d41
commit
d5d0caf5c7
1 changed files with 181 additions and 32 deletions
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue