diff --git a/docs/data-sources/volume_automation.md b/docs/data-sources/volume_automation.md
new file mode 100644
index 000000000..f10c43124
--- /dev/null
+++ b/docs/data-sources/volume_automation.md
@@ -0,0 +1,58 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "stackit_volume_automation Data Source - stackit"
+subcategory: ""
+description: |-
+ Volume automation datasource schema. Must have a region specified in the provider configuration.
+ ~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
+---
+
+# stackit_volume_automation (Data Source)
+
+Volume automation datasource schema. Must have a `region` specified in the provider configuration.
+
+~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.
+
+## Example Usage
+
+```terraform
+data "stackit_volume_automation" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ automation_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+}
+```
+
+
+## Schema
+
+### Required
+
+- `automation_id` (String) ID of the volume automation.
+- `project_id` (String) STACKIT Project ID to which the volume automation is associated.
+
+### Optional
+
+- `region` (String) The resource region. If not defined, the provider region is used.
+
+### Read-Only
+
+- `description` (String) The volume automation description.
+- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`region`,`automation_id`".
+- `input` (String) Configuration input for the volume automation. See [API Docs](https://docs.api.stackit.cloud/documentation/automation-service/version/v1#tag/Volume-Automations/operation/CreateVolumeAutomation) for possible configuration options.
+- `name` (String) The volume automation name.
+- `template_id` (String) ID of the automation template this volume automation is based on.
+- `triggers` (Attributes) Triggers that determine when the automation runs. (see [below for nested schema](#nestedatt--triggers))
+
+
+### Nested Schema for `triggers`
+
+Read-Only:
+
+- `schedule` (Attributes) Runs the automation on a recurring schedule. (see [below for nested schema](#nestedatt--triggers--schedule))
+
+
+### Nested Schema for `triggers.schedule`
+
+Read-Only:
+
+- `rrule` (String) An `rrule` (Recurrence Rule) is a standardized string format used in iCalendar (RFC 5545) to define repeating events, and you can generate one by using a dedicated library or by using online generator tools to specify parameters like frequency, interval, and end dates.
diff --git a/docs/resources/volume_automation.md b/docs/resources/volume_automation.md
new file mode 100644
index 000000000..262d09b23
--- /dev/null
+++ b/docs/resources/volume_automation.md
@@ -0,0 +1,92 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "stackit_volume_automation Resource - stackit"
+subcategory: ""
+description: |-
+ Volume automation resource 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_volume_automation (Resource)
+
+Volume automation resource 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
+resource "stackit_volume_automation" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ template_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name = "example-volume-automation"
+ description = "Creates daily volume snapshots and keeps the last 7."
+ input = jsonencode({
+ kind = "VolumeRecoveryPointManagement"
+ inheritVolumeLabels = false
+ recoveryPointLabels = {
+ "exampleLabelKey1" = "exampleLabelValue1"
+ "exampleLabelKey2" = "exampleLabelValue2"
+ }
+ snapshotRetentionPolicy = {
+ kind = "count"
+ value = 2
+ }
+ volumeLabelSelector = "myLabelkey1=myLabelValue,myLabelKey2=myOtherLabelValue"
+ })
+ triggers = {
+ schedule = {
+ rrule = "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"
+ }
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `project_id` (String) STACKIT Project ID to which the volume automation is associated.
+- `template_id` (String) ID of the automation template this volume automation is based on.
+
+### Optional
+
+- `description` (String) The volume automation description.
+- `input` (String) Configuration input for the volume automation. See [API Docs](https://docs.api.stackit.cloud/documentation/automation-service/version/v1#tag/Volume-Automations/operation/CreateVolumeAutomation) for possible configuration options.
+- `name` (String) The volume automation name.
+- `region` (String) The resource region. If not defined, the provider region is used.
+- `triggers` (Attributes) Triggers that determine when the automation runs. (see [below for nested schema](#nestedatt--triggers))
+
+### Read-Only
+
+- `automation_id` (String) ID of the volume automation.
+- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`region`,`automation_id`".
+
+
+### Nested Schema for `triggers`
+
+Optional:
+
+- `schedule` (Attributes) Runs the automation on a recurring schedule. (see [below for nested schema](#nestedatt--triggers--schedule))
+
+
+### Nested Schema for `triggers.schedule`
+
+Required:
+
+- `rrule` (String) An `rrule` (Recurrence Rule) is a standardized string format used in iCalendar (RFC 5545) to define repeating events, and you can generate one by using a dedicated library or by using online generator tools to specify parameters like frequency, interval, and end dates.
+
+## Import
+
+Import is supported using the following syntax:
+
+In Terraform v1.5.0 and later, the [` + "`" + `import` + "`" + ` block](https://developer.hashicorp.com/terraform/language/import) can be used with the ` + "`" + `id` + "`" + ` attribute, for example:
+
+```terraform
+# Only use the import statement, if you want to import an existing volume automation resource
+import {
+ to = stackit_volume_automation.import-example
+ id = "${var.project_id},${var.region},${var.automation_id}"
+}
+```
diff --git a/examples/data-sources/stackit_volume_automation/data-source.tf b/examples/data-sources/stackit_volume_automation/data-source.tf
new file mode 100644
index 000000000..1075b20a4
--- /dev/null
+++ b/examples/data-sources/stackit_volume_automation/data-source.tf
@@ -0,0 +1,4 @@
+data "stackit_volume_automation" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ automation_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+}
diff --git a/examples/resources/stackit_volume_automation/import-by-string-id.tf b/examples/resources/stackit_volume_automation/import-by-string-id.tf
new file mode 100644
index 000000000..e53efd952
--- /dev/null
+++ b/examples/resources/stackit_volume_automation/import-by-string-id.tf
@@ -0,0 +1,5 @@
+# Only use the import statement, if you want to import an existing volume automation resource
+import {
+ to = stackit_volume_automation.import-example
+ id = "${var.project_id},${var.region},${var.automation_id}"
+}
diff --git a/examples/resources/stackit_volume_automation/resource.tf b/examples/resources/stackit_volume_automation/resource.tf
new file mode 100644
index 000000000..8d541fa21
--- /dev/null
+++ b/examples/resources/stackit_volume_automation/resource.tf
@@ -0,0 +1,24 @@
+resource "stackit_volume_automation" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ template_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name = "example-volume-automation"
+ description = "Creates daily volume snapshots and keeps the last 7."
+ input = jsonencode({
+ kind = "VolumeRecoveryPointManagement"
+ inheritVolumeLabels = false
+ recoveryPointLabels = {
+ "exampleLabelKey1" = "exampleLabelValue1"
+ "exampleLabelKey2" = "exampleLabelValue2"
+ }
+ snapshotRetentionPolicy = {
+ kind = "count"
+ value = 2
+ }
+ volumeLabelSelector = "myLabelkey1=myLabelValue,myLabelKey2=myOtherLabelValue"
+ })
+ triggers = {
+ schedule = {
+ rrule = "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"
+ }
+ }
+}
diff --git a/go.mod b/go.mod
index e7c95e7d5..172e7a573 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/hashicorp/terraform-plugin-framework v1.19.0
+ github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0
github.com/hashicorp/terraform-plugin-framework-timeouts v0.7.0
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0
github.com/hashicorp/terraform-plugin-go v0.31.0
@@ -15,7 +16,7 @@ require (
github.com/stackitcloud/stackit-sdk-go/core v0.27.0
github.com/stackitcloud/stackit-sdk-go/services/alb v0.17.2
github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.13.4
- github.com/stackitcloud/stackit-sdk-go/services/automation v0.1.1
+ github.com/stackitcloud/stackit-sdk-go/services/automation v0.2.0
github.com/stackitcloud/stackit-sdk-go/services/cdn v1.21.0
github.com/stackitcloud/stackit-sdk-go/services/certificates v1.9.2
github.com/stackitcloud/stackit-sdk-go/services/dns v0.23.1
diff --git a/go.sum b/go.sum
index d49bec7aa..c947a141f 100644
--- a/go.sum
+++ b/go.sum
@@ -91,6 +91,8 @@ github.com/hashicorp/terraform-json v0.28.0 h1:dOkJT55rWfU6T1/VklHde51ym4LfNP+9x
github.com/hashicorp/terraform-json v0.28.0/go.mod h1:PJIRf+Yzu5iLb52c/xYp1tUOL4jzMzfIAB5gvWWKIWE=
github.com/hashicorp/terraform-plugin-framework v1.19.0 h1:q0bwyhxAOR3vfdgbk9iplv3MlTv/dhBHTXjQOtQDoBA=
github.com/hashicorp/terraform-plugin-framework v1.19.0/go.mod h1:YRXOBu0jvs7xp4AThBbX4mAzYaMJ1JgtFH//oGKxwLc=
+github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0 h1:SJXL5FfJJm17554Kpt9jFXngdM6fXbnUnZ6iT2IeiYA=
+github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0/go.mod h1:p0phD0IYhsu9bR4+6OetVvvH59I6LwjXGnTVEr8ox6E=
github.com/hashicorp/terraform-plugin-framework-timeouts v0.7.0 h1:jblRy1PkLfPm5hb5XeMa3tezusnMRziUGqtT5epSYoI=
github.com/hashicorp/terraform-plugin-framework-timeouts v0.7.0/go.mod h1:5jm2XK8uqrdiSRfD5O47OoxyGMCnwTcl8eoiDgSa+tc=
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0 h1:Zz3iGgzxe/1XBkooZCewS0nJAaCFPFPHdNJd8FgE4Ow=
@@ -161,8 +163,8 @@ github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.13.4 h1:KNEcAPj66Asb00
github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.13.4/go.mod h1:riXgEDfIjNdFjYk81zNnWt0npkv1+/P1iFRfFIt28Jo=
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.15.4 h1:YRfEQJxMn9B/yCf5ZGTsBzPLv69LM/jvvLuC8vb0Uf4=
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.15.4/go.mod h1:AVLUwOPKsoz6HvKzClu0a5OTjMOYS696gRjHUQvQH70=
-github.com/stackitcloud/stackit-sdk-go/services/automation v0.1.1 h1:naKG2lRw9ALCNaaJabhgtuCwSBVyOOlIX3NPkIFX1i0=
-github.com/stackitcloud/stackit-sdk-go/services/automation v0.1.1/go.mod h1:1vv2SZorcHJKxEPV15CnTDhavV+NcYSNjAX7cZGX31k=
+github.com/stackitcloud/stackit-sdk-go/services/automation v0.2.0 h1:X3tnz8Aij3fRk0vx7Ekvt5ONnP+ACMqwrBuKftHJJdY=
+github.com/stackitcloud/stackit-sdk-go/services/automation v0.2.0/go.mod h1:1vv2SZorcHJKxEPV15CnTDhavV+NcYSNjAX7cZGX31k=
github.com/stackitcloud/stackit-sdk-go/services/cdn v1.21.0 h1:lCs1eq/OOTdwGrrkxkJ8qCnS6yLM4XZhznpML3nd1NI=
github.com/stackitcloud/stackit-sdk-go/services/cdn v1.21.0/go.mod h1:NBgvZH5ekJ2hFCNI480C9MdJsa4989YGC1YgX5ELA48=
github.com/stackitcloud/stackit-sdk-go/services/certificates v1.9.2 h1:QEKzHzVwZ80mNdoEWEZxgynACnlPfXwIlVz9b7xceR4=
diff --git a/stackit/internal/services/automation/automation_acc_test.go b/stackit/internal/services/automation/automation_acc_test.go
index 69f299cbd..a03ff1755 100644
--- a/stackit/internal/services/automation/automation_acc_test.go
+++ b/stackit/internal/services/automation/automation_acc_test.go
@@ -1,17 +1,166 @@
package automation_test
import (
+ "context"
_ "embed"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "maps"
+ "net/http"
"testing"
+ "time"
"github.com/hashicorp/terraform-plugin-testing/config"
+ "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+
+ "github.com/stackitcloud/stackit-sdk-go/core/oapierror"
+ automation "github.com/stackitcloud/stackit-sdk-go/services/automation/v1api"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
)
-//go:embed testdata/datasource-templates.tf
-var templatesDataSourceConfig string
+var (
+ //go:embed testdata/datasource-templates.tf
+ templatesDataSourceConfig string
+
+ //go:embed testdata/resource-min.tf
+ resourceMinConfig string
+
+ //go:embed testdata/resource-max.tf
+ resourceMaxConfig string
+
+ //go:embed testdata/resource-max-cleared.tf
+ resourceMaxClearedConfig string
+
+ //go:embed testdata/datasource.tf
+ datasourceConfig string
+)
+
+// lookupVolumeRecoveryPointManagementTemplateID looks up the system-provided automation
+// template of kind "VolumeRecoveryPointManagement". Volume automation templates are not
+// created via Terraform, so acceptance tests have to discover a valid template_id at runtime.
+func lookupVolumeRecoveryPointManagementTemplateID(t *testing.T) string {
+ // skip if TF_ACC=1 is not set
+ if !testutil.E2ETestsEnabled {
+ return ""
+ }
+
+ t.Helper()
+ ctx := context.Background()
+
+ client, err := automation.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.AutomationCustomEndpoint, false)...)
+ if err != nil {
+ t.Fatalf("creating automation client: %v", err)
+ }
+
+ templatesResp, err := client.DefaultAPI.ListVolumeTemplates(ctx, testutil.ProjectId, testutil.Region).Execute()
+ if err != nil {
+ t.Fatalf("listing volume templates: %v", err)
+ }
+
+ for _, tpl := range templatesResp.Items {
+ templateResp, err := client.DefaultAPI.GetVolumeTemplate(ctx, testutil.ProjectId, testutil.Region, tpl.Id).Execute()
+ if err != nil {
+ t.Fatalf("getting volume template %q: %v", tpl.Id, err)
+ }
+ if templateResp.Input != nil && templateResp.Input.Kind == automation.VOLUMETEMPLATEAUTOMATIONINPUTKIND_VOLUME_RECOVERY_POINT_MANAGEMENT {
+ return tpl.Id
+ }
+ }
+ t.Fatal("no volume automation template of kind VolumeRecoveryPointManagement found")
+ return ""
+}
+
+// futureRrule builds an rrule with a DTSTART relative to now, instead of a hardcoded date that
+// would eventually be in the past.
+func futureRrule(afterDuration time.Duration, intervalDays int) string {
+ dtstart := time.Now().UTC().Add(afterDuration).Format("20060102T150405")
+ return fmt.Sprintf("DTSTART;TZID=UTC:%s RRULE:FREQ=DAILY;INTERVAL=%d", dtstart, intervalDays)
+}
+
+func testConfigVarsMin(templateID string) config.Variables {
+ // the input variable needs a json decoded string
+ inputMap := map[string]interface{}{
+ "kind": "VolumeRecoveryPointManagement",
+ "snapshotRetentionPolicy": map[string]interface{}{
+ "kind": "indefinitely",
+ },
+ }
+ inputJson, err := json.Marshal(inputMap)
+ if err != nil {
+ return nil
+ }
+
+ return config.Variables{
+ "project_id": config.StringVariable(testutil.ProjectId),
+ "template_id": config.StringVariable(templateID),
+ "name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)),
+ "rrule": config.StringVariable(futureRrule(time.Hour, 1)),
+ "input": config.StringVariable(string(inputJson)),
+ }
+}
+
+func testConfigVarsMax(templateID string) config.Variables {
+ // the input variable needs a json decoded string
+ inputMap := map[string]interface{}{
+ "kind": "VolumeRecoveryPointManagement",
+ "inheritVolumeLabels": true,
+ "recoveryPointLabels": map[string]interface{}{
+ "created-by": "tf-acc-test",
+ },
+ "snapshotRetentionPolicy": map[string]interface{}{
+ "kind": "count",
+ "value": 4,
+ },
+ }
+ inputJson, err := json.Marshal(inputMap)
+ if err != nil {
+ return nil
+ }
+
+ return config.Variables{
+ "project_id": config.StringVariable(testutil.ProjectId),
+ "region": config.StringVariable(testutil.Region),
+ "template_id": config.StringVariable(templateID),
+ "name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)),
+ "description": config.StringVariable("tf-acc-test description"),
+ "rrule": config.StringVariable(futureRrule(time.Hour, 1)),
+ "input": config.StringVariable(string(inputJson)),
+ }
+}
+
+func configVarsMinUpdated(base config.Variables) config.Variables {
+ tempConfig := maps.Clone(base)
+ tempConfig["name"] = config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha))
+ tempConfig["rrule"] = config.StringVariable(futureRrule(2*time.Hour, 3))
+ return tempConfig
+}
+
+func configVarsMaxUpdated(base config.Variables) config.Variables {
+ // the input variable needs a json decoded string
+ inputMap := map[string]interface{}{
+ "kind": "VolumeRecoveryPointManagement",
+ "recoveryPointLabels": map[string]interface{}{
+ "created-by": "tf-acc-test-updated",
+ },
+ "snapshotRetentionPolicy": map[string]interface{}{
+ "kind": "indefinitely",
+ },
+ }
+ inputJson, err := json.Marshal(inputMap)
+ if err != nil {
+ return nil
+ }
+
+ tempConfig := maps.Clone(base)
+ tempConfig["description"] = config.StringVariable("tf-acc-test description updated")
+ tempConfig["retention_count"] = config.IntegerVariable(5)
+ tempConfig["input"] = config.StringVariable(string(inputJson))
+ return tempConfig
+}
func TestAccAutomationTemplatesDataSource(t *testing.T) {
resource.Test(t, resource.TestCase{
@@ -35,3 +184,209 @@ func TestAccAutomationTemplatesDataSource(t *testing.T) {
},
})
}
+
+func TestAccVolumeAutomationMinResource(t *testing.T) {
+ varsMin := testConfigVarsMin(lookupVolumeRecoveryPointManagementTemplateID(t))
+ varsMinUpdated := configVarsMinUpdated(varsMin)
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
+ CheckDestroy: testAccCheckVolumeAutomationDestroy,
+ Steps: []resource.TestStep{
+ // Creation
+ {
+ Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceMinConfig,
+ ConfigVariables: varsMin,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "project_id", testutil.ConvertConfigVariable(varsMin["project_id"])),
+ resource.TestCheckResourceAttrSet("stackit_volume_automation.test", "region"),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "template_id", testutil.ConvertConfigVariable(varsMin["template_id"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "name", testutil.ConvertConfigVariable(varsMin["name"])),
+ resource.TestCheckNoResourceAttr("stackit_volume_automation.test", "description"),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "input", testutil.ConvertConfigVariable(varsMin["input"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "triggers.schedule.rrule", testutil.ConvertConfigVariable(varsMin["rrule"])),
+ resource.TestCheckResourceAttrSet("stackit_volume_automation.test", "automation_id"),
+ resource.TestCheckResourceAttrSet("stackit_volume_automation.test", "id"),
+ ),
+ },
+ // data source
+ {
+ Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceMinConfig + "\n" + datasourceConfig,
+ ConfigVariables: varsMin,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("data.stackit_volume_automation.test_data", "project_id", testutil.ConvertConfigVariable(varsMin["project_id"])),
+ resource.TestCheckResourceAttrSet("data.stackit_volume_automation.test_data", "region"),
+ resource.TestCheckResourceAttr("data.stackit_volume_automation.test_data", "template_id", testutil.ConvertConfigVariable(varsMin["template_id"])),
+ resource.TestCheckResourceAttrSet("data.stackit_volume_automation.test_data", "automation_id"),
+ resource.TestCheckResourceAttr("data.stackit_volume_automation.test_data", "name", testutil.ConvertConfigVariable(varsMin["name"])),
+ resource.TestCheckNoResourceAttr("data.stackit_volume_automation.test_data", "description"),
+ resource.TestCheckResourceAttr("data.stackit_volume_automation.test_data", "input", testutil.ConvertConfigVariable(varsMin["input"])),
+ resource.TestCheckResourceAttr("data.stackit_volume_automation.test_data", "triggers.schedule.rrule", testutil.ConvertConfigVariable(varsMin["rrule"])),
+ resource.TestCheckResourceAttrSet("data.stackit_volume_automation.test_data", "id"),
+ ),
+ },
+ // Import
+ {
+ ResourceName: "stackit_volume_automation.test",
+ ConfigVariables: varsMin,
+ ImportStateIdFunc: func(s *terraform.State) (string, error) {
+ r, ok := s.RootModule().Resources["stackit_volume_automation.test"]
+ if !ok {
+ return "", fmt.Errorf("couldn't find resource stackit_volume_automation.test")
+ }
+ automationId, ok := r.Primary.Attributes["automation_id"]
+ if !ok {
+ return "", fmt.Errorf("couldn't find attribute automation_id")
+ }
+ return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, automationId), nil
+ },
+ ImportState: true,
+ ImportStateVerify: true,
+ },
+ // Update
+ {
+ Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceMinConfig,
+ ConfigVariables: varsMinUpdated,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "project_id", testutil.ConvertConfigVariable(varsMinUpdated["project_id"])),
+ resource.TestCheckResourceAttrSet("stackit_volume_automation.test", "region"),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "template_id", testutil.ConvertConfigVariable(varsMinUpdated["template_id"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "name", testutil.ConvertConfigVariable(varsMinUpdated["name"])),
+ resource.TestCheckNoResourceAttr("stackit_volume_automation.test", "description"),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "input", testutil.ConvertConfigVariable(varsMinUpdated["input"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "triggers.schedule.rrule", testutil.ConvertConfigVariable(varsMinUpdated["rrule"])),
+ resource.TestCheckResourceAttrSet("stackit_volume_automation.test", "automation_id"),
+ resource.TestCheckResourceAttrSet("stackit_volume_automation.test", "id"),
+ ),
+ },
+ // Deletion is done by the framework implicitly
+ },
+ })
+}
+
+func TestAccVolumeAutomationMaxResource(t *testing.T) {
+ varsMax := testConfigVarsMax(lookupVolumeRecoveryPointManagementTemplateID(t))
+ varsMaxUpdated := configVarsMaxUpdated(varsMax)
+ varsMaxCleared := maps.Clone(varsMaxUpdated)
+ delete(varsMaxCleared, "name")
+ delete(varsMaxCleared, "description")
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
+ CheckDestroy: testAccCheckVolumeAutomationDestroy,
+ Steps: []resource.TestStep{
+ // Creation
+ {
+ Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceMaxConfig,
+ ConfigVariables: varsMax,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "project_id", testutil.ConvertConfigVariable(varsMax["project_id"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "region", testutil.ConvertConfigVariable(varsMax["region"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "template_id", testutil.ConvertConfigVariable(varsMax["template_id"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "name", testutil.ConvertConfigVariable(varsMax["name"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "description", testutil.ConvertConfigVariable(varsMax["description"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "input", testutil.ConvertConfigVariable(varsMax["input"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "triggers.schedule.rrule", testutil.ConvertConfigVariable(varsMax["rrule"])),
+ resource.TestCheckResourceAttrSet("stackit_volume_automation.test", "automation_id"),
+ resource.TestCheckResourceAttrSet("stackit_volume_automation.test", "id"),
+ ),
+ },
+ // data source
+ {
+ Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceMaxConfig + "\n" + datasourceConfig,
+ ConfigVariables: varsMax,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("data.stackit_volume_automation.test_data", "project_id", testutil.ConvertConfigVariable(varsMax["project_id"])),
+ resource.TestCheckResourceAttr("data.stackit_volume_automation.test_data", "region", testutil.ConvertConfigVariable(varsMax["region"])),
+ resource.TestCheckResourceAttr("data.stackit_volume_automation.test_data", "template_id", testutil.ConvertConfigVariable(varsMax["template_id"])),
+ resource.TestCheckResourceAttrSet("data.stackit_volume_automation.test_data", "automation_id"),
+ resource.TestCheckResourceAttr("data.stackit_volume_automation.test_data", "name", testutil.ConvertConfigVariable(varsMax["name"])),
+ resource.TestCheckResourceAttr("data.stackit_volume_automation.test_data", "description", testutil.ConvertConfigVariable(varsMax["description"])),
+ resource.TestCheckResourceAttr("data.stackit_volume_automation.test_data", "input", testutil.ConvertConfigVariable(varsMax["input"])),
+ resource.TestCheckResourceAttr("data.stackit_volume_automation.test_data", "triggers.schedule.rrule", testutil.ConvertConfigVariable(varsMax["rrule"])),
+ resource.TestCheckResourceAttrSet("data.stackit_volume_automation.test_data", "id"),
+ ),
+ },
+ // Import
+ {
+ ResourceName: "stackit_volume_automation.test",
+ ConfigVariables: varsMax,
+ ImportStateIdFunc: func(s *terraform.State) (string, error) {
+ r, ok := s.RootModule().Resources["stackit_volume_automation.test"]
+ if !ok {
+ return "", fmt.Errorf("couldn't find resource stackit_volume_automation.test")
+ }
+ automationId, ok := r.Primary.Attributes["automation_id"]
+ if !ok {
+ return "", fmt.Errorf("couldn't find attribute automation_id")
+ }
+ return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, automationId), nil
+ },
+ ImportState: true,
+ ImportStateVerify: true,
+ },
+ // Update
+ {
+ Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceMaxConfig,
+ ConfigVariables: varsMaxUpdated,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "project_id", testutil.ConvertConfigVariable(varsMaxUpdated["project_id"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "region", testutil.ConvertConfigVariable(varsMaxUpdated["region"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "template_id", testutil.ConvertConfigVariable(varsMaxUpdated["template_id"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "name", testutil.ConvertConfigVariable(varsMaxUpdated["name"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "description", testutil.ConvertConfigVariable(varsMaxUpdated["description"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "input", testutil.ConvertConfigVariable(varsMaxUpdated["input"])),
+ resource.TestCheckResourceAttr("stackit_volume_automation.test", "triggers.schedule.rrule", testutil.ConvertConfigVariable(varsMaxUpdated["rrule"])),
+ resource.TestCheckResourceAttrSet("stackit_volume_automation.test", "automation_id"),
+ resource.TestCheckResourceAttrSet("stackit_volume_automation.test", "id"),
+ ),
+ },
+ // Clear optional name and description
+ {
+ Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceMaxClearedConfig,
+ ConfigVariables: varsMaxCleared,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckNoResourceAttr("stackit_volume_automation.test", "name"),
+ resource.TestCheckNoResourceAttr("stackit_volume_automation.test", "description"),
+ ),
+ },
+ // Deletion is done by the framework implicitly
+ },
+ })
+}
+
+func testAccCheckVolumeAutomationDestroy(s *terraform.State) error {
+ ctx := context.Background()
+ client, err := automation.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.AutomationCustomEndpoint, false)...)
+ if err != nil {
+ return fmt.Errorf("creating automation client: %w", err)
+ }
+
+ var errs []error
+ for _, rs := range s.RootModule().Resources {
+ if rs.Type != "stackit_volume_automation" {
+ continue
+ }
+ projectId := rs.Primary.Attributes["project_id"]
+ if projectId == "" {
+ continue
+ }
+ region := rs.Primary.Attributes["region"]
+ if region == "" {
+ continue
+ }
+ automationId := rs.Primary.Attributes["automation_id"]
+ if automationId == "" {
+ continue
+ }
+
+ _, err = client.DefaultAPI.GetVolumeAutomation(ctx, projectId, region, automationId).Execute()
+ if err == nil {
+ errs = append(errs, fmt.Errorf("volume automation %s still exists", automationId))
+ continue
+ }
+ if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && oapiErr.StatusCode == http.StatusNotFound {
+ continue
+ }
+ errs = append(errs, fmt.Errorf("checking volume automation %s destruction: %w", automationId, err))
+ }
+ return errors.Join(errs...)
+}
diff --git a/stackit/internal/services/automation/templates/datasource.go b/stackit/internal/services/automation/templates/datasource.go
index 68f58ef1e..4771c01bb 100644
--- a/stackit/internal/services/automation/templates/datasource.go
+++ b/stackit/internal/services/automation/templates/datasource.go
@@ -13,7 +13,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/experimental/paginate"
- automation "github.com/stackitcloud/stackit-sdk-go/services/automation/v1betaapi"
+ automation "github.com/stackitcloud/stackit-sdk-go/services/automation/v1api"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
diff --git a/stackit/internal/services/automation/templates/datasource_test.go b/stackit/internal/services/automation/templates/datasource_test.go
index 4928ec6fe..59b49f814 100644
--- a/stackit/internal/services/automation/templates/datasource_test.go
+++ b/stackit/internal/services/automation/templates/datasource_test.go
@@ -6,7 +6,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/types"
- automation "github.com/stackitcloud/stackit-sdk-go/services/automation/v1betaapi"
+ automation "github.com/stackitcloud/stackit-sdk-go/services/automation/v1api"
)
var testTimestampValue = "2006-01-02T15:04:05Z"
diff --git a/stackit/internal/services/automation/testdata/datasource.tf b/stackit/internal/services/automation/testdata/datasource.tf
new file mode 100644
index 000000000..d602283aa
--- /dev/null
+++ b/stackit/internal/services/automation/testdata/datasource.tf
@@ -0,0 +1,4 @@
+data "stackit_volume_automation" "test_data" {
+ project_id = var.project_id
+ automation_id = stackit_volume_automation.test.automation_id
+}
diff --git a/stackit/internal/services/automation/testdata/resource-max-cleared.tf b/stackit/internal/services/automation/testdata/resource-max-cleared.tf
new file mode 100644
index 000000000..b9bcb13c7
--- /dev/null
+++ b/stackit/internal/services/automation/testdata/resource-max-cleared.tf
@@ -0,0 +1,17 @@
+variable "project_id" {}
+variable "region" {}
+variable "template_id" {}
+variable "rrule" {}
+variable "input" {}
+
+resource "stackit_volume_automation" "test" {
+ project_id = var.project_id
+ region = var.region
+ template_id = var.template_id
+ input = var.input
+ triggers = {
+ schedule = {
+ rrule = var.rrule
+ }
+ }
+}
diff --git a/stackit/internal/services/automation/testdata/resource-max.tf b/stackit/internal/services/automation/testdata/resource-max.tf
new file mode 100644
index 000000000..36bb71bcb
--- /dev/null
+++ b/stackit/internal/services/automation/testdata/resource-max.tf
@@ -0,0 +1,21 @@
+variable "project_id" {}
+variable "region" {}
+variable "template_id" {}
+variable "name" {}
+variable "description" {}
+variable "rrule" {}
+variable "input" {}
+
+resource "stackit_volume_automation" "test" {
+ project_id = var.project_id
+ region = var.region
+ template_id = var.template_id
+ name = var.name
+ description = var.description
+ input = var.input
+ triggers = {
+ schedule = {
+ rrule = var.rrule
+ }
+ }
+}
diff --git a/stackit/internal/services/automation/testdata/resource-min.tf b/stackit/internal/services/automation/testdata/resource-min.tf
new file mode 100644
index 000000000..2f46d382f
--- /dev/null
+++ b/stackit/internal/services/automation/testdata/resource-min.tf
@@ -0,0 +1,17 @@
+variable "project_id" {}
+variable "template_id" {}
+variable "name" {}
+variable "rrule" {}
+variable "input" {}
+
+resource "stackit_volume_automation" "test" {
+ project_id = var.project_id
+ template_id = var.template_id
+ name = var.name
+ input = var.input
+ triggers = {
+ schedule = {
+ rrule = var.rrule
+ }
+ }
+}
diff --git a/stackit/internal/services/automation/utils/util.go b/stackit/internal/services/automation/utils/util.go
index aca66c6df..96f9392b4 100644
--- a/stackit/internal/services/automation/utils/util.go
+++ b/stackit/internal/services/automation/utils/util.go
@@ -10,7 +10,7 @@ import (
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/stackit-sdk-go/core/config"
- automation "github.com/stackitcloud/stackit-sdk-go/services/automation/v1betaapi"
+ automation "github.com/stackitcloud/stackit-sdk-go/services/automation/v1api"
)
func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *automation.APIClient {
diff --git a/stackit/internal/services/automation/utils/util_test.go b/stackit/internal/services/automation/utils/util_test.go
index 8dc2a490d..70cf1f487 100644
--- a/stackit/internal/services/automation/utils/util_test.go
+++ b/stackit/internal/services/automation/utils/util_test.go
@@ -10,7 +10,7 @@ import (
sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients"
"github.com/stackitcloud/stackit-sdk-go/core/config"
- automation "github.com/stackitcloud/stackit-sdk-go/services/automation/v1betaapi"
+ automation "github.com/stackitcloud/stackit-sdk-go/services/automation/v1api"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
diff --git a/stackit/internal/services/automation/volume/datasource.go b/stackit/internal/services/automation/volume/datasource.go
new file mode 100644
index 000000000..4df27ff92
--- /dev/null
+++ b/stackit/internal/services/automation/volume/datasource.go
@@ -0,0 +1,183 @@
+package volume
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+
+ automation "github.com/stackitcloud/stackit-sdk-go/services/automation/v1api"
+
+ "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"
+ automationUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/automation/utils"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+ _ datasource.DataSource = &volumeAutomationDataSource{}
+)
+
+// NewVolumeAutomationDataSource is a helper function to simplify the provider implementation.
+func NewVolumeAutomationDataSource() datasource.DataSource {
+ return &volumeAutomationDataSource{}
+}
+
+// volumeAutomationDataSource is the data source implementation.
+type volumeAutomationDataSource struct {
+ client *automation.APIClient
+ providerData core.ProviderData
+}
+
+// Metadata returns the data source type name.
+func (d *volumeAutomationDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_volume_automation"
+}
+
+// Configure adds the provider configured client to the data source.
+func (d *volumeAutomationDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
+ if !ok {
+ return
+ }
+
+ features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_volume_automation", core.Datasource)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiClient := automationUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ d.providerData = providerData
+ d.client = apiClient
+ tflog.Info(ctx, "Volume automation client configured.")
+}
+
+// Schema defines the schema for the data source.
+func (d *volumeAutomationDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: features.AddBetaDescription("Volume automation datasource schema. Must have a `region` specified in the provider configuration.", core.Datasource),
+ Description: "Volume automation datasource schema. Must have a `region` specified in the provider configuration.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: descriptions["id"],
+ Computed: true,
+ },
+ "project_id": schema.StringAttribute{
+ Description: descriptions["project_id"],
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "region": schema.StringAttribute{
+ // the region cannot be looked up, so it has to be passed
+ Optional: true,
+ Description: descriptions["region"],
+ },
+ "automation_id": schema.StringAttribute{
+ Description: descriptions["automation_id"],
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "template_id": schema.StringAttribute{
+ Description: descriptions["template_id"],
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ Description: descriptions["name"],
+ Computed: true,
+ },
+ "description": schema.StringAttribute{
+ Description: descriptions["description"],
+ Computed: true,
+ },
+ "input": schema.StringAttribute{
+ CustomType: jsontypes.NormalizedType{},
+ Description: descriptions["input"],
+ Computed: true,
+ },
+ "triggers": schema.SingleNestedAttribute{
+ Description: descriptions["triggers"],
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "schedule": schema.SingleNestedAttribute{
+ Description: descriptions["schedule"],
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "rrule": schema.StringAttribute{
+ Description: descriptions["rrule"],
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (d *volumeAutomationDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
+ var model Model
+ diags := req.Config.Get(ctx, &model)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ ctx = core.InitProviderContext(ctx)
+
+ projectId := model.ProjectId.ValueString()
+ automationId := model.AutomationId.ValueString()
+ region := d.providerData.GetRegionWithOverride(model.Region)
+
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "automation_id", automationId)
+ ctx = tflog.SetField(ctx, "region", region)
+
+ automationResp, err := d.client.DefaultAPI.GetVolumeAutomation(ctx, projectId, region, automationId).Execute()
+ if err != nil {
+ utils.LogError(
+ ctx,
+ &resp.Diagnostics,
+ err,
+ "Reading volume automation",
+ fmt.Sprintf("Volume automation with ID %q does not exist in project %q.", automationId, projectId),
+ map[int]string{
+ http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId),
+ },
+ )
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ err = mapFields(ctx, automationResp, &model, region)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume automation", 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, "Volume automation read.")
+}
diff --git a/stackit/internal/services/automation/volume/resource.go b/stackit/internal/services/automation/volume/resource.go
new file mode 100644
index 000000000..ddce150a6
--- /dev/null
+++ b/stackit/internal/services/automation/volume/resource.go
@@ -0,0 +1,566 @@
+package volume
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "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-log/tflog"
+
+ "github.com/stackitcloud/stackit-sdk-go/core/oapierror"
+ automation "github.com/stackitcloud/stackit-sdk-go/services/automation/v1api"
+
+ "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"
+ automationUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/automation/utils"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+ _ resource.Resource = &volumeAutomationResource{}
+ _ resource.ResourceWithConfigure = &volumeAutomationResource{}
+ _ resource.ResourceWithImportState = &volumeAutomationResource{}
+ _ resource.ResourceWithModifyPlan = &volumeAutomationResource{}
+)
+
+// Model represents the schema for the stackit_volume_automation resource and datasource.
+type Model struct {
+ ID types.String `tfsdk:"id"`
+ ProjectId types.String `tfsdk:"project_id"`
+ Region types.String `tfsdk:"region"`
+ TemplateId types.String `tfsdk:"template_id"`
+ AutomationId types.String `tfsdk:"automation_id"`
+ Name types.String `tfsdk:"name"`
+ Description types.String `tfsdk:"description"`
+ Input jsontypes.Normalized `tfsdk:"input"`
+ Triggers *triggersModel `tfsdk:"triggers"`
+}
+
+type triggersModel struct {
+ Schedule *scheduleTriggerModel `tfsdk:"schedule"`
+}
+
+type scheduleTriggerModel struct {
+ Rrule types.String `tfsdk:"rrule"`
+}
+
+// descriptions for the attributes in the Schema
+var descriptions = map[string]string{
+ "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`automation_id`\".",
+ "project_id": "STACKIT Project ID to which the volume automation is associated.",
+ "region": "The resource region. If not defined, the provider region is used.",
+ "template_id": "ID of the automation template this volume automation is based on.",
+ "automation_id": "ID of the volume automation.",
+ "name": "The volume automation name.",
+ "description": "The volume automation description.",
+ "input": "Configuration input for the volume automation. See [API Docs](https://docs.api.stackit.cloud/documentation/automation-service/version/v1#tag/Volume-Automations/operation/CreateVolumeAutomation) for possible configuration options.",
+ "volume_recovery_point_management": "Configuration for automated volume recovery point (snapshot) management.",
+ "inherit_volume_labels": "Whether recovery points inherit the labels of the volume they were created from. Defaults to `false`.",
+ "recovery_point_labels": "Labels to attach to created recovery points.",
+ "volume_label_selector": "Label selector used to select the volumes this automation applies to.",
+ "snapshot_retention_policy": "Defines how long created recovery points (snapshots) are retained.",
+ "snapshot_retention_policy_kind": "The retention policy kind. Valid values are: `count`, `indefinitely`.",
+ "snapshot_retention_policy_value": "Number of recovery points to retain. Required if `kind` is `count`, must not be set otherwise.",
+ "triggers": "Triggers that determine when the automation runs.",
+ "schedule": "Runs the automation on a recurring schedule.",
+ "rrule": "An `rrule` (Recurrence Rule) is a standardized string format used in iCalendar (RFC 5545) to define repeating events, and you can generate one by using a dedicated library or by using online generator tools to specify parameters like frequency, interval, and end dates.",
+}
+
+// NewVolumeAutomationResource is a helper function to simplify the provider implementation.
+func NewVolumeAutomationResource() resource.Resource {
+ return &volumeAutomationResource{}
+}
+
+// volumeAutomationResource is the resource implementation.
+type volumeAutomationResource struct {
+ client *automation.APIClient
+ providerData core.ProviderData
+}
+
+// ModifyPlan implements resource.ResourceWithModifyPlan.
+// Use the modifier to set the effective region in the current plan.
+func (r *volumeAutomationResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
+ var configModel Model
+ // skip initial empty configuration to avoid follow-up errors
+ if req.Config.Raw.IsNull() {
+ return
+ }
+ resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var planModel Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+}
+
+// Metadata returns the resource type name.
+func (r *volumeAutomationResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_volume_automation"
+}
+
+// Configure adds the provider configured client to the resource.
+func (r *volumeAutomationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
+ if !ok {
+ return
+ }
+
+ features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_volume_automation", core.Resource)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiClient := automationUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ r.providerData = providerData
+ r.client = apiClient
+ tflog.Info(ctx, "Volume automation client configured.")
+}
+
+// Schema defines the schema for the resource.
+func (r *volumeAutomationResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: features.AddBetaDescription("Volume automation resource schema. Must have a `region` specified in the provider configuration.", core.Resource),
+ Description: "Volume automation resource schema. Must have a `region` specified in the provider configuration.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: descriptions["id"],
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "project_id": schema.StringAttribute{
+ Description: descriptions["project_id"],
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "region": schema.StringAttribute{
+ Optional: true,
+ // must be computed to allow for storing the override value from the provider
+ Computed: true,
+ Description: descriptions["region"],
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "template_id": schema.StringAttribute{
+ Description: descriptions["template_id"],
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "automation_id": schema.StringAttribute{
+ Description: descriptions["automation_id"],
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "name": schema.StringAttribute{
+ Description: descriptions["name"],
+ Optional: true,
+ },
+ "description": schema.StringAttribute{
+ Description: descriptions["description"],
+ Optional: true,
+ },
+ "input": schema.StringAttribute{
+ CustomType: jsontypes.NormalizedType{},
+ Description: descriptions["input"],
+ Optional: true,
+ },
+ "triggers": schema.SingleNestedAttribute{
+ Description: descriptions["triggers"],
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "schedule": schema.SingleNestedAttribute{
+ Description: descriptions["schedule"],
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "rrule": schema.StringAttribute{
+ Description: descriptions["rrule"],
+ Required: true,
+ Validators: []validator.String{
+ validate.Rrule(),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+// Create creates the resource and sets the initial Terraform state.
+func (r *volumeAutomationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
+ var model Model
+ diags := req.Plan.Get(ctx, &model)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ ctx = core.InitProviderContext(ctx)
+
+ projectId := model.ProjectId.ValueString()
+ region := r.providerData.GetRegionWithOverride(model.Region)
+
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "region", region)
+
+ payload, err := toCreatePayload(&model)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume automation", fmt.Sprintf("Creating API payload: %v", err))
+ return
+ }
+
+ automationResp, err := r.client.DefaultAPI.CreateVolumeAutomation(ctx, projectId, region).CreateVolumeAutomationPayload(*payload).Execute()
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume automation", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+ ctx = tflog.SetField(ctx, "automation_id", automationResp.Id)
+
+ err = mapFields(ctx, automationResp, &model, region)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume automation", 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, "Volume automation created.")
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (r *volumeAutomationResource) 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
+ }
+
+ ctx = core.InitProviderContext(ctx)
+
+ projectId := model.ProjectId.ValueString()
+ automationId := model.AutomationId.ValueString()
+ region := r.providerData.GetRegionWithOverride(model.Region)
+
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "automation_id", automationId)
+ ctx = tflog.SetField(ctx, "region", region)
+
+ automationResp, err := r.client.DefaultAPI.GetVolumeAutomation(ctx, projectId, region, automationId).Execute()
+ if err != nil {
+ if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && oapiErr.StatusCode == http.StatusNotFound {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume automation", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ err = mapFields(ctx, automationResp, &model, region)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume automation", 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, "Volume automation read.")
+}
+
+// Update updates the resource and sets the updated Terraform state on success.
+func (r *volumeAutomationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
+ var plan Model
+ diags := req.Plan.Get(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var state Model
+ diags = req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ ctx = core.InitProviderContext(ctx)
+
+ projectId := plan.ProjectId.ValueString()
+ automationId := state.AutomationId.ValueString()
+ region := r.providerData.GetRegionWithOverride(plan.Region)
+
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "automation_id", automationId)
+ ctx = tflog.SetField(ctx, "region", region)
+
+ payload, err := toUpdatePayload(&plan)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume automation", fmt.Sprintf("Creating API payload: %v", err))
+ return
+ }
+
+ // Workaround: The input field is an open object where we don't know all keys. If some input fields where removed,
+ // we can't set them here to null. For this reason we do one update with updateMask "input", to overwrite the whole input object.
+ // Setting it to '*' would cause issue when the API gets new fields in the future and is therefore no option.
+ _, err = r.client.DefaultAPI.PartialUpdateVolumeAutomation(ctx, projectId, region, automationId).
+ PartialUpdateVolumeAutomationPayload(*payload).
+ UpdateMask("input").
+ Execute()
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume automation", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ // Workaround: Updates all other fields accordingly, which were not already update with the previous update.
+ automationResp, err := r.client.DefaultAPI.PartialUpdateVolumeAutomation(ctx, projectId, region, automationId).
+ PartialUpdateVolumeAutomationPayload(*payload).
+ Execute()
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume automation", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ err = mapFields(ctx, automationResp, &plan, region)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume automation", fmt.Sprintf("Processing API payload: %v", err))
+ return
+ }
+ diags = resp.State.Set(ctx, plan)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ tflog.Info(ctx, "Volume automation updated.")
+}
+
+// Delete deletes the resource and removes the Terraform state on success.
+func (r *volumeAutomationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // 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
+ }
+
+ ctx = core.InitProviderContext(ctx)
+
+ projectId := model.ProjectId.ValueString()
+ automationId := model.AutomationId.ValueString()
+ region := r.providerData.GetRegionWithOverride(model.Region)
+
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "automation_id", automationId)
+ ctx = tflog.SetField(ctx, "region", region)
+
+ err := r.client.DefaultAPI.DeleteVolumeAutomation(ctx, projectId, region, automationId).Execute()
+ if err != nil {
+ if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && oapiErr.StatusCode == http.StatusNotFound {
+ return
+ }
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting volume automation", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ tflog.Info(ctx, "Volume automation deleted.")
+}
+
+// ImportState imports a resource into the Terraform state on success.
+// The expected format of the resource import identifier is: project_id,region,automation_id
+func (r *volumeAutomationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ idParts := strings.Split(req.ID, core.Separator)
+ if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
+ core.LogAndAddError(ctx, &resp.Diagnostics,
+ "Error importing volume automation",
+ fmt.Sprintf("Expected import identifier with format [project_id],[region],[automation_id], got %q", req.ID),
+ )
+ return
+ }
+
+ ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
+ "project_id": idParts[0],
+ "region": idParts[1],
+ "automation_id": idParts[2],
+ })
+
+ tflog.Info(ctx, "Volume automation state imported.")
+}
+
+// mapFields maps a VolumeAutomation API response to the model.
+func mapFields(_ context.Context, apiResp *automation.VolumeAutomation, model *Model, region string) error {
+ if apiResp == nil {
+ return fmt.Errorf("response input is nil")
+ }
+ if model == nil {
+ return fmt.Errorf("model input is nil")
+ }
+
+ model.AutomationId = types.StringValue(apiResp.Id)
+ model.ID = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, apiResp.Id)
+ model.Region = types.StringValue(region)
+
+ model.TemplateId = types.StringPointerValue(apiResp.TemplateId)
+ model.Name = types.StringPointerValue(apiResp.Name)
+ model.Description = types.StringPointerValue(apiResp.Description)
+
+ var inputString *string
+ if apiResp.Input.Get() != nil {
+ inputJson, err := apiResp.Input.MarshalJSON()
+ if err != nil {
+ return fmt.Errorf("error marshaling input field: %w", err)
+ }
+ inputString = new(string(inputJson))
+ }
+ model.Input = jsontypes.NewNormalizedPointerValue(inputString)
+
+ model.Triggers = mapTriggers(apiResp.Triggers)
+
+ return nil
+}
+
+func mapTriggers(triggers automation.NullableAutomationTriggers) *triggersModel {
+ if triggers.Get() == nil {
+ return nil
+ }
+ if triggers.Get().Schedule.Get() == nil {
+ return &triggersModel{
+ Schedule: nil,
+ }
+ }
+ return &triggersModel{
+ Schedule: &scheduleTriggerModel{
+ Rrule: types.StringValue(triggers.Get().Schedule.Get().Rrule),
+ },
+ }
+}
+
+func toCreatePayload(model *Model) (*automation.CreateVolumeAutomationPayload, error) {
+ if model == nil {
+ return nil, fmt.Errorf("nil model")
+ }
+
+ input, err := toInputPayload(model.Input)
+ if err != nil {
+ return nil, err
+ }
+
+ return &automation.CreateVolumeAutomationPayload{
+ TemplateId: model.TemplateId.ValueString(),
+ Name: *automation.NewNullableString(conversion.StringValueToPointer(model.Name)),
+ Description: *automation.NewNullableString(conversion.StringValueToPointer(model.Description)),
+ Input: *input,
+ Triggers: *toTriggersPayload(model),
+ }, nil
+}
+
+func toUpdatePayload(plan *Model) (*automation.PartialUpdateVolumeAutomationPayload, error) {
+ if plan == nil {
+ return nil, fmt.Errorf("nil plan model")
+ }
+
+ input, err := toInputPayload(plan.Input)
+ if err != nil {
+ return nil, err
+ }
+
+ return &automation.PartialUpdateVolumeAutomationPayload{
+ Name: *automation.NewNullableString(conversion.StringValueToPointer(plan.Name)),
+ Description: *automation.NewNullableString(conversion.StringValueToPointer(plan.Description)),
+ Input: *input,
+ Triggers: *toTriggersPayload(plan),
+ }, nil
+}
+
+func toInputPayload(modelInput jsontypes.Normalized) (*automation.NullableVolumeAutomationInput, error) {
+ if utils.IsUndefined(modelInput) {
+ return automation.NewNullableVolumeAutomationInput(nil), nil
+ }
+
+ inputJson := modelInput.ValueString()
+ inputPayload := &automation.NullableVolumeAutomationInput{}
+
+ err := json.Unmarshal([]byte(inputJson), inputPayload)
+ if err != nil {
+ return nil, fmt.Errorf("unmarshaling input payload: %w", err)
+ }
+ return inputPayload, nil
+}
+
+func toTriggersPayload(model *Model) *automation.NullableAutomationTriggers {
+ if model == nil || model.Triggers == nil {
+ return automation.NewNullableAutomationTriggers(nil)
+ }
+ if model.Triggers.Schedule == nil || utils.IsUndefined(model.Triggers.Schedule.Rrule) {
+ return automation.NewNullableAutomationTriggers(&automation.AutomationTriggers{
+ Schedule: *automation.NewNullableAutomationScheduleTrigger(nil),
+ })
+ }
+
+ return automation.NewNullableAutomationTriggers(&automation.AutomationTriggers{
+ Schedule: *automation.NewNullableAutomationScheduleTrigger(&automation.AutomationScheduleTrigger{
+ Rrule: model.Triggers.Schedule.Rrule.ValueString(),
+ }),
+ })
+}
diff --git a/stackit/internal/services/automation/volume/resource_test.go b/stackit/internal/services/automation/volume/resource_test.go
new file mode 100644
index 000000000..c36a47a10
--- /dev/null
+++ b/stackit/internal/services/automation/volume/resource_test.go
@@ -0,0 +1,336 @@
+package volume
+
+import (
+ "context"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+
+ automation "github.com/stackitcloud/stackit-sdk-go/services/automation/v1api"
+)
+
+func TestMapFields(t *testing.T) {
+ tests := []struct {
+ description string
+ input *automation.VolumeAutomation
+ expected Model
+ isValid bool
+ }{
+ {
+ "nil_response",
+ nil,
+ Model{},
+ false,
+ },
+ {
+ "default_values",
+ &automation.VolumeAutomation{
+ Id: "automation_uid",
+ },
+ Model{
+ ID: types.StringValue("project_uid,eu01,automation_uid"),
+ ProjectId: types.StringValue("project_uid"),
+ Region: types.StringValue("eu01"),
+ AutomationId: types.StringValue("automation_uid"),
+ Name: types.StringNull(),
+ Description: types.StringNull(),
+ },
+ true,
+ },
+ {
+ "empty_strings_for_name_and_description",
+ &automation.VolumeAutomation{
+ Id: "automation_uid",
+ Name: new(""),
+ Description: new(""),
+ Input: *automation.NewNullableVolumeAutomationInput(&automation.VolumeAutomationInput{
+ Kind: "VolumeRecoveryPointManagementInput",
+ AdditionalProperties: map[string]interface{}{
+ "volumeLabelSelector": "",
+ "snapshotRetentionPolicy": map[string]interface{}{
+ "kind": "indefinitely",
+ },
+ },
+ }),
+ },
+ Model{
+ ID: types.StringValue("project_uid,eu01,automation_uid"),
+ ProjectId: types.StringValue("project_uid"),
+ Region: types.StringValue("eu01"),
+ AutomationId: types.StringValue("automation_uid"),
+ Name: types.StringValue(""),
+ Description: types.StringValue(""),
+ Input: jsontypes.NewNormalizedValue("{\"kind\":\"VolumeRecoveryPointManagementInput\",\"snapshotRetentionPolicy\":{\"kind\":\"indefinitely\"},\"volumeLabelSelector\":\"\"}"),
+ },
+ true,
+ },
+ {
+ "full_values_count",
+ &automation.VolumeAutomation{
+ Id: "automation_uid",
+ TemplateId: new("template_uid"),
+ Name: new("name1"),
+ Description: new("desc1"),
+ Input: *automation.NewNullableVolumeAutomationInput(&automation.VolumeAutomationInput{
+ Kind: "VolumeRecoveryPointManagement",
+ AdditionalProperties: map[string]interface{}{
+ "inheritVolumeLabels": true,
+ "recoveryPointLabels": map[string]string{
+ "k": "v",
+ },
+ "snapshotRetentionPolicy": map[string]interface{}{
+ "kind": "count",
+ "value": 7,
+ },
+ "volumeLabelSelector": "sel",
+ },
+ }),
+ Triggers: *automation.NewNullableAutomationTriggers(&automation.AutomationTriggers{
+ Schedule: *automation.NewNullableAutomationScheduleTrigger(&automation.AutomationScheduleTrigger{
+ Rrule: "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1",
+ }),
+ }),
+ },
+ Model{
+ ID: types.StringValue("project_uid,eu01,automation_uid"),
+ ProjectId: types.StringValue("project_uid"),
+ Region: types.StringValue("eu01"),
+ AutomationId: types.StringValue("automation_uid"),
+ TemplateId: types.StringValue("template_uid"),
+ Name: types.StringValue("name1"),
+ Description: types.StringValue("desc1"),
+ Input: jsontypes.NewNormalizedValue("{\"inheritVolumeLabels\":true,\"kind\":\"VolumeRecoveryPointManagement\",\"recoveryPointLabels\":{\"k\":\"v\"},\"snapshotRetentionPolicy\":{\"kind\":\"count\",\"value\":7},\"volumeLabelSelector\":\"sel\"}"),
+ Triggers: &triggersModel{
+ Schedule: &scheduleTriggerModel{
+ Rrule: types.StringValue("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"),
+ },
+ },
+ },
+ true,
+ },
+ {
+ "full_values_indefinitely",
+ &automation.VolumeAutomation{
+ Id: "automation_uid",
+ Input: *automation.NewNullableVolumeAutomationInput(&automation.VolumeAutomationInput{
+ Kind: "VolumeRecoveryPointManagement",
+ AdditionalProperties: map[string]interface{}{
+ "snapshotRetentionPolicy": map[string]interface{}{
+ "kind": "indefinitely",
+ },
+ },
+ }),
+ },
+ Model{
+ ID: types.StringValue("project_uid,eu01,automation_uid"),
+ ProjectId: types.StringValue("project_uid"),
+ Region: types.StringValue("eu01"),
+ AutomationId: types.StringValue("automation_uid"),
+ Name: types.StringNull(),
+ Description: types.StringNull(),
+ Input: jsontypes.NewNormalizedValue("{\"kind\":\"VolumeRecoveryPointManagement\",\"snapshotRetentionPolicy\":{\"kind\":\"indefinitely\"}}"),
+ },
+ true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ state := &Model{
+ ProjectId: tt.expected.ProjectId,
+ }
+ ctx := context.TODO()
+ err := mapFields(ctx, tt.input, state, "eu01")
+ 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(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 *automation.CreateVolumeAutomationPayload
+ isValid bool
+ }{
+ {
+ "nil_model",
+ nil,
+ nil,
+ false,
+ },
+ {
+ "minimal",
+ &Model{
+ TemplateId: types.StringValue("template_uid"),
+ },
+ &automation.CreateVolumeAutomationPayload{
+ TemplateId: "template_uid",
+ Name: *automation.NewNullableString(nil),
+ Description: *automation.NewNullableString(nil),
+ Input: *automation.NewNullableVolumeAutomationInput(nil),
+ Triggers: *automation.NewNullableAutomationTriggers(nil),
+ },
+ true,
+ },
+ {
+ "full_values_count",
+ &Model{
+ TemplateId: types.StringValue("template_uid"),
+ Name: types.StringValue("name1"),
+ Description: types.StringValue("desc1"),
+ Input: jsontypes.NewNormalizedValue("{\"inheritVolumeLabels\":true,\"kind\":\"VolumeRecoveryPointManagement\",\"recoveryPointLabels\":{\"k\":\"v\"},\"snapshotRetentionPolicy\":{\"kind\":\"count\",\"value\":7},\"volumeLabelSelector\":\"sel\"}"),
+ Triggers: &triggersModel{
+ Schedule: &scheduleTriggerModel{
+ Rrule: types.StringValue("RRULE"),
+ },
+ },
+ },
+ &automation.CreateVolumeAutomationPayload{
+ TemplateId: "template_uid",
+ Name: *automation.NewNullableString(new("name1")),
+ Description: *automation.NewNullableString(new("desc1")),
+ Input: *automation.NewNullableVolumeAutomationInput(&automation.VolumeAutomationInput{
+ Kind: "VolumeRecoveryPointManagement",
+ AdditionalProperties: map[string]interface{}{
+ "inheritVolumeLabels": true,
+ "recoveryPointLabels": map[string]interface{}{
+ "k": "v",
+ },
+ "snapshotRetentionPolicy": map[string]interface{}{
+ "kind": "count",
+ "value": float64(7),
+ },
+ "volumeLabelSelector": "sel",
+ },
+ }),
+ Triggers: *automation.NewNullableAutomationTriggers(&automation.AutomationTriggers{
+ Schedule: *automation.NewNullableAutomationScheduleTrigger(&automation.AutomationScheduleTrigger{
+ Rrule: "RRULE",
+ }),
+ }),
+ },
+ true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ output, err := toCreatePayload(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 {
+ cmpOpts := cmp.Options{
+ cmp.AllowUnexported(
+ automation.NullableVolumeAutomationInput{},
+ automation.NullableAutomationTriggers{},
+ automation.NullableAutomationScheduleTrigger{},
+ automation.NullableString{},
+ ),
+ }
+ diff := cmp.Diff(tt.expected, output, cmpOpts)
+ if diff != "" {
+ t.Fatalf("Data does not match: %s", diff)
+ }
+ }
+ })
+ }
+}
+
+func TestToUpdatePayload(t *testing.T) {
+ tests := []struct {
+ description string
+ plan *Model
+ expected *automation.PartialUpdateVolumeAutomationPayload
+ isValid bool
+ }{
+ {
+ "nil_plan",
+ nil,
+ nil,
+ false,
+ },
+ {
+ "full_values",
+ &Model{
+ Name: types.StringValue("n"),
+ Description: types.StringValue("d"),
+ Input: jsontypes.NewNormalizedValue("{\"kind\":\"VolumeRecoveryPointManagement\",\"snapshotRetentionPolicy\":{\"kind\":\"count\",\"value\":3}}\n"),
+ Triggers: &triggersModel{
+ Schedule: &scheduleTriggerModel{Rrule: types.StringValue("RRULE")},
+ },
+ },
+ &automation.PartialUpdateVolumeAutomationPayload{
+ Name: *automation.NewNullableString(new("n")),
+ Description: *automation.NewNullableString(new("d")),
+ Input: *automation.NewNullableVolumeAutomationInput(&automation.VolumeAutomationInput{
+ Kind: "VolumeRecoveryPointManagement",
+ AdditionalProperties: map[string]interface{}{
+ "snapshotRetentionPolicy": map[string]interface{}{
+ "kind": "count",
+ "value": float64(3),
+ },
+ },
+ }),
+ Triggers: *automation.NewNullableAutomationTriggers(&automation.AutomationTriggers{
+ Schedule: *automation.NewNullableAutomationScheduleTrigger(&automation.AutomationScheduleTrigger{Rrule: "RRULE"}),
+ }),
+ },
+ true,
+ },
+ {
+ "clears_optional_fields",
+ &Model{
+ Name: types.StringNull(),
+ Description: types.StringNull(),
+ },
+ &automation.PartialUpdateVolumeAutomationPayload{
+ Name: *automation.NewNullableString(nil),
+ Description: *automation.NewNullableString(nil),
+ Input: *automation.NewNullableVolumeAutomationInput(nil),
+ Triggers: *automation.NewNullableAutomationTriggers(nil),
+ },
+ true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ output, err := toUpdatePayload(tt.plan)
+ 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 {
+ cmpOpts := cmp.Options{
+ cmp.AllowUnexported(
+ automation.NullableVolumeAutomationInput{},
+ automation.NullableAutomationTriggers{},
+ automation.NullableAutomationScheduleTrigger{},
+ automation.NullableString{},
+ ),
+ }
+ diff := cmp.Diff(output, tt.expected, cmpOpts)
+ if diff != "" {
+ t.Fatalf("Data does not match: %s", diff)
+ }
+ }
+ })
+ }
+}
diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go
index bcf1eda62..9328820ea 100644
--- a/stackit/internal/testutil/testutil.go
+++ b/stackit/internal/testutil/testutil.go
@@ -90,6 +90,7 @@ var (
ModelServingCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_MODELSERVING_CUSTOM_ENDPOINT", providerName: "modelserving_custom_endpoint"}
ModelExperimentsCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_MODELEXPERIMENTS_CUSTOM_ENDPOINT", providerName: "modelexperiments_custom_endpoint"}
AuthorizationCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_AUTHORIZATION_CUSTOM_ENDPOINT", providerName: "authorization_custom_endpoint"}
+ AutomationCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_AUTOMATION_CUSTOM_ENDPOINT", providerName: "automation_custom_endpoint"}
MongoDBFlexCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_MONGODBFLEX_CUSTOM_ENDPOINT", providerName: "mongodbflex_custom_endpoint"}
OpenSearchCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_OPENSEARCH_CUSTOM_ENDPOINT", providerName: "opensearch_custom_endpoint"}
ObservabilityCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_OBSERVABILITY_CUSTOM_ENDPOINT", providerName: "observability_custom_endpoint"}
@@ -131,6 +132,7 @@ var (
ModelServingCustomEndpoint,
ModelExperimentsCustomEndpoint,
AuthorizationCustomEndpoint,
+ AutomationCustomEndpoint,
MongoDBFlexCustomEndpoint,
OpenSearchCustomEndpoint,
ObservabilityCustomEndpoint,
diff --git a/stackit/provider.go b/stackit/provider.go
index 59d1cee5d..c3ec02597 100644
--- a/stackit/provider.go
+++ b/stackit/provider.go
@@ -27,6 +27,7 @@ import (
customRole "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/customrole"
roleAssignements "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/roleassignments"
automationTemplates "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/automation/templates"
+ automationVolume "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/automation/volume"
cdnCustomDomain "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/customdomain"
cdn "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/distribution"
dnsRecordSet "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/recordset"
@@ -703,6 +704,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
albWafManagedRuleSet.NewManagedRuleSetDataSource,
alertGroup.NewAlertGroupDataSource,
automationTemplates.NewAutomationTemplatesDataSource,
+ automationVolume.NewVolumeAutomationDataSource,
cdn.NewDistributionDataSource,
cert.NewCertificatesDataSource,
cdnCustomDomain.NewCustomDomainDataSource,
@@ -825,6 +827,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
albWaf.NewWafConfigurationResource,
albWafManagedRuleSet.NewManagedRuleSetResource,
alertGroup.NewAlertGroupResource,
+ automationVolume.NewVolumeAutomationResource,
cdn.NewDistributionResource,
cert.NewCertificatesResource,
cdnCustomDomain.NewCustomDomainResource,