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"