diff --git a/docs/data-sources/image.md b/docs/data-sources/image.md index 235665268..23e68257c 100644 --- a/docs/data-sources/image.md +++ b/docs/data-sources/image.md @@ -4,19 +4,78 @@ page_title: "stackit_image 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] + } + } --- # stackit_image (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] + } +} +``` + ## Example Usage ```terraform -data "stackit_image" "example" { +data "stackit_image" "default" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" image_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } + +data "stackit_image" "name_match" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "Ubuntu 22.04" +} + +data "stackit_image" "name_regex_latest" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name_regex = "^Ubuntu .*" +} + +data "stackit_image" "name_regex_oldest" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name_regex = "^Ubuntu .*" + sort_ascending = true +} + +data "stackit_image" "filter_distro_version" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + filter = { + distro = "debian" + version = "11" + } +} ``` @@ -24,9 +83,16 @@ data "stackit_image" "example" { ### Required -- `image_id` (String) The image ID. - `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)) @@ -36,10 +102,21 @@ data "stackit_image" "example" { - `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. -- `name` (String) The name of the image. - `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` diff --git a/docs/resources/server.md b/docs/resources/server.md index 212c44a38..c9dc3d5a3 100644 --- a/docs/resources/server.md +++ b/docs/resources/server.md @@ -7,6 +7,11 @@ description: |- Example Usage With key pair + data "stackit_image" "image" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "Ubuntu 22.04" + } + resource "stackit_key_pair" "keypair" { name = "example-key-pair" public_key = chomp(file("path/to/id_rsa.pub")) @@ -17,7 +22,7 @@ description: |- boot_volume = { size = 64 source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + source_id = data.stackit_image.image.id } name = "example-server" machine_type = "g1.1" @@ -28,13 +33,18 @@ description: |- Boot from volume + data "stackit_image" "image" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "Ubuntu 22.04" + } + resource "stackit_server" "boot-from-volume" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-server" boot_volume = { size = 64 source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + source_id = data.stackit_image.image.id } availability_zone = "eu01-1" machine_type = "g1.1" @@ -44,12 +54,17 @@ description: |- Boot from existing volume + data "stackit_image" "image" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "Ubuntu 22.04" + } + resource "stackit_volume" "example-volume" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" size = 12 source = { type = "image" - id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + id = data.stackit_image.image.id } name = "example-volume" availability_zone = "eu01-1" @@ -188,6 +203,11 @@ Server resource schema. Must have a region specified in the provider configurati ### With key pair ```terraform +data "stackit_image" "image" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "Ubuntu 22.04" +} + resource "stackit_key_pair" "keypair" { name = "example-key-pair" public_key = chomp(file("path/to/id_rsa.pub")) @@ -198,7 +218,7 @@ resource "stackit_server" "user-data-from-file" { boot_volume = { size = 64 source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + source_id = data.stackit_image.image.id } name = "example-server" machine_type = "g1.1" @@ -210,13 +230,18 @@ resource "stackit_server" "user-data-from-file" { ### Boot from volume ```terraform +data "stackit_image" "image" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "Ubuntu 22.04" +} + resource "stackit_server" "boot-from-volume" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-server" boot_volume = { size = 64 source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + source_id = data.stackit_image.image.id } availability_zone = "eu01-1" machine_type = "g1.1" @@ -227,12 +252,17 @@ resource "stackit_server" "boot-from-volume" { ### Boot from existing volume ```terraform +data "stackit_image" "image" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "Ubuntu 22.04" +} + resource "stackit_volume" "example-volume" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" size = 12 source = { type = "image" - id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + id = data.stackit_image.image.id } name = "example-volume" availability_zone = "eu01-1" diff --git a/examples/data-sources/stackit_image/data-source.tf b/examples/data-sources/stackit_image/data-source.tf index adc055873..059296c00 100644 --- a/examples/data-sources/stackit_image/data-source.tf +++ b/examples/data-sources/stackit_image/data-source.tf @@ -1,4 +1,28 @@ -data "stackit_image" "example" { +data "stackit_image" "default" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" image_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } + +data "stackit_image" "name_match" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "Ubuntu 22.04" +} + +data "stackit_image" "name_regex_latest" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name_regex = "^Ubuntu .*" +} + +data "stackit_image" "name_regex_oldest" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name_regex = "^Ubuntu .*" + sort_ascending = true +} + +data "stackit_image" "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 574c6e821..4c61f9009 100644 --- a/stackit/internal/services/iaas/iaas_acc_test.go +++ b/stackit/internal/services/iaas/iaas_acc_test.go @@ -32,6 +32,9 @@ var ( //go:embed testdata/resource-security-group-max.tf resourceSecurityGroupMaxConfig string + //go:embed testdata/datasource-image-variants.tf + dataSourceImageVariants string + //go:embed testdata/resource-image-min.tf resourceImageMinConfig string @@ -3542,6 +3545,133 @@ func TestAccImageMax(t *testing.T) { }) } +func TestAccImageDatasourceSearchVariants(t *testing.T) { + t.Log("TestDataSource Image 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.IaaSProviderConfig()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_image.name_match_ubuntu_22_04", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), + resource.TestCheckResourceAttrSet("data.stackit_image.name_match_ubuntu_22_04", "image_id"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_match_ubuntu_22_04", "name"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_match_ubuntu_22_04", "min_disk_size"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_match_ubuntu_22_04", "min_ram"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_match_ubuntu_22_04", "protected"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_match_ubuntu_22_04", "scope"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_match_ubuntu_22_04", "checksum.algorithm"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_match_ubuntu_22_04", "checksum.digest"), + + resource.TestCheckResourceAttr("data.stackit_image.ubuntu_by_image_id", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_by_image_id", "image_id"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_by_image_id", "name"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_by_image_id", "min_disk_size"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_by_image_id", "min_ram"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_by_image_id", "protected"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_by_image_id", "scope"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_by_image_id", "checksum.algorithm"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_by_image_id", "checksum.digest"), + + resource.TestCheckResourceAttr("data.stackit_image.regex_match_ubuntu_22_04", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), + resource.TestCheckResourceAttrSet("data.stackit_image.regex_match_ubuntu_22_04", "image_id"), + resource.TestCheckResourceAttrSet("data.stackit_image.regex_match_ubuntu_22_04", "name"), + resource.TestCheckResourceAttrSet("data.stackit_image.regex_match_ubuntu_22_04", "min_disk_size"), + resource.TestCheckResourceAttrSet("data.stackit_image.regex_match_ubuntu_22_04", "min_ram"), + resource.TestCheckResourceAttrSet("data.stackit_image.regex_match_ubuntu_22_04", "protected"), + resource.TestCheckResourceAttrSet("data.stackit_image.regex_match_ubuntu_22_04", "scope"), + resource.TestCheckResourceAttrSet("data.stackit_image.regex_match_ubuntu_22_04", "checksum.algorithm"), + resource.TestCheckResourceAttrSet("data.stackit_image.regex_match_ubuntu_22_04", "checksum.digest"), + + resource.TestCheckResourceAttr("data.stackit_image.filter_debian_11", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_debian_11", "image_id"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_debian_11", "name"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_debian_11", "min_disk_size"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_debian_11", "min_ram"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_debian_11", "protected"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_debian_11", "scope"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_debian_11", "checksum.algorithm"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_debian_11", "checksum.digest"), + + resource.TestCheckResourceAttr("data.stackit_image.filter_uefi_ubuntu", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_uefi_ubuntu", "image_id"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_uefi_ubuntu", "name"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_uefi_ubuntu", "min_disk_size"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_uefi_ubuntu", "min_ram"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_uefi_ubuntu", "protected"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_uefi_ubuntu", "scope"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_uefi_ubuntu", "checksum.algorithm"), + resource.TestCheckResourceAttrSet("data.stackit_image.filter_uefi_ubuntu", "checksum.digest"), + + resource.TestCheckResourceAttr("data.stackit_image.name_regex_and_filter_rhel_9_1", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), + resource.TestCheckResourceAttrSet("data.stackit_image.name_regex_and_filter_rhel_9_1", "image_id"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_regex_and_filter_rhel_9_1", "name"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_regex_and_filter_rhel_9_1", "min_disk_size"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_regex_and_filter_rhel_9_1", "min_ram"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_regex_and_filter_rhel_9_1", "protected"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_regex_and_filter_rhel_9_1", "scope"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_regex_and_filter_rhel_9_1", "checksum.algorithm"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_regex_and_filter_rhel_9_1", "checksum.digest"), + + resource.TestCheckResourceAttr("data.stackit_image.name_windows_2022_standard", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), + resource.TestCheckResourceAttrSet("data.stackit_image.name_windows_2022_standard", "image_id"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_windows_2022_standard", "name"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_windows_2022_standard", "min_disk_size"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_windows_2022_standard", "min_ram"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_windows_2022_standard", "protected"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_windows_2022_standard", "scope"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_windows_2022_standard", "checksum.algorithm"), + resource.TestCheckResourceAttrSet("data.stackit_image.name_windows_2022_standard", "checksum.digest"), + + resource.TestCheckResourceAttr("data.stackit_image.ubuntu_arm64_latest", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_latest", "image_id"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_latest", "name"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_latest", "min_disk_size"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_latest", "min_ram"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_latest", "protected"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_latest", "scope"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_latest", "checksum.algorithm"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_latest", "checksum.digest"), + + resource.TestCheckResourceAttr("data.stackit_image.ubuntu_arm64_oldest", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_oldest", "image_id"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_oldest", "name"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_oldest", "min_disk_size"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_oldest", "min_ram"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_oldest", "protected"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_oldest", "scope"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_oldest", "checksum.algorithm"), + resource.TestCheckResourceAttrSet("data.stackit_image.ubuntu_arm64_oldest", "checksum.digest"), + + // e2e test that ascending sort is working + func(s *terraform.State) error { + latest := s.RootModule().Resources["data.stackit_image.ubuntu_arm64_latest"] + oldest := s.RootModule().Resources["data.stackit_image.ubuntu_arm64_oldest"] + + if latest == nil { + return fmt.Errorf("datasource 'data.stackit_image.ubuntu_arm64_latest' not found") + } + if oldest == nil { + return fmt.Errorf("datasource 'data.stackit_image.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 testAccCheckDestroy(s *terraform.State) error { checkFunctions := []func(s *terraform.State) error{ testAccCheckNetworkDestroy, diff --git a/stackit/internal/services/iaas/image/datasource.go b/stackit/internal/services/iaas/image/datasource.go index 4b24a8ff7..a962fae03 100644 --- a/stackit/internal/services/iaas/image/datasource.go +++ b/stackit/internal/services/iaas/image/datasource.go @@ -4,9 +4,16 @@ 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" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "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" @@ -16,10 +23,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" ) // Ensure the implementation satisfies the expected interfaces. @@ -28,10 +33,14 @@ var ( ) 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"` + 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"` @@ -42,6 +51,14 @@ type DataSourceModel struct { 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"` +} + // NewImageDataSource is a helper function to simplify the provider implementation. func NewImageDataSource() datasource.DataSource { return &imageDataSource{} @@ -62,18 +79,54 @@ func (d *imageDataSource) Configure(ctx context.Context, req datasource.Configur if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } + d.client = apiClient tflog.Info(ctx, "iaas client configured") } +func (d *imageDataSource) 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 (r *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "Image datasource schema. Must have a `region` specified in the provider configuration." +func (d *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + description := 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"+ + "```", + ) resp.Schema = schema.Schema{ MarkdownDescription: description, Description: description, @@ -91,16 +144,50 @@ func (r *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, }, }, "image_id": schema.StringAttribute{ - Description: "The image ID.", - Required: true, + Description: "Image ID to fetch directly", + Optional: true, Validators: []validator.String{ validate.UUID(), validate.NoSeparator(), }, }, "name": schema.StringAttribute{ - Description: "The name of the image.", - Computed: true, + 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.", @@ -203,47 +290,122 @@ func (r *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, } } -// // Read refreshes the Terraform state with the latest data. -func (r *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +// Read refreshes the Terraform state with the latest data. +func (d *imageDataSource) 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() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "image_id", imageId) - imageResp, err := r.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 + 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 _, img := range *imageList.Items { + 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. + filteredImages := make([]*iaas.Image, 0, len(matchedImages)) + 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] } - // Map response body to schema 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") } @@ -342,3 +504,70 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data 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/image/datasource_test.go b/stackit/internal/services/iaas/image/datasource_test.go index a16120ac9..56b9930b1 100644 --- a/stackit/internal/services/iaas/image/datasource_test.go +++ b/stackit/internal/services/iaas/image/datasource_test.go @@ -161,3 +161,311 @@ func TestMapDataSourceFields(t *testing.T) { }) } } + +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/server/const.go b/stackit/internal/services/iaas/server/const.go index e923f6bca..d89cc3181 100644 --- a/stackit/internal/services/iaas/server/const.go +++ b/stackit/internal/services/iaas/server/const.go @@ -6,6 +6,11 @@ Server resource schema. Must have a region specified in the provider configurati ### With key pair` + "\n" + "```terraform" + ` +data "stackit_image" "image" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "Ubuntu 22.04" +} + resource "stackit_key_pair" "keypair" { name = "example-key-pair" public_key = chomp(file("path/to/id_rsa.pub")) @@ -16,7 +21,7 @@ resource "stackit_server" "user-data-from-file" { boot_volume = { size = 64 source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + source_id = data.stackit_image.image.id } name = "example-server" machine_type = "g1.1" @@ -27,13 +32,18 @@ resource "stackit_server" "user-data-from-file" { ### Boot from volume` + "\n" + "```terraform" + ` +data "stackit_image" "image" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "Ubuntu 22.04" +} + resource "stackit_server" "boot-from-volume" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-server" boot_volume = { size = 64 source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + source_id = data.stackit_image.image.id } availability_zone = "eu01-1" machine_type = "g1.1" @@ -43,12 +53,17 @@ resource "stackit_server" "boot-from-volume" { ### Boot from existing volume` + "\n" + "```terraform" + ` +data "stackit_image" "image" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "Ubuntu 22.04" +} + resource "stackit_volume" "example-volume" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" size = 12 source = { type = "image" - id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + id = data.stackit_image.image.id } name = "example-volume" availability_zone = "eu01-1" diff --git a/stackit/internal/services/iaas/testdata/datasource-image-variants.tf b/stackit/internal/services/iaas/testdata/datasource-image-variants.tf new file mode 100644 index 000000000..63c54b6e1 --- /dev/null +++ b/stackit/internal/services/iaas/testdata/datasource-image-variants.tf @@ -0,0 +1,62 @@ +variable "project_id" {} + +data "stackit_image" "name_match_ubuntu_22_04" { + project_id = var.project_id + name = "Ubuntu 22.04" +} + +data "stackit_image" "ubuntu_by_image_id" { + project_id = var.project_id + image_id = data.stackit_image.name_match_ubuntu_22_04.image_id +} + +data "stackit_image" "regex_match_ubuntu_22_04" { + project_id = var.project_id + name_regex = "(?i)^ubuntu 22.04$" +} + +data "stackit_image" "filter_debian_11" { + project_id = var.project_id + filter = { + distro = "debian" + version = "11" + } +} + +data "stackit_image" "filter_uefi_ubuntu" { + project_id = var.project_id + filter = { + distro = "ubuntu" + uefi = true + } +} + +data "stackit_image" "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" "name_windows_2022_standard" { + project_id = var.project_id + name = "Windows Server 2022 Standard" +} + +data "stackit_image" "ubuntu_arm64_latest" { + project_id = var.project_id + filter = { + distro = "ubuntu-arm64" + } +} + +data "stackit_image" "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 220a79b8c..24cc4b53d 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -35,9 +35,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 @@ -508,8 +505,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"