Implement new stackit_image resource and datasource (#609)
* feat: Implement image resource and datasource * feat: Add remaining config options * feat: Make protected field only computed * feat: Update dependency to use IaaS beta API * fix: Minor fix in acc test --------- Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>
This commit is contained in:
parent
7fcebacb21
commit
700bdc90d0
15 changed files with 2212 additions and 4 deletions
72
docs/data-sources/image.md
Normal file
72
docs/data-sources/image.md
Normal file
|
|
@ -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 generated by tfplugindocs -->
|
||||
## 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.
|
||||
|
||||
<a id="nestedatt--checksum"></a>
|
||||
### 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.
|
||||
|
||||
|
||||
<a id="nestedatt--config"></a>
|
||||
### 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.
|
||||
77
docs/resources/image.md
Normal file
77
docs/resources/image.md
Normal file
|
|
@ -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 generated by tfplugindocs -->
|
||||
## 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.
|
||||
|
||||
<a id="nestedatt--config"></a>
|
||||
### 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.
|
||||
|
||||
|
||||
<a id="nestedatt--checksum"></a>
|
||||
### 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.
|
||||
4
examples/data-sources/stackit_image/data-source.tf
Normal file
4
examples/data-sources/stackit_image/data-source.tf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
data "stackit_image" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
image_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
8
examples/resources/stackit_image/resource.tf
Normal file
8
examples/resources/stackit_image/resource.tf
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
387
stackit/internal/services/iaas/image/datasource.go
Normal file
387
stackit/internal/services/iaas/image/datasource.go
Normal file
|
|
@ -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
|
||||
}
|
||||
163
stackit/internal/services/iaas/image/datasource_test.go
Normal file
163
stackit/internal/services/iaas/image/datasource_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
859
stackit/internal/services/iaas/image/resource.go
Normal file
859
stackit/internal/services/iaas/image/resource.go
Normal file
|
|
@ -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
|
||||
}
|
||||
391
stackit/internal/services/iaas/image/resource_test.go
Normal file
391
stackit/internal/services/iaas/image/resource_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
1
stackit/internal/services/iaas/image/testdata/mock-image.txt
vendored
Normal file
1
stackit/internal/services/iaas/image/testdata/mock-image.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
I am a mock image file
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
1
stackit/internal/validate/testdata/file.txt
vendored
Normal file
1
stackit/internal/validate/testdata/file.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
I am a test file
|
||||
|
|
@ -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(),
|
||||
))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue