diff --git a/docs/data-sources/image_v2.md b/docs/data-sources/image_v2.md
new file mode 100644
index 00000000..12ad0f02
--- /dev/null
+++ b/docs/data-sources/image_v2.md
@@ -0,0 +1,150 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "stackit_image_v2 Data Source - stackit"
+subcategory: ""
+description: |-
+ Image datasource schema. Must have a region specified in the provider configuration.
+ ~> Important: When using the name, name_regex, or filter attributes to select images dynamically, be aware that image IDs may change frequently. Each OS patch or update results in a new unique image ID. If this data source is used to populate fields like boot_volume.source_id in a server resource, it may cause Terraform to detect changes and recreate the associated resource.
+ To avoid unintended updates or resource replacements:
+ Prefer using a static image_id to pin a specific image version.If you accept automatic image updates but wish to suppress resource changes, use a lifecycle block to ignore relevant changes. For example:
+
+ resource "stackit_server" "example" {
+ boot_volume = {
+ size = 64
+ source_type = "image"
+ source_id = data.stackit_image.latest.id
+ }
+
+ lifecycle {
+ ignore_changes = [boot_volume[0].source_id]
+ }
+ }
+
+ ~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
+---
+
+# stackit_image_v2 (Data Source)
+
+Image datasource schema. Must have a `region` specified in the provider configuration.
+
+~> Important: When using the `name`, `name_regex`, or `filter` attributes to select images dynamically, be aware that image IDs may change frequently. Each OS patch or update results in a new unique image ID. If this data source is used to populate fields like `boot_volume.source_id` in a server resource, it may cause Terraform to detect changes and recreate the associated resource.
+
+To avoid unintended updates or resource replacements:
+ - Prefer using a static `image_id` to pin a specific image version.
+ - If you accept automatic image updates but wish to suppress resource changes, use a `lifecycle` block to ignore relevant changes. For example:
+
+```hcl
+resource "stackit_server" "example" {
+ boot_volume = {
+ size = 64
+ source_type = "image"
+ source_id = data.stackit_image.latest.id
+ }
+
+ lifecycle {
+ ignore_changes = [boot_volume[0].source_id]
+ }
+}
+```
+
+~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.
+
+## Example Usage
+
+```terraform
+data "stackit_image_v2" "default" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ image_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+}
+
+data "stackit_image_v2" "name_match" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name = "Ubuntu 22.04"
+}
+
+data "stackit_image_v2" "name_regex_latest" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name_regex = "^Ubuntu .*"
+}
+
+data "stackit_image_v2" "name_regex_oldest" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name_regex = "^Ubuntu .*"
+ sort_ascending = true
+}
+
+data "stackit_image_v2" "filter_distro_version" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ filter = {
+ distro = "debian"
+ version = "11"
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `project_id` (String) STACKIT project ID to which the image is associated.
+
+### Optional
+
+- `filter` (Attributes) Additional filtering options based on image properties. Can be used independently or in conjunction with `name` or `name_regex`. (see [below for nested schema](#nestedatt--filter))
+- `image_id` (String) Image ID to fetch directly
+- `name` (String) Exact image name to match. Optionally applies a `filter` block to further refine results in case multiple images share the same name. The first match is returned, optionally sorted by name in ascending order. Cannot be used together with `name_regex`.
+- `name_regex` (String) Regular expression to match against image names. Optionally applies a `filter` block to narrow down results when multiple image names match the regex. The first match is returned, optionally sorted by name in ascending order. Cannot be used together with `name`.
+- `sort_ascending` (Boolean) If set to `true`, images are sorted in ascending lexicographical order by image name (such as `Ubuntu 18.04`, `Ubuntu 20.04`, `Ubuntu 22.04`) before selecting the first match. Defaults to `false` (descending such as `Ubuntu 22.04`, `Ubuntu 20.04`, `Ubuntu 18.04`).
+
+### Read-Only
+
+- `checksum` (Attributes) Representation of an image checksum. (see [below for nested schema](#nestedatt--checksum))
+- `config` (Attributes) Properties to set hardware and scheduling settings for an image. (see [below for nested schema](#nestedatt--config))
+- `disk_format` (String) The disk format of the image.
+- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`image_id`".
+- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
+- `min_disk_size` (Number) The minimum disk size of the image in GB.
+- `min_ram` (Number) The minimum RAM of the image in MB.
+- `protected` (Boolean) Whether the image is protected.
+- `scope` (String) The scope of the image.
+
+
+### Nested Schema for `filter`
+
+Optional:
+
+- `distro` (String) Filter images by operating system distribution. For example: `ubuntu`, `ubuntu-arm64`, `debian`, `rhel`, etc.
+- `os` (String) Filter images by operating system type, such as `linux` or `windows`.
+- `secure_boot` (Boolean) Filter images with Secure Boot support. Set to `true` to match images that support Secure Boot.
+- `uefi` (Boolean) Filter images based on UEFI support. Set to `true` to match images that support UEFI.
+- `version` (String) Filter images by OS distribution version, such as `22.04`, `11`, or `9.1`.
+
+
+
+### Nested Schema for `checksum`
+
+Read-Only:
+
+- `algorithm` (String) Algorithm for the checksum of the image data.
+- `digest` (String) Hexdigest of the checksum of the image data.
+
+
+
+### Nested Schema for `config`
+
+Read-Only:
+
+- `boot_menu` (Boolean) Enables the BIOS bootmenu.
+- `cdrom_bus` (String) Sets CDROM bus controller type.
+- `disk_bus` (String) Sets Disk bus controller type.
+- `nic_model` (String) Sets virtual network interface model.
+- `operating_system` (String) Enables operating system specific optimizations.
+- `operating_system_distro` (String) Operating system distribution.
+- `operating_system_version` (String) Version of the operating system.
+- `rescue_bus` (String) Sets the device bus when the image is used as a rescue image.
+- `rescue_device` (String) Sets the device when the image is used as a rescue image.
+- `secure_boot` (Boolean) Enables Secure Boot.
+- `uefi` (Boolean) Enables UEFI boot.
+- `video_model` (String) Sets Graphic device model.
+- `virtio_scsi` (Boolean) Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block.
diff --git a/examples/data-sources/stackit_image_v2/data-source.tf b/examples/data-sources/stackit_image_v2/data-source.tf
new file mode 100644
index 00000000..401488c4
--- /dev/null
+++ b/examples/data-sources/stackit_image_v2/data-source.tf
@@ -0,0 +1,28 @@
+data "stackit_image_v2" "default" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ image_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+}
+
+data "stackit_image_v2" "name_match" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name = "Ubuntu 22.04"
+}
+
+data "stackit_image_v2" "name_regex_latest" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name_regex = "^Ubuntu .*"
+}
+
+data "stackit_image_v2" "name_regex_oldest" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name_regex = "^Ubuntu .*"
+ sort_ascending = true
+}
+
+data "stackit_image_v2" "filter_distro_version" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ filter = {
+ distro = "debian"
+ version = "11"
+ }
+}
\ No newline at end of file
diff --git a/stackit/internal/services/iaas/iaas_acc_test.go b/stackit/internal/services/iaas/iaas_acc_test.go
index d0f91249..1ae62384 100644
--- a/stackit/internal/services/iaas/iaas_acc_test.go
+++ b/stackit/internal/services/iaas/iaas_acc_test.go
@@ -35,6 +35,9 @@ var (
//go:embed testdata/resource-security-group-max.tf
resourceSecurityGroupMaxConfig string
+ //go:embed testdata/datasource-image-v2-variants.tf
+ dataSourceImageVariants string
+
//go:embed testdata/resource-image-min.tf
resourceImageMinConfig string
@@ -4029,6 +4032,133 @@ func TestAccImageMax(t *testing.T) {
})
}
+func TestAccImageV2DatasourceSearchVariants(t *testing.T) {
+ t.Log("TestDataSource Image V2 Variants")
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ // Creation
+ {
+ ConfigVariables: config.Variables{"project_id": config.StringVariable(testutil.ProjectId)},
+ Config: fmt.Sprintf("%s\n%s", dataSourceImageVariants, testutil.IaaSProviderConfigWithBetaResourcesEnabled()),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("data.stackit_image_v2.name_match_ubuntu_22_04", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "image_id"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "name"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "min_disk_size"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "min_ram"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "protected"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "scope"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "checksum.algorithm"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "checksum.digest"),
+
+ resource.TestCheckResourceAttr("data.stackit_image_v2.ubuntu_by_image_id", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "image_id"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "name"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "min_disk_size"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "min_ram"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "protected"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "scope"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "checksum.algorithm"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "checksum.digest"),
+
+ resource.TestCheckResourceAttr("data.stackit_image_v2.regex_match_ubuntu_22_04", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "image_id"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "name"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "min_disk_size"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "min_ram"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "protected"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "scope"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "checksum.algorithm"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "checksum.digest"),
+
+ resource.TestCheckResourceAttr("data.stackit_image_v2.filter_debian_11", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "image_id"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "name"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "min_disk_size"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "min_ram"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "protected"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "scope"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "checksum.algorithm"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "checksum.digest"),
+
+ resource.TestCheckResourceAttr("data.stackit_image_v2.filter_uefi_ubuntu", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "image_id"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "name"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "min_disk_size"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "min_ram"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "protected"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "scope"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "checksum.algorithm"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "checksum.digest"),
+
+ resource.TestCheckResourceAttr("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "image_id"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "name"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "min_disk_size"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "min_ram"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "protected"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "scope"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "checksum.algorithm"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "checksum.digest"),
+
+ resource.TestCheckResourceAttr("data.stackit_image_v2.name_windows_2022_standard", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "image_id"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "name"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "min_disk_size"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "min_ram"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "protected"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "scope"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "checksum.algorithm"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "checksum.digest"),
+
+ resource.TestCheckResourceAttr("data.stackit_image_v2.ubuntu_arm64_latest", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "image_id"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "name"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "min_disk_size"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "min_ram"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "protected"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "scope"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "checksum.algorithm"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "checksum.digest"),
+
+ resource.TestCheckResourceAttr("data.stackit_image_v2.ubuntu_arm64_oldest", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "image_id"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "name"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "min_disk_size"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "min_ram"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "protected"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "scope"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "checksum.algorithm"),
+ resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "checksum.digest"),
+
+ // e2e test that ascending sort is working
+ func(s *terraform.State) error {
+ latest := s.RootModule().Resources["data.stackit_image_v2.ubuntu_arm64_latest"]
+ oldest := s.RootModule().Resources["data.stackit_image_v2.ubuntu_arm64_oldest"]
+
+ if latest == nil {
+ return fmt.Errorf("datasource 'data.stackit_image_v2.ubuntu_arm64_latest' not found")
+ }
+ if oldest == nil {
+ return fmt.Errorf("datasource 'data.stackit_image_v2.ubuntu_arm64_oldest' not found")
+ }
+
+ nameLatest := latest.Primary.Attributes["name"]
+ nameOldest := oldest.Primary.Attributes["name"]
+
+ if nameLatest == nameOldest {
+ return fmt.Errorf("expected image names to differ, but both are %q", nameLatest)
+ }
+
+ return nil
+ },
+ ),
+ },
+ },
+ })
+}
+
func TestAccProject(t *testing.T) {
projectId := testutil.ProjectId
resource.ParallelTest(t, resource.TestCase{
diff --git a/stackit/internal/services/iaas/imagev2/datasource.go b/stackit/internal/services/iaas/imagev2/datasource.go
new file mode 100644
index 00000000..ab364bb0
--- /dev/null
+++ b/stackit/internal/services/iaas/imagev2/datasource.go
@@ -0,0 +1,627 @@
+package image
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "regexp"
+ "sort"
+
+ "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/stackitcloud/stackit-sdk-go/services/iaas"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+
+ iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+ _ datasource.DataSource = &imageDataV2Source{}
+)
+
+type DataSourceModel struct {
+ Id types.String `tfsdk:"id"` // needed by TF
+ ProjectId types.String `tfsdk:"project_id"`
+ ImageId types.String `tfsdk:"image_id"`
+ Name types.String `tfsdk:"name"`
+ NameRegex types.String `tfsdk:"name_regex"`
+ SortAscending types.Bool `tfsdk:"sort_ascending"`
+ Filter types.Object `tfsdk:"filter"`
+
+ DiskFormat types.String `tfsdk:"disk_format"`
+ MinDiskSize types.Int64 `tfsdk:"min_disk_size"`
+ MinRAM types.Int64 `tfsdk:"min_ram"`
+ Protected types.Bool `tfsdk:"protected"`
+ Scope types.String `tfsdk:"scope"`
+ Config types.Object `tfsdk:"config"`
+ Checksum types.Object `tfsdk:"checksum"`
+ Labels types.Map `tfsdk:"labels"`
+}
+
+type Filter struct {
+ OS types.String `tfsdk:"os"`
+ Distro types.String `tfsdk:"distro"`
+ Version types.String `tfsdk:"version"`
+ UEFI types.Bool `tfsdk:"uefi"`
+ SecureBoot types.Bool `tfsdk:"secure_boot"`
+}
+
+// Struct corresponding to Model.Config
+type configModel struct {
+ BootMenu types.Bool `tfsdk:"boot_menu"`
+ CDROMBus types.String `tfsdk:"cdrom_bus"`
+ DiskBus types.String `tfsdk:"disk_bus"`
+ NICModel types.String `tfsdk:"nic_model"`
+ OperatingSystem types.String `tfsdk:"operating_system"`
+ OperatingSystemDistro types.String `tfsdk:"operating_system_distro"`
+ OperatingSystemVersion types.String `tfsdk:"operating_system_version"`
+ RescueBus types.String `tfsdk:"rescue_bus"`
+ RescueDevice types.String `tfsdk:"rescue_device"`
+ SecureBoot types.Bool `tfsdk:"secure_boot"`
+ UEFI types.Bool `tfsdk:"uefi"`
+ VideoModel types.String `tfsdk:"video_model"`
+ VirtioScsi types.Bool `tfsdk:"virtio_scsi"`
+}
+
+// Types corresponding to configModel
+var configTypes = map[string]attr.Type{
+ "boot_menu": basetypes.BoolType{},
+ "cdrom_bus": basetypes.StringType{},
+ "disk_bus": basetypes.StringType{},
+ "nic_model": basetypes.StringType{},
+ "operating_system": basetypes.StringType{},
+ "operating_system_distro": basetypes.StringType{},
+ "operating_system_version": basetypes.StringType{},
+ "rescue_bus": basetypes.StringType{},
+ "rescue_device": basetypes.StringType{},
+ "secure_boot": basetypes.BoolType{},
+ "uefi": basetypes.BoolType{},
+ "video_model": basetypes.StringType{},
+ "virtio_scsi": basetypes.BoolType{},
+}
+
+// Struct corresponding to Model.Checksum
+type checksumModel struct {
+ Algorithm types.String `tfsdk:"algorithm"`
+ Digest types.String `tfsdk:"digest"`
+}
+
+// Types corresponding to checksumModel
+var checksumTypes = map[string]attr.Type{
+ "algorithm": basetypes.StringType{},
+ "digest": basetypes.StringType{},
+}
+
+// NewImageV2DataSource is a helper function to simplify the provider implementation.
+func NewImageV2DataSource() datasource.DataSource {
+ return &imageDataV2Source{}
+}
+
+// imageDataV2Source is the data source implementation.
+type imageDataV2Source struct {
+ client *iaas.APIClient
+}
+
+// Metadata returns the data source type name.
+func (d *imageDataV2Source) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_image_v2"
+}
+
+func (d *imageDataV2Source) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
+ if !ok {
+ return
+ }
+
+ features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_image_v2", "datasource")
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ d.client = apiClient
+ tflog.Info(ctx, "iaas client configured")
+}
+
+func (d *imageDataV2Source) ConfigValidators(_ context.Context) []datasource.ConfigValidator {
+ return []datasource.ConfigValidator{
+ datasourcevalidator.Conflicting(
+ path.MatchRoot("name"),
+ path.MatchRoot("name_regex"),
+ path.MatchRoot("image_id"),
+ ),
+ datasourcevalidator.AtLeastOneOf(
+ path.MatchRoot("name"),
+ path.MatchRoot("name_regex"),
+ path.MatchRoot("image_id"),
+ path.MatchRoot("filter"),
+ ),
+ }
+}
+
+// Schema defines the schema for the datasource.
+func (d *imageDataV2Source) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ description := features.AddBetaDescription(fmt.Sprintf(
+ "%s\n\n~> %s",
+ "Image datasource schema. Must have a `region` specified in the provider configuration.",
+ "Important: When using the `name`, `name_regex`, or `filter` attributes to select images dynamically, be aware that image IDs may change frequently. Each OS patch or update results in a new unique image ID. If this data source is used to populate fields like `boot_volume.source_id` in a server resource, it may cause Terraform to detect changes and recreate the associated resource.\n\n"+
+ "To avoid unintended updates or resource replacements:\n"+
+ " - Prefer using a static `image_id` to pin a specific image version.\n"+
+ " - If you accept automatic image updates but wish to suppress resource changes, use a `lifecycle` block to ignore relevant changes. For example:\n\n"+
+ "```hcl\n"+
+ "resource \"stackit_server\" \"example\" {\n"+
+ " boot_volume = {\n"+
+ " size = 64\n"+
+ " source_type = \"image\"\n"+
+ " source_id = data.stackit_image.latest.id\n"+
+ " }\n"+
+ "\n"+
+ " lifecycle {\n"+
+ " ignore_changes = [boot_volume[0].source_id]\n"+
+ " }\n"+
+ "}\n"+
+ "```",
+ ), core.Datasource)
+ resp.Schema = schema.Schema{
+ MarkdownDescription: description,
+ Description: description,
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".",
+ Computed: true,
+ },
+ "project_id": schema.StringAttribute{
+ Description: "STACKIT project ID to which the image is associated.",
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "image_id": schema.StringAttribute{
+ Description: "Image ID to fetch directly",
+ Optional: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "name": schema.StringAttribute{
+ Description: "Exact image name to match. Optionally applies a `filter` block to further refine results in case multiple images share the same name. The first match is returned, optionally sorted by name in ascending order. Cannot be used together with `name_regex`.",
+ Optional: true,
+ },
+ "name_regex": schema.StringAttribute{
+ Description: "Regular expression to match against image names. Optionally applies a `filter` block to narrow down results when multiple image names match the regex. The first match is returned, optionally sorted by name in ascending order. Cannot be used together with `name`.",
+ Optional: true,
+ },
+ "sort_ascending": schema.BoolAttribute{
+ Description: "If set to `true`, images are sorted in ascending lexicographical order by image name (such as `Ubuntu 18.04`, `Ubuntu 20.04`, `Ubuntu 22.04`) before selecting the first match. Defaults to `false` (descending such as `Ubuntu 22.04`, `Ubuntu 20.04`, `Ubuntu 18.04`).",
+ Optional: true,
+ },
+ "filter": schema.SingleNestedAttribute{
+ Optional: true,
+ Description: "Additional filtering options based on image properties. Can be used independently or in conjunction with `name` or `name_regex`.",
+ Attributes: map[string]schema.Attribute{
+ "os": schema.StringAttribute{
+ Optional: true,
+ Description: "Filter images by operating system type, such as `linux` or `windows`.",
+ },
+ "distro": schema.StringAttribute{
+ Optional: true,
+ Description: "Filter images by operating system distribution. For example: `ubuntu`, `ubuntu-arm64`, `debian`, `rhel`, etc.",
+ },
+ "version": schema.StringAttribute{
+ Optional: true,
+ Description: "Filter images by OS distribution version, such as `22.04`, `11`, or `9.1`.",
+ },
+ "uefi": schema.BoolAttribute{
+ Optional: true,
+ Description: "Filter images based on UEFI support. Set to `true` to match images that support UEFI.",
+ },
+ "secure_boot": schema.BoolAttribute{
+ Optional: true,
+ Description: "Filter images with Secure Boot support. Set to `true` to match images that support Secure Boot.",
+ },
+ },
+ },
+ "disk_format": schema.StringAttribute{
+ Description: "The disk format of the image.",
+ Computed: true,
+ },
+ "min_disk_size": schema.Int64Attribute{
+ Description: "The minimum disk size of the image in GB.",
+ Computed: true,
+ },
+ "min_ram": schema.Int64Attribute{
+ Description: "The minimum RAM of the image in MB.",
+ Computed: true,
+ },
+ "protected": schema.BoolAttribute{
+ Description: "Whether the image is protected.",
+ Computed: true,
+ },
+ "scope": schema.StringAttribute{
+ Description: "The scope of the image.",
+ Computed: true,
+ },
+ "config": schema.SingleNestedAttribute{
+ Description: "Properties to set hardware and scheduling settings for an image.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "boot_menu": schema.BoolAttribute{
+ Description: "Enables the BIOS bootmenu.",
+ Computed: true,
+ },
+ "cdrom_bus": schema.StringAttribute{
+ Description: "Sets CDROM bus controller type.",
+ Computed: true,
+ },
+ "disk_bus": schema.StringAttribute{
+ Description: "Sets Disk bus controller type.",
+ Computed: true,
+ },
+ "nic_model": schema.StringAttribute{
+ Description: "Sets virtual network interface model.",
+ Computed: true,
+ },
+ "operating_system": schema.StringAttribute{
+ Description: "Enables operating system specific optimizations.",
+ Computed: true,
+ },
+ "operating_system_distro": schema.StringAttribute{
+ Description: "Operating system distribution.",
+ Computed: true,
+ },
+ "operating_system_version": schema.StringAttribute{
+ Description: "Version of the operating system.",
+ Computed: true,
+ },
+ "rescue_bus": schema.StringAttribute{
+ Description: "Sets the device bus when the image is used as a rescue image.",
+ Computed: true,
+ },
+ "rescue_device": schema.StringAttribute{
+ Description: "Sets the device when the image is used as a rescue image.",
+ Computed: true,
+ },
+ "secure_boot": schema.BoolAttribute{
+ Description: "Enables Secure Boot.",
+ Computed: true,
+ },
+ "uefi": schema.BoolAttribute{
+ Description: "Enables UEFI boot.",
+ Computed: true,
+ },
+ "video_model": schema.StringAttribute{
+ Description: "Sets Graphic device model.",
+ Computed: true,
+ },
+ "virtio_scsi": schema.BoolAttribute{
+ Description: "Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block.",
+ Computed: true,
+ },
+ },
+ },
+ "checksum": schema.SingleNestedAttribute{
+ Description: "Representation of an image checksum.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "algorithm": schema.StringAttribute{
+ Description: "Algorithm for the checksum of the image data.",
+ Computed: true,
+ },
+ "digest": schema.StringAttribute{
+ Description: "Hexdigest of the checksum of the image data.",
+ Computed: true,
+ },
+ },
+ },
+ "labels": schema.MapAttribute{
+ Description: "Labels are key-value string pairs which can be attached to a resource container",
+ ElementType: types.StringType,
+ Computed: true,
+ },
+ },
+ }
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
+ var model DataSourceModel
+ diags := req.Config.Get(ctx, &model)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ projectID := model.ProjectId.ValueString()
+ imageID := model.ImageId.ValueString()
+ name := model.Name.ValueString()
+ nameRegex := model.NameRegex.ValueString()
+ sortAscending := model.SortAscending.ValueBool()
+
+ var filter Filter
+ if !model.Filter.IsNull() && !model.Filter.IsUnknown() {
+ if diagnostics := model.Filter.As(ctx, &filter, basetypes.ObjectAsOptions{}); diagnostics.HasError() {
+ resp.Diagnostics.Append(diagnostics...)
+ return
+ }
+ }
+
+ ctx = tflog.SetField(ctx, "project_id", projectID)
+ ctx = tflog.SetField(ctx, "image_id", imageID)
+ ctx = tflog.SetField(ctx, "name", name)
+ ctx = tflog.SetField(ctx, "name_regex", nameRegex)
+ ctx = tflog.SetField(ctx, "sort_ascending", sortAscending)
+
+ var imageResp *iaas.Image
+ var err error
+
+ // Case 1: Direct lookup by image ID
+ if imageID != "" {
+ imageResp, err = d.client.GetImage(ctx, projectID, imageID).Execute()
+ if err != nil {
+ utils.LogError(ctx, &resp.Diagnostics, err, "Reading image",
+ fmt.Sprintf("Image with ID %q does not exist in project %q.", imageID, projectID),
+ map[int]string{
+ http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectID),
+ })
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ } else {
+ // Case 2: Lookup by name or name_regex
+
+ // Compile regex
+ var compiledRegex *regexp.Regexp
+ if nameRegex != "" {
+ compiledRegex, err = regexp.Compile(nameRegex)
+ if err != nil {
+ core.LogAndAddWarning(ctx, &resp.Diagnostics, "Invalid name_regex", err.Error())
+ return
+ }
+ }
+
+ // Fetch all available images
+ imageList, err := d.client.ListImages(ctx, projectID).Execute()
+ if err != nil {
+ utils.LogError(ctx, &resp.Diagnostics, err, "List images", "Unable to fetch images", nil)
+ return
+ }
+
+ // Step 1: Match images by name or regular expression (name or name_regex, if provided)
+ var matchedImages []*iaas.Image
+ for i := range *imageList.Items {
+ img := &(*imageList.Items)[i]
+ if name != "" && img.Name != nil && *img.Name == name {
+ matchedImages = append(matchedImages, img)
+ }
+ if compiledRegex != nil && img.Name != nil && compiledRegex.MatchString(*img.Name) {
+ matchedImages = append(matchedImages, img)
+ }
+ // If neither name nor name_regex is specified, include all images for filter evaluation later
+ if name == "" && nameRegex == "" {
+ matchedImages = append(matchedImages, img)
+ }
+ }
+
+ // Step 2: Sort matched images by name (optional, based on sortAscending flag)
+ if len(matchedImages) > 1 {
+ sortImagesByName(matchedImages, sortAscending)
+ }
+
+ // Step 3: Apply additional filtering based on OS, distro, version, UEFI, secure boot, etc.
+ var filteredImages []*iaas.Image
+ for _, img := range matchedImages {
+ if imageMatchesFilter(img, &filter) {
+ filteredImages = append(filteredImages, img)
+ }
+ }
+
+ // Check if any images passed all filters; warn if no matching image was found
+ if len(filteredImages) == 0 {
+ core.LogAndAddWarning(ctx, &resp.Diagnostics, "No match",
+ "No matching image found using name, name_regex, and filter criteria.")
+ return
+ }
+
+ // Step 4: Use the first image from the filtered and sorted result list
+ imageResp = filteredImages[0]
+ }
+
+ err = mapDataSourceFields(ctx, imageResp, &model)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", 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, "image read")
+}
+
+func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *DataSourceModel) error {
+ if imageResp == nil {
+ return fmt.Errorf("response input is nil")
+ }
+ if model == nil {
+ return fmt.Errorf("model input is nil")
+ }
+
+ var imageId string
+ if model.ImageId.ValueString() != "" {
+ imageId = model.ImageId.ValueString()
+ } else if imageResp.Id != nil {
+ imageId = *imageResp.Id
+ } else {
+ return fmt.Errorf("image id not present")
+ }
+
+ model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), imageId)
+
+ // Map config
+ var configModel = &configModel{}
+ var configObject basetypes.ObjectValue
+ diags := diag.Diagnostics{}
+ if imageResp.Config != nil {
+ configModel.BootMenu = types.BoolPointerValue(imageResp.Config.BootMenu)
+ configModel.CDROMBus = types.StringPointerValue(imageResp.Config.GetCdromBus())
+ configModel.DiskBus = types.StringPointerValue(imageResp.Config.GetDiskBus())
+ configModel.NICModel = types.StringPointerValue(imageResp.Config.GetNicModel())
+ configModel.OperatingSystem = types.StringPointerValue(imageResp.Config.OperatingSystem)
+ configModel.OperatingSystemDistro = types.StringPointerValue(imageResp.Config.GetOperatingSystemDistro())
+ configModel.OperatingSystemVersion = types.StringPointerValue(imageResp.Config.GetOperatingSystemVersion())
+ configModel.RescueBus = types.StringPointerValue(imageResp.Config.GetRescueBus())
+ configModel.RescueDevice = types.StringPointerValue(imageResp.Config.GetRescueDevice())
+ configModel.SecureBoot = types.BoolPointerValue(imageResp.Config.SecureBoot)
+ configModel.UEFI = types.BoolPointerValue(imageResp.Config.Uefi)
+ configModel.VideoModel = types.StringPointerValue(imageResp.Config.GetVideoModel())
+ configModel.VirtioScsi = types.BoolPointerValue(iaas.PtrBool(imageResp.Config.GetVirtioScsi()))
+
+ configObject, diags = types.ObjectValue(configTypes, map[string]attr.Value{
+ "boot_menu": configModel.BootMenu,
+ "cdrom_bus": configModel.CDROMBus,
+ "disk_bus": configModel.DiskBus,
+ "nic_model": configModel.NICModel,
+ "operating_system": configModel.OperatingSystem,
+ "operating_system_distro": configModel.OperatingSystemDistro,
+ "operating_system_version": configModel.OperatingSystemVersion,
+ "rescue_bus": configModel.RescueBus,
+ "rescue_device": configModel.RescueDevice,
+ "secure_boot": configModel.SecureBoot,
+ "uefi": configModel.UEFI,
+ "video_model": configModel.VideoModel,
+ "virtio_scsi": configModel.VirtioScsi,
+ })
+ } else {
+ configObject = types.ObjectNull(configTypes)
+ }
+ if diags.HasError() {
+ return fmt.Errorf("creating config: %w", core.DiagsToError(diags))
+ }
+
+ // Map checksum
+ var checksumModel = &checksumModel{}
+ var checksumObject basetypes.ObjectValue
+ if imageResp.Checksum != nil {
+ checksumModel.Algorithm = types.StringPointerValue(imageResp.Checksum.Algorithm)
+ checksumModel.Digest = types.StringPointerValue(imageResp.Checksum.Digest)
+ checksumObject, diags = types.ObjectValue(checksumTypes, map[string]attr.Value{
+ "algorithm": checksumModel.Algorithm,
+ "digest": checksumModel.Digest,
+ })
+ } else {
+ checksumObject = types.ObjectNull(checksumTypes)
+ }
+ if diags.HasError() {
+ return fmt.Errorf("creating checksum: %w", core.DiagsToError(diags))
+ }
+
+ // Map labels
+ labels, err := iaasUtils.MapLabels(ctx, imageResp.Labels, model.Labels)
+ if err != nil {
+ return err
+ }
+
+ model.ImageId = types.StringValue(imageId)
+ model.Name = types.StringPointerValue(imageResp.Name)
+ model.DiskFormat = types.StringPointerValue(imageResp.DiskFormat)
+ model.MinDiskSize = types.Int64PointerValue(imageResp.MinDiskSize)
+ model.MinRAM = types.Int64PointerValue(imageResp.MinRam)
+ model.Protected = types.BoolPointerValue(imageResp.Protected)
+ model.Scope = types.StringPointerValue(imageResp.Scope)
+ model.Labels = labels
+ model.Config = configObject
+ model.Checksum = checksumObject
+ return nil
+}
+
+// imageMatchesFilter checks whether a given image matches all specified filter conditions.
+// It returns true only if all non-null fields in the filter match corresponding fields in the image's config.
+func imageMatchesFilter(img *iaas.Image, filter *Filter) bool {
+ if filter == nil {
+ return true
+ }
+
+ if img.Config == nil {
+ return false
+ }
+
+ cfg := img.Config
+
+ if !filter.OS.IsNull() &&
+ (cfg.OperatingSystem == nil || filter.OS.ValueString() != *cfg.OperatingSystem) {
+ return false
+ }
+
+ if !filter.Distro.IsNull() &&
+ (cfg.OperatingSystemDistro == nil || cfg.OperatingSystemDistro.Get() == nil ||
+ filter.Distro.ValueString() != *cfg.OperatingSystemDistro.Get()) {
+ return false
+ }
+
+ if !filter.Version.IsNull() &&
+ (cfg.OperatingSystemVersion == nil || cfg.OperatingSystemVersion.Get() == nil ||
+ filter.Version.ValueString() != *cfg.OperatingSystemVersion.Get()) {
+ return false
+ }
+
+ if !filter.UEFI.IsNull() &&
+ (cfg.Uefi == nil || filter.UEFI.ValueBool() != *cfg.Uefi) {
+ return false
+ }
+
+ if !filter.SecureBoot.IsNull() &&
+ (cfg.SecureBoot == nil || filter.SecureBoot.ValueBool() != *cfg.SecureBoot) {
+ return false
+ }
+
+ return true
+}
+
+// sortImagesByName sorts a slice of images by name, respecting nils and order direction.
+func sortImagesByName(images []*iaas.Image, sortAscending bool) {
+ if len(images) <= 1 {
+ return
+ }
+
+ sort.SliceStable(images, func(i, j int) bool {
+ a, b := images[i].Name, images[j].Name
+
+ switch {
+ case a == nil && b == nil:
+ return false // Equal
+ case a == nil:
+ return false // Nil goes after non-nil
+ case b == nil:
+ return true // Non-nil goes before nil
+ case sortAscending:
+ return *a < *b
+ default:
+ return *a > *b
+ }
+ })
+}
diff --git a/stackit/internal/services/iaas/imagev2/datasource_test.go b/stackit/internal/services/iaas/imagev2/datasource_test.go
new file mode 100644
index 00000000..56b9930b
--- /dev/null
+++ b/stackit/internal/services/iaas/imagev2/datasource_test.go
@@ -0,0 +1,471 @@
+package image
+
+import (
+ "context"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/stackitcloud/stackit-sdk-go/core/utils"
+ "github.com/stackitcloud/stackit-sdk-go/services/iaas"
+)
+
+func TestMapDataSourceFields(t *testing.T) {
+ tests := []struct {
+ description string
+ state DataSourceModel
+ input *iaas.Image
+ expected DataSourceModel
+ isValid bool
+ }{
+ {
+ "default_values",
+ DataSourceModel{
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ },
+ &iaas.Image{
+ Id: utils.Ptr("iid"),
+ },
+ DataSourceModel{
+ Id: types.StringValue("pid,iid"),
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Labels: types.MapNull(types.StringType),
+ },
+ true,
+ },
+ {
+ "simple_values",
+ DataSourceModel{
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ },
+ &iaas.Image{
+ Id: utils.Ptr("iid"),
+ Name: utils.Ptr("name"),
+ DiskFormat: utils.Ptr("format"),
+ MinDiskSize: utils.Ptr(int64(1)),
+ MinRam: utils.Ptr(int64(1)),
+ Protected: utils.Ptr(true),
+ Scope: utils.Ptr("scope"),
+ Config: &iaas.ImageConfig{
+ BootMenu: utils.Ptr(true),
+ CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")),
+ DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")),
+ NicModel: iaas.NewNullableString(utils.Ptr("model")),
+ OperatingSystem: utils.Ptr("os"),
+ OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")),
+ OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")),
+ RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")),
+ RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")),
+ SecureBoot: utils.Ptr(true),
+ Uefi: utils.Ptr(true),
+ VideoModel: iaas.NewNullableString(utils.Ptr("model")),
+ VirtioScsi: utils.Ptr(true),
+ },
+ Checksum: &iaas.ImageChecksum{
+ Algorithm: utils.Ptr("algorithm"),
+ Digest: utils.Ptr("digest"),
+ },
+ Labels: &map[string]interface{}{
+ "key": "value",
+ },
+ },
+ DataSourceModel{
+ Id: types.StringValue("pid,iid"),
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Name: types.StringValue("name"),
+ DiskFormat: types.StringValue("format"),
+ MinDiskSize: types.Int64Value(1),
+ MinRAM: types.Int64Value(1),
+ Protected: types.BoolValue(true),
+ Scope: types.StringValue("scope"),
+ Config: types.ObjectValueMust(configTypes, map[string]attr.Value{
+ "boot_menu": types.BoolValue(true),
+ "cdrom_bus": types.StringValue("cdrom_bus"),
+ "disk_bus": types.StringValue("disk_bus"),
+ "nic_model": types.StringValue("model"),
+ "operating_system": types.StringValue("os"),
+ "operating_system_distro": types.StringValue("os_distro"),
+ "operating_system_version": types.StringValue("os_version"),
+ "rescue_bus": types.StringValue("rescue_bus"),
+ "rescue_device": types.StringValue("rescue_device"),
+ "secure_boot": types.BoolValue(true),
+ "uefi": types.BoolValue(true),
+ "video_model": types.StringValue("model"),
+ "virtio_scsi": types.BoolValue(true),
+ }),
+ Checksum: types.ObjectValueMust(checksumTypes, map[string]attr.Value{
+ "algorithm": types.StringValue("algorithm"),
+ "digest": types.StringValue("digest"),
+ }),
+ Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
+ "key": types.StringValue("value"),
+ }),
+ },
+ true,
+ },
+ {
+ "empty_labels",
+ DataSourceModel{
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
+ },
+ &iaas.Image{
+ Id: utils.Ptr("iid"),
+ },
+ DataSourceModel{
+ Id: types.StringValue("pid,iid"),
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
+ },
+ true,
+ },
+ {
+ "response_nil_fail",
+ DataSourceModel{},
+ nil,
+ DataSourceModel{},
+ false,
+ },
+ {
+ "no_resource_id",
+ DataSourceModel{
+ ProjectId: types.StringValue("pid"),
+ },
+ &iaas.Image{},
+ DataSourceModel{},
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ err := mapDataSourceFields(context.Background(), tt.input, &tt.state)
+ if !tt.isValid && err == nil {
+ t.Fatalf("Should have failed")
+ }
+ if tt.isValid && err != nil {
+ t.Fatalf("Should not have failed: %v", err)
+ }
+ if tt.isValid {
+ diff := cmp.Diff(tt.state, tt.expected)
+ if diff != "" {
+ t.Fatalf("Data does not match: %s", diff)
+ }
+ }
+ })
+ }
+}
+
+func TestImageMatchesFilter(t *testing.T) {
+ testCases := []struct {
+ name string
+ img *iaas.Image
+ filter *Filter
+ expected bool
+ }{
+ {
+ name: "nil filter - always match",
+ img: &iaas.Image{Config: &iaas.ImageConfig{}},
+ filter: nil,
+ expected: true,
+ },
+ {
+ name: "nil config - always false",
+ img: &iaas.Image{Config: nil},
+ filter: &Filter{},
+ expected: false,
+ },
+ {
+ name: "all fields match",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ OperatingSystem: utils.Ptr("linux"),
+ OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("ubuntu")),
+ OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("22.04")),
+ Uefi: utils.Ptr(true),
+ SecureBoot: utils.Ptr(true),
+ },
+ },
+ filter: &Filter{
+ OS: types.StringValue("linux"),
+ Distro: types.StringValue("ubuntu"),
+ Version: types.StringValue("22.04"),
+ UEFI: types.BoolValue(true),
+ SecureBoot: types.BoolValue(true),
+ },
+ expected: true,
+ },
+ {
+ name: "OS mismatch",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ OperatingSystem: utils.Ptr("windows"),
+ },
+ },
+ filter: &Filter{
+ OS: types.StringValue("linux"),
+ },
+ expected: false,
+ },
+ {
+ name: "Distro mismatch",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("debian")),
+ },
+ },
+ filter: &Filter{
+ Distro: types.StringValue("ubuntu"),
+ },
+ expected: false,
+ },
+ {
+ name: "Version mismatch",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("20.04")),
+ },
+ },
+ filter: &Filter{
+ Version: types.StringValue("22.04"),
+ },
+ expected: false,
+ },
+ {
+ name: "UEFI mismatch",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ Uefi: utils.Ptr(false),
+ },
+ },
+ filter: &Filter{
+ UEFI: types.BoolValue(true),
+ },
+ expected: false,
+ },
+ {
+ name: "SecureBoot mismatch",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ SecureBoot: utils.Ptr(false),
+ },
+ },
+ filter: &Filter{
+ SecureBoot: types.BoolValue(true),
+ },
+ expected: false,
+ },
+ {
+ name: "SecureBoot match - true",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ SecureBoot: utils.Ptr(true),
+ },
+ },
+ filter: &Filter{
+ SecureBoot: types.BoolValue(true),
+ },
+ expected: true,
+ },
+ {
+ name: "SecureBoot match - false",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ SecureBoot: utils.Ptr(false),
+ },
+ },
+ filter: &Filter{
+ SecureBoot: types.BoolValue(false),
+ },
+ expected: true,
+ },
+ {
+ name: "SecureBoot field missing in image but required in filter",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ SecureBoot: nil,
+ },
+ },
+ filter: &Filter{
+ SecureBoot: types.BoolValue(true),
+ },
+ expected: false,
+ },
+ {
+ name: "partial filter match - only distro set and match",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("ubuntu")),
+ },
+ },
+ filter: &Filter{
+ Distro: types.StringValue("ubuntu"),
+ },
+ expected: true,
+ },
+ {
+ name: "partial filter match - distro mismatch",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("centos")),
+ },
+ },
+ filter: &Filter{
+ Distro: types.StringValue("ubuntu"),
+ },
+ expected: false,
+ },
+ {
+ name: "filter provided but attribute is null in image",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ OperatingSystemDistro: nil,
+ },
+ },
+ filter: &Filter{
+ Distro: types.StringValue("ubuntu"),
+ },
+ expected: false,
+ },
+ {
+ name: "image has valid config, but filter has null values",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ OperatingSystem: utils.Ptr("linux"),
+ OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("ubuntu")),
+ OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("22.04")),
+ Uefi: utils.Ptr(false),
+ SecureBoot: utils.Ptr(false),
+ },
+ },
+ filter: &Filter{
+ OS: types.StringNull(),
+ Distro: types.StringNull(),
+ Version: types.StringNull(),
+ UEFI: types.BoolNull(),
+ SecureBoot: types.BoolNull(),
+ },
+ expected: true,
+ },
+ {
+ name: "image has nil fields in config, filter expects values",
+ img: &iaas.Image{
+ Config: &iaas.ImageConfig{
+ OperatingSystem: nil,
+ OperatingSystemDistro: nil,
+ OperatingSystemVersion: nil,
+ Uefi: nil,
+ SecureBoot: nil,
+ },
+ },
+ filter: &Filter{
+ OS: types.StringValue("linux"),
+ Distro: types.StringValue("ubuntu"),
+ Version: types.StringValue("22.04"),
+ UEFI: types.BoolValue(true),
+ SecureBoot: types.BoolValue(true),
+ },
+ expected: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ result := imageMatchesFilter(tc.img, tc.filter)
+ if result != tc.expected {
+ t.Errorf("Expected match = %v, got %v", tc.expected, result)
+ }
+ })
+ }
+}
+
+func TestSortImagesByName(t *testing.T) {
+ tests := []struct {
+ desc string
+ input []*iaas.Image
+ ascending bool
+ wantSorted []string
+ }{
+ {
+ desc: "ascending by name",
+ ascending: true,
+ input: []*iaas.Image{
+ {Name: utils.Ptr("Ubuntu 22.04")},
+ {Name: utils.Ptr("Ubuntu 18.04")},
+ {Name: utils.Ptr("Ubuntu 20.04")},
+ },
+ wantSorted: []string{"Ubuntu 18.04", "Ubuntu 20.04", "Ubuntu 22.04"},
+ },
+ {
+ desc: "descending by name",
+ ascending: false,
+ input: []*iaas.Image{
+ {Name: utils.Ptr("Ubuntu 22.04")},
+ {Name: utils.Ptr("Ubuntu 18.04")},
+ {Name: utils.Ptr("Ubuntu 20.04")},
+ },
+ wantSorted: []string{"Ubuntu 22.04", "Ubuntu 20.04", "Ubuntu 18.04"},
+ },
+ {
+ desc: "nil names go last ascending",
+ ascending: true,
+ input: []*iaas.Image{
+ {Name: nil},
+ {Name: utils.Ptr("Ubuntu 18.04")},
+ {Name: nil},
+ {Name: utils.Ptr("Ubuntu 20.04")},
+ },
+ wantSorted: []string{"Ubuntu 18.04", "Ubuntu 20.04", "", ""},
+ },
+ {
+ desc: "nil names go last descending",
+ ascending: false,
+ input: []*iaas.Image{
+ {Name: nil},
+ {Name: utils.Ptr("Ubuntu 18.04")},
+ {Name: utils.Ptr("Ubuntu 20.04")},
+ {Name: nil},
+ },
+ wantSorted: []string{"Ubuntu 20.04", "Ubuntu 18.04", "", ""},
+ },
+ {
+ desc: "empty slice",
+ ascending: true,
+ input: []*iaas.Image{},
+ wantSorted: []string{},
+ },
+ {
+ desc: "single element slice",
+ ascending: true,
+ input: []*iaas.Image{
+ {Name: utils.Ptr("Ubuntu 22.04")},
+ },
+ wantSorted: []string{"Ubuntu 22.04"},
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.desc, func(t *testing.T) {
+ sortImagesByName(tc.input, tc.ascending)
+
+ gotNames := make([]string, len(tc.input))
+ for i, img := range tc.input {
+ if img.Name == nil {
+ gotNames[i] = ""
+ } else {
+ gotNames[i] = *img.Name
+ }
+ }
+
+ if diff := cmp.Diff(tc.wantSorted, gotNames); diff != "" {
+ t.Fatalf("incorrect sort order (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
diff --git a/stackit/internal/services/iaas/testdata/datasource-image-v2-variants.tf b/stackit/internal/services/iaas/testdata/datasource-image-v2-variants.tf
new file mode 100644
index 00000000..379bae3f
--- /dev/null
+++ b/stackit/internal/services/iaas/testdata/datasource-image-v2-variants.tf
@@ -0,0 +1,62 @@
+variable "project_id" {}
+
+data "stackit_image_v2" "name_match_ubuntu_22_04" {
+ project_id = var.project_id
+ name = "Ubuntu 22.04"
+}
+
+data "stackit_image_v2" "ubuntu_by_image_id" {
+ project_id = var.project_id
+ image_id = data.stackit_image_v2.name_match_ubuntu_22_04.image_id
+}
+
+data "stackit_image_v2" "regex_match_ubuntu_22_04" {
+ project_id = var.project_id
+ name_regex = "(?i)^ubuntu 22.04$"
+}
+
+data "stackit_image_v2" "filter_debian_11" {
+ project_id = var.project_id
+ filter = {
+ distro = "debian"
+ version = "11"
+ }
+}
+
+data "stackit_image_v2" "filter_uefi_ubuntu" {
+ project_id = var.project_id
+ filter = {
+ distro = "ubuntu"
+ uefi = true
+ }
+}
+
+data "stackit_image_v2" "name_regex_and_filter_rhel_9_1" {
+ project_id = var.project_id
+ name_regex = "^Red Hat Enterprise Linux 9.1$"
+ filter = {
+ distro = "rhel"
+ version = "9.1"
+ uefi = true
+ }
+}
+
+data "stackit_image_v2" "name_windows_2022_standard" {
+ project_id = var.project_id
+ name = "Windows Server 2022 Standard"
+}
+
+data "stackit_image_v2" "ubuntu_arm64_latest" {
+ project_id = var.project_id
+ filter = {
+ distro = "ubuntu-arm64"
+ }
+}
+
+data "stackit_image_v2" "ubuntu_arm64_oldest" {
+ project_id = var.project_id
+ filter = {
+ distro = "ubuntu-arm64"
+ }
+ sort_ascending = true
+}
\ No newline at end of file
diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go
index 4c1d46a6..aee5ee68 100644
--- a/stackit/internal/testutil/testutil.go
+++ b/stackit/internal/testutil/testutil.go
@@ -38,9 +38,6 @@ var (
Region = os.Getenv("TF_ACC_REGION")
// ServerId is the id of a server used for some tests
ServerId = getenv("TF_ACC_SERVER_ID", "")
- // IaaSImageId is the id of an image used for IaaS acceptance tests.
- // Default image is ubuntu 24.04
- IaaSImageId = getenv("TF_ACC_IMAGE_ID", "59838a89-51b1-4892-b57f-b3caf598ee2f")
// TestProjectParentContainerID is the container id of the parent resource under which projects are created as part of the resource-manager acceptance tests
TestProjectParentContainerID = os.Getenv("TF_ACC_TEST_PROJECT_PARENT_CONTAINER_ID")
// TestProjectParentUUID is the uuid of the parent resource under which projects are created as part of the resource-manager acceptance tests
@@ -532,8 +529,7 @@ func getenv(key, defaultValue string) string {
return val
}
-// helper for local_file_path
-// no real data is created
+// CreateDefaultLocalFile is a helper for local_file_path. No real data is created
func CreateDefaultLocalFile() os.File {
// Define the file name and size
fileName := "test-512k.img"
diff --git a/stackit/provider.go b/stackit/provider.go
index 698af5c8..c3ac8f33 100644
--- a/stackit/provider.go
+++ b/stackit/provider.go
@@ -26,6 +26,7 @@ import (
gitInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/git/instance"
iaasAffinityGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/affinitygroup"
iaasImage "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/image"
+ iaasImageV2 "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/imagev2"
iaasKeyPair "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/keypair"
machineType "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/machinetype"
iaasNetwork "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network"
@@ -457,6 +458,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
gitInstance.NewGitDataSource,
iaasAffinityGroup.NewAffinityGroupDatasource,
iaasImage.NewImageDataSource,
+ iaasImageV2.NewImageV2DataSource,
iaasNetwork.NewNetworkDataSource,
iaasNetworkArea.NewNetworkAreaDataSource,
iaasNetworkAreaRoute.NewNetworkAreaRouteDataSource,