diff --git a/docs/data-sources/image.md b/docs/data-sources/image.md
new file mode 100644
index 00000000..50a2fe16
--- /dev/null
+++ b/docs/data-sources/image.md
@@ -0,0 +1,72 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "stackit_image Data Source - stackit"
+subcategory: ""
+description: |-
+ Image datasource schema. Must have a region specified in the provider configuration.
+ ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
+---
+
+# stackit_image (Data Source)
+
+Image datasource schema. Must have a `region` specified in the provider configuration.
+
+~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.
+
+## Example Usage
+
+```terraform
+data "stackit_image" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ image_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+}
+```
+
+
+## Schema
+
+### Required
+
+- `image_id` (String) The image ID.
+- `project_id` (String) STACKIT project ID to which the image is associated.
+
+### Read-Only
+
+- `checksum` (Attributes) Representation of an image checksum. (see [below for nested schema](#nestedatt--checksum))
+- `config` (Attributes) Properties to set hardware and scheduling settings for an image. (see [below for nested schema](#nestedatt--config))
+- `disk_format` (String) The disk format of the image.
+- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`image_id`".
+- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
+- `min_disk_size` (Number) The minimum disk size of the image in GB.
+- `min_ram` (Number) The minimum RAM of the image in MB.
+- `name` (String) The name of the image.
+- `protected` (Boolean) Whether the image is protected.
+- `scope` (String) The scope of the image.
+
+
+### Nested Schema for `checksum`
+
+Read-Only:
+
+- `algorithm` (String) Algorithm for the checksum of the image data.
+- `digest` (String) Hexdigest of the checksum of the image data.
+
+
+
+### Nested Schema for `config`
+
+Read-Only:
+
+- `boot_menu` (Boolean) Enables the BIOS bootmenu.
+- `cdrom_bus` (String) Sets CDROM bus controller type.
+- `disk_bus` (String) Sets Disk bus controller type.
+- `nic_model` (String) Sets virtual network interface model.
+- `operating_system` (String) Enables operating system specific optimizations.
+- `operating_system_distro` (String) Operating system distribution.
+- `operating_system_version` (String) Version of the operating system.
+- `rescue_bus` (String) Sets the device bus when the image is used as a rescue image.
+- `rescue_device` (String) Sets the device when the image is used as a rescue image.
+- `secure_boot` (Boolean) Enables Secure Boot.
+- `uefi` (Boolean) Enables UEFI boot.
+- `video_model` (String) Sets Graphic device model.
+- `virtio_scsi` (Boolean) Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block.
diff --git a/docs/resources/image.md b/docs/resources/image.md
new file mode 100644
index 00000000..eca41fac
--- /dev/null
+++ b/docs/resources/image.md
@@ -0,0 +1,77 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "stackit_image Resource - stackit"
+subcategory: ""
+description: |-
+ Image resource schema. Must have a region specified in the provider configuration.
+---
+
+# stackit_image (Resource)
+
+Image resource schema. Must have a `region` specified in the provider configuration.
+
+## Example Usage
+
+```terraform
+resource "stackit_image" "example_image" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name = "example-image"
+ disk_format = "qcow2"
+ local_file_path = "./path/to/image.qcow2"
+ min_disk_size = 10
+ min_ram = 5
+}
+```
+
+
+## Schema
+
+### Required
+
+- `disk_format` (String) The disk format of the image.
+- `local_file_path` (String) The filepath of the raw image file to be uploaded.
+- `name` (String) The name of the image.
+- `project_id` (String) STACKIT project ID to which the image is associated.
+
+### Optional
+
+- `config` (Attributes) Properties to set hardware and scheduling settings for an image. (see [below for nested schema](#nestedatt--config))
+- `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.
+
+### Read-Only
+
+- `checksum` (Attributes) Representation of an image checksum. (see [below for nested schema](#nestedatt--checksum))
+- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`image_id`".
+- `image_id` (String) The image ID.
+- `protected` (Boolean) Whether the image is protected.
+- `scope` (String) The scope of the image.
+
+
+### Nested Schema for `config`
+
+Optional:
+
+- `boot_menu` (Boolean) Enables the BIOS bootmenu.
+- `cdrom_bus` (String) Sets CDROM bus controller type.
+- `disk_bus` (String) Sets Disk bus controller type.
+- `nic_model` (String) Sets virtual network interface model.
+- `operating_system` (String) Enables operating system specific optimizations.
+- `operating_system_distro` (String) Operating system distribution.
+- `operating_system_version` (String) Version of the operating system.
+- `rescue_bus` (String) Sets the device bus when the image is used as a rescue image.
+- `rescue_device` (String) Sets the device when the image is used as a rescue image.
+- `secure_boot` (Boolean) Enables Secure Boot.
+- `uefi` (Boolean) Enables UEFI boot.
+- `video_model` (String) Sets Graphic device model.
+- `virtio_scsi` (Boolean) Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block.
+
+
+
+### Nested Schema for `checksum`
+
+Read-Only:
+
+- `algorithm` (String) Algorithm for the checksum of the image data.
+- `digest` (String) Hexdigest of the checksum of the image data.
diff --git a/examples/data-sources/stackit_image/data-source.tf b/examples/data-sources/stackit_image/data-source.tf
new file mode 100644
index 00000000..adc05587
--- /dev/null
+++ b/examples/data-sources/stackit_image/data-source.tf
@@ -0,0 +1,4 @@
+data "stackit_image" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ image_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+}
diff --git a/examples/resources/stackit_image/resource.tf b/examples/resources/stackit_image/resource.tf
new file mode 100644
index 00000000..de3253b0
--- /dev/null
+++ b/examples/resources/stackit_image/resource.tf
@@ -0,0 +1,8 @@
+resource "stackit_image" "example_image" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name = "example-image"
+ disk_format = "qcow2"
+ local_file_path = "./path/to/image.qcow2"
+ min_disk_size = 10
+ min_ram = 5
+}
diff --git a/go.mod b/go.mod
index 37a89c02..91489a7d 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,6 @@ require (
github.com/stackitcloud/stackit-sdk-go/services/argus v0.11.0
github.com/stackitcloud/stackit-sdk-go/services/dns v0.12.1
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.18.0
- github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.12-alpha
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.17.0
github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.1
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.1
diff --git a/go.sum b/go.sum
index 02bdc125..945d86a5 100644
--- a/go.sum
+++ b/go.sum
@@ -157,8 +157,6 @@ github.com/stackitcloud/stackit-sdk-go/services/dns v0.12.1 h1:nzOZQ2X6joM2iSteP
github.com/stackitcloud/stackit-sdk-go/services/dns v0.12.1/go.mod h1:mv8U7kuclXo+0VpDHtBCkve/3i9h1yT+RAId/MUi+C8=
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.18.0 h1:7QPYi7OZXUSO1uOtp1UXeCbxK0BGJbmjp71kaQOrMa8=
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.18.0/go.mod h1:YfuN+eXuqr846xeRyW2Vf1JM2jU0ikeQa76dDI66RsM=
-github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.12-alpha h1:jwpif4t2gthmKmCXsQ84rmtDdcZkw4QQTFiCd7nTW8M=
-github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.12-alpha/go.mod h1:nW/6vvumUHA7o1/JOOqsrEOBNrRHombEKB1U4jmg2wU=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.17.0 h1:06CGP64CEk3Zg6i9kZCMRdmCzLLiyMWQqGK1teBr9Oc=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.17.0/go.mod h1:JL94zc8K0ebWs+DBGXR28vNCF0EFV54ZLUtrlXOvWgA=
github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.1 h1:ogo7Ce4wA9ln/Z0VwvckH0FT5/i7d9/34bG85aayHn8=
diff --git a/stackit/internal/services/iaas/iaas_acc_test.go b/stackit/internal/services/iaas/iaas_acc_test.go
index bef29e09..27d55de2 100644
--- a/stackit/internal/services/iaas/iaas_acc_test.go
+++ b/stackit/internal/services/iaas/iaas_acc_test.go
@@ -12,7 +12,6 @@ import (
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
- "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
)
@@ -110,6 +109,18 @@ var keyPairResource = map[string]string{
"label1-updated": "value1-updated",
}
+// Image resource data
+var imageResource = map[string]string{
+ "project_id": testutil.ProjectId,
+ "name": fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha)),
+ "disk_format": "qcow2",
+ "local_file_path": testutil.TestImageLocalFilePath,
+ "min_disk_size": "1",
+ "min_ram": "1",
+ "label1": "value1",
+ "boot_menu": "true",
+}
+
func networkResourceConfig(name, nameservers string) string {
return fmt.Sprintf(`
resource "stackit_network" "network" {
@@ -304,6 +315,34 @@ func serviceAccountAttachmentResourceConfig() string {
)
}
+func imageResourceConfig(name string) string {
+ return fmt.Sprintf(`
+ resource "stackit_image" "image" {
+ project_id = "%s"
+ name = "%s"
+ disk_format = "%s"
+ local_file_path = "%s"
+ min_disk_size = %s
+ min_ram = %s
+ labels = {
+ "label1" = "%s"
+ }
+ config = {
+ boot_menu = %s
+ }
+ }
+ `,
+ imageResource["project_id"],
+ name,
+ imageResource["disk_format"],
+ imageResource["local_file_path"],
+ imageResource["min_disk_size"],
+ imageResource["min_ram"],
+ imageResource["label1"],
+ imageResource["boot_menu"],
+ )
+}
+
func testAccNetworkAreaConfig(areaname, networkranges, routeLabelValue string) string {
return fmt.Sprintf("%s\n\n%s\n\n%s",
testutil.IaaSProviderConfig(),
@@ -354,6 +393,13 @@ func testAccKeyPairConfig(keyPairResourceConfig string) string {
)
}
+func testAccImageConfig(name string) string {
+ return fmt.Sprintf("%s\n\n%s",
+ testutil.IaaSProviderConfig(),
+ imageResourceConfig(name),
+ )
+}
+
func TestAccNetworkArea(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
@@ -1282,6 +1328,95 @@ func TestAccKeyPair(t *testing.T) {
})
}
+func TestAccImage(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
+ CheckDestroy: testAccCheckIaaSImageDestroy,
+ Steps: []resource.TestStep{
+
+ // Creation
+ {
+ Config: testAccImageConfig(imageResource["name"]),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("stackit_image.image", "project_id", imageResource["project_id"]),
+ resource.TestCheckResourceAttrSet("stackit_image.image", "image_id"),
+ resource.TestCheckResourceAttr("stackit_image.image", "name", imageResource["name"]),
+ resource.TestCheckResourceAttr("stackit_image.image", "disk_format", imageResource["disk_format"]),
+ resource.TestCheckResourceAttr("stackit_image.image", "min_disk_size", imageResource["min_disk_size"]),
+ resource.TestCheckResourceAttr("stackit_image.image", "min_ram", imageResource["min_ram"]),
+ resource.TestCheckResourceAttr("stackit_image.image", "labels.label1", imageResource["label1"]),
+ resource.TestCheckResourceAttr("stackit_image.image", "config.boot_menu", imageResource["boot_menu"]),
+ resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.algorithm"),
+ resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.digest"),
+ ),
+ },
+ // Data source
+ {
+ Config: fmt.Sprintf(`
+ %s
+
+ data "stackit_image" "image" {
+ project_id = stackit_image.image.project_id
+ image_id = stackit_image.image.image_id
+ }
+ `,
+ testAccImageConfig(
+ fmt.Sprintf(`
+ resource "stackit_image" "image" {
+ project_id = "%s"
+ labels = {
+ "label1" = "%s"
+ }
+ }
+ `,
+ imageResource["project_id"],
+ imageResource["label1"],
+ ),
+ ),
+ ),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ // Instance
+ resource.TestCheckResourceAttr("data.stackit_image.image", "project_id", imageResource["project_id"]),
+ resource.TestCheckResourceAttrPair("data.stackit_image.image", "image_id", "stackit_image.image", "image_id"),
+ resource.TestCheckResourceAttrPair("data.stackit_image.image", "name", "stackit_image.image", "name"),
+ resource.TestCheckResourceAttrPair("data.stackit_image.image", "disk_format", "stackit_image.image", "disk_format"),
+ resource.TestCheckResourceAttrPair("data.stackit_image.image", "min_disk_size", "stackit_image.image", "min_disk_size"),
+ resource.TestCheckResourceAttrPair("data.stackit_image.image", "min_ram", "stackit_image.image", "min_ram"),
+ resource.TestCheckResourceAttrPair("data.stackit_image.image", "protected", "stackit_image.image", "protected"),
+ resource.TestCheckResourceAttrPair("data.stackit_image.image", "labels.label1", "stackit_image.image", "labels.label1"),
+ ),
+ },
+ // Import
+ {
+ ResourceName: "stackit_image.image",
+ ImportStateIdFunc: func(s *terraform.State) (string, error) {
+ r, ok := s.RootModule().Resources["stackit_image.image"]
+ if !ok {
+ return "", fmt.Errorf("couldn't find resource stackit_image.image")
+ }
+ imageId, ok := r.Primary.Attributes["image_id"]
+ if !ok {
+ return "", fmt.Errorf("couldn't find attribute image_id")
+ }
+ return fmt.Sprintf("%s,%s", testutil.ProjectId, imageId), nil
+ },
+ ImportState: true,
+ ImportStateVerify: true,
+ },
+ // Update
+ {
+ Config: testAccImageConfig(fmt.Sprintf("%s-updated", imageResource["name"])),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("stackit_image.image", "name", fmt.Sprintf("%s-updated", imageResource["name"])),
+ resource.TestCheckResourceAttr("stackit_image.image", "project_id", imageResource["project_id"]),
+ resource.TestCheckResourceAttr("stackit_image.image", "labels.label1", imageResource["label1"]),
+ ),
+ },
+ // Deletion is done by the framework implicitly
+ },
+ })
+}
+
func testAccCheckNetworkAreaDestroy(s *terraform.State) error {
ctx := context.Background()
var client *iaas.APIClient
@@ -1560,14 +1695,14 @@ func testAccCheckIaaSPublicIpDestroy(s *terraform.State) error {
func testAccCheckIaaSKeyPairDestroy(s *terraform.State) error {
ctx := context.Background()
- var client *iaasalpha.APIClient
+ var client *iaas.APIClient
var err error
if testutil.IaaSCustomEndpoint == "" {
- client, err = iaasalpha.NewAPIClient(
+ client, err = iaas.NewAPIClient(
config.WithRegion("eu01"),
)
} else {
- client, err = iaasalpha.NewAPIClient(
+ client, err = iaas.NewAPIClient(
config.WithEndpoint(testutil.IaaSCustomEndpoint),
)
}
@@ -1603,3 +1738,50 @@ func testAccCheckIaaSKeyPairDestroy(s *terraform.State) error {
}
return nil
}
+
+func testAccCheckIaaSImageDestroy(s *terraform.State) error {
+ ctx := context.Background()
+ var client *iaas.APIClient
+ var err error
+ if testutil.IaaSCustomEndpoint == "" {
+ client, err = iaas.NewAPIClient(
+ config.WithRegion("eu01"),
+ )
+ } else {
+ client, err = iaas.NewAPIClient(
+ config.WithEndpoint(testutil.IaaSCustomEndpoint),
+ )
+ }
+ if err != nil {
+ return fmt.Errorf("creating client: %w", err)
+ }
+
+ imagesToDestroy := []string{}
+ for _, rs := range s.RootModule().Resources {
+ if rs.Type != "stackit_image" {
+ continue
+ }
+ // Image terraform ID: "[project_id],[image_id]"
+ imageId := strings.Split(rs.Primary.ID, core.Separator)[1]
+ imagesToDestroy = append(imagesToDestroy, imageId)
+ }
+
+ imagesResp, err := client.ListImagesExecute(ctx, testutil.ProjectId)
+ if err != nil {
+ return fmt.Errorf("getting images: %w", err)
+ }
+
+ images := *imagesResp.Items
+ for i := range images {
+ if images[i].Id == nil {
+ continue
+ }
+ if utils.Contains(imagesToDestroy, *images[i].Id) {
+ err := client.DeleteImageExecute(ctx, testutil.ProjectId, *images[i].Id)
+ if err != nil {
+ return fmt.Errorf("destroying image %s during CheckDestroy: %w", *images[i].Id, err)
+ }
+ }
+ }
+ return nil
+}
diff --git a/stackit/internal/services/iaas/image/datasource.go b/stackit/internal/services/iaas/image/datasource.go
new file mode 100644
index 00000000..b12e0527
--- /dev/null
+++ b/stackit/internal/services/iaas/image/datasource.go
@@ -0,0 +1,387 @@
+package image
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+ "github.com/stackitcloud/stackit-sdk-go/core/config"
+ "github.com/stackitcloud/stackit-sdk-go/core/oapierror"
+ "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/features"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+)
+
+// imageDataSourceBetaCheckDone is used to prevent multiple checks for beta resources.
+// This is a workaround for the lack of a global state in the provider and
+// needs to exist because the Configure method is called twice.
+var imageDataSourceBetaCheckDone bool
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+ _ datasource.DataSource = &imageDataSource{}
+)
+
+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"`
+ DiskFormat types.String `tfsdk:"disk_format"`
+ MinDiskSize types.Int64 `tfsdk:"min_disk_size"`
+ MinRAM types.Int64 `tfsdk:"min_ram"`
+ Protected types.Bool `tfsdk:"protected"`
+ Scope types.String `tfsdk:"scope"`
+ Config types.Object `tfsdk:"config"`
+ Checksum types.Object `tfsdk:"checksum"`
+ Labels types.Map `tfsdk:"labels"`
+}
+
+// NewImageDataSource is a helper function to simplify the provider implementation.
+func NewImageDataSource() datasource.DataSource {
+ return &imageDataSource{}
+}
+
+// imageDataSource is the data source implementation.
+type imageDataSource struct {
+ client *iaas.APIClient
+}
+
+// Metadata returns the data source type name.
+func (d *imageDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_image"
+}
+
+func (d *imageDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ // Prevent panic if the provider has not been configured.
+ if req.ProviderData == nil {
+ return
+ }
+
+ var apiClient *iaas.APIClient
+ var err error
+
+ providerData, ok := req.ProviderData.(core.ProviderData)
+ if !ok {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
+ return
+ }
+
+ if !imageDataSourceBetaCheckDone {
+ features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_image", "data source")
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ imageDataSourceBetaCheckDone = true
+ }
+
+ if providerData.IaaSCustomEndpoint != "" {
+ apiClient, err = iaas.NewAPIClient(
+ config.WithCustomAuth(providerData.RoundTripper),
+ config.WithEndpoint(providerData.IaaSCustomEndpoint),
+ )
+ } else {
+ apiClient, err = iaas.NewAPIClient(
+ config.WithCustomAuth(providerData.RoundTripper),
+ config.WithRegion(providerData.Region),
+ )
+ }
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the data source configuration", err))
+ return
+ }
+
+ d.client = apiClient
+ tflog.Info(ctx, "iaas client configured")
+}
+
+// Schema defines the schema for the datasource.
+func (r *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: features.AddBetaDescription("Image datasource schema. Must have a `region` specified in the provider configuration."),
+ Description: "Image datasource schema. Must have a `region` specified in the provider configuration.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".",
+ Computed: true,
+ },
+ "project_id": schema.StringAttribute{
+ Description: "STACKIT project ID to which the image is associated.",
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "image_id": schema.StringAttribute{
+ Description: "The image ID.",
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "name": schema.StringAttribute{
+ Description: "The name of the image.",
+ Computed: true,
+ },
+ "disk_format": schema.StringAttribute{
+ Description: "The disk format of the image.",
+ Computed: true,
+ },
+ "min_disk_size": schema.Int64Attribute{
+ Description: "The minimum disk size of the image in GB.",
+ Computed: true,
+ },
+ "min_ram": schema.Int64Attribute{
+ Description: "The minimum RAM of the image in MB.",
+ Computed: true,
+ },
+ "protected": schema.BoolAttribute{
+ Description: "Whether the image is protected.",
+ Computed: true,
+ },
+ "scope": schema.StringAttribute{
+ Description: "The scope of the image.",
+ Computed: true,
+ },
+ "config": schema.SingleNestedAttribute{
+ Description: "Properties to set hardware and scheduling settings for an image.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "boot_menu": schema.BoolAttribute{
+ Description: "Enables the BIOS bootmenu.",
+ Computed: true,
+ },
+ "cdrom_bus": schema.StringAttribute{
+ Description: "Sets CDROM bus controller type.",
+ Computed: true,
+ },
+ "disk_bus": schema.StringAttribute{
+ Description: "Sets Disk bus controller type.",
+ Computed: true,
+ },
+ "nic_model": schema.StringAttribute{
+ Description: "Sets virtual network interface model.",
+ Computed: true,
+ },
+ "operating_system": schema.StringAttribute{
+ Description: "Enables operating system specific optimizations.",
+ Computed: true,
+ },
+ "operating_system_distro": schema.StringAttribute{
+ Description: "Operating system distribution.",
+ Computed: true,
+ },
+ "operating_system_version": schema.StringAttribute{
+ Description: "Version of the operating system.",
+ Computed: true,
+ },
+ "rescue_bus": schema.StringAttribute{
+ Description: "Sets the device bus when the image is used as a rescue image.",
+ Computed: true,
+ },
+ "rescue_device": schema.StringAttribute{
+ Description: "Sets the device when the image is used as a rescue image.",
+ Computed: true,
+ },
+ "secure_boot": schema.BoolAttribute{
+ Description: "Enables Secure Boot.",
+ Computed: true,
+ },
+ "uefi": schema.BoolAttribute{
+ Description: "Enables UEFI boot.",
+ Computed: true,
+ },
+ "video_model": schema.StringAttribute{
+ Description: "Sets Graphic device model.",
+ Computed: true,
+ },
+ "virtio_scsi": schema.BoolAttribute{
+ Description: "Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block.",
+ Computed: true,
+ },
+ },
+ },
+ "checksum": schema.SingleNestedAttribute{
+ Description: "Representation of an image checksum.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "algorithm": schema.StringAttribute{
+ Description: "Algorithm for the checksum of the image data.",
+ Computed: true,
+ },
+ "digest": schema.StringAttribute{
+ Description: "Hexdigest of the checksum of the image data.",
+ Computed: true,
+ },
+ },
+ },
+ "labels": schema.MapAttribute{
+ Description: "Labels are key-value string pairs which can be attached to a resource container",
+ ElementType: types.StringType,
+ Computed: true,
+ },
+ },
+ }
+}
+
+// // Read refreshes the Terraform state with the latest data.
+func (r *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 {
+ oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
+ if ok && oapiErr.StatusCode == http.StatusNotFound {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ // 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")
+}
+
+func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *DataSourceModel) error {
+ if imageResp == nil {
+ return fmt.Errorf("response input is nil")
+ }
+ if model == nil {
+ return fmt.Errorf("model input is nil")
+ }
+
+ var imageId string
+ if model.ImageId.ValueString() != "" {
+ imageId = model.ImageId.ValueString()
+ } else if imageResp.Id != nil {
+ imageId = *imageResp.Id
+ } else {
+ return fmt.Errorf("image id not present")
+ }
+
+ idParts := []string{
+ model.ProjectId.ValueString(),
+ imageId,
+ }
+ model.Id = types.StringValue(
+ strings.Join(idParts, core.Separator),
+ )
+
+ // Map config
+ var configModel = &configModel{}
+ var configObject basetypes.ObjectValue
+ diags := diag.Diagnostics{}
+ if imageResp.Config != nil {
+ configModel.BootMenu = types.BoolPointerValue(imageResp.Config.BootMenu)
+ configModel.CDROMBus = types.StringPointerValue(imageResp.Config.GetCdromBus())
+ configModel.DiskBus = types.StringPointerValue(imageResp.Config.GetDiskBus())
+ configModel.NICModel = types.StringPointerValue(imageResp.Config.GetNicModel())
+ configModel.OperatingSystem = types.StringPointerValue(imageResp.Config.OperatingSystem)
+ configModel.OperatingSystemDistro = types.StringPointerValue(imageResp.Config.GetOperatingSystemDistro())
+ configModel.OperatingSystemVersion = types.StringPointerValue(imageResp.Config.GetOperatingSystemVersion())
+ configModel.RescueBus = types.StringPointerValue(imageResp.Config.GetRescueBus())
+ configModel.RescueDevice = types.StringPointerValue(imageResp.Config.GetRescueDevice())
+ configModel.SecureBoot = types.BoolPointerValue(imageResp.Config.SecureBoot)
+ configModel.UEFI = types.BoolPointerValue(imageResp.Config.Uefi)
+ configModel.VideoModel = types.StringPointerValue(imageResp.Config.GetVideoModel())
+ configModel.VirtioScsi = types.BoolPointerValue(imageResp.Config.VirtioScsi)
+
+ configObject, diags = types.ObjectValue(configTypes, map[string]attr.Value{
+ "boot_menu": configModel.BootMenu,
+ "cdrom_bus": configModel.CDROMBus,
+ "disk_bus": configModel.DiskBus,
+ "nic_model": configModel.NICModel,
+ "operating_system": configModel.OperatingSystem,
+ "operating_system_distro": configModel.OperatingSystemDistro,
+ "operating_system_version": configModel.OperatingSystemVersion,
+ "rescue_bus": configModel.RescueBus,
+ "rescue_device": configModel.RescueDevice,
+ "secure_boot": configModel.SecureBoot,
+ "uefi": configModel.UEFI,
+ "video_model": configModel.VideoModel,
+ "virtio_scsi": configModel.VirtioScsi,
+ })
+ } else {
+ configObject = types.ObjectNull(configTypes)
+ }
+ if diags.HasError() {
+ return fmt.Errorf("creating config: %w", core.DiagsToError(diags))
+ }
+
+ // Map checksum
+ var checksumModel = &checksumModel{}
+ var checksumObject basetypes.ObjectValue
+ if imageResp.Checksum != nil {
+ checksumModel.Algorithm = types.StringPointerValue(imageResp.Checksum.Algorithm)
+ checksumModel.Digest = types.StringPointerValue(imageResp.Checksum.Digest)
+ checksumObject, diags = types.ObjectValue(checksumTypes, map[string]attr.Value{
+ "algorithm": checksumModel.Algorithm,
+ "digest": checksumModel.Digest,
+ })
+ } else {
+ checksumObject = types.ObjectNull(checksumTypes)
+ }
+ if diags.HasError() {
+ return fmt.Errorf("creating checksum: %w", core.DiagsToError(diags))
+ }
+
+ // Map labels
+ labels, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{})
+ if diags.HasError() {
+ return fmt.Errorf("convert labels to StringValue map: %w", core.DiagsToError(diags))
+ }
+ if imageResp.Labels != nil && len(*imageResp.Labels) != 0 {
+ var diags diag.Diagnostics
+ labels, diags = types.MapValueFrom(ctx, types.StringType, *imageResp.Labels)
+ if diags.HasError() {
+ return fmt.Errorf("convert labels to StringValue map: %w", core.DiagsToError(diags))
+ }
+ } else if model.Labels.IsNull() {
+ labels = types.MapNull(types.StringType)
+ }
+
+ model.ImageId = types.StringValue(imageId)
+ model.Name = types.StringPointerValue(imageResp.Name)
+ model.DiskFormat = types.StringPointerValue(imageResp.DiskFormat)
+ model.MinDiskSize = types.Int64PointerValue(imageResp.MinDiskSize)
+ model.MinRAM = types.Int64PointerValue(imageResp.MinRam)
+ model.Protected = types.BoolPointerValue(imageResp.Protected)
+ model.Scope = types.StringPointerValue(imageResp.Scope)
+ model.Labels = labels
+ model.Config = configObject
+ model.Checksum = checksumObject
+ return nil
+}
diff --git a/stackit/internal/services/iaas/image/datasource_test.go b/stackit/internal/services/iaas/image/datasource_test.go
new file mode 100644
index 00000000..a16120ac
--- /dev/null
+++ b/stackit/internal/services/iaas/image/datasource_test.go
@@ -0,0 +1,163 @@
+package image
+
+import (
+ "context"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/stackitcloud/stackit-sdk-go/core/utils"
+ "github.com/stackitcloud/stackit-sdk-go/services/iaas"
+)
+
+func TestMapDataSourceFields(t *testing.T) {
+ tests := []struct {
+ description string
+ state DataSourceModel
+ input *iaas.Image
+ expected DataSourceModel
+ isValid bool
+ }{
+ {
+ "default_values",
+ DataSourceModel{
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ },
+ &iaas.Image{
+ Id: utils.Ptr("iid"),
+ },
+ DataSourceModel{
+ Id: types.StringValue("pid,iid"),
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Labels: types.MapNull(types.StringType),
+ },
+ true,
+ },
+ {
+ "simple_values",
+ DataSourceModel{
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ },
+ &iaas.Image{
+ Id: utils.Ptr("iid"),
+ Name: utils.Ptr("name"),
+ DiskFormat: utils.Ptr("format"),
+ MinDiskSize: utils.Ptr(int64(1)),
+ MinRam: utils.Ptr(int64(1)),
+ Protected: utils.Ptr(true),
+ Scope: utils.Ptr("scope"),
+ Config: &iaas.ImageConfig{
+ BootMenu: utils.Ptr(true),
+ CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")),
+ DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")),
+ NicModel: iaas.NewNullableString(utils.Ptr("model")),
+ OperatingSystem: utils.Ptr("os"),
+ OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")),
+ OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")),
+ RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")),
+ RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")),
+ SecureBoot: utils.Ptr(true),
+ Uefi: utils.Ptr(true),
+ VideoModel: iaas.NewNullableString(utils.Ptr("model")),
+ VirtioScsi: utils.Ptr(true),
+ },
+ Checksum: &iaas.ImageChecksum{
+ Algorithm: utils.Ptr("algorithm"),
+ Digest: utils.Ptr("digest"),
+ },
+ Labels: &map[string]interface{}{
+ "key": "value",
+ },
+ },
+ DataSourceModel{
+ Id: types.StringValue("pid,iid"),
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Name: types.StringValue("name"),
+ DiskFormat: types.StringValue("format"),
+ MinDiskSize: types.Int64Value(1),
+ MinRAM: types.Int64Value(1),
+ Protected: types.BoolValue(true),
+ Scope: types.StringValue("scope"),
+ Config: types.ObjectValueMust(configTypes, map[string]attr.Value{
+ "boot_menu": types.BoolValue(true),
+ "cdrom_bus": types.StringValue("cdrom_bus"),
+ "disk_bus": types.StringValue("disk_bus"),
+ "nic_model": types.StringValue("model"),
+ "operating_system": types.StringValue("os"),
+ "operating_system_distro": types.StringValue("os_distro"),
+ "operating_system_version": types.StringValue("os_version"),
+ "rescue_bus": types.StringValue("rescue_bus"),
+ "rescue_device": types.StringValue("rescue_device"),
+ "secure_boot": types.BoolValue(true),
+ "uefi": types.BoolValue(true),
+ "video_model": types.StringValue("model"),
+ "virtio_scsi": types.BoolValue(true),
+ }),
+ Checksum: types.ObjectValueMust(checksumTypes, map[string]attr.Value{
+ "algorithm": types.StringValue("algorithm"),
+ "digest": types.StringValue("digest"),
+ }),
+ Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
+ "key": types.StringValue("value"),
+ }),
+ },
+ true,
+ },
+ {
+ "empty_labels",
+ DataSourceModel{
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
+ },
+ &iaas.Image{
+ Id: utils.Ptr("iid"),
+ },
+ DataSourceModel{
+ Id: types.StringValue("pid,iid"),
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
+ },
+ true,
+ },
+ {
+ "response_nil_fail",
+ DataSourceModel{},
+ nil,
+ DataSourceModel{},
+ false,
+ },
+ {
+ "no_resource_id",
+ DataSourceModel{
+ ProjectId: types.StringValue("pid"),
+ },
+ &iaas.Image{},
+ DataSourceModel{},
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ err := mapDataSourceFields(context.Background(), tt.input, &tt.state)
+ if !tt.isValid && err == nil {
+ t.Fatalf("Should have failed")
+ }
+ if tt.isValid && err != nil {
+ t.Fatalf("Should not have failed: %v", err)
+ }
+ if tt.isValid {
+ diff := cmp.Diff(tt.state, tt.expected)
+ if diff != "" {
+ t.Fatalf("Data does not match: %s", diff)
+ }
+ }
+ })
+ }
+}
diff --git a/stackit/internal/services/iaas/image/resource.go b/stackit/internal/services/iaas/image/resource.go
new file mode 100644
index 00000000..425b563f
--- /dev/null
+++ b/stackit/internal/services/iaas/image/resource.go
@@ -0,0 +1,859 @@
+package image
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+ "github.com/stackitcloud/stackit-sdk-go/core/config"
+ "github.com/stackitcloud/stackit-sdk-go/core/oapierror"
+ "github.com/stackitcloud/stackit-sdk-go/services/iaas"
+ "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+)
+
+// resourceBetaCheckDone is used to prevent multiple checks for beta resources.
+// This is a workaround for the lack of a global state in the provider and
+// needs to exist because the Configure method is called twice.
+var resourceBetaCheckDone bool
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+ _ resource.Resource = &imageResource{}
+ _ resource.ResourceWithConfigure = &imageResource{}
+ _ resource.ResourceWithImportState = &imageResource{}
+)
+
+type Model 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"`
+ DiskFormat types.String `tfsdk:"disk_format"`
+ MinDiskSize types.Int64 `tfsdk:"min_disk_size"`
+ MinRAM types.Int64 `tfsdk:"min_ram"`
+ Protected types.Bool `tfsdk:"protected"`
+ Scope types.String `tfsdk:"scope"`
+ Config types.Object `tfsdk:"config"`
+ Checksum types.Object `tfsdk:"checksum"`
+ Labels types.Map `tfsdk:"labels"`
+ LocalFilePath types.String `tfsdk:"local_file_path"`
+}
+
+// Struct corresponding to Model.Config
+type configModel struct {
+ BootMenu types.Bool `tfsdk:"boot_menu"`
+ CDROMBus types.String `tfsdk:"cdrom_bus"`
+ DiskBus types.String `tfsdk:"disk_bus"`
+ NICModel types.String `tfsdk:"nic_model"`
+ OperatingSystem types.String `tfsdk:"operating_system"`
+ OperatingSystemDistro types.String `tfsdk:"operating_system_distro"`
+ OperatingSystemVersion types.String `tfsdk:"operating_system_version"`
+ RescueBus types.String `tfsdk:"rescue_bus"`
+ RescueDevice types.String `tfsdk:"rescue_device"`
+ SecureBoot types.Bool `tfsdk:"secure_boot"`
+ UEFI types.Bool `tfsdk:"uefi"`
+ VideoModel types.String `tfsdk:"video_model"`
+ VirtioScsi types.Bool `tfsdk:"virtio_scsi"`
+}
+
+// Types corresponding to configModel
+var configTypes = map[string]attr.Type{
+ "boot_menu": basetypes.BoolType{},
+ "cdrom_bus": basetypes.StringType{},
+ "disk_bus": basetypes.StringType{},
+ "nic_model": basetypes.StringType{},
+ "operating_system": basetypes.StringType{},
+ "operating_system_distro": basetypes.StringType{},
+ "operating_system_version": basetypes.StringType{},
+ "rescue_bus": basetypes.StringType{},
+ "rescue_device": basetypes.StringType{},
+ "secure_boot": basetypes.BoolType{},
+ "uefi": basetypes.BoolType{},
+ "video_model": basetypes.StringType{},
+ "virtio_scsi": basetypes.BoolType{},
+}
+
+// Struct corresponding to Model.Checksum
+type checksumModel struct {
+ Algorithm types.String `tfsdk:"algorithm"`
+ Digest types.String `tfsdk:"digest"`
+}
+
+// Types corresponding to checksumModel
+var checksumTypes = map[string]attr.Type{
+ "algorithm": basetypes.StringType{},
+ "digest": basetypes.StringType{},
+}
+
+// NewImageResource is a helper function to simplify the provider implementation.
+func NewImageResource() resource.Resource {
+ return &imageResource{}
+}
+
+// imageResource is the resource implementation.
+type imageResource struct {
+ client *iaas.APIClient
+}
+
+// Metadata returns the resource type name.
+func (r *imageResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_image"
+}
+
+// Configure adds the provider configured client to the resource.
+func (r *imageResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ // Prevent panic if the provider has not been configured.
+ if req.ProviderData == nil {
+ return
+ }
+
+ providerData, ok := req.ProviderData.(core.ProviderData)
+ if !ok {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
+ return
+ }
+
+ if !resourceBetaCheckDone {
+ features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_image", "resource")
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resourceBetaCheckDone = true
+ }
+
+ var apiClient *iaas.APIClient
+ var err error
+ if providerData.IaaSCustomEndpoint != "" {
+ ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint)
+ apiClient, err = iaas.NewAPIClient(
+ config.WithCustomAuth(providerData.RoundTripper),
+ config.WithEndpoint(providerData.IaaSCustomEndpoint),
+ )
+ } else {
+ apiClient, err = iaas.NewAPIClient(
+ config.WithCustomAuth(providerData.RoundTripper),
+ config.WithRegion(providerData.Region),
+ )
+ }
+
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
+ return
+ }
+
+ r.client = apiClient
+ tflog.Info(ctx, "iaas client configured")
+}
+
+// Schema defines the schema for the resource.
+func (r *imageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ Description: "Image resource schema. Must have a `region` specified in the provider configuration.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "project_id": schema.StringAttribute{
+ Description: "STACKIT project ID to which the image is associated.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "image_id": schema.StringAttribute{
+ Description: "The image ID.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "name": schema.StringAttribute{
+ Description: "The name of the image.",
+ Required: true,
+ },
+ "disk_format": schema.StringAttribute{
+ Description: "The disk format of the image.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "local_file_path": schema.StringAttribute{
+ Description: "The filepath of the raw image file to be uploaded.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ Validators: []validator.String{
+ // Validating that the file exists in the plan is useful to avoid
+ // creating an image resource where the local image upload will fail
+ validate.FileExists(),
+ },
+ },
+ "min_disk_size": schema.Int64Attribute{
+ Description: "The minimum disk size of the image in GB.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.UseStateForUnknown(),
+ },
+ },
+ "min_ram": schema.Int64Attribute{
+ Description: "The minimum RAM of the image in MB.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.UseStateForUnknown(),
+ },
+ },
+ "protected": schema.BoolAttribute{
+ Description: "Whether the image is protected.",
+ Computed: true,
+ PlanModifiers: []planmodifier.Bool{
+ boolplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "scope": schema.StringAttribute{
+ Description: "The scope of the image.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "config": schema.SingleNestedAttribute{
+ Description: "Properties to set hardware and scheduling settings for an image.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.Object{
+ objectplanmodifier.UseStateForUnknown(),
+ },
+ Attributes: map[string]schema.Attribute{
+ "boot_menu": schema.BoolAttribute{
+ Description: "Enables the BIOS bootmenu.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.Bool{
+ boolplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "cdrom_bus": schema.StringAttribute{
+ Description: "Sets CDROM bus controller type.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "disk_bus": schema.StringAttribute{
+ Description: "Sets Disk bus controller type.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "nic_model": schema.StringAttribute{
+ Description: "Sets virtual network interface model.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "operating_system": schema.StringAttribute{
+ Description: "Enables operating system specific optimizations.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "operating_system_distro": schema.StringAttribute{
+ Description: "Operating system distribution.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "operating_system_version": schema.StringAttribute{
+ Description: "Version of the operating system.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "rescue_bus": schema.StringAttribute{
+ Description: "Sets the device bus when the image is used as a rescue image.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "rescue_device": schema.StringAttribute{
+ Description: "Sets the device when the image is used as a rescue image.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "secure_boot": schema.BoolAttribute{
+ Description: "Enables Secure Boot.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.Bool{
+ boolplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "uefi": schema.BoolAttribute{
+ Description: "Enables UEFI boot.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.Bool{
+ boolplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "video_model": schema.StringAttribute{
+ Description: "Sets Graphic device model.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "virtio_scsi": schema.BoolAttribute{
+ Description: "Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.Bool{
+ boolplanmodifier.UseStateForUnknown(),
+ },
+ },
+ },
+ },
+ "checksum": schema.SingleNestedAttribute{
+ Description: "Representation of an image checksum.",
+ Computed: true,
+ PlanModifiers: []planmodifier.Object{
+ objectplanmodifier.UseStateForUnknown(),
+ },
+ Attributes: map[string]schema.Attribute{
+ "algorithm": schema.StringAttribute{
+ Description: "Algorithm for the checksum of the image data.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "digest": schema.StringAttribute{
+ Description: "Hexdigest of the checksum of the image data.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ },
+ },
+ "labels": schema.MapAttribute{
+ Description: "Labels are key-value string pairs which can be attached to a resource container",
+ ElementType: types.StringType,
+ Optional: true,
+ },
+ },
+ }
+}
+
+// Create creates the resource and sets the initial Terraform state.
+func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
+ // Retrieve values from plan
+ var model Model
+ diags := req.Plan.Get(ctx, &model)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ projectId := model.ProjectId.ValueString()
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+
+ // Generate API request body from model
+ payload, err := toCreatePayload(ctx, &model)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Creating API payload: %v", err))
+ return
+ }
+
+ // Create new image
+ imageCreateResp, err := r.client.CreateImage(ctx, projectId).CreateImagePayload(*payload).Execute()
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+ ctx = tflog.SetField(ctx, "image_id", *imageCreateResp.Id)
+
+ // Get the image object, as the create response does not contain all fields
+ image, err := r.client.GetImage(ctx, projectId, *imageCreateResp.Id).Execute()
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ // Map response body to schema
+ err = mapFields(ctx, image, &model)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Processing API payload: %v", err))
+ return
+ }
+
+ // Set state to partially populated data
+ diags = resp.State.Set(ctx, model)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Upload image
+ err = uploadImage(ctx, &resp.Diagnostics, model.LocalFilePath.ValueString(), *imageCreateResp.UploadUrl)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Uploading image: %v", err))
+ return
+ }
+
+ // Wait for image to become available
+ waitResp, err := wait.UploadImageWaitHandler(ctx, r.client, projectId, *imageCreateResp.Id).WaitWithContext(ctx)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Waiting for image to become available: %v", err))
+ return
+ }
+
+ // Map response body to schema
+ err = mapFields(ctx, waitResp, &model)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Processing API payload: %v", err))
+ return
+ }
+
+ // Set state to fully populated data
+ diags = resp.State.Set(ctx, model)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ tflog.Info(ctx, "Image created")
+}
+
+// // Read refreshes the Terraform state with the latest data.
+func (r *imageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
+ var model Model
+ diags := req.State.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 {
+ oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
+ if ok && oapiErr.StatusCode == http.StatusNotFound {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ // Map response body to schema
+ err = mapFields(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")
+}
+
+// Update updates the resource and sets the updated Terraform state on success.
+func (r *imageResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
+ // Retrieve values from plan
+ var model Model
+ diags := req.Plan.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)
+
+ // Retrieve values from state
+ var stateModel Model
+ diags = req.State.Get(ctx, &stateModel)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Generate API request body from model
+ payload, err := toUpdatePayload(ctx, &model, stateModel.Labels)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating image", fmt.Sprintf("Creating API payload: %v", err))
+ return
+ }
+ // Update existing image
+ updatedImage, err := r.client.UpdateImage(ctx, projectId, imageId).UpdateImagePayload(*payload).Execute()
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating image", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ err = mapFields(ctx, updatedImage, &model)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating image", fmt.Sprintf("Processing API payload: %v", err))
+ return
+ }
+ diags = resp.State.Set(ctx, model)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ tflog.Info(ctx, "Image updated")
+}
+
+// Delete deletes the resource and removes the Terraform state on success.
+func (r *imageResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
+ // Retrieve values from state
+ var model Model
+ diags := req.State.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)
+
+ // Delete existing image
+ err := r.client.DeleteImage(ctx, projectId, imageId).Execute()
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting image", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+ _, err = wait.DeleteImageWaitHandler(ctx, r.client, projectId, imageId).WaitWithContext(ctx)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting image", fmt.Sprintf("image deletion waiting: %v", err))
+ return
+ }
+
+ tflog.Info(ctx, "Image deleted")
+}
+
+// ImportState imports a resource into the Terraform state on success.
+// The expected format of the resource import identifier is: project_id,image_id
+func (r *imageResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ idParts := strings.Split(req.ID, core.Separator)
+
+ if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
+ core.LogAndAddError(ctx, &resp.Diagnostics,
+ "Error importing image",
+ fmt.Sprintf("Expected import identifier with format: [project_id],[image_id] Got: %q", req.ID),
+ )
+ return
+ }
+
+ projectId := idParts[0]
+ imageId := idParts[1]
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "image_id", imageId)
+
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("image_id"), imageId)...)
+ tflog.Info(ctx, "Image state imported")
+}
+
+func mapFields(ctx context.Context, imageResp *iaas.Image, model *Model) error {
+ if imageResp == nil {
+ return fmt.Errorf("response input is nil")
+ }
+ if model == nil {
+ return fmt.Errorf("model input is nil")
+ }
+
+ var imageId string
+ if model.ImageId.ValueString() != "" {
+ imageId = model.ImageId.ValueString()
+ } else if imageResp.Id != nil {
+ imageId = *imageResp.Id
+ } else {
+ return fmt.Errorf("image id not present")
+ }
+
+ idParts := []string{
+ model.ProjectId.ValueString(),
+ imageId,
+ }
+ model.Id = types.StringValue(
+ strings.Join(idParts, core.Separator),
+ )
+
+ // Map config
+ var configModel = &configModel{}
+ var configObject basetypes.ObjectValue
+ diags := diag.Diagnostics{}
+ if imageResp.Config != nil {
+ configModel.BootMenu = types.BoolPointerValue(imageResp.Config.BootMenu)
+ configModel.CDROMBus = types.StringPointerValue(imageResp.Config.GetCdromBus())
+ configModel.DiskBus = types.StringPointerValue(imageResp.Config.GetDiskBus())
+ configModel.NICModel = types.StringPointerValue(imageResp.Config.GetNicModel())
+ configModel.OperatingSystem = types.StringPointerValue(imageResp.Config.OperatingSystem)
+ configModel.OperatingSystemDistro = types.StringPointerValue(imageResp.Config.GetOperatingSystemDistro())
+ configModel.OperatingSystemVersion = types.StringPointerValue(imageResp.Config.GetOperatingSystemVersion())
+ configModel.RescueBus = types.StringPointerValue(imageResp.Config.GetRescueBus())
+ configModel.RescueDevice = types.StringPointerValue(imageResp.Config.GetRescueDevice())
+ configModel.SecureBoot = types.BoolPointerValue(imageResp.Config.SecureBoot)
+ configModel.UEFI = types.BoolPointerValue(imageResp.Config.Uefi)
+ configModel.VideoModel = types.StringPointerValue(imageResp.Config.GetVideoModel())
+ configModel.VirtioScsi = types.BoolPointerValue(imageResp.Config.VirtioScsi)
+
+ configObject, diags = types.ObjectValue(configTypes, map[string]attr.Value{
+ "boot_menu": configModel.BootMenu,
+ "cdrom_bus": configModel.CDROMBus,
+ "disk_bus": configModel.DiskBus,
+ "nic_model": configModel.NICModel,
+ "operating_system": configModel.OperatingSystem,
+ "operating_system_distro": configModel.OperatingSystemDistro,
+ "operating_system_version": configModel.OperatingSystemVersion,
+ "rescue_bus": configModel.RescueBus,
+ "rescue_device": configModel.RescueDevice,
+ "secure_boot": configModel.SecureBoot,
+ "uefi": configModel.UEFI,
+ "video_model": configModel.VideoModel,
+ "virtio_scsi": configModel.VirtioScsi,
+ })
+ } else {
+ configObject = types.ObjectNull(configTypes)
+ }
+ if diags.HasError() {
+ return fmt.Errorf("creating config: %w", core.DiagsToError(diags))
+ }
+
+ // Map checksum
+ var checksumModel = &checksumModel{}
+ var checksumObject basetypes.ObjectValue
+ if imageResp.Checksum != nil {
+ checksumModel.Algorithm = types.StringPointerValue(imageResp.Checksum.Algorithm)
+ checksumModel.Digest = types.StringPointerValue(imageResp.Checksum.Digest)
+ checksumObject, diags = types.ObjectValue(checksumTypes, map[string]attr.Value{
+ "algorithm": checksumModel.Algorithm,
+ "digest": checksumModel.Digest,
+ })
+ } else {
+ checksumObject = types.ObjectNull(checksumTypes)
+ }
+ if diags.HasError() {
+ return fmt.Errorf("creating checksum: %w", core.DiagsToError(diags))
+ }
+
+ // Map labels
+ labels, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{})
+ if diags.HasError() {
+ return fmt.Errorf("convert labels to StringValue map: %w", core.DiagsToError(diags))
+ }
+ if imageResp.Labels != nil && len(*imageResp.Labels) != 0 {
+ var diags diag.Diagnostics
+ labels, diags = types.MapValueFrom(ctx, types.StringType, *imageResp.Labels)
+ if diags.HasError() {
+ return fmt.Errorf("convert labels to StringValue map: %w", core.DiagsToError(diags))
+ }
+ } else if model.Labels.IsNull() {
+ labels = types.MapNull(types.StringType)
+ }
+
+ model.ImageId = types.StringValue(imageId)
+ model.Name = types.StringPointerValue(imageResp.Name)
+ model.DiskFormat = types.StringPointerValue(imageResp.DiskFormat)
+ model.MinDiskSize = types.Int64PointerValue(imageResp.MinDiskSize)
+ model.MinRAM = types.Int64PointerValue(imageResp.MinRam)
+ model.Protected = types.BoolPointerValue(imageResp.Protected)
+ model.Scope = types.StringPointerValue(imageResp.Scope)
+ model.Labels = labels
+ model.Config = configObject
+ model.Checksum = checksumObject
+ return nil
+}
+
+func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateImagePayload, error) {
+ if model == nil {
+ return nil, fmt.Errorf("nil model")
+ }
+
+ var configModel = &configModel{}
+ if !(model.Config.IsNull() || model.Config.IsUnknown()) {
+ diags := model.Config.As(ctx, configModel, basetypes.ObjectAsOptions{})
+ if diags.HasError() {
+ return nil, fmt.Errorf("convert boot volume object to struct: %w", core.DiagsToError(diags))
+ }
+ }
+
+ configPayload := &iaas.ImageConfig{
+ BootMenu: conversion.BoolValueToPointer(configModel.BootMenu),
+ CdromBus: iaas.NewNullableString(conversion.StringValueToPointer(configModel.CDROMBus)),
+ DiskBus: iaas.NewNullableString(conversion.StringValueToPointer(configModel.DiskBus)),
+ NicModel: iaas.NewNullableString(conversion.StringValueToPointer(configModel.NICModel)),
+ OperatingSystem: conversion.StringValueToPointer(configModel.OperatingSystem),
+ OperatingSystemDistro: iaas.NewNullableString(conversion.StringValueToPointer(configModel.OperatingSystemDistro)),
+ OperatingSystemVersion: iaas.NewNullableString(conversion.StringValueToPointer(configModel.OperatingSystemVersion)),
+ RescueBus: iaas.NewNullableString(conversion.StringValueToPointer(configModel.RescueBus)),
+ RescueDevice: iaas.NewNullableString(conversion.StringValueToPointer(configModel.RescueDevice)),
+ SecureBoot: conversion.BoolValueToPointer(configModel.SecureBoot),
+ Uefi: conversion.BoolValueToPointer(configModel.UEFI),
+ VideoModel: iaas.NewNullableString(conversion.StringValueToPointer(configModel.VideoModel)),
+ VirtioScsi: conversion.BoolValueToPointer(configModel.VirtioScsi),
+ }
+
+ labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels)
+ if err != nil {
+ return nil, fmt.Errorf("converting to Go map: %w", err)
+ }
+
+ return &iaas.CreateImagePayload{
+ Name: conversion.StringValueToPointer(model.Name),
+ DiskFormat: conversion.StringValueToPointer(model.DiskFormat),
+ MinDiskSize: conversion.Int64ValueToPointer(model.MinDiskSize),
+ MinRam: conversion.Int64ValueToPointer(model.MinRAM),
+ Protected: conversion.BoolValueToPointer(model.Protected),
+ Config: configPayload,
+ Labels: &labels,
+ }, nil
+}
+
+func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateImagePayload, error) {
+ if model == nil {
+ return nil, fmt.Errorf("nil model")
+ }
+
+ var configModel = &configModel{}
+ if !(model.Config.IsNull() || model.Config.IsUnknown()) {
+ diags := model.Config.As(ctx, configModel, basetypes.ObjectAsOptions{})
+ if diags.HasError() {
+ return nil, fmt.Errorf("convert boot volume object to struct: %w", core.DiagsToError(diags))
+ }
+ }
+
+ configPayload := &iaas.ImageConfig{
+ BootMenu: conversion.BoolValueToPointer(configModel.BootMenu),
+ CdromBus: iaas.NewNullableString(conversion.StringValueToPointer(configModel.CDROMBus)),
+ DiskBus: iaas.NewNullableString(conversion.StringValueToPointer(configModel.DiskBus)),
+ NicModel: iaas.NewNullableString(conversion.StringValueToPointer(configModel.NICModel)),
+ OperatingSystem: conversion.StringValueToPointer(configModel.OperatingSystem),
+ OperatingSystemDistro: iaas.NewNullableString(conversion.StringValueToPointer(configModel.OperatingSystemDistro)),
+ OperatingSystemVersion: iaas.NewNullableString(conversion.StringValueToPointer(configModel.OperatingSystemVersion)),
+ RescueBus: iaas.NewNullableString(conversion.StringValueToPointer(configModel.RescueBus)),
+ RescueDevice: iaas.NewNullableString(conversion.StringValueToPointer(configModel.RescueDevice)),
+ SecureBoot: conversion.BoolValueToPointer(configModel.SecureBoot),
+ Uefi: conversion.BoolValueToPointer(configModel.UEFI),
+ VideoModel: iaas.NewNullableString(conversion.StringValueToPointer(configModel.VideoModel)),
+ VirtioScsi: conversion.BoolValueToPointer(configModel.VirtioScsi),
+ }
+
+ labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels)
+ if err != nil {
+ return nil, fmt.Errorf("converting to go map: %w", err)
+ }
+
+ // DiskFormat is not sent in the update payload as does not have effect after image upload,
+ // and the field has RequiresReplace set
+ return &iaas.UpdateImagePayload{
+ Name: conversion.StringValueToPointer(model.Name),
+ MinDiskSize: conversion.Int64ValueToPointer(model.MinDiskSize),
+ MinRam: conversion.Int64ValueToPointer(model.MinRAM),
+ Protected: conversion.BoolValueToPointer(model.Protected),
+ Config: configPayload,
+ Labels: &labels,
+ }, nil
+}
+
+func uploadImage(ctx context.Context, diags *diag.Diagnostics, filePath, uploadURL string) error {
+ if filePath == "" {
+ return fmt.Errorf("file path is empty")
+ }
+ if uploadURL == "" {
+ return fmt.Errorf("upload URL is empty")
+ }
+
+ fileContents, err := os.ReadFile(filePath)
+ if err != nil {
+ return fmt.Errorf("read file: %w", err)
+ }
+
+ req, err := http.NewRequest(http.MethodPut, uploadURL, bytes.NewReader(fileContents))
+ if err != nil {
+ return fmt.Errorf("create upload request: %w", err)
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return fmt.Errorf("upload image: %w", err)
+ }
+ defer func() {
+ err = resp.Body.Close()
+ if err != nil {
+ core.LogAndAddError(ctx, diags, "Error uploading image", fmt.Sprintf("Closing response body: %v", err))
+ }
+ }()
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("upload image: %s", resp.Status)
+ }
+
+ return nil
+}
diff --git a/stackit/internal/services/iaas/image/resource_test.go b/stackit/internal/services/iaas/image/resource_test.go
new file mode 100644
index 00000000..23b894df
--- /dev/null
+++ b/stackit/internal/services/iaas/image/resource_test.go
@@ -0,0 +1,391 @@
+package image
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/stackitcloud/stackit-sdk-go/core/utils"
+ "github.com/stackitcloud/stackit-sdk-go/services/iaas"
+)
+
+func TestMapFields(t *testing.T) {
+ tests := []struct {
+ description string
+ state Model
+ input *iaas.Image
+ expected Model
+ isValid bool
+ }{
+ {
+ "default_values",
+ Model{
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ },
+ &iaas.Image{
+ Id: utils.Ptr("iid"),
+ },
+ Model{
+ Id: types.StringValue("pid,iid"),
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Labels: types.MapNull(types.StringType),
+ },
+ true,
+ },
+ {
+ "simple_values",
+ Model{
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ },
+ &iaas.Image{
+ Id: utils.Ptr("iid"),
+ Name: utils.Ptr("name"),
+ DiskFormat: utils.Ptr("format"),
+ MinDiskSize: utils.Ptr(int64(1)),
+ MinRam: utils.Ptr(int64(1)),
+ Protected: utils.Ptr(true),
+ Scope: utils.Ptr("scope"),
+ Config: &iaas.ImageConfig{
+ BootMenu: utils.Ptr(true),
+ CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")),
+ DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")),
+ NicModel: iaas.NewNullableString(utils.Ptr("model")),
+ OperatingSystem: utils.Ptr("os"),
+ OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")),
+ OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")),
+ RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")),
+ RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")),
+ SecureBoot: utils.Ptr(true),
+ Uefi: utils.Ptr(true),
+ VideoModel: iaas.NewNullableString(utils.Ptr("model")),
+ VirtioScsi: utils.Ptr(true),
+ },
+ Checksum: &iaas.ImageChecksum{
+ Algorithm: utils.Ptr("algorithm"),
+ Digest: utils.Ptr("digest"),
+ },
+ Labels: &map[string]interface{}{
+ "key": "value",
+ },
+ },
+ Model{
+ Id: types.StringValue("pid,iid"),
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Name: types.StringValue("name"),
+ DiskFormat: types.StringValue("format"),
+ MinDiskSize: types.Int64Value(1),
+ MinRAM: types.Int64Value(1),
+ Protected: types.BoolValue(true),
+ Scope: types.StringValue("scope"),
+ Config: types.ObjectValueMust(configTypes, map[string]attr.Value{
+ "boot_menu": types.BoolValue(true),
+ "cdrom_bus": types.StringValue("cdrom_bus"),
+ "disk_bus": types.StringValue("disk_bus"),
+ "nic_model": types.StringValue("model"),
+ "operating_system": types.StringValue("os"),
+ "operating_system_distro": types.StringValue("os_distro"),
+ "operating_system_version": types.StringValue("os_version"),
+ "rescue_bus": types.StringValue("rescue_bus"),
+ "rescue_device": types.StringValue("rescue_device"),
+ "secure_boot": types.BoolValue(true),
+ "uefi": types.BoolValue(true),
+ "video_model": types.StringValue("model"),
+ "virtio_scsi": types.BoolValue(true),
+ }),
+ Checksum: types.ObjectValueMust(checksumTypes, map[string]attr.Value{
+ "algorithm": types.StringValue("algorithm"),
+ "digest": types.StringValue("digest"),
+ }),
+ Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
+ "key": types.StringValue("value"),
+ }),
+ },
+ true,
+ },
+ {
+ "empty_labels",
+ Model{
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
+ },
+ &iaas.Image{
+ Id: utils.Ptr("iid"),
+ },
+ Model{
+ Id: types.StringValue("pid,iid"),
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
+ },
+ true,
+ },
+ {
+ "response_nil_fail",
+ Model{},
+ nil,
+ Model{},
+ false,
+ },
+ {
+ "no_resource_id",
+ Model{
+ ProjectId: types.StringValue("pid"),
+ },
+ &iaas.Image{},
+ Model{},
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ err := mapFields(context.Background(), tt.input, &tt.state)
+ if !tt.isValid && err == nil {
+ t.Fatalf("Should have failed")
+ }
+ if tt.isValid && err != nil {
+ t.Fatalf("Should not have failed: %v", err)
+ }
+ if tt.isValid {
+ diff := cmp.Diff(tt.state, tt.expected)
+ if diff != "" {
+ t.Fatalf("Data does not match: %s", diff)
+ }
+ }
+ })
+ }
+}
+
+func TestToCreatePayload(t *testing.T) {
+ tests := []struct {
+ description string
+ input *Model
+ expected *iaas.CreateImagePayload
+ isValid bool
+ }{
+ {
+ "ok",
+ &Model{
+ Id: types.StringValue("pid,iid"),
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Name: types.StringValue("name"),
+ DiskFormat: types.StringValue("format"),
+ MinDiskSize: types.Int64Value(1),
+ MinRAM: types.Int64Value(1),
+ Protected: types.BoolValue(true),
+ Config: types.ObjectValueMust(configTypes, map[string]attr.Value{
+ "boot_menu": types.BoolValue(true),
+ "cdrom_bus": types.StringValue("cdrom_bus"),
+ "disk_bus": types.StringValue("disk_bus"),
+ "nic_model": types.StringValue("nic_model"),
+ "operating_system": types.StringValue("os"),
+ "operating_system_distro": types.StringValue("os_distro"),
+ "operating_system_version": types.StringValue("os_version"),
+ "rescue_bus": types.StringValue("rescue_bus"),
+ "rescue_device": types.StringValue("rescue_device"),
+ "secure_boot": types.BoolValue(true),
+ "uefi": types.BoolValue(true),
+ "video_model": types.StringValue("video_model"),
+ "virtio_scsi": types.BoolValue(true),
+ }),
+ Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
+ "key": types.StringValue("value"),
+ }),
+ },
+ &iaas.CreateImagePayload{
+ Name: utils.Ptr("name"),
+ DiskFormat: utils.Ptr("format"),
+ MinDiskSize: utils.Ptr(int64(1)),
+ MinRam: utils.Ptr(int64(1)),
+ Protected: utils.Ptr(true),
+ Config: &iaas.ImageConfig{
+ BootMenu: utils.Ptr(true),
+ CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")),
+ DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")),
+ NicModel: iaas.NewNullableString(utils.Ptr("nic_model")),
+ OperatingSystem: utils.Ptr("os"),
+ OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")),
+ OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")),
+ RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")),
+ RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")),
+ SecureBoot: utils.Ptr(true),
+ Uefi: utils.Ptr(true),
+ VideoModel: iaas.NewNullableString(utils.Ptr("video_model")),
+ VirtioScsi: utils.Ptr(true),
+ },
+ Labels: &map[string]interface{}{
+ "key": "value",
+ },
+ },
+ true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ output, err := toCreatePayload(context.Background(), tt.input)
+ if !tt.isValid && err == nil {
+ t.Fatalf("Should have failed")
+ }
+ if tt.isValid && err != nil {
+ t.Fatalf("Should not have failed: %v", err)
+ }
+ if tt.isValid {
+ diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{}))
+ if diff != "" {
+ t.Fatalf("Data does not match: %s", diff)
+ }
+ }
+ })
+ }
+}
+
+func TestToUpdatePayload(t *testing.T) {
+ tests := []struct {
+ description string
+ input *Model
+ expected *iaas.UpdateImagePayload
+ isValid bool
+ }{
+ {
+ "default_ok",
+ &Model{
+ Id: types.StringValue("pid,iid"),
+ ProjectId: types.StringValue("pid"),
+ ImageId: types.StringValue("iid"),
+ Name: types.StringValue("name"),
+ DiskFormat: types.StringValue("format"),
+ MinDiskSize: types.Int64Value(1),
+ MinRAM: types.Int64Value(1),
+ Protected: types.BoolValue(true),
+ Config: types.ObjectValueMust(configTypes, map[string]attr.Value{
+ "boot_menu": types.BoolValue(true),
+ "cdrom_bus": types.StringValue("cdrom_bus"),
+ "disk_bus": types.StringValue("disk_bus"),
+ "nic_model": types.StringValue("nic_model"),
+ "operating_system": types.StringValue("os"),
+ "operating_system_distro": types.StringValue("os_distro"),
+ "operating_system_version": types.StringValue("os_version"),
+ "rescue_bus": types.StringValue("rescue_bus"),
+ "rescue_device": types.StringValue("rescue_device"),
+ "secure_boot": types.BoolValue(true),
+ "uefi": types.BoolValue(true),
+ "video_model": types.StringValue("video_model"),
+ "virtio_scsi": types.BoolValue(true),
+ }),
+ Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
+ "key": types.StringValue("value"),
+ }),
+ },
+ &iaas.UpdateImagePayload{
+ Name: utils.Ptr("name"),
+ MinDiskSize: utils.Ptr(int64(1)),
+ MinRam: utils.Ptr(int64(1)),
+ Protected: utils.Ptr(true),
+ Config: &iaas.ImageConfig{
+ BootMenu: utils.Ptr(true),
+ CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")),
+ DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")),
+ NicModel: iaas.NewNullableString(utils.Ptr("nic_model")),
+ OperatingSystem: utils.Ptr("os"),
+ OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")),
+ OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")),
+ RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")),
+ RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")),
+ SecureBoot: utils.Ptr(true),
+ Uefi: utils.Ptr(true),
+ VideoModel: iaas.NewNullableString(utils.Ptr("video_model")),
+ VirtioScsi: utils.Ptr(true),
+ },
+ Labels: &map[string]interface{}{
+ "key": "value",
+ },
+ },
+ true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType))
+ if !tt.isValid && err == nil {
+ t.Fatalf("Should have failed")
+ }
+ if tt.isValid && err != nil {
+ t.Fatalf("Should not have failed: %v", err)
+ }
+ if tt.isValid {
+ diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{}))
+ if diff != "" {
+ t.Fatalf("Data does not match: %s", diff)
+ }
+ }
+ })
+ }
+}
+
+func Test_UploadImage(t *testing.T) {
+ tests := []struct {
+ name string
+ filePath string
+ uploadFails bool
+ wantErr bool
+ }{
+ {
+ name: "ok",
+ filePath: "testdata/mock-image.txt",
+ uploadFails: false,
+ wantErr: false,
+ },
+ {
+ name: "upload_fails",
+ filePath: "testdata/mock-image.txt",
+ uploadFails: true,
+ wantErr: true,
+ },
+ {
+ name: "file_not_found",
+ filePath: "testdata/non-existing-file.txt",
+ uploadFails: false,
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Setup a test server
+ handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ if tt.uploadFails {
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = fmt.Fprintln(w, `{"status":"some error occurred"}`)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = fmt.Fprintln(w, `{"status":"ok"}`)
+ })
+ server := httptest.NewServer(handler)
+ defer server.Close()
+ uploadURL, err := url.Parse(server.URL)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // Call the function
+ err = uploadImage(context.Background(), &diag.Diagnostics{}, tt.filePath, uploadURL.String())
+ if (err != nil) != tt.wantErr {
+ t.Errorf("uploadImage() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/stackit/internal/services/iaas/image/testdata/mock-image.txt b/stackit/internal/services/iaas/image/testdata/mock-image.txt
new file mode 100644
index 00000000..eaa3529c
--- /dev/null
+++ b/stackit/internal/services/iaas/image/testdata/mock-image.txt
@@ -0,0 +1 @@
+I am a mock image file
\ No newline at end of file
diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go
index 0b0786c7..a5ddf321 100644
--- a/stackit/internal/testutil/testutil.go
+++ b/stackit/internal/testutil/testutil.go
@@ -46,6 +46,8 @@ var (
TestProjectServiceAccountEmail = os.Getenv("TF_ACC_TEST_PROJECT_SERVICE_ACCOUNT_EMAIL")
// TestProjectUserEmail is the e-mail of a user for the project created as part of the resource-manager acceptance tests
TestProjectUserEmail = os.Getenv("TF_ACC_TEST_PROJECT_USER_EMAIL")
+ // TestImageLocalFilePath is the local path to an image file used for image acceptance tests
+ TestImageLocalFilePath = os.Getenv("TF_ACC_TEST_IMAGE_LOCAL_FILE_PATH")
ArgusCustomEndpoint = os.Getenv("TF_ACC_ARGUS_CUSTOM_ENDPOINT")
DnsCustomEndpoint = os.Getenv("TF_ACC_DNS_CUSTOM_ENDPOINT")
diff --git a/stackit/internal/validate/testdata/file.txt b/stackit/internal/validate/testdata/file.txt
new file mode 100644
index 00000000..996393d8
--- /dev/null
+++ b/stackit/internal/validate/testdata/file.txt
@@ -0,0 +1 @@
+I am a test file
\ No newline at end of file
diff --git a/stackit/internal/validate/validate.go b/stackit/internal/validate/validate.go
index 3633c6b5..7dd1a286 100644
--- a/stackit/internal/validate/validate.go
+++ b/stackit/internal/validate/validate.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net"
+ "os"
"regexp"
"strings"
"time"
@@ -272,3 +273,21 @@ func Rrule() *Validator {
},
}
}
+
+func FileExists() *Validator {
+ description := "file must exist"
+
+ return &Validator{
+ description: description,
+ validate: func(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
+ _, err := os.Stat(req.ConfigValue.ValueString())
+ if err != nil {
+ resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic(
+ req.Path,
+ description,
+ req.ConfigValue.ValueString(),
+ ))
+ }
+ },
+ }
+}
diff --git a/stackit/internal/validate/validate_test.go b/stackit/internal/validate/validate_test.go
index 584adf2e..428799a4 100644
--- a/stackit/internal/validate/validate_test.go
+++ b/stackit/internal/validate/validate_test.go
@@ -682,3 +682,42 @@ func TestRrule(t *testing.T) {
})
}
}
+
+func TestFileExists(t *testing.T) {
+ tests := []struct {
+ description string
+ input string
+ isValid bool
+ }{
+ {
+ "ok",
+ "testdata/file.txt",
+ true,
+ },
+ {
+ "not ok",
+ "testdata/non-existing-file.txt",
+ false,
+ },
+ {
+ "empty",
+ "",
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ r := validator.StringResponse{}
+ FileExists().ValidateString(context.Background(), validator.StringRequest{
+ ConfigValue: types.StringValue(tt.input),
+ }, &r)
+
+ if !tt.isValid && !r.Diagnostics.HasError() {
+ t.Fatalf("Should have failed")
+ }
+ if tt.isValid && r.Diagnostics.HasError() {
+ t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors())
+ }
+ })
+ }
+}
diff --git a/stackit/provider.go b/stackit/provider.go
index 4b26cf31..cfe4d1ac 100644
--- a/stackit/provider.go
+++ b/stackit/provider.go
@@ -14,6 +14,7 @@ import (
argusScrapeConfig "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/argus/scrapeconfig"
dnsRecordSet "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/recordset"
dnsZone "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/zone"
+ iaasImage "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/image"
iaasKeyPair "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/keypair"
iaasNetwork "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network"
iaasNetworkArea "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarea"
@@ -412,6 +413,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
argusScrapeConfig.NewScrapeConfigDataSource,
dnsZone.NewZoneDataSource,
dnsRecordSet.NewRecordSetDataSource,
+ iaasImage.NewImageDataSource,
iaasNetwork.NewNetworkDataSource,
iaasNetworkArea.NewNetworkAreaDataSource,
iaasNetworkAreaRoute.NewNetworkAreaRouteDataSource,
@@ -465,6 +467,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
argusScrapeConfig.NewScrapeConfigResource,
dnsZone.NewZoneResource,
dnsRecordSet.NewRecordSetResource,
+ iaasImage.NewImageResource,
iaasNetwork.NewNetworkResource,
iaasNetworkArea.NewNetworkAreaResource,
iaasNetworkAreaRoute.NewNetworkAreaRouteResource,