diff --git a/docs/data-sources/postgresflex_flavors.md b/docs/data-sources/postgresflex_flavors.md
new file mode 100644
index 000000000..931144aa8
--- /dev/null
+++ b/docs/data-sources/postgresflex_flavors.md
@@ -0,0 +1,84 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "stackit_postgresflex_flavors Data Source - stackit"
+subcategory: ""
+description: |-
+ Postgres Flex flavors data source schema.
+---
+
+# stackit_postgresflex_flavors (Data Source)
+
+Postgres Flex flavors data source schema.
+
+## Example Usage
+
+```terraform
+data "stackit_postgresflex_flavors" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ # region is taken from the provider configuration
+}
+
+# Example usage with an instance
+resource "stackit_postgresflex_instance" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name = "example"
+ flavor_id = one([for flavor in data.stackit_postgresflex_flavors.example.flavors : flavor.id if flavor.cpu == 2 && flavor.memory == 4 && flavor.node_type == "Single"])
+ backup_schedule = "0 16 * * *"
+ storage = {
+ class = "premium-perf2-stackit"
+ size = 5
+ }
+ version = "17"
+ network = {
+ acl = ["192.168.0.0/24"]
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `project_id` (String) STACKIT project ID.
+
+### Optional
+
+- `region` (String) Postgres Flex flavors data source region. If undefined, the provider region is used.
+- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))
+
+### Read-Only
+
+- `flavors` (Attributes List) List of flavors available for the project. (see [below for nested schema](#nestedatt--flavors))
+- `id` (String) Terraform's internal data source ID, structured as "`project_id`,`region`".
+
+
+### Nested Schema for `timeouts`
+
+Optional:
+
+- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours).
+
+
+
+### Nested Schema for `flavors`
+
+Read-Only:
+
+- `cpu` (Number) CPU count of the instance.
+- `description` (String) Flavor description.
+- `id` (String) Flavor ID.
+- `max_gb` (Number) Maximum storage capacity available for the flavor in GB.
+- `memory` (Number) Memory of the instance in GiB.
+- `min_gb` (Number) Minimum storage capacity available for the flavor in GB.
+- `node_type` (String) Node type of the flavor, either single or replica.
+- `storage_classes` (Attributes List) Storage classes available for the flavor. (see [below for nested schema](#nestedatt--flavors--storage_classes))
+
+
+### Nested Schema for `flavors.storage_classes`
+
+Read-Only:
+
+- `class` (String) Storage class.
+- `max_io_per_sec` (Number) Maximum I/O operations per second.
+- `max_through_in_mb` (Number) Maximum throughput in MB per second.
diff --git a/examples/data-sources/stackit_postgresflex_flavors/data-source.tf b/examples/data-sources/stackit_postgresflex_flavors/data-source.tf
new file mode 100644
index 000000000..20787602f
--- /dev/null
+++ b/examples/data-sources/stackit_postgresflex_flavors/data-source.tf
@@ -0,0 +1,20 @@
+data "stackit_postgresflex_flavors" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ # region is taken from the provider configuration
+}
+
+# Example usage with an instance
+resource "stackit_postgresflex_instance" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name = "example"
+ flavor_id = one([for flavor in data.stackit_postgresflex_flavors.example.flavors : flavor.id if flavor.cpu == 2 && flavor.memory == 4 && flavor.node_type == "Single"])
+ backup_schedule = "0 16 * * *"
+ storage = {
+ class = "premium-perf2-stackit"
+ size = 5
+ }
+ version = "17"
+ network = {
+ acl = ["192.168.0.0/24"]
+ }
+}
diff --git a/stackit/internal/services/postgresflex/flavors/datasource.go b/stackit/internal/services/postgresflex/flavors/datasource.go
new file mode 100644
index 000000000..89ba99a3a
--- /dev/null
+++ b/stackit/internal/services/postgresflex/flavors/datasource.go
@@ -0,0 +1,264 @@
+package flavors
+
+import (
+ "context"
+ "fmt"
+ "slices"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+ postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api"
+
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+ postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+)
+
+var (
+ _ datasource.DataSource = new(flavors)
+ _ datasource.DataSourceWithConfigure = new(flavors)
+)
+
+type model struct {
+ ID types.String `tfsdk:"id"`
+ ProjectId types.String `tfsdk:"project_id"`
+ Region types.String `tfsdk:"region"`
+ Flavors []flavor `tfsdk:"flavors"`
+ Timeouts timeouts.Value `tfsdk:"timeouts"`
+}
+
+type flavor struct {
+ Id types.String `tfsdk:"id"`
+ Description types.String `tfsdk:"description"`
+ CPU types.Int64 `tfsdk:"cpu"`
+ Memory types.Int64 `tfsdk:"memory"`
+ MinGB types.Int32 `tfsdk:"min_gb"`
+ MaxGB types.Int32 `tfsdk:"max_gb"`
+ NodeType types.String `tfsdk:"node_type"`
+ StorageClasses []storageClass `tfsdk:"storage_classes"`
+}
+
+type storageClass struct {
+ Class types.String `tfsdk:"class"`
+ MaxIOPerSec types.Int32 `tfsdk:"max_io_per_sec"`
+ MaxThroughInMB types.Int32 `tfsdk:"max_through_in_mb"`
+}
+
+type flavors struct {
+ client *postgresflex.APIClient
+ providerData core.ProviderData
+}
+
+func NewFlavorsDataSource() datasource.DataSource {
+ return new(flavors)
+}
+
+func (f *flavors) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_postgresflex_flavors"
+}
+
+func (f *flavors) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ var ok bool
+ f.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
+ if !ok {
+ return
+ }
+
+ apiClient := postgresflexUtils.ConfigureClient(ctx, &f.providerData, &resp.Diagnostics)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ f.client = apiClient
+ tflog.Info(ctx, "Postgres Flex flavors client configured")
+}
+
+func (f *flavors) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ Description: "Postgres Flex flavors data source schema.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "Terraform's internal data source ID, structured as \"`project_id`,`region`\".",
+ Computed: true,
+ },
+ "project_id": schema.StringAttribute{
+ Description: "STACKIT project ID.",
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "region": schema.StringAttribute{
+ Description: "Postgres Flex flavors data source region. If undefined, the provider region is used.",
+ Optional: true,
+ Computed: true,
+ },
+ "timeouts": timeouts.Attributes(ctx),
+ "flavors": schema.ListNestedAttribute{
+ Description: "List of flavors available for the project.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "Flavor ID.",
+ Computed: true,
+ },
+ "description": schema.StringAttribute{
+ Description: "Flavor description.",
+ Computed: true,
+ },
+ "cpu": schema.Int64Attribute{
+ Description: "CPU count of the instance.",
+ Computed: true,
+ },
+ "memory": schema.Int64Attribute{
+ Description: "Memory of the instance in GiB.",
+ Computed: true,
+ },
+ "min_gb": schema.Int32Attribute{
+ Description: "Minimum storage capacity available for the flavor in GB.",
+ Computed: true,
+ },
+ "max_gb": schema.Int32Attribute{
+ Description: "Maximum storage capacity available for the flavor in GB.",
+ Computed: true,
+ },
+ "node_type": schema.StringAttribute{
+ Description: "Node type of the flavor, either single or replica.",
+ Computed: true,
+ },
+ "storage_classes": schema.ListNestedAttribute{
+ Description: "Storage classes available for the flavor.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "class": schema.StringAttribute{
+ Description: "Storage class.",
+ Computed: true,
+ },
+ "max_io_per_sec": schema.Int32Attribute{
+ Description: "Maximum I/O operations per second.",
+ Computed: true,
+ },
+ "max_through_in_mb": schema.Int32Attribute{
+ Description: "Maximum throughput in MB per second.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (f *flavors) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
+ var model model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &model)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ readTimeout, diags := model.Timeouts.Read(ctx, core.DefaultOperationTimeout)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ ctx, cancel := context.WithTimeout(ctx, readTimeout)
+ defer cancel()
+
+ projectId := model.ProjectId.ValueString()
+ region := f.providerData.GetRegionWithOverride(model.Region)
+ model.Region = types.StringValue(region)
+ ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
+ "project_id": projectId,
+ "region": region,
+ })
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ ctx = core.InitProviderContext(ctx)
+
+ const pageSize int64 = 100
+ flavorsResp, err := f.client.DefaultAPI.ListFlavors(ctx, projectId, region).Size(pageSize).Execute()
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Reading flavors", fmt.Sprintf("Calling ListFlavors: %v", err))
+ return
+ }
+ if flavorsResp == nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Reading flavors", "ListFlavors returned an empty response")
+ return
+ }
+ if flavorsResp.Pagination.TotalRows > pageSize {
+ core.LogAndAddWarning(ctx, &resp.Diagnostics,
+ "Truncated results",
+ fmt.Sprintf("Due to API limitations, only the first %d of %d available flavors are returned.", pageSize, flavorsResp.Pagination.TotalRows),
+ )
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ if err := mapFields(flavorsResp, &model); err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Reading flavors", fmt.Sprintf("Processing API payload: %v", err))
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, model)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ tflog.Info(ctx, "Postgres Flex flavors read")
+}
+
+func mapFields(resp *postgresflex.ListFlavorsResponse, m *model) error {
+ if resp == nil {
+ return fmt.Errorf("nil response")
+ }
+ if m == nil {
+ return fmt.Errorf("nil model")
+ }
+
+ m.ID = utils.BuildInternalTerraformId(m.ProjectId.ValueString(), m.Region.ValueString())
+ m.Flavors = make([]flavor, 0, len(resp.Flavors))
+
+ slices.SortFunc(resp.Flavors, func(a, b postgresflex.ListFlavors) int {
+ return strings.Compare(a.Id, b.Id)
+ })
+
+ for _, respFlavor := range resp.Flavors {
+ modelFlavor := flavor{
+ Id: types.StringValue(respFlavor.Id),
+ Description: types.StringValue(respFlavor.Description),
+ CPU: types.Int64Value(respFlavor.Cpu),
+ Memory: types.Int64Value(respFlavor.Memory),
+ MinGB: types.Int32Value(respFlavor.MinGB),
+ MaxGB: types.Int32Value(respFlavor.MaxGB),
+ NodeType: types.StringValue(respFlavor.NodeType),
+ }
+
+ slices.SortFunc(respFlavor.StorageClasses, func(a, b postgresflex.FlavorStorageClassesStorageClass) int {
+ return strings.Compare(a.Class, b.Class)
+ })
+
+ modelFlavor.StorageClasses = make([]storageClass, 0, len(respFlavor.StorageClasses))
+ for _, respStorageClass := range respFlavor.StorageClasses {
+ modelFlavor.StorageClasses = append(modelFlavor.StorageClasses, storageClass{
+ Class: types.StringValue(respStorageClass.Class),
+ MaxIOPerSec: types.Int32Value(respStorageClass.MaxIoPerSec),
+ MaxThroughInMB: types.Int32Value(respStorageClass.MaxThroughInMb),
+ })
+ }
+ m.Flavors = append(m.Flavors, modelFlavor)
+ }
+ return nil
+}
diff --git a/stackit/internal/services/postgresflex/flavors/datasource_test.go b/stackit/internal/services/postgresflex/flavors/datasource_test.go
new file mode 100644
index 000000000..55dbab17b
--- /dev/null
+++ b/stackit/internal/services/postgresflex/flavors/datasource_test.go
@@ -0,0 +1,155 @@
+package flavors
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3api"
+)
+
+func TestMapFields(t *testing.T) {
+ tests := []struct {
+ name string
+ response *postgresflex.ListFlavorsResponse
+ model *model
+ expected *model
+ valid bool
+ }{
+ {
+ name: "maps and sorts flavors and storage classes",
+ response: &postgresflex.ListFlavorsResponse{
+ Flavors: []postgresflex.ListFlavors{
+ {
+ Id: "flavor-2",
+ Description: "second",
+ Cpu: 4,
+ Memory: 8,
+ MinGB: 20,
+ MaxGB: 200,
+ NodeType: "Replica",
+ StorageClasses: []postgresflex.FlavorStorageClassesStorageClass{
+ {
+ Class: "class-2",
+ MaxIoPerSec: 2000,
+ MaxThroughInMb: 200,
+ },
+ {
+ Class: "class-1",
+ MaxIoPerSec: 1000,
+ MaxThroughInMb: 100,
+ },
+ },
+ },
+ {
+ Id: "flavor-1",
+ Description: "first",
+ Cpu: 2,
+ Memory: 4,
+ MinGB: 10,
+ MaxGB: 100,
+ NodeType: "Single",
+ StorageClasses: []postgresflex.FlavorStorageClassesStorageClass{
+ {
+ Class: "class-3",
+ MaxIoPerSec: 3000,
+ MaxThroughInMb: 300,
+ },
+ },
+ },
+ },
+ },
+ model: &model{
+ ProjectId: types.StringValue("project-id"),
+ Region: types.StringValue("eu01"),
+ },
+ expected: &model{
+ ID: types.StringValue("project-id,eu01"),
+ ProjectId: types.StringValue("project-id"),
+ Region: types.StringValue("eu01"),
+ Flavors: []flavor{
+ {
+ Id: types.StringValue("flavor-1"),
+ Description: types.StringValue("first"),
+ CPU: types.Int64Value(2),
+ Memory: types.Int64Value(4),
+ MinGB: types.Int32Value(10),
+ MaxGB: types.Int32Value(100),
+ NodeType: types.StringValue("Single"),
+ StorageClasses: []storageClass{
+ {
+ Class: types.StringValue("class-3"),
+ MaxIOPerSec: types.Int32Value(3000),
+ MaxThroughInMB: types.Int32Value(300),
+ },
+ },
+ },
+ {
+ Id: types.StringValue("flavor-2"),
+ Description: types.StringValue("second"),
+ CPU: types.Int64Value(4),
+ Memory: types.Int64Value(8),
+ MinGB: types.Int32Value(20),
+ MaxGB: types.Int32Value(200),
+ NodeType: types.StringValue("Replica"),
+ StorageClasses: []storageClass{
+ {
+ Class: types.StringValue("class-1"),
+ MaxIOPerSec: types.Int32Value(1000),
+ MaxThroughInMB: types.Int32Value(100),
+ },
+ {
+ Class: types.StringValue("class-2"),
+ MaxIOPerSec: types.Int32Value(2000),
+ MaxThroughInMB: types.Int32Value(200),
+ },
+ },
+ },
+ },
+ },
+ valid: true,
+ },
+ {
+ name: "maps empty response",
+ response: &postgresflex.ListFlavorsResponse{},
+ model: &model{
+ ProjectId: types.StringValue("project-id"),
+ Region: types.StringValue("eu01"),
+ },
+ expected: &model{
+ ID: types.StringValue("project-id,eu01"),
+ ProjectId: types.StringValue("project-id"),
+ Region: types.StringValue("eu01"),
+ Flavors: []flavor{},
+ },
+ valid: true,
+ },
+ {
+ name: "rejects nil response",
+ model: &model{
+ ProjectId: types.StringValue("project-id"),
+ Region: types.StringValue("eu01"),
+ },
+ valid: false,
+ },
+ {
+ name: "rejects nil model",
+ response: &postgresflex.ListFlavorsResponse{},
+ valid: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := mapFields(tt.response, tt.model)
+ if (err == nil) != tt.valid {
+ t.Fatalf("mapFields() error = %v, valid = %t", err, tt.valid)
+ }
+ if tt.valid {
+ if diff := cmp.Diff(tt.expected, tt.model); diff != "" {
+ t.Fatalf("mapFields() mismatch (-want +got):\n%s", diff)
+ }
+ }
+ })
+ }
+}
diff --git a/stackit/internal/services/postgresflex/postgresflex_acc_test.go b/stackit/internal/services/postgresflex/postgresflex_acc_test.go
index 7bbd9a7bf..a5922ec4b 100644
--- a/stackit/internal/services/postgresflex/postgresflex_acc_test.go
+++ b/stackit/internal/services/postgresflex/postgresflex_acc_test.go
@@ -868,6 +868,41 @@ func TestAccPostgresFlexUserMin(t *testing.T) {
})
}
+func TestAccPostgresFlexFlavorsDataSource(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ ConfigVariables: config.Variables{
+ "project_id": config.StringVariable(testutil.ProjectId),
+ },
+ Config: fmt.Sprintf(`
+ %s
+
+ variable project_id {}
+
+ data "stackit_postgresflex_flavors" "datasource" {
+ project_id = var.project_id
+ }`,
+ testutil.NewConfigBuilder().BuildProviderConfig(),
+ ),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet("data.stackit_postgresflex_flavors.datasource", "flavors.0.id"),
+ resource.TestCheckResourceAttrSet("data.stackit_postgresflex_flavors.datasource", "flavors.0.description"),
+ resource.TestCheckResourceAttrSet("data.stackit_postgresflex_flavors.datasource", "flavors.0.cpu"),
+ resource.TestCheckResourceAttrSet("data.stackit_postgresflex_flavors.datasource", "flavors.0.memory"),
+ resource.TestCheckResourceAttrSet("data.stackit_postgresflex_flavors.datasource", "flavors.0.min_gb"),
+ resource.TestCheckResourceAttrSet("data.stackit_postgresflex_flavors.datasource", "flavors.0.max_gb"),
+ resource.TestCheckResourceAttrSet("data.stackit_postgresflex_flavors.datasource", "flavors.0.node_type"),
+ resource.TestCheckResourceAttrSet("data.stackit_postgresflex_flavors.datasource", "flavors.0.storage_classes.0.class"),
+ resource.TestCheckResourceAttrSet("data.stackit_postgresflex_flavors.datasource", "flavors.0.storage_classes.0.max_io_per_sec"),
+ resource.TestCheckResourceAttrSet("data.stackit_postgresflex_flavors.datasource", "flavors.0.storage_classes.0.max_through_in_mb"),
+ ),
+ },
+ },
+ })
+}
+
func testCheckDestroy(s *terraform.State) error {
checkDestroyFuncs := []resource.TestCheckFunc{
testDatabaseDestroy,
diff --git a/stackit/provider.go b/stackit/provider.go
index b89bb0895..5c99aaa50 100644
--- a/stackit/provider.go
+++ b/stackit/provider.go
@@ -95,6 +95,7 @@ import (
openSearchCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/credential"
openSearchInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/instance"
postgresFlexDatabase "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/database"
+ postgresFlexFlavors "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/flavors"
postgresFlexInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/instance"
postgresFlexUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/user"
rabbitMQCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/credential"
@@ -724,6 +725,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
openSearchInstance.NewInstanceDataSource,
openSearchCredential.NewCredentialDataSource,
postgresFlexDatabase.NewDatabaseDataSource,
+ postgresFlexFlavors.NewFlavorsDataSource,
postgresFlexInstance.NewInstanceDataSource,
postgresFlexUser.NewUserDataSource,
rabbitMQInstance.NewInstanceDataSource,