diff --git a/docs/data-sources/pricing.md b/docs/data-sources/pricing.md
new file mode 100644
index 000000000..be9aa124b
--- /dev/null
+++ b/docs/data-sources/pricing.md
@@ -0,0 +1,268 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "hcloud_pricing Data Source - hcloud"
+subcategory: ""
+description: |-
+ Provides prices for the resources offered by the Hetzner Cloud.
+ The prices are returned for the Hetzner Cloud project the API token used to configure the provider belongs to.
+ See the Pricing API documentation https://docs.hetzner.cloud/reference/cloud#pricing for more details.
+---
+
+# hcloud_pricing (Data Source)
+
+Provides prices for the resources offered by the Hetzner Cloud.
+
+The prices are returned for the Hetzner Cloud project the API token used to configure the provider belongs to.
+
+See the [Pricing API documentation](https://docs.hetzner.cloud/reference/cloud#pricing) for more details.
+
+## Example Usage
+
+```terraform
+data "hcloud_pricing" "prices" {}
+
+output "monthly_traffic_price_per_tb" {
+ value = data.hcloud_pricing.prices.server_types[0].prices[0].per_tb_traffic.gross
+}
+```
+
+
+## Schema
+
+### Optional
+
+- `load_balancer_type` (String) Only return prices for the Load Balancer Type with this name (e.g. `lb11`).
+- `location` (String) Only return per-location prices for the location with this name (e.g. `fsn1`).
+- `server_type` (String) Only return prices for the Server Type with this name (e.g. `cx22`).
+
+### Read-Only
+
+- `currency` (String) Currency all prices are denominated in.
+- `floating_ips` (Attributes List) Prices of the Floating IPs, grouped by type. (see [below for nested schema](#nestedatt--floating_ips))
+- `image` (Attributes) Price of the disk space used by Images (Snapshots and Backups). (see [below for nested schema](#nestedatt--image))
+- `load_balancer_types` (Attributes List) Prices of the Load Balancer Types, grouped by type. (see [below for nested schema](#nestedatt--load_balancer_types))
+- `primary_ips` (Attributes List) Prices of the Primary IPs, grouped by type. (see [below for nested schema](#nestedatt--primary_ips))
+- `server_backup` (Attributes) Will increase base server costs by specific percentage if server backups are enabled. (see [below for nested schema](#nestedatt--server_backup))
+- `server_types` (Attributes List) Prices of the Server Types, grouped by type. (see [below for nested schema](#nestedatt--server_types))
+- `vat_rate` (String) VAT rate in percent used to calculate the gross prices from the net prices.
+- `volume` (Attributes) Price of the disk space used by Volumes. (see [below for nested schema](#nestedatt--volume))
+
+
+### Nested Schema for `floating_ips`
+
+Read-Only:
+
+- `prices` (Attributes List) Prices of the Floating IP type, per location. (see [below for nested schema](#nestedatt--floating_ips--prices))
+- `type` (String) Type of the Floating IP (`ipv4` or `ipv6`).
+
+
+### Nested Schema for `floating_ips.prices`
+
+Read-Only:
+
+- `location` (String) Name of the location the price applies to.
+- `monthly` (Attributes) Monthly costs for a Floating IP type in this location. (see [below for nested schema](#nestedatt--floating_ips--prices--monthly))
+
+
+### Nested Schema for `floating_ips.prices.monthly`
+
+Read-Only:
+
+- `currency` (String) Currency the price is denominated in.
+- `gross` (String) Price with VAT added.
+- `net` (String) Price without VAT.
+- `vat_rate` (String) VAT rate in percent used to calculate the gross price from the net price.
+
+
+
+
+
+### Nested Schema for `image`
+
+Read-Only:
+
+- `per_gb_month` (Attributes) Price per GB and month. (see [below for nested schema](#nestedatt--image--per_gb_month))
+
+
+### Nested Schema for `image.per_gb_month`
+
+Read-Only:
+
+- `currency` (String) Currency the price is denominated in.
+- `gross` (String) Price with VAT added.
+- `net` (String) Price without VAT.
+- `vat_rate` (String) VAT rate in percent used to calculate the gross price from the net price.
+
+
+
+
+### Nested Schema for `load_balancer_types`
+
+Read-Only:
+
+- `id` (Number) ID of the Load Balancer Type.
+- `name` (String) Name of the Load Balancer Type.
+- `prices` (Attributes List) Prices of the Load Balancer Type, per location. (see [below for nested schema](#nestedatt--load_balancer_types--prices))
+
+
+### Nested Schema for `load_balancer_types.prices`
+
+Read-Only:
+
+- `hourly` (Attributes) Hourly costs for a Load Balancer Type in this location. (see [below for nested schema](#nestedatt--load_balancer_types--prices--hourly))
+- `included_traffic` (Number) Free traffic per month in bytes.
+- `location` (String) Name of the location the price applies to.
+- `monthly` (Attributes) Monthly costs for a Load Balancer Type in this location. (see [below for nested schema](#nestedatt--load_balancer_types--prices--monthly))
+- `per_tb_traffic` (Attributes) Costs per additional TB of traffic in this location. (see [below for nested schema](#nestedatt--load_balancer_types--prices--per_tb_traffic))
+
+
+### Nested Schema for `load_balancer_types.prices.hourly`
+
+Read-Only:
+
+- `currency` (String) Currency the price is denominated in.
+- `gross` (String) Price with VAT added.
+- `net` (String) Price without VAT.
+- `vat_rate` (String) VAT rate in percent used to calculate the gross price from the net price.
+
+
+
+### Nested Schema for `load_balancer_types.prices.monthly`
+
+Read-Only:
+
+- `currency` (String) Currency the price is denominated in.
+- `gross` (String) Price with VAT added.
+- `net` (String) Price without VAT.
+- `vat_rate` (String) VAT rate in percent used to calculate the gross price from the net price.
+
+
+
+### Nested Schema for `load_balancer_types.prices.per_tb_traffic`
+
+Read-Only:
+
+- `currency` (String) Currency the price is denominated in.
+- `gross` (String) Price with VAT added.
+- `net` (String) Price without VAT.
+- `vat_rate` (String) VAT rate in percent used to calculate the gross price from the net price.
+
+
+
+
+
+### Nested Schema for `primary_ips`
+
+Read-Only:
+
+- `prices` (Attributes List) Prices of the Primary IP type, per location. (see [below for nested schema](#nestedatt--primary_ips--prices))
+- `type` (String) Type of the Primary IP (`ipv4` or `ipv6`).
+
+
+### Nested Schema for `primary_ips.prices`
+
+Read-Only:
+
+- `hourly` (Attributes) Hourly costs for a Primary IP type in this location. (see [below for nested schema](#nestedatt--primary_ips--prices--hourly))
+- `location` (String) Name of the location the price applies to.
+- `monthly` (Attributes) Monthly costs for a Primary IP type in this location. (see [below for nested schema](#nestedatt--primary_ips--prices--monthly))
+
+
+### Nested Schema for `primary_ips.prices.hourly`
+
+Read-Only:
+
+- `gross` (String) Price with VAT added.
+- `net` (String) Price without VAT.
+
+
+
+### Nested Schema for `primary_ips.prices.monthly`
+
+Read-Only:
+
+- `gross` (String) Price with VAT added.
+- `net` (String) Price without VAT.
+
+
+
+
+
+### Nested Schema for `server_backup`
+
+Read-Only:
+
+- `percentage` (String) Percentage by which the base server costs increase if server backups are enabled.
+
+
+
+### Nested Schema for `server_types`
+
+Read-Only:
+
+- `id` (Number) ID of the Server Type.
+- `name` (String) Name of the Server Type.
+- `prices` (Attributes List) Prices of the Server Type, per location. (see [below for nested schema](#nestedatt--server_types--prices))
+
+
+### Nested Schema for `server_types.prices`
+
+Read-Only:
+
+- `hourly` (Attributes) Hourly costs for a Server Type in this location. (see [below for nested schema](#nestedatt--server_types--prices--hourly))
+- `included_traffic` (Number) Free traffic per month in bytes.
+- `location` (String) Name of the location the price applies to.
+- `monthly` (Attributes) Monthly costs for a Server Type in this location. (see [below for nested schema](#nestedatt--server_types--prices--monthly))
+- `per_tb_traffic` (Attributes) Costs per additional TB of traffic in this location. (see [below for nested schema](#nestedatt--server_types--prices--per_tb_traffic))
+
+
+### Nested Schema for `server_types.prices.hourly`
+
+Read-Only:
+
+- `currency` (String) Currency the price is denominated in.
+- `gross` (String) Price with VAT added.
+- `net` (String) Price without VAT.
+- `vat_rate` (String) VAT rate in percent used to calculate the gross price from the net price.
+
+
+
+### Nested Schema for `server_types.prices.monthly`
+
+Read-Only:
+
+- `currency` (String) Currency the price is denominated in.
+- `gross` (String) Price with VAT added.
+- `net` (String) Price without VAT.
+- `vat_rate` (String) VAT rate in percent used to calculate the gross price from the net price.
+
+
+
+### Nested Schema for `server_types.prices.per_tb_traffic`
+
+Read-Only:
+
+- `currency` (String) Currency the price is denominated in.
+- `gross` (String) Price with VAT added.
+- `net` (String) Price without VAT.
+- `vat_rate` (String) VAT rate in percent used to calculate the gross price from the net price.
+
+
+
+
+
+### Nested Schema for `volume`
+
+Read-Only:
+
+- `per_gb_month` (Attributes) Price per GB and month. (see [below for nested schema](#nestedatt--volume--per_gb_month))
+
+
+### Nested Schema for `volume.per_gb_month`
+
+Read-Only:
+
+- `currency` (String) Currency the price is denominated in.
+- `gross` (String) Price with VAT added.
+- `net` (String) Price without VAT.
+- `vat_rate` (String) VAT rate in percent used to calculate the gross price from the net price.
diff --git a/examples/data-sources/hcloud_pricing/data-source.tf b/examples/data-sources/hcloud_pricing/data-source.tf
new file mode 100644
index 000000000..ec0269659
--- /dev/null
+++ b/examples/data-sources/hcloud_pricing/data-source.tf
@@ -0,0 +1,5 @@
+data "hcloud_pricing" "prices" {}
+
+output "monthly_traffic_price_per_tb" {
+ value = data.hcloud_pricing.prices.server_types[0].prices[0].per_tb_traffic.gross
+}
diff --git a/hcloud/plugin_provider.go b/hcloud/plugin_provider.go
index b928decda..4009695d5 100644
--- a/hcloud/plugin_provider.go
+++ b/hcloud/plugin_provider.go
@@ -25,6 +25,7 @@ import (
"github.com/hetznercloud/terraform-provider-hcloud/internal/loadbalancer"
"github.com/hetznercloud/terraform-provider-hcloud/internal/loadbalancertype"
"github.com/hetznercloud/terraform-provider-hcloud/internal/location"
+ "github.com/hetznercloud/terraform-provider-hcloud/internal/pricing"
"github.com/hetznercloud/terraform-provider-hcloud/internal/primaryip"
"github.com/hetznercloud/terraform-provider-hcloud/internal/rdns"
"github.com/hetznercloud/terraform-provider-hcloud/internal/server"
@@ -216,6 +217,7 @@ func (p *PluginProvider) DataSources(_ context.Context) []func() datasource.Data
loadbalancertype.NewDataSourceList,
location.NewDataSource,
location.NewDataSourceList,
+ pricing.NewDataSource,
primaryip.NewDataSource,
primaryip.NewDataSourceList,
servertype.NewDataSource,
diff --git a/internal/pricing/data_source.go b/internal/pricing/data_source.go
new file mode 100644
index 000000000..b6d0ad84b
--- /dev/null
+++ b/internal/pricing/data_source.go
@@ -0,0 +1,264 @@
+package pricing
+
+import (
+ "context"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+
+ "github.com/hetznercloud/hcloud-go/v2/hcloud"
+ "github.com/hetznercloud/terraform-provider-hcloud/internal/util/hcloudutil"
+)
+
+// DataSourceType is the type name of the Hetzner Cloud Pricing data source.
+const DataSourceType = "hcloud_pricing"
+
+var _ datasource.DataSource = (*DataSource)(nil)
+var _ datasource.DataSourceWithConfigure = (*DataSource)(nil)
+
+type DataSource struct {
+ client *hcloud.Client
+}
+
+func NewDataSource() datasource.DataSource {
+ return &DataSource{}
+}
+
+// Metadata should return the full name of the data source.
+func (d *DataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = DataSourceType
+}
+
+// Configure enables provider-level data or clients to be set in the
+// provider-defined DataSource type. It is separately executed for each
+// ReadDataSource RPC.
+func (d *DataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ var newDiags diag.Diagnostics
+
+ d.client, newDiags = hcloudutil.ConfigureClient(req.ProviderData)
+ resp.Diagnostics.Append(newDiags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+}
+
+// priceSchema returns the schema for a net/gross price including the currency
+// and VAT rate.
+func priceSchema(description string) schema.SingleNestedAttribute {
+ return schema.SingleNestedAttribute{
+ MarkdownDescription: description,
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "currency": schema.StringAttribute{
+ MarkdownDescription: "Currency the price is denominated in.",
+ Computed: true,
+ },
+ "vat_rate": schema.StringAttribute{
+ MarkdownDescription: "VAT rate in percent used to calculate the gross price from the net price.",
+ Computed: true,
+ },
+ "net": schema.StringAttribute{
+ MarkdownDescription: "Price without VAT.",
+ Computed: true,
+ },
+ "gross": schema.StringAttribute{
+ MarkdownDescription: "Price with VAT added.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+// primaryIPPriceSchema returns the schema for a net/gross Primary IP price.
+func primaryIPPriceSchema(description string) schema.SingleNestedAttribute {
+ return schema.SingleNestedAttribute{
+ MarkdownDescription: description,
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "net": schema.StringAttribute{
+ MarkdownDescription: "Price without VAT.",
+ Computed: true,
+ },
+ "gross": schema.StringAttribute{
+ MarkdownDescription: "Price with VAT added.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+// perGBPricingSchema returns the schema for a per GB and month price.
+func perGBPricingSchema(description string) schema.SingleNestedAttribute {
+ return schema.SingleNestedAttribute{
+ MarkdownDescription: description,
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "per_gb_month": priceSchema("Price per GB and month."),
+ },
+ }
+}
+
+// Schema should return the schema for this data source.
+func (d *DataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema.MarkdownDescription = `
+Provides prices for the resources offered by the Hetzner Cloud.
+
+The prices are returned for the Hetzner Cloud project the API token used to configure the provider belongs to.
+
+See the [Pricing API documentation](https://docs.hetzner.cloud/reference/cloud#pricing) for more details.
+`
+
+ resp.Schema.Attributes = map[string]schema.Attribute{
+ "location": schema.StringAttribute{
+ MarkdownDescription: "Only return per-location prices for the location with this name (e.g. `fsn1`).",
+ Optional: true,
+ },
+ "server_type": schema.StringAttribute{
+ MarkdownDescription: "Only return prices for the Server Type with this name (e.g. `cx22`).",
+ Optional: true,
+ },
+ "load_balancer_type": schema.StringAttribute{
+ MarkdownDescription: "Only return prices for the Load Balancer Type with this name (e.g. `lb11`).",
+ Optional: true,
+ },
+ "currency": schema.StringAttribute{
+ MarkdownDescription: "Currency all prices are denominated in.",
+ Computed: true,
+ },
+ "vat_rate": schema.StringAttribute{
+ MarkdownDescription: "VAT rate in percent used to calculate the gross prices from the net prices.",
+ Computed: true,
+ },
+ "image": perGBPricingSchema("Price of the disk space used by Images (Snapshots and Backups)."),
+ "floating_ips": schema.ListNestedAttribute{
+ MarkdownDescription: "Prices of the Floating IPs, grouped by type.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "type": schema.StringAttribute{
+ MarkdownDescription: "Type of the Floating IP (`ipv4` or `ipv6`).",
+ Computed: true,
+ },
+ "prices": schema.ListNestedAttribute{
+ MarkdownDescription: "Prices of the Floating IP type, per location.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "location": schema.StringAttribute{
+ MarkdownDescription: "Name of the location the price applies to.",
+ Computed: true,
+ },
+ "monthly": priceSchema("Monthly costs for a Floating IP type in this location."),
+ },
+ },
+ },
+ },
+ },
+ },
+ "primary_ips": schema.ListNestedAttribute{
+ MarkdownDescription: "Prices of the Primary IPs, grouped by type.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "type": schema.StringAttribute{
+ MarkdownDescription: "Type of the Primary IP (`ipv4` or `ipv6`).",
+ Computed: true,
+ },
+ "prices": schema.ListNestedAttribute{
+ MarkdownDescription: "Prices of the Primary IP type, per location.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "location": schema.StringAttribute{
+ MarkdownDescription: "Name of the location the price applies to.",
+ Computed: true,
+ },
+ "hourly": primaryIPPriceSchema("Hourly costs for a Primary IP type in this location."),
+ "monthly": primaryIPPriceSchema("Monthly costs for a Primary IP type in this location."),
+ },
+ },
+ },
+ },
+ },
+ },
+ "server_backup": schema.SingleNestedAttribute{
+ MarkdownDescription: "Will increase base server costs by specific percentage if server backups are enabled.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "percentage": schema.StringAttribute{
+ MarkdownDescription: "Percentage by which the base server costs increase if server backups are enabled.",
+ Computed: true,
+ },
+ },
+ },
+ "server_types": typePricingSchema("Server Type"),
+ "load_balancer_types": typePricingSchema("Load Balancer Type"),
+ "volume": perGBPricingSchema("Price of the disk space used by Volumes."),
+ }
+}
+
+// typePricingSchema returns the schema for the per-location pricing of a Server
+// Type or Load Balancer Type.
+func typePricingSchema(name string) schema.ListNestedAttribute {
+ return schema.ListNestedAttribute{
+ MarkdownDescription: "Prices of the " + name + "s, grouped by type.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "ID of the " + name + ".",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "Name of the " + name + ".",
+ Computed: true,
+ },
+ "prices": schema.ListNestedAttribute{
+ MarkdownDescription: "Prices of the " + name + ", per location.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "location": schema.StringAttribute{
+ MarkdownDescription: "Name of the location the price applies to.",
+ Computed: true,
+ },
+ "hourly": priceSchema("Hourly costs for a " + name + " in this location."),
+ "monthly": priceSchema("Monthly costs for a " + name + " in this location."),
+ "included_traffic": schema.Int64Attribute{
+ MarkdownDescription: "Free traffic per month in bytes.",
+ Computed: true,
+ },
+ "per_tb_traffic": priceSchema("Costs per additional TB of traffic in this location."),
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+// Read is called when the provider must read data source values in
+// order to update state. Config values should be read from the
+// ReadRequest and new state values set on the ReadResponse.
+func (d *DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data model
+
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ result, _, err := d.client.Pricing.Get(ctx)
+ if err != nil {
+ resp.Diagnostics.Append(hcloudutil.APIErrorDiagnostics(err)...)
+ return
+ }
+
+ resp.Diagnostics.Append(data.FromAPI(ctx, filterPricing(result, data))...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/pricing/data_source_test.go b/internal/pricing/data_source_test.go
new file mode 100644
index 000000000..782c9a7d2
--- /dev/null
+++ b/internal/pricing/data_source_test.go
@@ -0,0 +1,47 @@
+package pricing_test
+
+import (
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+
+ "github.com/hetznercloud/terraform-provider-hcloud/internal/pricing"
+ "github.com/hetznercloud/terraform-provider-hcloud/internal/teste2e"
+ "github.com/hetznercloud/terraform-provider-hcloud/internal/testmux"
+ "github.com/hetznercloud/terraform-provider-hcloud/internal/testtemplate"
+)
+
+func TestAccPricingDataSource(t *testing.T) {
+ tmplMan := testtemplate.Manager{}
+
+ res := &pricing.DData{}
+ res.SetRName("test")
+
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: teste2e.PreCheck(t),
+ ProtoV6ProviderFactories: testmux.ProtoV6ProviderFactories(),
+ Steps: []resource.TestStep{
+ {
+ Config: tmplMan.Render(t,
+ "testdata/d/hcloud_pricing", res,
+ ),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttrSet(res.TFID(), "currency"),
+ resource.TestCheckResourceAttrSet(res.TFID(), "vat_rate"),
+ resource.TestCheckResourceAttrSet(res.TFID(), "image.per_gb_month.net"),
+ resource.TestCheckResourceAttrSet(res.TFID(), "server_backup.percentage"),
+ resource.TestCheckResourceAttrSet(res.TFID(), "volume.per_gb_month.net"),
+ resource.TestCheckResourceAttrSet(res.TFID(), "floating_ips.0.type"),
+ resource.TestCheckResourceAttrSet(res.TFID(), "floating_ips.0.prices.0.location"),
+ resource.TestCheckResourceAttrSet(res.TFID(), "primary_ips.0.type"),
+ resource.TestCheckResourceAttrSet(res.TFID(), "primary_ips.0.prices.0.hourly.net"),
+ resource.TestCheckResourceAttrSet(res.TFID(), "server_types.0.name"),
+ resource.TestCheckResourceAttrSet(res.TFID(), "server_types.0.prices.0.monthly.net"),
+ resource.TestCheckResourceAttrSet(res.TFID(), "server_types.0.prices.0.included_traffic"),
+ resource.TestCheckResourceAttrSet(res.TFID(), "load_balancer_types.0.name"),
+ resource.TestCheckResourceAttrSet(res.TFID(), "load_balancer_types.0.prices.0.monthly.net"),
+ ),
+ },
+ },
+ })
+}
diff --git a/internal/pricing/model.go b/internal/pricing/model.go
new file mode 100644
index 000000000..789d1924e
--- /dev/null
+++ b/internal/pricing/model.go
@@ -0,0 +1,634 @@
+package pricing
+
+import (
+ "context"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+
+ "github.com/hetznercloud/hcloud-go/v2/hcloud"
+ "github.com/hetznercloud/terraform-provider-hcloud/internal/util"
+)
+
+// model is the top level model for the Hetzner Cloud pricing data source.
+type model struct {
+ // Filters
+ Location types.String `tfsdk:"location"`
+ ServerType types.String `tfsdk:"server_type"`
+ LoadBalancerType types.String `tfsdk:"load_balancer_type"`
+
+ Currency types.String `tfsdk:"currency"`
+ VATRate types.String `tfsdk:"vat_rate"`
+ Image types.Object `tfsdk:"image"`
+ FloatingIPs types.List `tfsdk:"floating_ips"`
+ PrimaryIPs types.List `tfsdk:"primary_ips"`
+ ServerBackup types.Object `tfsdk:"server_backup"`
+ ServerTypes types.List `tfsdk:"server_types"`
+ LoadBalancerTypes types.List `tfsdk:"load_balancer_types"`
+ Volume types.Object `tfsdk:"volume"`
+}
+
+var _ util.ModelFromAPI[hcloud.Pricing] = &model{}
+
+var (
+ _ util.ModelFromAPI[hcloud.Price] = (*priceModel)(nil)
+ _ util.ModelToTerraform[types.Object] = (*priceModel)(nil)
+ _ util.ModelFromAPI[hcloud.PrimaryIPPrice] = (*primaryIPPriceModel)(nil)
+ _ util.ModelToTerraform[types.Object] = (*primaryIPPriceModel)(nil)
+ _ util.ModelFromAPI[hcloud.Price] = (*perGBPricingModel)(nil)
+ _ util.ModelToTerraform[types.Object] = (*perGBPricingModel)(nil)
+ _ util.ModelFromAPI[hcloud.ServerBackupPricing] = (*serverBackupPricingModel)(nil)
+ _ util.ModelToTerraform[types.Object] = (*serverBackupPricingModel)(nil)
+ _ util.ModelFromAPI[hcloud.FloatingIPTypePricing] = (*floatingIPPricingModel)(nil)
+ _ util.ModelToTerraform[types.Object] = (*floatingIPPricingModel)(nil)
+ _ util.ModelFromAPI[hcloud.FloatingIPTypeLocationPricing] = (*floatingIPLocationPricingModel)(nil)
+ _ util.ModelToTerraform[types.Object] = (*floatingIPLocationPricingModel)(nil)
+ _ util.ModelFromAPI[hcloud.PrimaryIPPricing] = (*primaryIPPricingModel)(nil)
+ _ util.ModelToTerraform[types.Object] = (*primaryIPPricingModel)(nil)
+ _ util.ModelFromAPI[hcloud.PrimaryIPTypePricing] = (*primaryIPLocationPricingModel)(nil)
+ _ util.ModelToTerraform[types.Object] = (*primaryIPLocationPricingModel)(nil)
+ _ util.ModelToTerraform[types.Object] = (*typePricingModel)(nil)
+ _ util.ModelToTerraform[types.Object] = (*typeLocationPricingModel)(nil)
+)
+
+func (m *model) FromAPI(ctx context.Context, hc hcloud.Pricing) diag.Diagnostics {
+ var diags diag.Diagnostics
+ var newDiags diag.Diagnostics
+
+ m.Currency = types.StringValue(hc.Currency)
+ m.VATRate = types.StringValue(hc.VATRate)
+
+ {
+ value := perGBPricingModel{}
+ diags.Append(value.FromAPI(ctx, hc.Image.PerGBMonth)...)
+
+ m.Image, newDiags = value.ToTerraform(ctx)
+ diags.Append(newDiags...)
+ }
+
+ {
+ value := perGBPricingModel{}
+ diags.Append(value.FromAPI(ctx, hc.Volume.PerGBMonthly)...)
+
+ m.Volume, newDiags = value.ToTerraform(ctx)
+ diags.Append(newDiags...)
+ }
+
+ {
+ value := serverBackupPricingModel{}
+ diags.Append(value.FromAPI(ctx, hc.ServerBackup)...)
+
+ m.ServerBackup, newDiags = value.ToTerraform(ctx)
+ diags.Append(newDiags...)
+ }
+
+ m.FloatingIPs, newDiags = listValueFromAPI(ctx, (&floatingIPPricingModel{}).tfType(), hc.FloatingIPs, floatingIPPricingObjectFromAPI)
+ diags.Append(newDiags...)
+
+ m.PrimaryIPs, newDiags = listValueFromAPI(ctx, (&primaryIPPricingModel{}).tfType(), hc.PrimaryIPs, primaryIPPricingObjectFromAPI)
+ diags.Append(newDiags...)
+
+ m.ServerTypes, newDiags = listValueFromAPI(ctx, (&typePricingModel{}).tfType(), hc.ServerTypes, serverTypePricingObjectFromAPI)
+ diags.Append(newDiags...)
+
+ m.LoadBalancerTypes, newDiags = listValueFromAPI(ctx, (&typePricingModel{}).tfType(), hc.LoadBalancerTypes, loadBalancerTypePricingObjectFromAPI)
+ diags.Append(newDiags...)
+
+ return diags
+}
+
+// priceModel represents a net/gross price including the currency and VAT rate.
+type priceModel struct {
+ Currency types.String `tfsdk:"currency"`
+ VATRate types.String `tfsdk:"vat_rate"`
+ Net types.String `tfsdk:"net"`
+ Gross types.String `tfsdk:"gross"`
+}
+
+func (m *priceModel) tfAttributesTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "currency": types.StringType,
+ "vat_rate": types.StringType,
+ "net": types.StringType,
+ "gross": types.StringType,
+ }
+}
+
+func (m *priceModel) tfType() attr.Type {
+ return basetypes.ObjectType{AttrTypes: m.tfAttributesTypes()}
+}
+
+func (m *priceModel) FromAPI(_ context.Context, hc hcloud.Price) diag.Diagnostics {
+ m.Currency = types.StringValue(hc.Currency)
+ m.VATRate = types.StringValue(hc.VATRate)
+ m.Net = types.StringValue(hc.Net)
+ m.Gross = types.StringValue(hc.Gross)
+
+ return nil
+}
+
+func (m *priceModel) ToTerraform(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, m.tfAttributesTypes(), m)
+}
+
+// priceObjectFromAPI converts a single hcloud.Price into its Terraform object.
+func priceObjectFromAPI(ctx context.Context, hc hcloud.Price) (types.Object, diag.Diagnostics) {
+ value := priceModel{}
+ diags := value.FromAPI(ctx, hc)
+
+ obj, newDiags := value.ToTerraform(ctx)
+ diags.Append(newDiags...)
+
+ return obj, diags
+}
+
+// primaryIPPriceModel represents a net/gross price for Primary IPs. Unlike
+// priceModel it does not include the currency or VAT rate.
+type primaryIPPriceModel struct {
+ Net types.String `tfsdk:"net"`
+ Gross types.String `tfsdk:"gross"`
+}
+
+func (m *primaryIPPriceModel) tfAttributesTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "net": types.StringType,
+ "gross": types.StringType,
+ }
+}
+
+func (m *primaryIPPriceModel) tfType() attr.Type {
+ return basetypes.ObjectType{AttrTypes: m.tfAttributesTypes()}
+}
+
+func (m *primaryIPPriceModel) FromAPI(_ context.Context, hc hcloud.PrimaryIPPrice) diag.Diagnostics {
+ m.Net = types.StringValue(hc.Net)
+ m.Gross = types.StringValue(hc.Gross)
+
+ return nil
+}
+
+func (m *primaryIPPriceModel) ToTerraform(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, m.tfAttributesTypes(), m)
+}
+
+func primaryIPPriceObjectFromAPI(ctx context.Context, hc hcloud.PrimaryIPPrice) (types.Object, diag.Diagnostics) {
+ value := primaryIPPriceModel{}
+ diags := value.FromAPI(ctx, hc)
+
+ obj, newDiags := value.ToTerraform(ctx)
+ diags.Append(newDiags...)
+
+ return obj, diags
+}
+
+// perGBPricingModel represents a per GB and month price, used by Images and
+// Volumes.
+type perGBPricingModel struct {
+ PerGBMonth types.Object `tfsdk:"per_gb_month"`
+}
+
+func (m *perGBPricingModel) tfAttributesTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "per_gb_month": (&priceModel{}).tfType(),
+ }
+}
+
+func (m *perGBPricingModel) FromAPI(ctx context.Context, hc hcloud.Price) diag.Diagnostics {
+ var diags diag.Diagnostics
+ var newDiags diag.Diagnostics
+
+ m.PerGBMonth, newDiags = priceObjectFromAPI(ctx, hc)
+ diags.Append(newDiags...)
+
+ return diags
+}
+
+func (m *perGBPricingModel) ToTerraform(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, m.tfAttributesTypes(), m)
+}
+
+// serverBackupPricingModel represents the surcharge for server backups.
+type serverBackupPricingModel struct {
+ Percentage types.String `tfsdk:"percentage"`
+}
+
+func (m *serverBackupPricingModel) tfAttributesTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "percentage": types.StringType,
+ }
+}
+
+func (m *serverBackupPricingModel) FromAPI(_ context.Context, hc hcloud.ServerBackupPricing) diag.Diagnostics {
+ m.Percentage = types.StringValue(hc.Percentage)
+
+ return nil
+}
+
+func (m *serverBackupPricingModel) ToTerraform(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, m.tfAttributesTypes(), m)
+}
+
+// floatingIPPricingModel represents the pricing of a Floating IP type across
+// all locations.
+type floatingIPPricingModel struct {
+ Type types.String `tfsdk:"type"`
+ Prices types.List `tfsdk:"prices"`
+}
+
+func (m *floatingIPPricingModel) tfAttributesTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "type": types.StringType,
+ "prices": types.ListType{ElemType: (&floatingIPLocationPricingModel{}).tfType()},
+ }
+}
+
+func (m *floatingIPPricingModel) tfType() attr.Type {
+ return basetypes.ObjectType{AttrTypes: m.tfAttributesTypes()}
+}
+
+func (m *floatingIPPricingModel) FromAPI(ctx context.Context, hc hcloud.FloatingIPTypePricing) diag.Diagnostics {
+ var diags diag.Diagnostics
+ var newDiags diag.Diagnostics
+
+ m.Type = types.StringValue(string(hc.Type))
+
+ m.Prices, newDiags = listValueFromAPI(ctx, (&floatingIPLocationPricingModel{}).tfType(), hc.Pricings, floatingIPLocationPricingObjectFromAPI)
+ diags.Append(newDiags...)
+
+ return diags
+}
+
+func (m *floatingIPPricingModel) ToTerraform(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, m.tfAttributesTypes(), m)
+}
+
+func floatingIPPricingObjectFromAPI(ctx context.Context, hc hcloud.FloatingIPTypePricing) (types.Object, diag.Diagnostics) {
+ value := floatingIPPricingModel{}
+ diags := value.FromAPI(ctx, hc)
+
+ obj, newDiags := value.ToTerraform(ctx)
+ diags.Append(newDiags...)
+
+ return obj, diags
+}
+
+// floatingIPLocationPricingModel represents the pricing of a Floating IP type
+// at a single location.
+type floatingIPLocationPricingModel struct {
+ Location types.String `tfsdk:"location"`
+ Monthly types.Object `tfsdk:"monthly"`
+}
+
+func (m *floatingIPLocationPricingModel) tfAttributesTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "location": types.StringType,
+ "monthly": (&priceModel{}).tfType(),
+ }
+}
+
+func (m *floatingIPLocationPricingModel) tfType() attr.Type {
+ return basetypes.ObjectType{AttrTypes: m.tfAttributesTypes()}
+}
+
+func (m *floatingIPLocationPricingModel) FromAPI(ctx context.Context, hc hcloud.FloatingIPTypeLocationPricing) diag.Diagnostics {
+ var diags diag.Diagnostics
+ var newDiags diag.Diagnostics
+
+ m.Location = types.StringValue(locationName(hc.Location))
+
+ m.Monthly, newDiags = priceObjectFromAPI(ctx, hc.Monthly)
+ diags.Append(newDiags...)
+
+ return diags
+}
+
+func (m *floatingIPLocationPricingModel) ToTerraform(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, m.tfAttributesTypes(), m)
+}
+
+func floatingIPLocationPricingObjectFromAPI(ctx context.Context, hc hcloud.FloatingIPTypeLocationPricing) (types.Object, diag.Diagnostics) {
+ value := floatingIPLocationPricingModel{}
+ diags := value.FromAPI(ctx, hc)
+
+ obj, newDiags := value.ToTerraform(ctx)
+ diags.Append(newDiags...)
+
+ return obj, diags
+}
+
+// primaryIPPricingModel represents the pricing of a Primary IP type across all
+// locations.
+type primaryIPPricingModel struct {
+ Type types.String `tfsdk:"type"`
+ Prices types.List `tfsdk:"prices"`
+}
+
+func (m *primaryIPPricingModel) tfAttributesTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "type": types.StringType,
+ "prices": types.ListType{ElemType: (&primaryIPLocationPricingModel{}).tfType()},
+ }
+}
+
+func (m *primaryIPPricingModel) tfType() attr.Type {
+ return basetypes.ObjectType{AttrTypes: m.tfAttributesTypes()}
+}
+
+func (m *primaryIPPricingModel) FromAPI(ctx context.Context, hc hcloud.PrimaryIPPricing) diag.Diagnostics {
+ var diags diag.Diagnostics
+ var newDiags diag.Diagnostics
+
+ m.Type = types.StringValue(hc.Type)
+
+ m.Prices, newDiags = listValueFromAPI(ctx, (&primaryIPLocationPricingModel{}).tfType(), hc.Pricings, primaryIPLocationPricingObjectFromAPI)
+ diags.Append(newDiags...)
+
+ return diags
+}
+
+func (m *primaryIPPricingModel) ToTerraform(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, m.tfAttributesTypes(), m)
+}
+
+func primaryIPPricingObjectFromAPI(ctx context.Context, hc hcloud.PrimaryIPPricing) (types.Object, diag.Diagnostics) {
+ value := primaryIPPricingModel{}
+ diags := value.FromAPI(ctx, hc)
+
+ obj, newDiags := value.ToTerraform(ctx)
+ diags.Append(newDiags...)
+
+ return obj, diags
+}
+
+// primaryIPLocationPricingModel represents the pricing of a Primary IP type at
+// a single location.
+type primaryIPLocationPricingModel struct {
+ Location types.String `tfsdk:"location"`
+ Hourly types.Object `tfsdk:"hourly"`
+ Monthly types.Object `tfsdk:"monthly"`
+}
+
+func (m *primaryIPLocationPricingModel) tfAttributesTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "location": types.StringType,
+ "hourly": (&primaryIPPriceModel{}).tfType(),
+ "monthly": (&primaryIPPriceModel{}).tfType(),
+ }
+}
+
+func (m *primaryIPLocationPricingModel) tfType() attr.Type {
+ return basetypes.ObjectType{AttrTypes: m.tfAttributesTypes()}
+}
+
+func (m *primaryIPLocationPricingModel) FromAPI(ctx context.Context, hc hcloud.PrimaryIPTypePricing) diag.Diagnostics {
+ var diags diag.Diagnostics
+ var newDiags diag.Diagnostics
+
+ m.Location = types.StringValue(hc.Location)
+
+ m.Hourly, newDiags = primaryIPPriceObjectFromAPI(ctx, hc.Hourly)
+ diags.Append(newDiags...)
+
+ m.Monthly, newDiags = primaryIPPriceObjectFromAPI(ctx, hc.Monthly)
+ diags.Append(newDiags...)
+
+ return diags
+}
+
+func (m *primaryIPLocationPricingModel) ToTerraform(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, m.tfAttributesTypes(), m)
+}
+
+func primaryIPLocationPricingObjectFromAPI(ctx context.Context, hc hcloud.PrimaryIPTypePricing) (types.Object, diag.Diagnostics) {
+ value := primaryIPLocationPricingModel{}
+ diags := value.FromAPI(ctx, hc)
+
+ obj, newDiags := value.ToTerraform(ctx)
+ diags.Append(newDiags...)
+
+ return obj, diags
+}
+
+// typePricingModel represents the pricing of a Server Type or Load Balancer
+// Type across all locations.
+type typePricingModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Prices types.List `tfsdk:"prices"`
+}
+
+func (m *typePricingModel) tfAttributesTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "id": types.Int64Type,
+ "name": types.StringType,
+ "prices": types.ListType{ElemType: (&typeLocationPricingModel{}).tfType()},
+ }
+}
+
+func (m *typePricingModel) tfType() attr.Type {
+ return basetypes.ObjectType{AttrTypes: m.tfAttributesTypes()}
+}
+
+func (m *typePricingModel) ToTerraform(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, m.tfAttributesTypes(), m)
+}
+
+func serverTypePricingObjectFromAPI(ctx context.Context, hc hcloud.ServerTypePricing) (types.Object, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ value := typePricingModel{}
+ if hc.ServerType != nil {
+ value.ID = types.Int64Value(hc.ServerType.ID)
+ value.Name = types.StringValue(hc.ServerType.Name)
+ }
+
+ prices, newDiags := listValueFromAPI(ctx, (&typeLocationPricingModel{}).tfType(), hc.Pricings,
+ func(ctx context.Context, p hcloud.ServerTypeLocationPricing) (types.Object, diag.Diagnostics) {
+ item := typeLocationPricingModel{}
+ diags := item.FromAPI(ctx, locationName(p.Location), p.Hourly, p.Monthly, p.IncludedTraffic, p.PerTBTraffic)
+
+ obj, newDiags := item.ToTerraform(ctx)
+ diags.Append(newDiags...)
+
+ return obj, diags
+ },
+ )
+ diags.Append(newDiags...)
+ value.Prices = prices
+
+ obj, newDiags := value.ToTerraform(ctx)
+ diags.Append(newDiags...)
+
+ return obj, diags
+}
+
+func loadBalancerTypePricingObjectFromAPI(ctx context.Context, hc hcloud.LoadBalancerTypePricing) (types.Object, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ value := typePricingModel{}
+ if hc.LoadBalancerType != nil {
+ value.ID = types.Int64Value(hc.LoadBalancerType.ID)
+ value.Name = types.StringValue(hc.LoadBalancerType.Name)
+ }
+
+ prices, newDiags := listValueFromAPI(ctx, (&typeLocationPricingModel{}).tfType(), hc.Pricings,
+ func(ctx context.Context, p hcloud.LoadBalancerTypeLocationPricing) (types.Object, diag.Diagnostics) {
+ item := typeLocationPricingModel{}
+ diags := item.FromAPI(ctx, locationName(p.Location), p.Hourly, p.Monthly, p.IncludedTraffic, p.PerTBTraffic)
+
+ obj, newDiags := item.ToTerraform(ctx)
+ diags.Append(newDiags...)
+
+ return obj, diags
+ },
+ )
+ diags.Append(newDiags...)
+ value.Prices = prices
+
+ obj, newDiags := value.ToTerraform(ctx)
+ diags.Append(newDiags...)
+
+ return obj, diags
+}
+
+// typeLocationPricingModel represents the pricing of a Server Type or Load
+// Balancer Type at a single location.
+type typeLocationPricingModel struct {
+ Location types.String `tfsdk:"location"`
+ Hourly types.Object `tfsdk:"hourly"`
+ Monthly types.Object `tfsdk:"monthly"`
+ IncludedTraffic types.Int64 `tfsdk:"included_traffic"`
+ PerTBTraffic types.Object `tfsdk:"per_tb_traffic"`
+}
+
+func (m *typeLocationPricingModel) tfAttributesTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "location": types.StringType,
+ "hourly": (&priceModel{}).tfType(),
+ "monthly": (&priceModel{}).tfType(),
+ "included_traffic": types.Int64Type,
+ "per_tb_traffic": (&priceModel{}).tfType(),
+ }
+}
+
+func (m *typeLocationPricingModel) tfType() attr.Type {
+ return basetypes.ObjectType{AttrTypes: m.tfAttributesTypes()}
+}
+
+func (m *typeLocationPricingModel) FromAPI(ctx context.Context, location string, hourly, monthly hcloud.Price, includedTraffic uint64, perTBTraffic hcloud.Price) diag.Diagnostics {
+ var diags diag.Diagnostics
+ var newDiags diag.Diagnostics
+
+ m.Location = types.StringValue(location)
+
+ m.Hourly, newDiags = priceObjectFromAPI(ctx, hourly)
+ diags.Append(newDiags...)
+
+ m.Monthly, newDiags = priceObjectFromAPI(ctx, monthly)
+ diags.Append(newDiags...)
+
+ m.IncludedTraffic = types.Int64Value(int64(includedTraffic)) //nolint:gosec // traffic in bytes always fits in int64
+
+ m.PerTBTraffic, newDiags = priceObjectFromAPI(ctx, perTBTraffic)
+ diags.Append(newDiags...)
+
+ return diags
+}
+
+func (m *typeLocationPricingModel) ToTerraform(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, m.tfAttributesTypes(), m)
+}
+
+// listValueFromAPI converts a slice of API values into a Terraform list of
+// objects, using the provided conversion function for each element.
+func listValueFromAPI[API any](
+ ctx context.Context,
+ elemType attr.Type,
+ items []API,
+ convert func(context.Context, API) (types.Object, diag.Diagnostics),
+) (types.List, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ var newDiags diag.Diagnostics
+
+ values := make([]attr.Value, 0, len(items))
+ for _, item := range items {
+ obj, newDiags := convert(ctx, item)
+ diags.Append(newDiags...)
+
+ values = append(values, obj)
+ }
+
+ list, newDiags := types.ListValue(elemType, values)
+ diags.Append(newDiags...)
+
+ return list, diags
+}
+
+// locationName returns the name of the given location, or an empty string if it
+// is not set.
+func locationName(location *hcloud.Location) string {
+ if location == nil {
+ return ""
+ }
+ return location.Name
+}
+
+// filterPricing applies the optional filters configured on the data source. The
+// Pricing API does not support server side filtering, so the full response is
+// fetched and filtered locally.
+func filterPricing(p hcloud.Pricing, data model) hcloud.Pricing {
+ location := data.Location.ValueString()
+ serverType := data.ServerType.ValueString()
+ loadBalancerType := data.LoadBalancerType.ValueString()
+
+ if location != "" {
+ for i := range p.FloatingIPs {
+ p.FloatingIPs[i].Pricings = filterSlice(p.FloatingIPs[i].Pricings, func(v hcloud.FloatingIPTypeLocationPricing) bool {
+ return locationName(v.Location) == location
+ })
+ }
+ for i := range p.PrimaryIPs {
+ p.PrimaryIPs[i].Pricings = filterSlice(p.PrimaryIPs[i].Pricings, func(v hcloud.PrimaryIPTypePricing) bool {
+ return v.Location == location
+ })
+ }
+ for i := range p.ServerTypes {
+ p.ServerTypes[i].Pricings = filterSlice(p.ServerTypes[i].Pricings, func(v hcloud.ServerTypeLocationPricing) bool {
+ return locationName(v.Location) == location
+ })
+ }
+ for i := range p.LoadBalancerTypes {
+ p.LoadBalancerTypes[i].Pricings = filterSlice(p.LoadBalancerTypes[i].Pricings, func(v hcloud.LoadBalancerTypeLocationPricing) bool {
+ return locationName(v.Location) == location
+ })
+ }
+ }
+
+ if serverType != "" {
+ p.ServerTypes = filterSlice(p.ServerTypes, func(v hcloud.ServerTypePricing) bool {
+ return v.ServerType != nil && v.ServerType.Name == serverType
+ })
+ }
+
+ if loadBalancerType != "" {
+ p.LoadBalancerTypes = filterSlice(p.LoadBalancerTypes, func(v hcloud.LoadBalancerTypePricing) bool {
+ return v.LoadBalancerType != nil && v.LoadBalancerType.Name == loadBalancerType
+ })
+ }
+
+ return p
+}
+
+// filterSlice returns a new slice containing only the items for which keep
+// returns true.
+func filterSlice[T any](items []T, keep func(T) bool) []T {
+ result := make([]T, 0, len(items))
+ for _, item := range items {
+ if keep(item) {
+ result = append(result, item)
+ }
+ }
+ return result
+}
diff --git a/internal/pricing/testing.go b/internal/pricing/testing.go
new file mode 100644
index 000000000..9e0150d91
--- /dev/null
+++ b/internal/pricing/testing.go
@@ -0,0 +1,17 @@
+package pricing
+
+import (
+ "fmt"
+
+ "github.com/hetznercloud/terraform-provider-hcloud/internal/testtemplate"
+)
+
+// DData defines the fields for the "testdata/d/hcloud_pricing" template.
+type DData struct {
+ testtemplate.DataCommon
+}
+
+// TFID returns the data source identifier.
+func (d *DData) TFID() string {
+ return fmt.Sprintf("data.%s.%s", DataSourceType, d.RName())
+}
diff --git a/internal/testdata/d/hcloud_pricing.tf.tmpl b/internal/testdata/d/hcloud_pricing.tf.tmpl
new file mode 100644
index 000000000..0e3dc837b
--- /dev/null
+++ b/internal/testdata/d/hcloud_pricing.tf.tmpl
@@ -0,0 +1,3 @@
+{{- /* vim: set ft=terraform: */ -}}
+
+data "hcloud_pricing" "{{ .RName }}" {}