From ba579760d5c0c2546175f10902385248bc40d25a Mon Sep 17 00:00:00 2001 From: "Marcel S. Henselin" Date: Mon, 16 Mar 2026 15:45:53 +0100 Subject: [PATCH] fix: remove identity from postgres-flex [skip ci] --- .../postgresflexalpha_database.md | 3 + .../postgresflexalpha_instance.md | 4 + docs/data-sources/postgresflexalpha_user.md | 2 +- docs/resources/postgresflexalpha_database.md | 2 +- docs/resources/postgresflexalpha_user.md | 2 +- .../database_data_source_gen.go | 2 +- .../postgresflexalpha/database/mapper.go | 10 +- .../postgresflexalpha/database/resource.go | 296 ++++++------------ .../resources_gen/database_resource_gen.go | 4 +- .../postgresflexalpha/instance/datasource.go | 23 +- .../instance_data_source_gen.go | 2 +- .../postgresflexalpha/instance/functions.go | 25 +- .../postgresflexalpha/instance/resource.go | 175 ++++------- .../postgresflex_acc_test.go | 279 +++++++++++++++-- .../testdata/instance_template.gompl | 36 +++ .../datasources_gen/user_data_source_gen.go | 2 +- .../services/postgresflexalpha/user/mapper.go | 7 +- .../postgresflexalpha/user/resource.go | 228 +++----------- .../user/resources_gen/user_resource_gen.go | 4 +- .../internal/wait/postgresflexalpha/wait.go | 45 +-- 20 files changed, 570 insertions(+), 581 deletions(-) diff --git a/docs/data-sources/postgresflexalpha_database.md b/docs/data-sources/postgresflexalpha_database.md index 95c115e3..7e4c7183 100644 --- a/docs/data-sources/postgresflexalpha_database.md +++ b/docs/data-sources/postgresflexalpha_database.md @@ -28,6 +28,9 @@ data "stackitprivatepreview_postgresflexalpha_database" "example" { - `database_id` (Number) The ID of the database. - `instance_id` (String) The ID of the instance. - `project_id` (String) The STACKIT project ID. + +### Optional + - `region` (String) The region which should be addressed ### Read-Only diff --git a/docs/data-sources/postgresflexalpha_instance.md b/docs/data-sources/postgresflexalpha_instance.md index d21a5f10..cb1d183a 100644 --- a/docs/data-sources/postgresflexalpha_instance.md +++ b/docs/data-sources/postgresflexalpha_instance.md @@ -26,6 +26,9 @@ data "stackitprivatepreview_postgresflexalpha_instance" "example" { - `instance_id` (String) The ID of the instance. - `project_id` (String) The STACKIT project ID. + +### Optional + - `region` (String) The region which should be addressed ### Read-Only @@ -37,6 +40,7 @@ data "stackitprivatepreview_postgresflexalpha_instance" "example" { ⚠︝ **Note:** This feature is in private preview. Supplying this object is only permitted for enabled accounts. If your account does not have access, the request will be rejected. (see [below for nested schema](#nestedatt--encryption)) - `flavor_id` (String) The id of the instance flavor. +- `id` (String) internal ID - `is_deletable` (Boolean) Whether the instance can be deleted or not. - `name` (String) The name of the instance. - `network` (Attributes) The access configuration of the instance (see [below for nested schema](#nestedatt--network)) diff --git a/docs/data-sources/postgresflexalpha_user.md b/docs/data-sources/postgresflexalpha_user.md index c3553c7b..b5a8af2d 100644 --- a/docs/data-sources/postgresflexalpha_user.md +++ b/docs/data-sources/postgresflexalpha_user.md @@ -27,12 +27,12 @@ data "stackitprivatepreview_postgresflexalpha_user" "example" { - `instance_id` (String) The ID of the instance. - `project_id` (String) The STACKIT project ID. -- `region` (String) The region which should be addressed - `user_id` (Number) The ID of the user. ### Optional - `id` (String) Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`,`user_id`\".", +- `region` (String) The region which should be addressed ### Read-Only diff --git a/docs/resources/postgresflexalpha_database.md b/docs/resources/postgresflexalpha_database.md index 6c94fd62..29f43024 100644 --- a/docs/resources/postgresflexalpha_database.md +++ b/docs/resources/postgresflexalpha_database.md @@ -54,4 +54,4 @@ import { ### Read-Only -- `id` (Number) The id of the database. +- `id` (String) The id of the database. diff --git a/docs/resources/postgresflexalpha_user.md b/docs/resources/postgresflexalpha_user.md index b83de15d..eebab22d 100644 --- a/docs/resources/postgresflexalpha_user.md +++ b/docs/resources/postgresflexalpha_user.md @@ -54,6 +54,6 @@ import { ### Read-Only -- `id` (Number) The ID of the user. +- `id` (String) The ID of the user. - `password` (String) The password for the user. - `status` (String) The current status of the user. diff --git a/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go b/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go index d5683a6c..f4a08793 100644 --- a/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go +++ b/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go @@ -45,7 +45,7 @@ func DatabaseDataSourceSchema(ctx context.Context) schema.Schema { MarkdownDescription: "The STACKIT project ID.", }, "region": schema.StringAttribute{ - Required: true, + Optional: true, Description: "The region which should be addressed", MarkdownDescription: "The region which should be addressed", Validators: []validator.String{ diff --git a/stackit/internal/services/postgresflexalpha/database/mapper.go b/stackit/internal/services/postgresflexalpha/database/mapper.go index 6ce2200c..818462f9 100644 --- a/stackit/internal/services/postgresflexalpha/database/mapper.go +++ b/stackit/internal/services/postgresflexalpha/database/mapper.go @@ -65,15 +65,19 @@ func mapResourceFields(source *v3alpha1api.GetDatabaseResponse, model *resourceM } var databaseId int64 - if model.Id.ValueInt64() != 0 { - databaseId = model.Id.ValueInt64() + if model.DatabaseId.ValueInt64() != 0 { + if source.Id != 0 { + if model.DatabaseId.ValueInt64() != int64(source.Id) { + return fmt.Errorf("retrieved ID does not match known ID") + } + } + databaseId = model.DatabaseId.ValueInt64() } else if source.Id != 0 { databaseId = int64(source.Id) } else { return fmt.Errorf("database id not present") } - model.Id = types.Int64Value(databaseId) model.DatabaseId = types.Int64Value(databaseId) model.Name = types.StringValue(source.GetName()) model.Owner = types.StringValue(cleanString(source.Owner)) diff --git a/stackit/internal/services/postgresflexalpha/database/resource.go b/stackit/internal/services/postgresflexalpha/database/resource.go index 6db70746..ab31a2fd 100644 --- a/stackit/internal/services/postgresflexalpha/database/resource.go +++ b/stackit/internal/services/postgresflexalpha/database/resource.go @@ -11,9 +11,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3alpha1api" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" @@ -30,7 +30,6 @@ var ( _ resource.ResourceWithConfigure = &databaseResource{} _ resource.ResourceWithImportState = &databaseResource{} _ resource.ResourceWithModifyPlan = &databaseResource{} - _ resource.ResourceWithIdentity = &databaseResource{} // Error message constants extractErrorSummary = "extracting failed" @@ -45,14 +44,6 @@ func NewDatabaseResource() resource.Resource { // resourceModel describes the resource data model. type resourceModel = postgresflexalphaResGen.DatabaseModel -// DatabaseResourceIdentityModel describes the resource's identity attributes. -type DatabaseResourceIdentityModel struct { - ProjectID types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - InstanceID types.String `tfsdk:"instance_id"` - DatabaseID types.Int64 `tfsdk:"database_id"` -} - // databaseResource is the resource implementation. type databaseResource struct { client *v3alpha1api.APIClient @@ -138,30 +129,6 @@ func (r *databaseResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp.Schema = s } -// IdentitySchema defines the schema for the resource's identity attributes. -func (r *databaseResource) IdentitySchema( - _ context.Context, - _ resource.IdentitySchemaRequest, - response *resource.IdentitySchemaResponse, -) { - response.IdentitySchema = identityschema.Schema{ - Attributes: map[string]identityschema.Attribute{ - "project_id": identityschema.StringAttribute{ - RequiredForImport: true, - }, - "region": identityschema.StringAttribute{ - RequiredForImport: true, - }, - "instance_id": identityschema.StringAttribute{ - RequiredForImport: true, - }, - "database_id": identityschema.Int64Attribute{ - RequiredForImport: true, - }, - }, - } -} - // Create creates the resource and sets the initial Terraform state. func (r *databaseResource) Create( ctx context.Context, @@ -178,12 +145,12 @@ func (r *databaseResource) Create( ctx = core.InitProviderContext(ctx) - projectId := model.ProjectId.ValueString() + projectID := model.ProjectId.ValueString() region := model.Region.ValueString() - instanceId := model.InstanceId.ValueString() + instanceID := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "project_id", projectID) + ctx = tflog.SetField(ctx, "instance_id", instanceID) ctx = tflog.SetField(ctx, "region", region) // Generate API request body from model @@ -200,9 +167,9 @@ func (r *databaseResource) Create( // Create new database databaseResp, err := r.client.DefaultAPI.CreateDatabaseRequest( ctx, - projectId, + projectID, region, - instanceId, + instanceID, ).CreateDatabaseRequestPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, funcErrorSummary, fmt.Sprintf("Calling API: %v", err)) @@ -219,23 +186,33 @@ func (r *databaseResource) Create( ) return } - databaseId := int64(*dbID) - ctx = tflog.SetField(ctx, "database_id", databaseId) + databaseID := int64(*dbID) + databaseIDString := strconv.Itoa(int(*dbID)) + + ctx = tflog.SetField(ctx, "database_id", databaseID) ctx = core.LogResponse(ctx) - // Save identity into Terraform state - identity := DatabaseResourceIdentityModel{ - ProjectID: types.StringValue(projectId), - Region: types.StringValue(region), - InstanceID: types.StringValue(instanceId), - DatabaseID: types.Int64Value(databaseId), - } - resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...) - if resp.Diagnostics.HasError() { - return - } + model.DatabaseId = types.Int64Value(databaseID) + model.Id = utils.BuildInternalTerraformId(projectID, region, instanceID, databaseIDString) - database, err := postgresflexalphaWait.GetDatabaseByIdWaitHandler(ctx, r.client.DefaultAPI, projectId, instanceId, region, databaseId). + // Set data returned by API in id + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("database_id"), + databaseID, + )..., + ) + // Set data returned by API in id + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("id"), + model.Id, + )..., + ) + + database, err := postgresflexalphaWait.GetDatabaseByIdWaitHandler(ctx, r.client.DefaultAPI, projectID, instanceID, region, databaseID). SetTimeout(15 * time.Minute). SetSleepBeforeWait(15 * time.Second). WaitWithContext(ctx) @@ -284,17 +261,28 @@ func (r *databaseResource) Read( ctx = core.InitProviderContext(ctx) - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() + projectID := model.ProjectId.ValueString() + instanceID := model.InstanceId.ValueString() region := model.Region.ValueString() - databaseId := model.DatabaseId.ValueInt64() + databaseID := model.DatabaseId.ValueInt64() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) + databaseIDString := strconv.Itoa(int(databaseID)) + + ctx = tflog.SetField(ctx, "project_id", projectID) + ctx = tflog.SetField(ctx, "instance_id", instanceID) ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "database_id", databaseId) + ctx = tflog.SetField(ctx, "database_id", databaseID) - databaseResp, err := postgresflexalphaWait.GetDatabaseByIdWaitHandler(ctx, r.client.DefaultAPI, projectId, instanceId, region, databaseId). + // Set data returned by API in id + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("id"), + utils.BuildInternalTerraformId(projectID, region, instanceID, databaseIDString), + )..., + ) + + databaseResp, err := postgresflexalphaWait.GetDatabaseByIdWaitHandler(ctx, r.client.DefaultAPI, projectID, instanceID, region, databaseID). SetTimeout(15 * time.Minute). SetSleepBeforeWait(15 * time.Second). WaitWithContext(ctx) @@ -322,18 +310,6 @@ func (r *databaseResource) Read( return } - // Save identity into Terraform state - identity := DatabaseResourceIdentityModel{ - ProjectID: types.StringValue(projectId), - Region: types.StringValue(region), - InstanceID: types.StringValue(instanceId), - DatabaseID: types.Int64Value(int64(databaseResp.GetId())), - } - resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...) - if resp.Diagnostics.HasError() { - return - } - // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) @@ -436,18 +412,6 @@ func (r *databaseResource) Update( return } - // Save identity into Terraform state - identity := DatabaseResourceIdentityModel{ - ProjectID: types.StringValue(projectId), - Region: types.StringValue(region), - InstanceID: types.StringValue(instanceId), - DatabaseID: types.Int64Value(databaseId), - } - resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...) - if resp.Diagnostics.HasError() { - return - } - // Set state to fully populated data resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) if resp.Diagnostics.HasError() { @@ -469,38 +433,33 @@ func (r *databaseResource) Delete( return } - // Read identity data - var identityData DatabaseResourceIdentityModel - resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) - if resp.Diagnostics.HasError() { - return - } - ctx = core.InitProviderContext(ctx) - projectId, region, instanceId, databaseId64, errExt := r.extractIdentityData(model, identityData) - if errExt != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - extractErrorSummary, - fmt.Sprintf(extractErrorMessage, errExt), - ) - } + projectID := model.ProjectId.ValueString() + instanceID := model.InstanceId.ValueString() + region := model.Region.ValueString() + databaseID64 := model.DatabaseId.ValueInt64() - if databaseId64 > math.MaxInt32 { + if databaseID64 > math.MaxInt32 { core.LogAndAddError(ctx, &resp.Diagnostics, "Error in type conversion", "int value too large (databaseId)") return } - databaseId := int32(databaseId64) // nolint:gosec // check is performed above - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) + databaseID := int32(databaseID64) // nolint:gosec // check is performed above + ctx = tflog.SetField(ctx, "project_id", projectID) + ctx = tflog.SetField(ctx, "instance_id", instanceID) ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "database_id", databaseId) + ctx = tflog.SetField(ctx, "database_id", databaseID) // Delete existing record set - err := r.client.DefaultAPI.DeleteDatabaseRequest(ctx, projectId, region, instanceId, databaseId).Execute() + err := r.client.DefaultAPI.DeleteDatabaseRequest(ctx, projectID, region, instanceID, databaseID).Execute() if err != nil { + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) // nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + if ok { + if oapiErr.StatusCode == 404 { + resp.State.RemoveResource(ctx) + return + } + } core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting database", fmt.Sprintf("Calling API: %v", err)) } @@ -517,109 +476,44 @@ func (r *databaseResource) ImportState( resp *resource.ImportStateResponse, ) { ctx = core.InitProviderContext(ctx) + idParts := strings.Split(req.ID, core.Separator) - if req.ID != "" { - idParts := strings.Split(req.ID, core.Separator) + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { + core.LogAndAddError( + ctx, &resp.Diagnostics, + "Error importing database", + fmt.Sprintf( + "Expected import identifier with format [project_id],[region],[instance_id],[database_id], got %q", + req.ID, + ), + ) + return + } - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError( - ctx, &resp.Diagnostics, - "Error importing database", - fmt.Sprintf( - "Expected import identifier with format [project_id],[region],[instance_id],[database_id], got %q", - req.ID, - ), - ) - return - } - - databaseId, err := strconv.ParseInt(idParts[3], 10, 64) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error importing database", - fmt.Sprintf("Invalid database_id 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("database_id"), databaseId)...) - - core.LogAndAddWarning( + databaseID, err := strconv.ParseInt(idParts[3], 10, 64) + if err != nil { + core.LogAndAddError( ctx, &resp.Diagnostics, - "Postgresflex database imported with empty password", - "The database password is not imported as it is only available upon creation of a new database. The password field will be empty.", + "Error importing database", + fmt.Sprintf("Invalid database_id format: %q. It must be a valid integer.", idParts[3]), ) - - tflog.Info(ctx, "Postgres Flex database state imported") - return } - // If no ID is provided, attempt to read identity attributes from the import configuration - var identityData DatabaseResourceIdentityModel - resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) - if resp.Diagnostics.HasError() { - return - } + tfIDString := utils.BuildInternalTerraformId(idParts...).ValueString() + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), tfIDString)...) + 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("database_id"), databaseID)...) - projectId := identityData.ProjectID.ValueString() - region := identityData.Region.ValueString() - instanceId := identityData.InstanceID.ValueString() - databaseId := identityData.DatabaseID.ValueInt64() - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), instanceId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_id"), databaseId)...) + core.LogAndAddWarning( + ctx, + &resp.Diagnostics, + "Postgresflex database imported with empty password", + "The database password is not imported as it is only available upon creation of a new database. The password field will be empty.", + ) tflog.Info(ctx, "Postgres Flex database state imported") } - -// extractIdentityData extracts essential identifiers from the resource model, falling back to the identity model. -func (r *databaseResource) extractIdentityData( - model resourceModel, - identity DatabaseResourceIdentityModel, -) (projectId, region, instanceId string, databaseId int64, err error) { - if !model.DatabaseId.IsNull() && !model.DatabaseId.IsUnknown() { - databaseId = model.DatabaseId.ValueInt64() - } else { - if identity.DatabaseID.IsNull() || identity.DatabaseID.IsUnknown() { - return "", "", "", 0, fmt.Errorf("database_id not found in config") - } - databaseId = identity.DatabaseID.ValueInt64() - } - - if !model.ProjectId.IsNull() && !model.ProjectId.IsUnknown() { - projectId = model.ProjectId.ValueString() - } else { - if identity.ProjectID.IsNull() || identity.ProjectID.IsUnknown() { - return "", "", "", 0, 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 "", "", "", 0, 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 "", "", "", 0, fmt.Errorf("instance_id not found in config") - } - instanceId = identity.InstanceID.ValueString() - } - return projectId, region, instanceId, databaseId, nil -} diff --git a/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go b/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go index 6affc956..fe8871a2 100644 --- a/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go +++ b/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go @@ -20,7 +20,7 @@ func DatabaseResourceSchema(ctx context.Context) schema.Schema { Description: "The ID of the database.", MarkdownDescription: "The ID of the database.", }, - "id": schema.Int64Attribute{ + "id": schema.StringAttribute{ Computed: true, Description: "The id of the database.", MarkdownDescription: "The id of the database.", @@ -65,7 +65,7 @@ func DatabaseResourceSchema(ctx context.Context) schema.Schema { type DatabaseModel struct { DatabaseId types.Int64 `tfsdk:"database_id"` - Id types.Int64 `tfsdk:"id"` + Id types.String `tfsdk:"id"` InstanceId types.String `tfsdk:"instance_id"` Name types.String `tfsdk:"name"` Owner types.String `tfsdk:"owner"` diff --git a/stackit/internal/services/postgresflexalpha/instance/datasource.go b/stackit/internal/services/postgresflexalpha/instance/datasource.go index cd7048e3..edb1a9a9 100644 --- a/stackit/internal/services/postgresflexalpha/instance/datasource.go +++ b/stackit/internal/services/postgresflexalpha/instance/datasource.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3alpha1api" @@ -72,7 +73,13 @@ func (r *instanceDataSource) Configure( // Schema defines the schema for the data source. func (r *instanceDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = postgresflexalpha2.InstanceDataSourceSchema(ctx) + sch := postgresflexalpha2.InstanceDataSourceSchema(ctx) + sch.Attributes["id"] = schema.StringAttribute{ + Computed: true, + Description: "internal ID", + MarkdownDescription: "internal ID", + } + resp.Schema = sch } // Read refreshes the Terraform state with the latest data. @@ -90,22 +97,22 @@ func (r *instanceDataSource) Read( ctx = core.InitProviderContext(ctx) - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() + projectID := model.ProjectId.ValueString() + instanceID := model.InstanceId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "project_id", projectID) + ctx = tflog.SetField(ctx, "instance_id", instanceID) ctx = tflog.SetField(ctx, "region", region) - instanceResp, err := r.client.DefaultAPI.GetInstanceRequest(ctx, projectId, region, instanceId).Execute() + instanceResp, err := r.client.DefaultAPI.GetInstanceRequest(ctx, projectID, region, instanceID).Execute() if err != nil { utils.LogError( ctx, &resp.Diagnostics, err, "Reading instance", - fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceId, projectId), + fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceID, projectID), map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), + http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectID), }, ) resp.State.RemoveResource(ctx) diff --git a/stackit/internal/services/postgresflexalpha/instance/datasources_gen/instance_data_source_gen.go b/stackit/internal/services/postgresflexalpha/instance/datasources_gen/instance_data_source_gen.go index 58f88e01..9b3e28ce 100644 --- a/stackit/internal/services/postgresflexalpha/instance/datasources_gen/instance_data_source_gen.go +++ b/stackit/internal/services/postgresflexalpha/instance/datasources_gen/instance_data_source_gen.go @@ -155,7 +155,7 @@ func InstanceDataSourceSchema(ctx context.Context) schema.Schema { MarkdownDescription: "The STACKIT project ID.", }, "region": schema.StringAttribute{ - Required: true, + Optional: true, Description: "The region which should be addressed", MarkdownDescription: "The region which should be addressed", Validators: []validator.String{ diff --git a/stackit/internal/services/postgresflexalpha/instance/functions.go b/stackit/internal/services/postgresflexalpha/instance/functions.go index 6e7164b9..ead4e6f7 100644 --- a/stackit/internal/services/postgresflexalpha/instance/functions.go +++ b/stackit/internal/services/postgresflexalpha/instance/functions.go @@ -66,12 +66,12 @@ func mapGetInstanceResponseToModel( m.IsDeletable = types.BoolValue(resp.GetIsDeletable()) - netAcl, diags := types.ListValueFrom(ctx, types.StringType, resp.Network.GetAcl()) + netACL, diags := types.ListValueFrom(ctx, types.StringType, resp.Network.GetAcl()) if diags.HasError() { return fmt.Errorf("failed converting network acl from response") } - m.Acl = netAcl + m.Acl = netACL netInstAdd := types.StringValue("") if instAdd, ok := resp.Network.GetInstanceAddressOk(); ok { @@ -87,7 +87,7 @@ func mapGetInstanceResponseToModel( postgresflexalpharesource.NetworkValue{}.AttributeTypes(ctx), map[string]attr.Value{ "access_scope": basetypes.NewStringValue(string(resp.Network.GetAccessScope())), - "acl": netAcl, + "acl": netACL, "instance_address": netInstAdd, "router_address": netRtrAdd, }, @@ -130,7 +130,8 @@ func mapGetDataInstanceResponseToModel( handleConnectionInfo(ctx, m, resp) m.FlavorId = types.StringValue(resp.GetFlavorId()) - m.Id = utils.BuildInternalTerraformId(m.ProjectId.ValueString(), m.Region.ValueString(), m.InstanceId.ValueString()) + m.Id = types.StringValue(resp.Id) + m.TerraformID = utils.BuildInternalTerraformId(m.ProjectId.ValueString(), m.Region.ValueString(), m.InstanceId.ValueString()) m.InstanceId = types.StringValue(resp.Id) m.IsDeletable = types.BoolValue(resp.GetIsDeletable()) m.Name = types.StringValue(resp.GetName()) @@ -212,14 +213,14 @@ func handleNetwork(ctx context.Context, m *dataSourceModel, resp *postgresflex.G } func handleEncryption(m *dataSourceModel, resp *postgresflex.GetInstanceResponse) { - keyId := "" - if keyIdVal, ok := resp.Encryption.GetKekKeyIdOk(); ok { - keyId = *keyIdVal + keyID := "" + if keyIDVal, ok := resp.Encryption.GetKekKeyIdOk(); ok { + keyID = *keyIDVal } - keyRingId := "" - if keyRingIdVal, ok := resp.Encryption.GetKekKeyRingIdOk(); ok { - keyRingId = *keyRingIdVal + keyRingID := "" + if keyRingIDVal, ok := resp.Encryption.GetKekKeyRingIdOk(); ok { + keyRingID = *keyRingIDVal } keyVersion := "" @@ -233,8 +234,8 @@ func handleEncryption(m *dataSourceModel, resp *postgresflex.GetInstanceResponse } m.Encryption = postgresflexalphadatasource.EncryptionValue{ - KekKeyId: types.StringValue(keyId), - KekKeyRingId: types.StringValue(keyRingId), + KekKeyId: types.StringValue(keyID), + KekKeyRingId: types.StringValue(keyRingID), KekKeyVersion: types.StringValue(keyVersion), ServiceAccount: types.StringValue(svcAcc), } diff --git a/stackit/internal/services/postgresflexalpha/instance/resource.go b/stackit/internal/services/postgresflexalpha/instance/resource.go index 7df2ed21..26eb9930 100644 --- a/stackit/internal/services/postgresflexalpha/instance/resource.go +++ b/stackit/internal/services/postgresflexalpha/instance/resource.go @@ -11,8 +11,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" coreUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" @@ -33,7 +31,6 @@ var ( _ resource.ResourceWithImportState = &instanceResource{} _ resource.ResourceWithModifyPlan = &instanceResource{} _ resource.ResourceWithValidateConfig = &instanceResource{} - _ resource.ResourceWithIdentity = &instanceResource{} ) // NewInstanceResource is a helper function to simplify the provider implementation. @@ -41,15 +38,6 @@ func NewInstanceResource() resource.Resource { return &instanceResource{} } -// resourceModel describes the resource data model. -type resourceModel = postgresflexalpha.InstanceModel - -type InstanceResourceIdentityModel struct { - ProjectID types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - InstanceID types.String `tfsdk:"instance_id"` -} - // instanceResource is the resource implementation. type instanceResource struct { client *v3alpha1api.APIClient @@ -61,7 +49,7 @@ func (r *instanceResource) ValidateConfig( req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse, ) { - var data resourceModel + var data postgresflexalpha.InstanceModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) if resp.Diagnostics.HasError() { @@ -85,7 +73,7 @@ func (r *instanceResource) ModifyPlan( req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse, ) { // nolint:gocritic // function signature required by Terraform - var configModel resourceModel + var configModel postgresflexalpha.InstanceModel // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return @@ -95,7 +83,7 @@ func (r *instanceResource) ModifyPlan( return } - var planModel resourceModel + var planModel postgresflexalpha.InstanceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) if resp.Diagnostics.HasError() { return @@ -161,33 +149,13 @@ func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp.Schema = schema } -func (r *instanceResource) IdentitySchema( - _ context.Context, - _ resource.IdentitySchemaRequest, - resp *resource.IdentitySchemaResponse, -) { - resp.IdentitySchema = identityschema.Schema{ - Attributes: map[string]identityschema.Attribute{ - "project_id": identityschema.StringAttribute{ - RequiredForImport: true, // must be set during import by the practitioner - }, - "region": identityschema.StringAttribute{ - RequiredForImport: true, // can be defaulted by the provider configuration - }, - "instance_id": identityschema.StringAttribute{ - RequiredForImport: true, // can be defaulted by the provider configuration - }, - }, - } -} - // Create creates the resource and sets the initial Terraform state. func (r *instanceResource) Create( ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, ) { // nolint:gocritic // function signature required by Terraform - var model resourceModel + var model postgresflexalpha.InstanceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -202,15 +170,15 @@ func (r *instanceResource) Create( ctx = tflog.SetField(ctx, "project_id", projectID) ctx = tflog.SetField(ctx, "region", region) - var netAcl []string - diag := model.Network.Acl.ElementsAs(ctx, &netAcl, false) + var netACL []string + diag := model.Network.Acl.ElementsAs(ctx, &netACL, false) resp.Diagnostics.Append(diags...) if diag.HasError() { return } replVal := model.Replicas.ValueInt64() // nolint:gosec // check is performed above - payload := modelToCreateInstancePayload(netAcl, model, replVal) + payload := modelToCreateInstancePayload(netACL, model, replVal) // Create new instance createResp, err := r.client.DefaultAPI.CreateInstanceRequest( @@ -230,16 +198,14 @@ func (r *instanceResource) Create( return } - // Set data returned by API in identity - identity := InstanceResourceIdentityModel{ - ProjectID: types.StringValue(projectID), - Region: types.StringValue(region), - InstanceID: types.StringPointerValue(instanceID), - } - resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...) - if resp.Diagnostics.HasError() { - return - } + // Set data returned by API in id + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("id"), + utils.BuildInternalTerraformId(projectID, region, *instanceID), + )..., + ) waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client.DefaultAPI, projectID, region, *instanceID). SetTimeout(30 * time.Minute). @@ -317,7 +283,7 @@ func (r *instanceResource) Read( ) { // nolint:gocritic // function signature required by Terraform functionErrorSummary := "read instance failed" - var model resourceModel + var model postgresflexalpha.InstanceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -326,9 +292,9 @@ func (r *instanceResource) Read( ctx = core.InitProviderContext(ctx) - var projectId string + var projectID string if !model.ProjectId.IsNull() && !model.ProjectId.IsUnknown() { - projectId = model.ProjectId.ValueString() + projectID = model.ProjectId.ValueString() } var region string @@ -336,16 +302,16 @@ func (r *instanceResource) Read( region = r.providerData.GetRegionWithOverride(model.Region) } - var instanceId string + var instanceID string if !model.InstanceId.IsNull() && !model.InstanceId.IsUnknown() { - instanceId = model.InstanceId.ValueString() + instanceID = model.InstanceId.ValueString() } - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "project_id", projectID) + ctx = tflog.SetField(ctx, "instance_id", instanceID) ctx = tflog.SetField(ctx, "region", region) - instanceResp, err := r.client.DefaultAPI.GetInstanceRequest(ctx, projectId, region, instanceId).Execute() + instanceResp, err := r.client.DefaultAPI.GetInstanceRequest(ctx, projectID, region, instanceID).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -364,7 +330,7 @@ func (r *instanceResource) Read( return } if !model.InstanceId.IsUnknown() && !model.InstanceId.IsNull() { - if *respInstanceID != instanceId { + if *respInstanceID != instanceID { core.LogAndAddError( ctx, &resp.Diagnostics, @@ -375,6 +341,10 @@ func (r *instanceResource) Read( } } + if !model.Id.IsUnknown() && !model.Id.IsNull() { + model.Id = utils.BuildInternalTerraformId(projectID, region, instanceID) + } + err = mapGetInstanceResponseToModel(ctx, &model, instanceResp) if err != nil { core.LogAndAddError( @@ -392,17 +362,6 @@ func (r *instanceResource) Read( return } - // Set data returned by API in identity - identity := InstanceResourceIdentityModel{ - ProjectID: types.StringValue(projectId), - Region: types.StringValue(region), - InstanceID: types.StringValue(instanceId), - } - resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex instance read") } @@ -412,7 +371,7 @@ func (r *instanceResource) Update( req resource.UpdateRequest, resp *resource.UpdateResponse, ) { // nolint:gocritic // function signature required by Terraform - var model resourceModel + var model postgresflexalpha.InstanceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -422,15 +381,8 @@ func (r *instanceResource) Update( ctx = core.InitProviderContext(ctx) - // Read identity data - var identityData InstanceResourceIdentityModel - resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) - if resp.Diagnostics.HasError() { - return - } - - projectID := identityData.ProjectID.ValueString() - instanceID := identityData.InstanceID.ValueString() + projectID := model.ProjectId.ValueString() + instanceID := model.InstanceId.ValueString() region := model.Region.ValueString() ctx = tflog.SetField(ctx, "project_id", projectID) ctx = tflog.SetField(ctx, "instance_id", instanceID) @@ -532,7 +484,7 @@ func (r *instanceResource) Delete( req resource.DeleteRequest, resp *resource.DeleteResponse, ) { // nolint:gocritic // function signature required by Terraform - var model resourceModel + var model postgresflexalpha.InstanceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -542,15 +494,15 @@ func (r *instanceResource) Delete( ctx = core.InitProviderContext(ctx) - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() + projectID := model.ProjectId.ValueString() + instanceID := model.InstanceId.ValueString() region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "project_id", projectID) + ctx = tflog.SetField(ctx, "instance_id", instanceID) ctx = tflog.SetField(ctx, "region", region) // Delete existing instance - err := r.client.DefaultAPI.DeleteInstanceRequest(ctx, projectId, region, instanceId).Execute() + err := r.client.DefaultAPI.DeleteInstanceRequest(ctx, projectID, region, instanceID).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return @@ -558,7 +510,7 @@ func (r *instanceResource) Delete( ctx = core.LogResponse(ctx) - _, err = r.client.DefaultAPI.GetInstanceRequest(ctx, projectId, region, instanceId).Execute() + _, err = r.client.DefaultAPI.GetInstanceRequest(ctx, projectID, region, instanceID).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode != http.StatusNotFound { @@ -580,41 +532,30 @@ func (r *instanceResource) ImportState( ) { ctx = core.InitProviderContext(ctx) - if req.ID != "" { - idParts := strings.Split(req.ID, core.Separator) + idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError( - ctx, &resp.Diagnostics, - "Error importing instance", - fmt.Sprintf( - "Expected import identifier with format [project_id],[region],[instance_id] Got: %q", - req.ID, - ), - ) - 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])...) + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + core.LogAndAddError( + ctx, &resp.Diagnostics, + "Error importing instance", + fmt.Sprintf( + "Expected import identifier with format [project_id],[region],[instance_id] Got: %q", + req.ID, + ), + ) return } - // If no ID is provided, attempt to read identity attributes from the import configuration - var identityData InstanceResourceIdentityModel - resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) - if resp.Diagnostics.HasError() { - return - } - - projectId := identityData.ProjectID.ValueString() - region := identityData.Region.ValueString() - instanceId := identityData.InstanceID.ValueString() - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), instanceId)...) + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("id"), + utils.BuildInternalTerraformId(idParts...), + )..., + ) + 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])...) tflog.Info(ctx, "Postgres Flex instance state imported") } diff --git a/stackit/internal/services/postgresflexalpha/postgresflex_acc_test.go b/stackit/internal/services/postgresflexalpha/postgresflex_acc_test.go index 49e51665..b93ded45 100644 --- a/stackit/internal/services/postgresflexalpha/postgresflex_acc_test.go +++ b/stackit/internal/services/postgresflexalpha/postgresflex_acc_test.go @@ -37,7 +37,8 @@ import ( ) const ( - pfx = "stackitprivatepreview_postgresflexalpha" + pfx = "stackitprivatepreview_postgresflexalpha" + dataPfx = "data.stackitprivatepreview_postgresflexalpha" singleFlavorID = "2.4" replicasFlavorID = "2.4-replica" @@ -100,6 +101,7 @@ type resData struct { Version string Users []User Databases []Database + DataSourceTest bool } type User struct { @@ -137,6 +139,7 @@ func getExample() resData { func TestAccInstance(t *testing.T) { exData := getExample() + exData.Version = "16" updNameData := exData updNameData.Name = "name-updated" @@ -151,6 +154,9 @@ func TestAccInstance(t *testing.T) { updNetACL := updBackupSched updNetACL.ACLStrings = append(updNetACL.ACLStrings, "192.168.0.0/24") + updVersion := updNetACL + updVersion.Version = "17" + testItemID := testutils.ResStr(pfx, "instance", exData.TfName) compareValuesSame := statecheck.CompareValue(compare.ValuesSame()) resource.ParallelTest( @@ -299,19 +305,31 @@ func TestAccInstance(t *testing.T) { resource.TestCheckResourceAttr(testItemID, "network.acl.#", "2"), ), }, + // Update version + { + Config: testutils.StringFromTemplateMust( + "testdata/instance_template.gompl", + updVersion, + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + testItemID, + "version", + updVersion.Version, + ), + ), + }, // Import test - /* - { - ImportState: true, - ImportStateKind: resource.ImportBlockWithResourceIdentity, - ResourceName: testItemID, - }, - { - ImportState: true, - ImportStateKind: resource.ImportCommandWithID, - ResourceName: testItemID, - }, - */ + // test instance imports + { + ResourceName: testItemID, + // ImportStateIdPrefix: "", + // ImportStateVerifyIdentifierAttribute: "id", + ImportStateIdFunc: getInstanceTestID(exData.TfName), + ImportStateKind: resource.ImportCommandWithID, + ImportState: true, + ImportStateVerify: true, + }, }, }, ) @@ -410,6 +428,7 @@ func TestAccInstanceWithDatabases(t *testing.T) { Owner: userName, }, } + data.DataSourceTest = true testItemID := testutils.ResStr(pfx, "instance", data.TfName) resource.ParallelTest( @@ -432,11 +451,145 @@ func TestAccInstanceWithDatabases(t *testing.T) { resource.TestCheckResourceAttr(testutils.ResStr(pfx, "user", userName), "name", userName), resource.TestCheckResourceAttrSet(testutils.ResStr(pfx, "user", userName), "id"), + resource.TestCheckResourceAttrPair( + testItemID, "project_id", + testutils.ResStr(pfx, "user", userName), "project_id", + ), + resource.TestCheckResourceAttrPair( + testItemID, "instance_id", + testutils.ResStr(pfx, "user", userName), "instance_id", + ), + resource.TestCheckResourceAttr(testutils.ResStr(pfx, "database", dbName), "name", dbName), resource.TestCheckResourceAttr(testutils.ResStr(pfx, "database", dbName), "owner", userName), resource.TestCheckResourceAttrSet(testutils.ResStr(pfx, "database", dbName), "id"), + resource.TestCheckResourceAttrPair( + testItemID, "project_id", + testutils.ResStr(pfx, "database", dbName), "project_id", + ), + resource.TestCheckResourceAttrPair( + testItemID, "instance_id", + testutils.ResStr(pfx, "database", dbName), "instance_id", + ), ), }, + // data source + { + Config: testutils.StringFromTemplateMust( + "testdata/instance_template.gompl", + data, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Instance data + resource.TestCheckResourceAttr( + testutils.ResStr(dataPfx, "instance", data.TfName), + "project_id", + data.ProjectID, + ), + resource.TestCheckResourceAttr( + testutils.ResStr(dataPfx, "instance", data.TfName), + "name", + data.Name, + ), + resource.TestCheckResourceAttrPair( + testutils.ResStr(dataPfx, "instance", data.TfName), "project_id", + testutils.ResStr(pfx, "instance", data.TfName), "project_id", + ), + resource.TestCheckResourceAttrPair( + testutils.ResStr(dataPfx, "database", dbName), "instance_id", + testutils.ResStr(pfx, "instance", data.TfName), "instance_id", + ), + resource.TestCheckResourceAttrPair( + testutils.ResStr(dataPfx, "user", userName), "instance_id", + testutils.ResStr(pfx, "instance", data.TfName), "instance_id", + ), + resource.TestCheckResourceAttrPair( + testutils.ResStr(dataPfx, "user", userName), "instance_id", + testutils.ResStr(pfx, "user", userName), "instance_id", + ), + + // User data + resource.TestCheckResourceAttr( + testutils.ResStr(dataPfx, "user", userName), + "project_id", + data.ProjectID, + ), + resource.TestCheckResourceAttrSet( + testutils.ResStr(dataPfx, "user", userName), + "user_id", + ), + resource.TestCheckResourceAttr( + testutils.ResStr(dataPfx, "user", userName), + "name", + data.Users[0].Name, + ), + resource.TestCheckResourceAttr( + testutils.ResStr(dataPfx, "user", userName), + "roles.#", + "1", + ), + resource.TestCheckResourceAttr( + testutils.ResStr(dataPfx, "user", userName), + "roles.0", + data.Users[0].Roles[0], + ), + + // Database data + resource.TestCheckResourceAttr( + testutils.ResStr(dataPfx, "database", dbName), + "project_id", + data.ProjectID, + ), + resource.TestCheckResourceAttr( + testutils.ResStr(dataPfx, "database", dbName), + "name", + dbName, + ), + resource.TestCheckResourceAttrPair( + testutils.ResStr(dataPfx, "database", dbName), + "instance_id", + testutils.ResStr(pfx, "database", dbName), + "instance_id", + ), + resource.TestCheckResourceAttrPair( + testutils.ResStr(dataPfx, "database", dbName), + "owner", + testutils.ResStr(dataPfx, "user", userName), + "name", + ), + ), + }, + // test instance imports + { + ResourceName: testItemID, + // ImportStateIdPrefix: "", + ImportStateVerifyIdentifierAttribute: "id", + ImportStateIdFunc: getInstanceTestID(data.TfName), + ImportStateKind: resource.ImportCommandWithID, + ImportState: true, + ImportStateVerify: true, + }, + // test database imports + { + ResourceName: testutils.ResStr(pfx, "database", dbName), + // ImportStateIdPrefix: "", + // ImportStateVerifyIdentifierAttribute: "id", + ImportStateIdFunc: getDatabaseTestID(dbName), + ImportStateKind: resource.ImportCommandWithID, + ImportState: true, + ImportStateVerify: true, + }, + // test user imports + { + ResourceName: testutils.ResStr(pfx, "user", userName), + // ImportStateIdPrefix: "", + // ImportStateVerifyIdentifierAttribute: "id", + ImportStateIdFunc: getUserTestID(userName), + ImportStateKind: resource.ImportCommandWithID, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + }, }, }, ) @@ -614,10 +767,6 @@ func defaultNoEncInstanceTestChecks(testItemID string, data resData) resource.Te return resource.ComposeAggregateTestCheckFunc( defaultInstanceTestChecks(testItemID, data), - // on unencrypted instances we expect this to be empty - resource.TestCheckResourceAttr(testItemID, "network.instance_address", ""), - resource.TestCheckResourceAttr(testItemID, "network.router_address", ""), - // check absent attr resource.TestCheckNoResourceAttr(testItemID, "encryption"), resource.TestCheckNoResourceAttr(testItemID, "encryption.kek_key_id"), @@ -631,10 +780,6 @@ func defaultEncInstanceTestChecks(testItemID string, data resData) resource.Test return resource.ComposeAggregateTestCheckFunc( defaultInstanceTestChecks(testItemID, data), - // on unencrypted instances we expect this to be empty - resource.TestCheckResourceAttrSet(testItemID, "network.instance_address"), - resource.TestCheckResourceAttrSet(testItemID, "network.router_address"), - // check absent attr resource.TestCheckResourceAttr(testItemID, "encryption.%", "4"), resource.TestCheckResourceAttrSet(testItemID, "encryption.kek_key_id"), @@ -649,6 +794,24 @@ func defaultEncInstanceTestChecks(testItemID string, data resData) resource.Test } func defaultInstanceTestChecks(testItemID string, data resData) resource.TestCheckFunc { + // if AccessScope == SNA these are set + if data.AccessScope == "SNA" { + return resource.ComposeAggregateTestCheckFunc( + basicInstanceTestChecks(testItemID, data), + resource.TestCheckResourceAttrSet(testItemID, "network.instance_address"), + resource.TestCheckResourceAttrSet(testItemID, "network.router_address"), + ) + } + + // if AccessScope == PUBLIC these are empty - but they are set + return resource.ComposeAggregateTestCheckFunc( + basicInstanceTestChecks(testItemID, data), + resource.TestCheckResourceAttr(testItemID, "network.instance_address", ""), + resource.TestCheckResourceAttr(testItemID, "network.router_address", ""), + ) +} + +func basicInstanceTestChecks(testItemID string, data resData) resource.TestCheckFunc { return resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet(testItemID, "backup_schedule"), resource.TestCheckResourceAttr(testItemID, "backup_schedule", data.BackupSchedule), @@ -704,3 +867,77 @@ func defaultInstanceTestChecks(testItemID string, data resData) resource.TestChe resource.TestCheckResourceAttr(testItemID, "version", data.Version), ) } + +func getInstanceTestID(name string) func(s *terraform.State) (string, error) { + return func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources[testutils.ResStr(pfx, "instance", name)] + if !ok { + return "", fmt.Errorf("couldn't find resource stackitprivatepreview_postgresflexalpha_instance.%s", name) + } + projectID, ok := r.Primary.Attributes["project_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute project_id") + } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } + instanceID, ok := r.Primary.Attributes["instance_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute instance_id") + } + return fmt.Sprintf("%s,%s,%s", projectID, region, instanceID), nil + } +} + +func getDatabaseTestID(name string) func(s *terraform.State) (string, error) { + return func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources[testutils.ResStr(pfx, "database", name)] + if !ok { + return "", fmt.Errorf("couldn't find resource stackitprivatepreview_postgresflexalpha_instance.%s", name) + } + projectID, ok := r.Primary.Attributes["project_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute project_id") + } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } + instanceID, ok := r.Primary.Attributes["instance_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute instance_id") + } + databaseID, ok := r.Primary.Attributes["database_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute database_id") + } + return fmt.Sprintf("%s,%s,%s,%s", projectID, region, instanceID, databaseID), nil + } +} + +func getUserTestID(name string) func(s *terraform.State) (string, error) { + return func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources[testutils.ResStr(pfx, "user", name)] + if !ok { + return "", fmt.Errorf("couldn't find resource stackitprivatepreview_postgresflexalpha_instance.%s", name) + } + projectID, ok := r.Primary.Attributes["project_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute project_id") + } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } + instanceID, ok := r.Primary.Attributes["instance_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute instance_id") + } + userID, ok := r.Primary.Attributes["user_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute user_id") + } + return fmt.Sprintf("%s,%s,%s,%s", projectID, region, instanceID, userID), nil + } +} diff --git a/stackit/internal/services/postgresflexalpha/testdata/instance_template.gompl b/stackit/internal/services/postgresflexalpha/testdata/instance_template.gompl index 00a51e60..eca3bacf 100644 --- a/stackit/internal/services/postgresflexalpha/testdata/instance_template.gompl +++ b/stackit/internal/services/postgresflexalpha/testdata/instance_template.gompl @@ -33,6 +33,9 @@ resource "stackitprivatepreview_postgresflexalpha_instance" "{{ .TfName }}" { {{ $tfName := .TfName }} {{ range $user := .Users }} resource "stackitprivatepreview_postgresflexalpha_user" "{{ $user.Name }}" { + depends_on = [ + stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }} + ] project_id = "{{ $user.ProjectID }}" instance_id = stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}.instance_id name = "{{ $user.Name }}" @@ -45,6 +48,10 @@ resource "stackitprivatepreview_postgresflexalpha_user" "{{ $user.Name }}" { {{ $tfName := .TfName }} {{ range $db := .Databases }} resource "stackitprivatepreview_postgresflexalpha_database" "{{ $db.Name }}" { + depends_on = [ + stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}, + stackitprivatepreview_postgresflexalpha_user.{{ $db.Owner }} + ] project_id = "{{ $db.ProjectID }}" instance_id = stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}.instance_id name = "{{ $db.Name }}" @@ -52,3 +59,32 @@ resource "stackitprivatepreview_postgresflexalpha_database" "{{ $db.Name }}" { } {{ end }} {{ end }} + +{{ if .DataSourceTest }} +data "stackitprivatepreview_postgresflexalpha_instance" "{{ .TfName }}" { + project_id = stackitprivatepreview_postgresflexalpha_instance.{{ .TfName }}.project_id + instance_id = stackitprivatepreview_postgresflexalpha_instance.{{ .TfName }}.instance_id +} + +{{ if .Users }} +{{ $tfName := .TfName }} +{{ range $user := .Users }} +data "stackitprivatepreview_postgresflexalpha_user" "{{ $user.Name }}" { + project_id = stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}.project_id + instance_id = stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}.instance_id + user_id = stackitprivatepreview_postgresflexalpha_user.{{ $user.Name }}.user_id +} +{{ end }} +{{ end }} + +{{ if .Databases }} +{{ $tfName := .TfName }} +{{ range $db := .Databases }} +data "stackitprivatepreview_postgresflexalpha_database" "{{ $db.Name }}" { + project_id = stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}.project_id + instance_id = stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}.instance_id + database_id = stackitprivatepreview_postgresflexalpha_database.{{ $db.Name }}.database_id +} +{{ end }} +{{ end }} +{{ end }} diff --git a/stackit/internal/services/postgresflexalpha/user/datasources_gen/user_data_source_gen.go b/stackit/internal/services/postgresflexalpha/user/datasources_gen/user_data_source_gen.go index 29a7cca0..37f3c7c6 100644 --- a/stackit/internal/services/postgresflexalpha/user/datasources_gen/user_data_source_gen.go +++ b/stackit/internal/services/postgresflexalpha/user/datasources_gen/user_data_source_gen.go @@ -35,7 +35,7 @@ func UserDataSourceSchema(ctx context.Context) schema.Schema { MarkdownDescription: "The STACKIT project ID.", }, "region": schema.StringAttribute{ - Required: true, + Optional: true, Description: "The region which should be addressed", MarkdownDescription: "The region which should be addressed", Validators: []validator.String{ diff --git a/stackit/internal/services/postgresflexalpha/user/mapper.go b/stackit/internal/services/postgresflexalpha/user/mapper.go index dcf4545c..70c53b83 100644 --- a/stackit/internal/services/postgresflexalpha/user/mapper.go +++ b/stackit/internal/services/postgresflexalpha/user/mapper.go @@ -116,7 +116,12 @@ func mapResourceFields(userResp *v3alpha1api.GetUserResponse, model *resourceMod return fmt.Errorf("user id not present") } - model.Id = types.Int64Value(userID) + model.Id = utils.BuildInternalTerraformId( + model.ProjectId.ValueString(), + model.Region.ValueString(), + model.InstanceId.ValueString(), + strconv.FormatInt(userID, 10), + ) model.UserId = types.Int64Value(userID) model.Name = types.StringValue(user.Name) diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index 065c9552..0a57e89f 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -11,7 +11,7 @@ import ( "time" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3alpha1api" postgresflexalpha "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/user/resources_gen" @@ -34,7 +34,6 @@ var ( _ resource.ResourceWithConfigure = &userResource{} _ resource.ResourceWithImportState = &userResource{} _ resource.ResourceWithModifyPlan = &userResource{} - _ resource.ResourceWithIdentity = &userResource{} _ resource.ResourceWithValidateConfig = &userResource{} // Error message constants @@ -50,14 +49,6 @@ func NewUserResource() resource.Resource { // resourceModel represents the Terraform resource state for a PostgreSQL Flex user. type resourceModel = postgresflexalpha.UserModel -// 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:"user_id"` -} - // userResource implements the resource handling for a PostgreSQL Flex user. type userResource struct { client *v3alpha1api.APIClient @@ -232,23 +223,14 @@ func (r *userResource) Create( } arg.userID = int64(*id) + model.Id = utils.BuildInternalTerraformId(arg.projectID, arg.region, arg.instanceID, strconv.FormatInt(arg.userID, 10)) + + ctx = tflog.SetField(ctx, "id", model.Id.ValueString()) ctx = tflog.SetField(ctx, "user_id", id) ctx = core.LogResponse(ctx) - // Set data returned by API in identity - identity := UserResourceIdentityModel{ - ProjectID: types.StringValue(arg.projectID), - Region: types.StringValue(arg.region), - InstanceID: types.StringValue(arg.instanceID), - UserID: types.Int64Value(int64(*id)), - } - resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...) - if resp.Diagnostics.HasError() { - return - } - - model.Id = types.Int64Value(int64(*id)) + model.Id = utils.BuildInternalTerraformId(arg.projectID, arg.region, arg.instanceID, strconv.FormatInt(arg.userID, 10)) model.UserId = types.Int64Value(int64(*id)) model.Password = types.StringValue(userResp.GetPassword()) model.Status = types.StringValue(userResp.GetStatus()) @@ -370,15 +352,14 @@ func (r *userResource) Read( ctx = core.LogResponse(ctx) - // Set data returned by API in identity - identity := UserResourceIdentityModel{ - ProjectID: types.StringValue(arg.projectID), - Region: types.StringValue(arg.region), - InstanceID: types.StringValue(arg.instanceID), - UserID: types.Int64Value(arg.userID), - } - resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...) - if resp.Diagnostics.HasError() { + err = mapResourceFields(waitResp, &model, model.Region.ValueString()) + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "read user", + fmt.Sprintf("Wait response mapping: %v", err), + ) return } @@ -457,18 +438,6 @@ func (r *userResource) Update( ctx = core.LogResponse(ctx) - // Set data returned by API in identity - identity := UserResourceIdentityModel{ - ProjectID: types.StringValue(arg.projectID), - Region: types.StringValue(arg.region), - InstanceID: types.StringValue(arg.instanceID), - UserID: types.Int64Value(userID64), - } - resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...) - if resp.Diagnostics.HasError() { - return - } - // Verify update waitResp, err := postgresflexalphaWait.GetUserByIdWaitHandler( ctx, @@ -525,26 +494,17 @@ 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) - arg, errExt := r.extractIdentityData(model, identityData) - if errExt != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - extractErrorSummary, - fmt.Sprintf(extractErrorMessage, errExt), - ) + arg := clientArg{ + projectID: model.ProjectId.ValueString(), + instanceID: model.InstanceId.ValueString(), + region: model.Region.ValueString(), + userID: model.UserId.ValueInt64(), } - ctx = r.setTFLogFields(ctx, arg) + ctx = r.setTFLogFields(ctx, &arg) ctx = core.InitProviderContext(ctx) userID64 := arg.userID @@ -557,7 +517,14 @@ func (r *userResource) Delete( // Delete existing record set err := r.client.DefaultAPI.DeleteUserRequest(ctx, arg.projectID, arg.region, arg.instanceID, userID).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) // nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + if ok { + if oapiErr.StatusCode == 404 { + resp.State.RemoveResource(ctx) + return + } + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("error from API: %v", err)) } ctx = core.LogResponse(ctx) @@ -581,30 +548,6 @@ func (r *userResource) Delete( tflog.Info(ctx, "Postgres Flex user deleted") } -// IdentitySchema defines the fields that are required to uniquely identify a resource. -func (r *userResource) IdentitySchema( - _ context.Context, - _ resource.IdentitySchemaRequest, - response *resource.IdentitySchemaResponse, -) { - response.IdentitySchema = identityschema.Schema{ - Attributes: map[string]identityschema.Attribute{ - "project_id": identityschema.StringAttribute{ - RequiredForImport: true, - }, - "region": identityschema.StringAttribute{ - RequiredForImport: true, - }, - "instance_id": identityschema.StringAttribute{ - RequiredForImport: true, - }, - "user_id": identityschema.Int64Attribute{ - RequiredForImport: true, - }, - }, - } -} - // clientArg holds the arguments for API calls. type clientArg struct { projectID string @@ -622,112 +565,41 @@ func (r *userResource) ImportState( ) { ctx = core.InitProviderContext(ctx) - if req.ID != "" { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError( - ctx, &resp.Diagnostics, - "Error importing user", - fmt.Sprintf( - "Expected import identifier with format [project_id],[region],[instance_id],[user_id], got %q", - req.ID, - ), - ) - return - } - - userID, err := strconv.ParseInt(idParts[3], 10, 64) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error importing user", - fmt.Sprintf("Invalid user_id 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"), userID)...) - - tflog.Info(ctx, "Postgres Flex user state imported") + idParts := strings.Split(req.ID, core.Separator) + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { + core.LogAndAddError( + ctx, &resp.Diagnostics, + "Error importing user", + fmt.Sprintf( + "Expected import identifier with format [project_id],[region],[instance_id],[user_id], got %q", + req.ID, + ), + ) return } - // If no ID is provided, attempt to read identity attributes from the import configuration - var identityData UserResourceIdentityModel - resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) - if resp.Diagnostics.HasError() { + userID, err := strconv.ParseInt(idParts[3], 10, 64) + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error importing user", + fmt.Sprintf("Invalid user_id format: %q. It must be a valid integer.", idParts[3]), + ) return } - projectID := identityData.ProjectID.ValueString() - region := identityData.Region.ValueString() - instanceID := identityData.InstanceID.ValueString() - userID := identityData.UserID.ValueInt64() - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectID)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), instanceID)...) + idString := utils.BuildInternalTerraformId(idParts...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idString)...) + 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"), userID)...) tflog.Info(ctx, "Postgres Flex user state imported") } -// 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, arg *clientArg) context.Context { ctx = tflog.SetField(ctx, "project_id", arg.projectID) diff --git a/stackit/internal/services/postgresflexalpha/user/resources_gen/user_resource_gen.go b/stackit/internal/services/postgresflexalpha/user/resources_gen/user_resource_gen.go index f96d8d93..3e2d1e63 100644 --- a/stackit/internal/services/postgresflexalpha/user/resources_gen/user_resource_gen.go +++ b/stackit/internal/services/postgresflexalpha/user/resources_gen/user_resource_gen.go @@ -14,7 +14,7 @@ import ( func UserResourceSchema(ctx context.Context) schema.Schema { return schema.Schema{ Attributes: map[string]schema.Attribute{ - "id": schema.Int64Attribute{ + "id": schema.StringAttribute{ Computed: true, Description: "The ID of the user.", MarkdownDescription: "The ID of the user.", @@ -75,7 +75,7 @@ func UserResourceSchema(ctx context.Context) schema.Schema { } type UserModel struct { - Id types.Int64 `tfsdk:"id"` + Id types.String `tfsdk:"id"` InstanceId types.String `tfsdk:"instance_id"` Name types.String `tfsdk:"name"` Password types.String `tfsdk:"password"` diff --git a/stackit/internal/wait/postgresflexalpha/wait.go b/stackit/internal/wait/postgresflexalpha/wait.go index 730ccbe3..96145b8c 100644 --- a/stackit/internal/wait/postgresflexalpha/wait.go +++ b/stackit/internal/wait/postgresflexalpha/wait.go @@ -53,8 +53,8 @@ type APIClientDatabaseInterface interface { // CreateInstanceWaitHandler will wait for instance creation func CreateInstanceWaitHandler( - ctx context.Context, a APIClientInstanceInterface, projectId, region, - instanceId string, + ctx context.Context, a APIClientInstanceInterface, projectID, region, + instanceID string, ) *wait.AsyncActionHandler[v3alpha1api.GetInstanceResponse] { instanceCreated := false var instanceGetResponse *v3alpha1api.GetInstanceResponse @@ -67,11 +67,11 @@ func CreateInstanceWaitHandler( handler := wait.New( func() (waitFinished bool, response *v3alpha1api.GetInstanceResponse, err error) { if !instanceCreated { - s, err := a.GetInstanceRequest(ctx, projectId, region, instanceId).Execute() - if err != nil { - return false, nil, err + s, getErr := a.GetInstanceRequest(ctx, projectID, region, instanceID).Execute() + if getErr != nil { + return false, nil, getErr } - if s == nil || s.Id != instanceId { + if s == nil || s.Id != instanceID { return false, nil, nil } tflog.Debug( @@ -81,7 +81,7 @@ func CreateInstanceWaitHandler( ) switch s.Status { default: - return true, s, fmt.Errorf("instance with id %s has unexpected status %s", instanceId, s.Status) + return true, s, fmt.Errorf("instance with id %s has unexpected status %s", instanceID, s.Status) case InstanceStateEmpty: return false, nil, nil case InstanceStatePending: @@ -98,30 +98,15 @@ func CreateInstanceWaitHandler( "Wait handler still got status %s after %v for instance: %s", InstanceStateProgressing, maxWait, - instanceId, + instanceID, ), ) if extendedTimeout < 3 { maxWait += time.Minute * 5 extendedTimeout++ - if *s.Network.AccessScope == "SNA" { - ready := true - if s.Network.InstanceAddress == nil { - tflog.Warn(ctx, "Waiting for instance_address") - ready = false - } - if s.Network.RouterAddress == nil { - tflog.Warn(ctx, "Waiting for router_address") - ready = false - } - if !ready { - return false, nil, nil - } - } + return false, nil, nil } - - instanceCreated = true - instanceGetResponse = s + return false, nil, fmt.Errorf("instance after max timeout still in state %s", InstanceStateProgressing) case InstanceStateSuccess: if s.Network.AccessScope != nil && *s.Network.AccessScope == "SNA" { if s.Network.InstanceAddress == nil { @@ -144,9 +129,9 @@ func CreateInstanceWaitHandler( }, ) var waitCounter int64 = 1 - maxWait := big.NewInt(7) - n, err := rand.Int(rand.Reader, maxWait) - if err == nil { + maxWaitInt := big.NewInt(7) + n, randErr := rand.Int(rand.Reader, maxWaitInt) + if randErr == nil { waitCounter = n.Int64() + 1 } time.Sleep(time.Duration(waitCounter*30) * time.Second) //nolint:gosec // not that important and temporary @@ -154,7 +139,7 @@ func CreateInstanceWaitHandler( } return true, s, fmt.Errorf( "update got status FAILURE for instance with id %s after %d retries", - instanceId, + instanceID, failedCount, ) // API responds with FAILURE for some seconds and then the instance goes to READY @@ -165,7 +150,7 @@ func CreateInstanceWaitHandler( tflog.Info(ctx, "Waiting for instance (calling list users") // // User operations aren't available right after an instance is deemed successful // // To check if they are, perform a users request - _, err = a.ListUsersRequest(ctx, projectId, region, instanceId).Execute() + _, err = a.ListUsersRequest(ctx, projectID, region, instanceID).Execute() if err == nil { return true, instanceGetResponse, nil }