diff --git a/stackit/internal/services/postgresflexalpha/database/datasource.go b/stackit/internal/services/postgresflexalpha/database/datasource.go index 36fc5333..d6890954 100644 --- a/stackit/internal/services/postgresflexalpha/database/datasource.go +++ b/stackit/internal/services/postgresflexalpha/database/datasource.go @@ -5,18 +5,16 @@ import ( "fmt" "net/http" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" + postgresflexalpha2 "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/database/datasources_gen" postgresflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils" "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" "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/validate" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" ) @@ -66,132 +64,38 @@ func (r *databaseDataSource) Configure( } // Schema defines the schema for the data source. -func (r *databaseDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Postgres Flex database resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`,`database_id`\".", - "database_id": "Database ID.", - "instance_id": "ID of the Postgres Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Database name.", - "owner": "Username of the database owner.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "database_id": schema.Int64Attribute{ - Description: descriptions["database_id"], - Optional: true, - Computed: true, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Optional: true, - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "owner": schema.StringAttribute{ - Description: descriptions["owner"], - Computed: true, - }, - "region": schema.StringAttribute{ - // the region cannot be found, so it has to be passed - Optional: true, - Description: descriptions["region"], - }, - }, - } +func (r *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = postgresflexalpha2.DatabaseResourceSchema(ctx) } -// Read refreshes the Terraform state with the latest data. +// Read fetches the data for the data source. func (r *databaseDataSource) Read( ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse, ) { // nolint:gocritic // function signature required by Terraform - var model Model + var model postgresflexalpha2.DatabaseModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - // validation for exactly one of database_id or name - isIdSet := !model.DatabaseId.IsNull() && !model.DatabaseId.IsUnknown() - isNameSet := !model.Name.IsNull() && !model.Name.IsUnknown() - - if (isIdSet && isNameSet) || (!isIdSet && !isNameSet) { - core.LogAndAddError( - ctx, &resp.Diagnostics, - "Invalid configuration", "Exactly one of 'database_id' or 'name' must be specified.", - ) - return - } - ctx = core.InitProviderContext(ctx) projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() - databaseId := model.DatabaseId.ValueInt64() region := r.providerData.GetRegionWithOverride(model.Region) ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "database_id", databaseId) ctx = tflog.SetField(ctx, "region", region) - var databaseResp *postgresflexalpha.ListDatabase - var err error - - if isIdSet { - databaseId := model.DatabaseId.ValueInt64() - ctx = tflog.SetField(ctx, "database_id", databaseId) - databaseResp, err = getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) - } else { - databaseName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "name", databaseName) - databaseResp, err = getDatabaseByName(ctx, r.client, projectId, region, instanceId, databaseName) + databaseResp, err := r.getDatabaseByNameOrID(ctx, &model, projectId, region, instanceId, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return } - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading database", - fmt.Sprintf( - "Database with ID %q or instance with ID %q does not exist in project %q.", - databaseId, - instanceId, - projectId, - ), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) + handleReadError(ctx, &resp.Diagnostics, err, projectId, instanceId) resp.State.RemoveResource(ctx) return } @@ -218,3 +122,52 @@ func (r *databaseDataSource) Read( } tflog.Info(ctx, "Postgres Flex database read") } + +// getDatabase retrieves a single database by either ID or name. +// It adds a diagnostic if the configuration is invalid. +func (r *databaseDataSource) getDatabaseByNameOrID( + ctx context.Context, + model *postgresflexalpha2.DatabaseModel, + projectId, region, instanceId string, + diags *diag.Diagnostics, +) (*postgresflexalpha.ListDatabase, error) { + isIdSet := !model.Id.IsNull() && !model.Id.IsUnknown() + isNameSet := !model.Name.IsNull() && !model.Name.IsUnknown() + + if (isIdSet && isNameSet) || (!isIdSet && !isNameSet) { + diags.AddError( + "Invalid configuration", + "Exactly one of 'id' or 'name' must be specified.", + ) + return nil, nil + } + + if isIdSet { + databaseId := model.Id.ValueInt64() + ctx = tflog.SetField(ctx, "database_id", databaseId) + return getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) + } + + databaseName := model.Name.ValueString() + ctx = tflog.SetField(ctx, "name", databaseName) + return getDatabaseByName(ctx, r.client, projectId, region, instanceId, databaseName) +} + +// handleReadError logs and adds a structured error to diagnostics for API errors. +func handleReadError(ctx context.Context, diags *diag.Diagnostics, err error, projectId, instanceId string) { + utils.LogError( + ctx, + diags, + err, + "Reading database", + fmt.Sprintf( + "Could not retrieve database for instance %q in project %q.", + instanceId, + projectId, + ), + map[int]string{ + http.StatusNotFound: fmt.Sprintf("Database, instance %q, or project %q not found.", instanceId, projectId), + http.StatusForbidden: fmt.Sprintf("Forbidden access to project %q.", projectId), + }, + ) +} diff --git a/stackit/internal/services/postgresflexalpha/database/datasource.go.bak b/stackit/internal/services/postgresflexalpha/database/datasource.go.bak new file mode 100644 index 00000000..36fc5333 --- /dev/null +++ b/stackit/internal/services/postgresflexalpha/database/datasource.go.bak @@ -0,0 +1,220 @@ +package postgresflexalpha + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" + postgresflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-log/tflog" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" + "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/validate" + + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &databaseDataSource{} +) + +// NewDatabaseDataSource is a helper function to simplify the provider implementation. +func NewDatabaseDataSource() datasource.DataSource { + return &databaseDataSource{} +} + +// databaseDataSource is the data source implementation. +type databaseDataSource struct { + client *postgresflexalpha.APIClient + providerData core.ProviderData +} + +// Metadata returns the data source type name. +func (r *databaseDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_postgresflexalpha_database" +} + +// Configure adds the provider configured client to the data source. +func (r *databaseDataSource) Configure( + ctx context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "Postgres Flex database client configured") +} + +// Schema defines the schema for the data source. +func (r *databaseDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Postgres Flex database resource schema. Must have a `region` specified in the provider configuration.", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`,`database_id`\".", + "database_id": "Database ID.", + "instance_id": "ID of the Postgres Flex instance.", + "project_id": "STACKIT project ID to which the instance is associated.", + "name": "Database name.", + "owner": "Username of the database owner.", + "region": "The resource region. If not defined, the provider region is used.", + } + + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "database_id": schema.Int64Attribute{ + Description: descriptions["database_id"], + Optional: true, + Computed: true, + }, + "instance_id": schema.StringAttribute{ + Description: descriptions["instance_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "owner": schema.StringAttribute{ + Description: descriptions["owner"], + Computed: true, + }, + "region": schema.StringAttribute{ + // the region cannot be found, so it has to be passed + Optional: true, + Description: descriptions["region"], + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *databaseDataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // validation for exactly one of database_id or name + isIdSet := !model.DatabaseId.IsNull() && !model.DatabaseId.IsUnknown() + isNameSet := !model.Name.IsNull() && !model.Name.IsUnknown() + + if (isIdSet && isNameSet) || (!isIdSet && !isNameSet) { + core.LogAndAddError( + ctx, &resp.Diagnostics, + "Invalid configuration", "Exactly one of 'database_id' or 'name' must be specified.", + ) + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + databaseId := model.DatabaseId.ValueInt64() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "database_id", databaseId) + ctx = tflog.SetField(ctx, "region", region) + + var databaseResp *postgresflexalpha.ListDatabase + var err error + + if isIdSet { + databaseId := model.DatabaseId.ValueInt64() + ctx = tflog.SetField(ctx, "database_id", databaseId) + databaseResp, err = getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) + } else { + databaseName := model.Name.ValueString() + ctx = tflog.SetField(ctx, "name", databaseName) + databaseResp, err = getDatabaseByName(ctx, r.client, projectId, region, instanceId, databaseName) + } + + if err != nil { + utils.LogError( + ctx, + &resp.Diagnostics, + err, + "Reading database", + fmt.Sprintf( + "Database with ID %q or instance with ID %q does not exist in project %q.", + databaseId, + instanceId, + projectId, + ), + map[int]string{ + http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), + }, + ) + resp.State.RemoveResource(ctx) + return + } + + ctx = core.LogResponse(ctx) + + // Map response body to schema and populate Computed attribute values + err = mapFields(databaseResp, &model, region) + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error reading database", + fmt.Sprintf("Processing API payload: %v", err), + ) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Postgres Flex database read") +}