diff --git a/MIGRATION.md b/MIGRATION.md
index 9f3a3e087..d120af593 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -128,39 +128,41 @@ resource "stackit_logme_credential" "example-credential" {
## Available resources
-| Community provider | Official provider | Import available? | `id` format | Notes |
-|------------------------------------------|-----------------------------------------|-|-|------------------------------------------------------------------|
-| stackit_argus_credential | stackit_observability_credential | :x: | | Service deprecated, use stackit_observability_credential instead |
-| stackit_argus_instance | stackit_observability_instance | :white_check_mark: | [project_id],[instance_id] | Service deprecated, use stackit_observability_instance instead |
-| stackit_argus_job | stackit_observability_scrapeconfig | :white_check_mark: | [project_id],[instance_id],[name] | Service deprecated, use stackit_observability_scrapeconfig instead |
-| stackit_elasticsearch_credential | | | | Service deprecated |
-| stackit_elasticsearch_instance | | | | Service deprecated |
-| stackit_kubernetes_cluster | stackit_ske_cluster | :white_check_mark: | [project_id],[name] | |
-| stackit_kubernetes_project | stackit_ske_project | :white_check_mark: | [project_id] | |
-| stackit_load_balancer | stackit_loadbalancer | :white_check_mark: | [project_id],[name] | |
-| stackit_logme_credential | stackit_logme_credential | :white_check_mark: | [project_id],[instance_id],[credential_id] | |
-| stackit_logme_instance | stackit_logme_instance | :white_check_mark: | [project_id],[instance_id] | |
-| stackit_mariadb_credential | stackit_mariadb_credential | :white_check_mark: | [project_id],[instance_id],[credential_id] | |
-| stackit_mariadb_instance | stackit_mariadb_instance | :white_check_mark: | [project_id],[instance_id] | |
-| stackit_mongodb_flex_instance | stackit_mongodbflex_instance | :white_check_mark: | [project_id],[instance_id] | |
-| stackit_mongodb_flex_user | stackit_mongodbflex_user | :warning: | [project_id],[instance_id],[user_id] | `password` field will be empty |
-| stackit_object_storage_bucket | stackit_objectstorage_bucket | :white_check_mark: | [project_id],[name] | |
+| Community provider | Official provider | Import available? | `id` format | Notes |
+|------------------------------------------|-----------------------------------------|-|-----------------------------------------------------|------------------------------------------------------------------|
+| stackit_argus_credential | stackit_observability_credential | :x: | | Service deprecated, use stackit_observability_credential instead |
+| stackit_argus_instance | stackit_observability_instance | :white_check_mark: | [project_id],[instance_id] | Service deprecated, use stackit_observability_instance instead |
+| stackit_argus_job | stackit_observability_scrapeconfig | :white_check_mark: | [project_id],[instance_id],[name] | Service deprecated, use stackit_observability_scrapeconfig instead |
+| stackit_elasticsearch_credential | | | | Service deprecated |
+| stackit_elasticsearch_instance | | | | Service deprecated |
+| stackit_kubernetes_cluster | stackit_ske_cluster | :white_check_mark: | [project_id],[name] | |
+| stackit_kubernetes_project | stackit_ske_project | :white_check_mark: | [project_id] | |
+| stackit_load_balancer | stackit_loadbalancer | :white_check_mark: | [project_id],[name] | |
+| stackit_logme_credential | stackit_logme_credential | :white_check_mark: | [project_id],[instance_id],[credential_id] | |
+| stackit_logme_instance | stackit_logme_instance | :white_check_mark: | [project_id],[instance_id] | |
+| stackit_mariadb_credential | stackit_mariadb_credential | :white_check_mark: | [project_id],[instance_id],[credential_id] | |
+| stackit_mariadb_instance | stackit_mariadb_instance | :white_check_mark: | [project_id],[instance_id] | |
+| stackit_mongodb_flex_instance | stackit_mongodbflex_instance | :white_check_mark: | [project_id],[instance_id] | |
+| stackit_mongodb_flex_user | stackit_mongodbflex_user | :warning: | [project_id],[instance_id],[user_id] | `password` field will be empty |
+| stackit_object_storage_bucket | stackit_objectstorage_bucket | :white_check_mark: | [project_id],[name] | |
| stackit_object_storage_credential | stackit_objectstorage_credential | :white_check_mark: | [project_id],[credentials_group_id],[credential_id] | |
-| stackit_object_storage_credentials_group | stackit_objectstorage_credentials_group | :white_check_mark: | [project_id],[credentials_group_id] | |
-| stackit_object_storage_project | | | | Resource deprecated |
-| stackit_observability_credential | stackit_observability_credential | :x: | | |
-| stackit_observability_instance | stackit_observability_instance | :white_check_mark: | [project_id],[instance_id] | |
-| stackit_observability_job | stackit_observability_scrapeconfig | :white_check_mark: | [project_id],[instance_id],[name] | |
+| stackit_object_storage_credentials_group | stackit_objectstorage_credentials_group | :white_check_mark: | [project_id],[credentials_group_id] | |
+| stackit_object_storage_project | | | | Resource deprecated |
+| stackit_observability_credential | stackit_observability_credential | :x: | | |
+| stackit_observability_instance | stackit_observability_instance | :white_check_mark: | [project_id],[instance_id] | |
+| stackit_observability_job | stackit_observability_scrapeconfig | :white_check_mark: | [project_id],[instance_id],[name] | |
| stackit_opensearch_credential | stackit_opensearch_credential | :white_check_mark: | [project_id],[credentials_group_id],[credential_id] | |
-| stackit_opensearch_instance | stackit_opensearch_instance | :white_check_mark: | [project_id],[instance_id] | | |
-| stackit_postgres_flex_instance | stackit_postgresflex_instance | :white_check_mark: | [project_id],[instance_id] | |
-| stackit_postgres_flex_user | stackit_postgresflex_user | :warning: | [project_id],[instance_id],[user_id] | `password` field will be empty |
-| stackit_postgres_instance | | | | Resource deprecated |
-| stackit_postgres_credential | | | | Resource deprecated
-| stackit_project | stackit_resourcemanager_project | :white_check_mark: | [container_id] | |
+| stackit_opensearch_instance | stackit_opensearch_instance | :white_check_mark: | [project_id],[instance_id] | | |
+| stackit_postgres_flex_instance | stackit_postgresflex_instance | :white_check_mark: | [project_id],[instance_id] | |
+| stackit_postgres_flex_user | stackit_postgresflex_user | :warning: | [project_id],[instance_id],[user_id] | `password` field will be empty |
+| stackit_postgres_instance | | | | Resource deprecated |
+| stackit_postgres_credential | | | | Resource deprecated
+| stackit_project | stackit_resourcemanager_project | :white_check_mark: | [container_id] | |
| stackit_rabbitmq_credential | stackit_rabbitmq_credential | :white_check_mark: | [project_id],[credentials_group_id],[credential_id] | |
-| stackit_rabbitmq_instance | stackit_rabbitmq_instance | :white_check_mark: | [project_id],[instance_id] | |
+| stackit_rabbitmq_instance | stackit_rabbitmq_instance | :white_check_mark: | [project_id],[instance_id] | |
| stackit_redis_credential | stackit_redis_credential | :white_check_mark: | [project_id],[credentials_group_id],[credential_id] | |
-| stackit_redis_instance | stackit_redis_instance | :white_check_mark: | [project_id],[instance_id] | |
-| stackit_secrets_manager_instance | stackit_secretsmanager_instance | :white_check_mark: | [project_id],[instance_id] | |
-| stackit_secrets_manager_user | stackit_secretsmanager_user | :warning: | [project_id],[instance_id],[user_id] | `password` field will be empty |
+| stackit_redis_instance | stackit_redis_instance | :white_check_mark: | [project_id],[instance_id] | |
+| stackit_secrets_manager_instance | stackit_secretsmanager_instance | :white_check_mark: | [project_id],[instance_id] | |
+| stackit_secrets_manager_user | stackit_secretsmanager_user | :warning: | [project_id],[instance_id],[user_id] | `password` field will be empty |
+| stackit_valkey_credential | stackit_valkey_credential | :white_check_mark: | [project_id],[region],[instance_id],[credential_id] | |
+| stackit_valkey_instance | stackit_valkey_instance | :white_check_mark: | [project_id],[region],[instance_id] | |
diff --git a/docs/data-sources/valkey_credential.md b/docs/data-sources/valkey_credential.md
new file mode 100644
index 000000000..e7f0f1f62
--- /dev/null
+++ b/docs/data-sources/valkey_credential.md
@@ -0,0 +1,45 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "stackit_valkey_credential Data Source - stackit"
+subcategory: ""
+description: |-
+ Valkey credential data source schema. Must have a region specified in the provider configuration.
+---
+
+# stackit_valkey_credential (Data Source)
+
+Valkey credential data source schema. Must have a `region` specified in the provider configuration.
+
+## Example Usage
+
+```terraform
+data "stackit_valkey_credential" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ credential_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+}
+```
+
+
+## Schema
+
+### Required
+
+- `credential_id` (String) The credential's ID.
+- `instance_id` (String) ID of the Valkey instance.
+- `project_id` (String) STACKIT project ID to which the instance is associated.
+
+### Optional
+
+- `region` (String) The resource region. If not defined, the provider region is used.
+
+### Read-Only
+
+- `host` (String)
+- `hosts` (List of String)
+- `id` (String) Terraform's internal data source. identifier. It is structured as "`project_id`,`region`,`instance_id`,`credential_id`".
+- `load_balanced_host` (String)
+- `password` (String, Sensitive)
+- `port` (Number)
+- `uri` (String, Sensitive) Connection URI.
+- `username` (String)
diff --git a/docs/data-sources/valkey_instance.md b/docs/data-sources/valkey_instance.md
new file mode 100644
index 000000000..79222d3e0
--- /dev/null
+++ b/docs/data-sources/valkey_instance.md
@@ -0,0 +1,73 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "stackit_valkey_instance Data Source - stackit"
+subcategory: ""
+description: |-
+ Valkey instance data source schema. Must have a region specified in the provider configuration.
+---
+
+# stackit_valkey_instance (Data Source)
+
+Valkey instance data source schema. Must have a `region` specified in the provider configuration.
+
+## Example Usage
+
+```terraform
+data "stackit_valkey_instance" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+}
+```
+
+
+## Schema
+
+### Required
+
+- `instance_id` (String) ID of the Valkey instance.
+- `project_id` (String) STACKIT Project ID to which the instance is associated.
+
+### Optional
+
+- `region` (String) The resource region. If not defined, the provider region is used.
+
+### Read-Only
+
+- `cf_guid` (String)
+- `cf_organization_guid` (String)
+- `cf_space_guid` (String)
+- `dashboard_url` (String)
+- `id` (String) Terraform's internal data source. identifier. It is structured as "`project_id`,`region`,`instance_id`".
+- `image_url` (String)
+- `name` (String) Instance name.
+- `parameters` (Attributes) (see [below for nested schema](#nestedatt--parameters))
+- `plan_id` (String) The selected plan ID.
+- `plan_name` (String) The selected plan name.
+- `version` (String) The service version.
+
+
+### Nested Schema for `parameters`
+
+Read-Only:
+
+- `down_after_milliseconds` (Number) The number of milliseconds after which the instance is considered down.
+- `enable_monitoring` (Boolean) Enable monitoring.
+- `failover_timeout` (Number) The failover timeout in milliseconds.
+- `graphite` (String) Graphite server URL (host and port). If set, monitoring with Graphite will be enabled.
+- `lazyfree_lazy_eviction` (String) The lazy eviction enablement (yes or no).
+- `lazyfree_lazy_expire` (String) The lazy expire enablement (yes or no).
+- `lua_time_limit` (Number) The Lua time limit.
+- `max_disk_threshold` (Number) The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.
+- `maxclients` (Number) The maximum number of clients.
+- `maxmemory_policy` (String) The policy to handle the maximum memory (volatile-lru, noeviction, etc).
+- `maxmemory_samples` (Number) The maximum memory samples.
+- `metrics_frequency` (Number) The frequency in seconds at which metrics are emitted.
+- `metrics_prefix` (String) The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key.
+- `min_replicas_max_lag` (Number) The minimum replicas maximum lag.
+- `min_replicas_to_write` (Number) The amount of connected replicas that are required for the primary to accept write operations. It can be set to 0 to disable it.
+- `monitoring_instance_id` (String) The ID of the STACKIT monitoring instance.
+- `notify_keyspace_events` (String) The notify keyspace events.
+- `repl_backlog_size` (String) The replication backlog size for the cluster.
+- `sgw_acl` (String) Comma separated list of IP networks in CIDR notation which are allowed to access this instance.
+- `snapshot` (String) The snapshot configuration.
+- `syslog` (List of String) List of syslog servers to send logs to.
diff --git a/docs/index.md b/docs/index.md
index 160f8a9b8..48c309992 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -223,4 +223,5 @@ See this [example](https://professional-service.git.onstackit.cloud/professional
- `telemetryrouter_custom_endpoint` (String) Custom endpoint for the Telemetry Router service
- `token_custom_endpoint` (String) Custom endpoint for the token API, which is used to request access tokens when using the key flow
- `use_oidc` (Boolean) Enables OIDC for Authentication. This can also be sourced from the `STACKIT_USE_OIDC` Environment Variable. Defaults to `false`.
+- `valkey_custom_endpoint` (String) Custom endpoint for the Key Value Store service
- `vpn_custom_endpoint` (String) Custom endpoint for the VPN service
diff --git a/docs/resources/valkey_credential.md b/docs/resources/valkey_credential.md
new file mode 100644
index 000000000..26b063609
--- /dev/null
+++ b/docs/resources/valkey_credential.md
@@ -0,0 +1,72 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "stackit_valkey_credential Resource - stackit"
+subcategory: ""
+description: |-
+ Valkey credential resource schema. Must have a region specified in the provider configuration.
+---
+
+# stackit_valkey_credential (Resource)
+
+Valkey credential resource schema. Must have a `region` specified in the provider configuration.
+
+## Example Usage
+
+```terraform
+resource "stackit_valkey_credential" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+}
+
+resource "time_rotating" "rotate" {
+ rotation_days = 80
+}
+
+resource "stackit_valkey_credential" "example_rotate" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+
+ rotate_when_changed = {
+ rotation = time_rotating.rotate.id
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `instance_id` (String) ID of the Valkey instance.
+- `project_id` (String) STACKIT Project ID to which the instance is associated.
+
+### Optional
+
+- `region` (String) The resource region. If not defined, the provider region is used.
+- `rotate_when_changed` (Map of String) A map of arbitrary key/value pairs that will force recreation of the resource when they change, enabling resource rotation based on external conditions such as a rotating timestamp. Changing this forces a new resource to be created.
+
+### Read-Only
+
+- `credential_id` (String) The credential's ID.
+- `host` (String)
+- `hosts` (List of String)
+- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`region`,`instance_id`,`credential_id`".
+- `load_balanced_host` (String)
+- `password` (String, Sensitive)
+- `port` (Number)
+- `uri` (String, Sensitive) Connection URI.
+- `username` (String)
+
+## 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 valkey credential
+import {
+ to = stackit_valkey_credential.import-example
+ id = "${var.project_id},${var.valkey_instance_id},${var.valkey_credential_id}"
+}
+```
diff --git a/docs/resources/valkey_instance.md b/docs/resources/valkey_instance.md
new file mode 100644
index 000000000..c6ded6960
--- /dev/null
+++ b/docs/resources/valkey_instance.md
@@ -0,0 +1,95 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "stackit_valkey_instance Resource - stackit"
+subcategory: ""
+description: |-
+ Valkey instance resource schema. Must have a region specified in the provider configuration.
+---
+
+# stackit_valkey_instance (Resource)
+
+Valkey instance resource schema. Must have a `region` specified in the provider configuration.
+
+## Example Usage
+
+```terraform
+resource "stackit_valkey_instance" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name = "example-instance"
+ version = "7"
+ plan_name = "stackit-keyvalue-1.2.10-replica"
+ parameters = {
+ sgw_acl = "193.148.160.0/19,45.129.40.0/21,45.135.244.0/22"
+ enable_monitoring = false
+ down_after_milliseconds = 30000
+ syslog = ["logs4.your-syslog-endpoint.com:54321"]
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `name` (String) Instance name.
+- `plan_name` (String) The selected plan name.
+- `project_id` (String) STACKIT project ID to which the instance is associated.
+- `version` (String) The service version.
+
+### Optional
+
+- `parameters` (Attributes) Configuration parameters. Please note that removing a previously configured field from your Terraform configuration won't replace its value in the API. To update a previously configured field, explicitly set a new value for it. (see [below for nested schema](#nestedatt--parameters))
+- `region` (String) The resource region. If not defined, the provider region is used.
+
+### Read-Only
+
+- `cf_guid` (String)
+- `cf_organization_guid` (String)
+- `cf_space_guid` (String)
+- `dashboard_url` (String)
+- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`instance_id`".
+- `image_url` (String)
+- `instance_id` (String) ID of the Valkey instance.
+- `plan_id` (String) The selected plan ID.
+
+
+### Nested Schema for `parameters`
+
+Optional:
+
+- `down_after_milliseconds` (Number) The number of milliseconds after which the instance is considered down.
+- `enable_monitoring` (Boolean) Enable monitoring.
+- `failover_timeout` (Number) The failover timeout in milliseconds.
+- `graphite` (String) Graphite server URL (host and port). If set, monitoring with Graphite will be enabled.
+- `lazyfree_lazy_eviction` (String) The lazy eviction enablement (yes or no).
+- `lazyfree_lazy_expire` (String) The lazy expire enablement (yes or no).
+- `lua_time_limit` (Number) The Lua time limit.
+- `max_disk_threshold` (Number) The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.
+- `maxclients` (Number) The maximum number of clients.
+- `maxmemory_policy` (String) The policy to handle the maximum memory (volatile-lru, noeviction, etc).
+- `maxmemory_samples` (Number) The maximum memory samples.
+- `metrics_frequency` (Number) The frequency in seconds at which metrics are emitted.
+- `metrics_prefix` (String) The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key.
+- `min_replicas_max_lag` (Number) The minimum replicas maximum lag.
+- `min_replicas_to_write` (Number) The amount of connected replicas that are required for the primary to accept write operations. It can be set to 0 to disable it.
+- `monitoring_instance_id` (String) The ID of the STACKIT monitoring instance.
+- `notify_keyspace_events` (String) The notify keyspace events.
+- `repl_backlog_size` (String) The replication backlog size for the cluster.
+- `sgw_acl` (String) Comma separated list of IP networks in CIDR notation which are allowed to access this instance.
+- `snapshot` (String) The snapshot configuration.
+- `syslog` (List of String) List of syslog servers to send logs to.
+
+## 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 valkey instance
+import {
+ to = stackit_valkey_instance.import-example
+ id = "${var.project_id},${var.valkey_instance_id}"
+}
+```
diff --git a/examples/data-sources/stackit_valkey_credential/data-source.tf b/examples/data-sources/stackit_valkey_credential/data-source.tf
new file mode 100644
index 000000000..1002d11bd
--- /dev/null
+++ b/examples/data-sources/stackit_valkey_credential/data-source.tf
@@ -0,0 +1,5 @@
+data "stackit_valkey_credential" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ credential_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+}
diff --git a/examples/data-sources/stackit_valkey_instance/data-source.tf b/examples/data-sources/stackit_valkey_instance/data-source.tf
new file mode 100644
index 000000000..90ca822fe
--- /dev/null
+++ b/examples/data-sources/stackit_valkey_instance/data-source.tf
@@ -0,0 +1,4 @@
+data "stackit_valkey_instance" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+}
diff --git a/examples/resources/stackit_valkey_credential/import-by-string-id.tf b/examples/resources/stackit_valkey_credential/import-by-string-id.tf
new file mode 100644
index 000000000..ebe7fdd5d
--- /dev/null
+++ b/examples/resources/stackit_valkey_credential/import-by-string-id.tf
@@ -0,0 +1,5 @@
+# Only use the import statement, if you want to import an existing valkey credential
+import {
+ to = stackit_valkey_credential.import-example
+ id = "${var.project_id},${var.valkey_instance_id},${var.valkey_credential_id}"
+}
diff --git a/examples/resources/stackit_valkey_credential/resource.tf b/examples/resources/stackit_valkey_credential/resource.tf
new file mode 100644
index 000000000..0e2f066b8
--- /dev/null
+++ b/examples/resources/stackit_valkey_credential/resource.tf
@@ -0,0 +1,17 @@
+resource "stackit_valkey_credential" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+}
+
+resource "time_rotating" "rotate" {
+ rotation_days = 80
+}
+
+resource "stackit_valkey_credential" "example_rotate" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+
+ rotate_when_changed = {
+ rotation = time_rotating.rotate.id
+ }
+}
\ No newline at end of file
diff --git a/examples/resources/stackit_valkey_instance/import-by-string-id.tf b/examples/resources/stackit_valkey_instance/import-by-string-id.tf
new file mode 100644
index 000000000..c0fb5f95e
--- /dev/null
+++ b/examples/resources/stackit_valkey_instance/import-by-string-id.tf
@@ -0,0 +1,5 @@
+# Only use the import statement, if you want to import an existing valkey instance
+import {
+ to = stackit_valkey_instance.import-example
+ id = "${var.project_id},${var.valkey_instance_id}"
+}
diff --git a/examples/resources/stackit_valkey_instance/resource.tf b/examples/resources/stackit_valkey_instance/resource.tf
new file mode 100644
index 000000000..46d1f518e
--- /dev/null
+++ b/examples/resources/stackit_valkey_instance/resource.tf
@@ -0,0 +1,12 @@
+resource "stackit_valkey_instance" "example" {
+ project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ name = "example-instance"
+ version = "7"
+ plan_name = "stackit-keyvalue-1.2.10-replica"
+ parameters = {
+ sgw_acl = "193.148.160.0/19,45.129.40.0/21,45.135.244.0/22"
+ enable_monitoring = false
+ down_after_milliseconds = 30000
+ syslog = ["logs4.your-syslog-endpoint.com:54321"]
+ }
+}
diff --git a/go.mod b/go.mod
index 77b7345a1..87630f57e 100644
--- a/go.mod
+++ b/go.mod
@@ -48,6 +48,7 @@ require (
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.17.0
github.com/stackitcloud/stackit-sdk-go/services/telemetrylink v0.4.0
github.com/stackitcloud/stackit-sdk-go/services/telemetryrouter v0.4.0
+ github.com/stackitcloud/stackit-sdk-go/services/valkey v0.1.1
github.com/stackitcloud/stackit-sdk-go/services/vpn v0.14.0
github.com/teambition/rrule-go v1.8.2
golang.org/x/mod v0.38.0
diff --git a/go.sum b/go.sum
index bef815f9f..ad6777a58 100644
--- a/go.sum
+++ b/go.sum
@@ -227,6 +227,8 @@ github.com/stackitcloud/stackit-sdk-go/services/telemetrylink v0.4.0 h1:RG+cZzvI
github.com/stackitcloud/stackit-sdk-go/services/telemetrylink v0.4.0/go.mod h1:hgw8janWmDfP2bnuZensxqcAePr49BX5ug8Rq85o+h8=
github.com/stackitcloud/stackit-sdk-go/services/telemetryrouter v0.4.0 h1:tEKBl3g7SIjZ8aa43nNjS1Rqr/IwxQ3Pr1MArYF0fno=
github.com/stackitcloud/stackit-sdk-go/services/telemetryrouter v0.4.0/go.mod h1:WUmgKtwpe90Yq3YbgNxc2clTTULVxCu0ha6lMTjUnII=
+github.com/stackitcloud/stackit-sdk-go/services/valkey v0.1.1 h1:8UraYJJE+gOcfmizHbJZZ+Tef+GC8nzBvhTCWrcACBQ=
+github.com/stackitcloud/stackit-sdk-go/services/valkey v0.1.1/go.mod h1:etzt/a723p327dqha1V72cCoBWjUBS2DtHy3ZTac0SA=
github.com/stackitcloud/stackit-sdk-go/services/vpn v0.14.0 h1:LMgbzhPunuelsIsfyEj/5O/aYfNcg/eGHsnZ7AZOhYg=
github.com/stackitcloud/stackit-sdk-go/services/vpn v0.14.0/go.mod h1:toIjQk1dhxdUFVyCWJJja0w/0nFpDid8MWX0ukQfvfo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go
index d539952e8..b74478a73 100644
--- a/stackit/internal/core/core.go
+++ b/stackit/internal/core/core.go
@@ -74,6 +74,7 @@ type ProviderData struct {
ServiceAccountCustomEndpoint string
TelemetryLinkCustomEndpoint string
TelemetryRouterCustomEndpoint string
+ ValkeyCustomEndpoint string
VpnCustomEndpoint string
EnableBetaResources bool
Experiments []string
diff --git a/stackit/internal/services/valkey/credential/datasource.go b/stackit/internal/services/valkey/credential/datasource.go
new file mode 100644
index 000000000..f1e40d55b
--- /dev/null
+++ b/stackit/internal/services/valkey/credential/datasource.go
@@ -0,0 +1,260 @@
+package valkey
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
+ valkeyUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/valkey/utils"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ valkey "github.com/stackitcloud/stackit-sdk-go/services/valkey/v2api"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+ _ datasource.DataSource = &credentialDataSource{}
+)
+
+type DataSourceModel struct {
+ Id types.String `tfsdk:"id"` // needed by TF
+ CredentialId types.String `tfsdk:"credential_id"`
+ InstanceId types.String `tfsdk:"instance_id"`
+ ProjectId types.String `tfsdk:"project_id"`
+ Region types.String `tfsdk:"region"`
+ Host types.String `tfsdk:"host"`
+ Hosts types.List `tfsdk:"hosts"`
+ LoadBalancedHost types.String `tfsdk:"load_balanced_host"`
+ Password types.String `tfsdk:"password"`
+ Port types.Int32 `tfsdk:"port"`
+ Uri types.String `tfsdk:"uri"`
+ Username types.String `tfsdk:"username"`
+}
+
+// NewCredentialDataSource is a helper function to simplify the provider implementation.
+func NewCredentialDataSource() datasource.DataSource {
+ return &credentialDataSource{}
+}
+
+// credentialDataSource is the data source implementation.
+type credentialDataSource struct {
+ client *valkey.APIClient
+ providerData core.ProviderData
+}
+
+// Metadata returns the data source type name.
+func (r *credentialDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_valkey_credential"
+}
+
+// Configure adds the provider configured client to the data source.
+func (r *credentialDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ var ok bool
+ r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
+ if !ok {
+ return
+ }
+
+ apiClient := valkeyUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ r.client = apiClient
+ tflog.Info(ctx, "Valkey credential client configured")
+}
+
+// Schema defines the schema for the data source.
+func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ descriptions := map[string]string{ //nolint:gosec // description for credential id
+ "main": "Valkey credential data source schema. Must have a `region` specified in the provider configuration.",
+ "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`region`,`instance_id`,`credential_id`\".",
+ "credential_id": "The credential's ID.",
+ "instance_id": "ID of the Valkey instance.",
+ "project_id": "STACKIT project ID to which the instance is associated.",
+ "uri": "Connection URI.",
+ "region": "The resource region. If not defined, the provider region is used.",
+ }
+
+ resp.Schema = schema.Schema{
+ Description: descriptions["main"],
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: descriptions["id"],
+ Computed: true,
+ },
+ "credential_id": schema.StringAttribute{
+ Description: descriptions["credential_id"],
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "instance_id": schema.StringAttribute{
+ Description: descriptions["instance_id"],
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "project_id": schema.StringAttribute{
+ Description: descriptions["project_id"],
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "host": schema.StringAttribute{
+ Computed: true,
+ },
+ "hosts": schema.ListAttribute{
+ ElementType: types.StringType,
+ Computed: true,
+ },
+ "load_balanced_host": schema.StringAttribute{
+ Computed: true,
+ },
+ "password": schema.StringAttribute{
+ Computed: true,
+ Sensitive: true,
+ },
+ "port": schema.Int32Attribute{
+ Computed: true,
+ },
+ "uri": schema.StringAttribute{
+ Description: descriptions["uri"],
+ Computed: true,
+ Sensitive: true,
+ },
+ "username": schema.StringAttribute{
+ Computed: true,
+ },
+ "region": schema.StringAttribute{
+ Optional: true,
+ // must be computed to allow for storing the override value from the provider
+ Computed: true,
+ Description: descriptions["region"],
+ },
+ },
+ }
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (r *credentialDataSource) 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
+ }
+
+ ctx = core.InitProviderContext(ctx)
+
+ projectId := model.ProjectId.ValueString()
+ region := r.providerData.GetRegionWithOverride(model.Region)
+ instanceId := model.InstanceId.ValueString()
+ credentialId := model.CredentialId.ValueString()
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "region", region)
+ ctx = tflog.SetField(ctx, "instance_id", instanceId)
+ ctx = tflog.SetField(ctx, "credential_id", credentialId)
+
+ recordSetResp, err := r.client.DefaultAPI.GetCredentials(ctx, projectId, region, instanceId, credentialId).Execute()
+ if err != nil {
+ utils.LogError(
+ ctx,
+ &resp.Diagnostics,
+ err,
+ "Reading credential",
+ fmt.Sprintf("Credential with ID %q or instance with ID %q does not exist in project %q.", credentialId, instanceId, 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)
+
+ // Map response body to schema
+ err = mapDataSourceFields(ctx, recordSetResp, &model, region)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", 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, "Valkey credential read")
+}
+
+func mapDataSourceFields(ctx context.Context, credentialsResp *valkey.CredentialsResponse, model *DataSourceModel, region string) error {
+ if credentialsResp == nil {
+ return fmt.Errorf("response input is nil")
+ }
+ if credentialsResp.Raw == nil {
+ return fmt.Errorf("response credentials raw is nil")
+ }
+ if model == nil {
+ return fmt.Errorf("model input is nil")
+ }
+ credentials := credentialsResp.Raw.Credentials
+
+ var credentialId string
+ if model.CredentialId.ValueString() != "" {
+ credentialId = model.CredentialId.ValueString()
+ } else if credentialsResp.Id != "" {
+ credentialId = credentialsResp.Id
+ } else {
+ return fmt.Errorf("credentials id not present")
+ }
+
+ model.Region = types.StringValue(region)
+ model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.Region.ValueString(), model.InstanceId.ValueString(), credentialId)
+
+ modelHosts, err := utils.ListValueToStringSlice(model.Hosts)
+ if err != nil {
+ return err
+ }
+
+ model.Hosts = types.ListNull(types.StringType)
+ model.CredentialId = types.StringValue(credentialId)
+
+ if credentials.Hosts != nil {
+ respHosts := credentials.Hosts
+
+ reconciledHosts := utils.ReconcileStringSlices(modelHosts, respHosts)
+
+ hostsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledHosts)
+ if diags.HasError() {
+ return fmt.Errorf("failed to map hosts: %w", core.DiagsToError(diags))
+ }
+
+ model.Hosts = hostsTF
+ }
+ model.Host = types.StringValue(credentials.Host)
+ model.LoadBalancedHost = types.StringPointerValue(credentials.LoadBalancedHost)
+ model.Password = types.StringValue(credentials.Password)
+ model.Port = types.Int32PointerValue(credentials.Port)
+ model.Uri = types.StringPointerValue(credentials.Uri)
+ model.Username = types.StringValue(credentials.Username)
+
+ return nil
+}
diff --git a/stackit/internal/services/valkey/credential/datasource_test.go b/stackit/internal/services/valkey/credential/datasource_test.go
new file mode 100644
index 000000000..9868f55d2
--- /dev/null
+++ b/stackit/internal/services/valkey/credential/datasource_test.go
@@ -0,0 +1,226 @@
+package valkey
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ valkey "github.com/stackitcloud/stackit-sdk-go/services/valkey/v2api"
+)
+
+func TestMapDataSourceFields(t *testing.T) {
+ const testRegion = "eu01"
+ tests := []struct {
+ description string
+ state DataSourceModel
+ input *valkey.CredentialsResponse
+ expected DataSourceModel
+ isValid bool
+ }{
+ {
+ "default_values",
+ DataSourceModel{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ },
+ &valkey.CredentialsResponse{
+ Id: "cid",
+ Raw: &valkey.RawCredentials{},
+ },
+ DataSourceModel{
+ Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)),
+ CredentialId: types.StringValue("cid"),
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ Region: types.StringValue(testRegion),
+ Host: types.StringValue(""),
+ Hosts: types.ListNull(types.StringType),
+ LoadBalancedHost: types.StringNull(),
+ Password: types.StringValue(""),
+ Port: types.Int32Null(),
+ Uri: types.StringNull(),
+ Username: types.StringValue(""),
+ },
+ true,
+ },
+ {
+ "simple_values",
+ DataSourceModel{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ },
+ &valkey.CredentialsResponse{
+ Id: "cid",
+ Raw: &valkey.RawCredentials{
+ Credentials: valkey.Credentials{
+ Host: "host",
+ Hosts: []string{
+ "host_1",
+ "",
+ },
+ LoadBalancedHost: new("load_balanced_host"),
+ Password: "password",
+ Port: new(int32(1234)),
+ Uri: new("uri"),
+ Username: "username",
+ },
+ },
+ },
+ DataSourceModel{
+ Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)),
+ CredentialId: types.StringValue("cid"),
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ Region: types.StringValue(testRegion),
+ Host: types.StringValue("host"),
+ Hosts: types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("host_1"),
+ types.StringValue(""),
+ }),
+ LoadBalancedHost: types.StringValue("load_balanced_host"),
+ Password: types.StringValue("password"),
+ Port: types.Int32Value(1234),
+ Uri: types.StringValue("uri"),
+ Username: types.StringValue("username"),
+ },
+ true,
+ },
+ {
+ "hosts_unordered",
+ DataSourceModel{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ Hosts: types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("host_2"),
+ types.StringValue(""),
+ types.StringValue("host_1"),
+ }),
+ },
+ &valkey.CredentialsResponse{
+ Id: "cid",
+ Raw: &valkey.RawCredentials{
+ Credentials: valkey.Credentials{
+ Host: "host",
+ Hosts: []string{
+ "",
+ "host_1",
+ "host_2",
+ },
+ LoadBalancedHost: new("load_balanced_host"),
+ Password: "password",
+ Port: new(int32(1234)),
+ Uri: new("uri"),
+ Username: "username",
+ },
+ },
+ },
+ DataSourceModel{
+ Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)),
+ CredentialId: types.StringValue("cid"),
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ Region: types.StringValue(testRegion),
+ Host: types.StringValue("host"),
+ Hosts: types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("host_2"),
+ types.StringValue(""),
+ types.StringValue("host_1"),
+ }),
+ LoadBalancedHost: types.StringValue("load_balanced_host"),
+ Password: types.StringValue("password"),
+ Port: types.Int32Value(1234),
+ Uri: types.StringValue("uri"),
+ Username: types.StringValue("username"),
+ },
+ true,
+ },
+ {
+ "null_fields_and_int_conversions",
+ DataSourceModel{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ },
+ &valkey.CredentialsResponse{
+ Id: "cid",
+ Raw: &valkey.RawCredentials{
+ Credentials: valkey.Credentials{
+ Host: "",
+ Hosts: []string{},
+ LoadBalancedHost: nil,
+ Password: "",
+ Port: new(int32(2123456789)),
+ Uri: nil,
+ Username: "",
+ },
+ },
+ },
+ DataSourceModel{
+ Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)),
+ CredentialId: types.StringValue("cid"),
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ Region: types.StringValue(testRegion),
+ Host: types.StringValue(""),
+ Hosts: types.ListValueMust(types.StringType, []attr.Value{}),
+ LoadBalancedHost: types.StringNull(),
+ Password: types.StringValue(""),
+ Port: types.Int32Value(2123456789),
+ Uri: types.StringNull(),
+ Username: types.StringValue(""),
+ },
+ true,
+ },
+ {
+ "nil_response",
+ DataSourceModel{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ },
+ nil,
+ DataSourceModel{},
+ false,
+ },
+ {
+ "no_resource_id",
+ DataSourceModel{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ },
+ &valkey.CredentialsResponse{},
+ DataSourceModel{},
+ false,
+ },
+ {
+ "nil_raw_credential",
+ DataSourceModel{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ },
+ &valkey.CredentialsResponse{
+ Id: "cid",
+ },
+ DataSourceModel{},
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ err := mapDataSourceFields(context.Background(), tt.input, &tt.state, testRegion)
+ if !tt.isValid && err == nil {
+ t.Fatalf("Should have failed")
+ }
+ if tt.isValid && err != nil {
+ t.Fatalf("Should not have failed: %v", err)
+ }
+ if tt.isValid {
+ diff := cmp.Diff(tt.state, tt.expected)
+ if diff != "" {
+ t.Fatalf("Data does not match: %s", diff)
+ }
+ }
+ })
+ }
+}
diff --git a/stackit/internal/services/valkey/credential/resource.go b/stackit/internal/services/valkey/credential/resource.go
new file mode 100644
index 000000000..4ddb900a5
--- /dev/null
+++ b/stackit/internal/services/valkey/credential/resource.go
@@ -0,0 +1,435 @@
+package valkey
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
+
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
+ valkeyUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/valkey/utils"
+
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+
+ "github.com/hashicorp/terraform-plugin-framework/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/types"
+ "github.com/stackitcloud/stackit-sdk-go/core/oapierror"
+ valkey "github.com/stackitcloud/stackit-sdk-go/services/valkey/v2api"
+ "github.com/stackitcloud/stackit-sdk-go/services/valkey/v2api/wait"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+ _ resource.Resource = &credentialResource{}
+ _ resource.ResourceWithConfigure = &credentialResource{}
+ _ resource.ResourceWithImportState = &credentialResource{}
+)
+
+type Model struct {
+ Id types.String `tfsdk:"id"` // needed by TF
+ CredentialId types.String `tfsdk:"credential_id"`
+ InstanceId types.String `tfsdk:"instance_id"`
+ ProjectId types.String `tfsdk:"project_id"`
+ Region types.String `tfsdk:"region"`
+ Host types.String `tfsdk:"host"`
+ Hosts types.List `tfsdk:"hosts"`
+ LoadBalancedHost types.String `tfsdk:"load_balanced_host"`
+ Password types.String `tfsdk:"password"`
+ Port types.Int32 `tfsdk:"port"`
+ Uri types.String `tfsdk:"uri"`
+ Username types.String `tfsdk:"username"`
+ // RotateWhenChanged is a map of arbitrary key/value pairs that will force
+ // recreation of the resource when they change, enabling resource rotation based on
+ // external conditions such as a rotating timestamp. Changing this forces a new
+ // resource to be created.
+ RotateWhenChanged types.Map `tfsdk:"rotate_when_changed"`
+}
+
+// NewCredentialResource is a helper function to simplify the provider implementation.
+func NewCredentialResource() resource.Resource {
+ return &credentialResource{}
+}
+
+// credentialResource is the resource implementation.
+type credentialResource struct {
+ client *valkey.APIClient
+ providerData core.ProviderData
+}
+
+// Metadata returns the resource type name.
+func (r *credentialResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_valkey_credential"
+}
+
+// Configure adds the provider configured client to the resource.
+func (r *credentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ var ok bool
+ r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
+ if !ok {
+ return
+ }
+
+ apiClient := valkeyUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ r.client = apiClient
+ tflog.Info(ctx, "Valkey credential client configured")
+}
+
+// Schema defines the schema for the resource.
+func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ descriptions := map[string]string{ //nolint:gosec // description for credential id
+ "main": "Valkey credential resource schema. Must have a `region` specified in the provider configuration.",
+ "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`instance_id`,`credential_id`\".",
+ "credential_id": "The credential's ID.",
+ "instance_id": "ID of the Valkey instance.",
+ "project_id": "STACKIT Project ID to which the instance is associated.",
+ "uri": "Connection URI.",
+ "region": "The resource region. If not defined, the provider region is used.",
+ }
+
+ resp.Schema = schema.Schema{
+ Description: descriptions["main"],
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: descriptions["id"],
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "credential_id": schema.StringAttribute{
+ Description: descriptions["credential_id"],
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "instance_id": schema.StringAttribute{
+ Description: descriptions["instance_id"],
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "project_id": schema.StringAttribute{
+ Description: descriptions["project_id"],
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "host": schema.StringAttribute{
+ Computed: true,
+ },
+ "hosts": schema.ListAttribute{
+ ElementType: types.StringType,
+ Computed: true,
+ },
+ "load_balanced_host": schema.StringAttribute{
+ Computed: true,
+ },
+ "password": schema.StringAttribute{
+ Computed: true,
+ Sensitive: true,
+ },
+ "port": schema.Int32Attribute{
+ Computed: true,
+ },
+ "uri": schema.StringAttribute{
+ Description: descriptions["uri"],
+ Computed: true,
+ Sensitive: true,
+ },
+ "username": schema.StringAttribute{
+ Computed: true,
+ },
+ "rotate_when_changed": schema.MapAttribute{
+ Description: "A map of arbitrary key/value pairs that will force " +
+ "recreation of the resource when they change, enabling resource rotation " +
+ "based on external conditions such as a rotating timestamp. Changing " +
+ "this forces a new resource to be created.",
+ Optional: true,
+ Required: false,
+ ElementType: types.StringType,
+ PlanModifiers: []planmodifier.Map{
+ mapplanmodifier.RequiresReplace(),
+ },
+ },
+ "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(),
+ },
+ },
+ },
+ }
+}
+
+// Create creates the resource and sets the initial Terraform state.
+func (r *credentialResource) 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()
+ instanceId := model.InstanceId.ValueString()
+ region := r.providerData.GetRegionWithOverride(model.Region)
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "instance_id", instanceId)
+ ctx = tflog.SetField(ctx, "region", region)
+
+ // Create new recordset
+ credentialsResp, err := r.client.DefaultAPI.CreateCredentials(ctx, projectId, region, instanceId).Execute()
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ if credentialsResp.Id == "" {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id")
+ return
+ }
+ credentialId := credentialsResp.Id
+ // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler
+ ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
+ "project_id": projectId,
+ "region": region,
+ "instance_id": instanceId,
+ "credential_id": credentialId,
+ })
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ waitResp, err := wait.CreateCredentialsWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId, credentialId).WaitWithContext(ctx)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Instance creation waiting: %v", err))
+ return
+ }
+
+ // Map response body to schema
+ err = mapFields(ctx, waitResp, &model, region)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", 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, "Valkey credential created")
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (r *credentialResource) 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()
+ region := r.providerData.GetRegionWithOverride(model.Region)
+ instanceId := model.InstanceId.ValueString()
+ credentialId := model.CredentialId.ValueString()
+ if credentialId == "" {
+ // Resource not yet created; ID is unknown.
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "region", region)
+ ctx = tflog.SetField(ctx, "instance_id", instanceId)
+ ctx = tflog.SetField(ctx, "credential_id", credentialId)
+
+ recordSetResp, err := r.client.DefaultAPI.GetCredentials(ctx, projectId, region, instanceId, credentialId).Execute()
+ if err != nil {
+ var oapiErr *oapierror.GenericOpenAPIError
+ if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ // Map response body to schema
+ err = mapFields(ctx, recordSetResp, &model, region)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", 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, "Valkey credential read")
+}
+
+// Update updates the resource and sets the updated Terraform state on success.
+func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
+ // Update shouldn't be called
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated")
+}
+
+// Delete deletes the resource and removes the Terraform state on success.
+func (r *credentialResource) 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()
+ region := r.providerData.GetRegionWithOverride(model.Region)
+ instanceId := model.InstanceId.ValueString()
+ credentialId := model.CredentialId.ValueString()
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "region", region)
+ ctx = tflog.SetField(ctx, "instance_id", instanceId)
+ ctx = tflog.SetField(ctx, "credential_id", credentialId)
+
+ // Delete existing record set
+ err := r.client.DefaultAPI.DeleteCredentials(ctx, projectId, region, instanceId, credentialId).Execute()
+ if err != nil {
+ var oapiErr *oapierror.GenericOpenAPIError
+ if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound {
+ return
+ }
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ _, err = wait.DeleteCredentialsWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId, credentialId).WaitWithContext(ctx)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Instance deletion waiting: %v", err))
+ return
+ }
+ tflog.Info(ctx, "Valkey credential deleted")
+}
+
+// ImportState imports a resource into the Terraform state on success.
+// The expected format of the resource import identifier is: project_id,region,instance_id,credential_id
+func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ idParts := strings.Split(req.ID, core.Separator)
+ if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
+ core.LogAndAddError(ctx, &resp.Diagnostics,
+ "Error importing credential",
+ fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id],[credential_id], got %q", req.ID),
+ )
+ return
+ }
+
+ ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
+ "project_id": idParts[0],
+ "region": idParts[1],
+ "instance_id": idParts[2],
+ "credential_id": idParts[3],
+ })
+ tflog.Info(ctx, "Valkey credential state imported")
+}
+
+func mapFields(ctx context.Context, credentialsResp *valkey.CredentialsResponse, model *Model, region string) error {
+ if credentialsResp == nil {
+ return fmt.Errorf("response input is nil")
+ }
+ if credentialsResp.Raw == nil {
+ return fmt.Errorf("response credentials raw is nil")
+ }
+ if model == nil {
+ return fmt.Errorf("model input is nil")
+ }
+ credentials := credentialsResp.Raw.Credentials
+
+ var credentialId string
+ if model.CredentialId.ValueString() != "" {
+ credentialId = model.CredentialId.ValueString()
+ } else if credentialsResp.Id != "" {
+ credentialId = credentialsResp.Id
+ } else {
+ return fmt.Errorf("credentials id not present")
+ }
+
+ model.Region = types.StringValue(region)
+ model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.Region.ValueString(), model.InstanceId.ValueString(), credentialId)
+
+ modelHosts, err := utils.ListValueToStringSlice(model.Hosts)
+ if err != nil {
+ return err
+ }
+
+ model.Hosts = types.ListNull(types.StringType)
+ model.CredentialId = types.StringValue(credentialId)
+
+ if credentials.Hosts != nil {
+ respHosts := credentials.Hosts
+
+ reconciledHosts := utils.ReconcileStringSlices(modelHosts, respHosts)
+
+ hostsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledHosts)
+ if diags.HasError() {
+ return fmt.Errorf("failed to map hosts: %w", core.DiagsToError(diags))
+ }
+
+ model.Hosts = hostsTF
+ }
+ model.Host = types.StringValue(credentials.Host)
+ model.LoadBalancedHost = types.StringPointerValue(credentials.LoadBalancedHost)
+ model.Password = types.StringValue(credentials.Password)
+ model.Port = types.Int32PointerValue(credentials.Port)
+ model.Uri = types.StringPointerValue(credentials.Uri)
+ model.Username = types.StringValue(credentials.Username)
+
+ return nil
+}
diff --git a/stackit/internal/services/valkey/credential/resource_test.go b/stackit/internal/services/valkey/credential/resource_test.go
new file mode 100644
index 000000000..9fc6c0f28
--- /dev/null
+++ b/stackit/internal/services/valkey/credential/resource_test.go
@@ -0,0 +1,237 @@
+package valkey
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ valkey "github.com/stackitcloud/stackit-sdk-go/services/valkey/v2api"
+)
+
+func TestMapFields(t *testing.T) {
+ const testRegion = "eu01"
+ tests := []struct {
+ description string
+ state Model
+ input *valkey.CredentialsResponse
+ expected Model
+ isValid bool
+ }{
+ {
+ "default_values",
+ Model{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ RotateWhenChanged: types.MapNull(types.StringType),
+ },
+ &valkey.CredentialsResponse{
+ Id: "cid",
+ Raw: &valkey.RawCredentials{},
+ },
+ Model{
+ Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)),
+ CredentialId: types.StringValue("cid"),
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ Region: types.StringValue(testRegion),
+ Host: types.StringValue(""),
+ Hosts: types.ListNull(types.StringType),
+ LoadBalancedHost: types.StringNull(),
+ Password: types.StringValue(""),
+ Port: types.Int32Null(),
+ Uri: types.StringNull(),
+ Username: types.StringValue(""),
+ RotateWhenChanged: types.MapNull(types.StringType),
+ },
+ true,
+ },
+ {
+ "simple_values",
+ Model{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ RotateWhenChanged: types.MapNull(types.StringType),
+ },
+ &valkey.CredentialsResponse{
+ Id: "cid",
+ Raw: &valkey.RawCredentials{
+ Credentials: valkey.Credentials{
+ Host: "host",
+ Hosts: []string{
+ "host_1",
+ "",
+ },
+ LoadBalancedHost: new("load_balanced_host"),
+ Password: "password",
+ Port: new(int32(1234)),
+ Uri: new("uri"),
+ Username: "username",
+ },
+ },
+ },
+ Model{
+ Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)),
+ CredentialId: types.StringValue("cid"),
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ Region: types.StringValue(testRegion),
+ Host: types.StringValue("host"),
+ Hosts: types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("host_1"),
+ types.StringValue(""),
+ }),
+ LoadBalancedHost: types.StringValue("load_balanced_host"),
+ Password: types.StringValue("password"),
+ Port: types.Int32Value(1234),
+ Uri: types.StringValue("uri"),
+ Username: types.StringValue("username"),
+ RotateWhenChanged: types.MapNull(types.StringType),
+ },
+ true,
+ },
+ {
+ "hosts_unordered",
+ Model{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ Hosts: types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("host_2"),
+ types.StringValue(""),
+ types.StringValue("host_1"),
+ }),
+ RotateWhenChanged: types.MapNull(types.StringType),
+ },
+ &valkey.CredentialsResponse{
+ Id: "cid",
+ Raw: &valkey.RawCredentials{
+ Credentials: valkey.Credentials{
+ Host: "host",
+ Hosts: []string{
+ "",
+ "host_1",
+ "host_2",
+ },
+ LoadBalancedHost: new("load_balanced_host"),
+ Password: "password",
+ Port: new(int32(1234)),
+ Uri: new("uri"),
+ Username: "username",
+ },
+ },
+ },
+ Model{
+ Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)),
+ CredentialId: types.StringValue("cid"),
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ Region: types.StringValue(testRegion),
+ Host: types.StringValue("host"),
+ Hosts: types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("host_2"),
+ types.StringValue(""),
+ types.StringValue("host_1"),
+ }),
+ LoadBalancedHost: types.StringValue("load_balanced_host"),
+ Password: types.StringValue("password"),
+ Port: types.Int32Value(1234),
+ Uri: types.StringValue("uri"),
+ Username: types.StringValue("username"),
+ RotateWhenChanged: types.MapNull(types.StringType),
+ },
+ true,
+ },
+ {
+ "null_fields_and_int_conversions",
+ Model{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ RotateWhenChanged: types.MapNull(types.StringType),
+ },
+ &valkey.CredentialsResponse{
+ Id: "cid",
+ Raw: &valkey.RawCredentials{
+ Credentials: valkey.Credentials{
+ Host: "",
+ Hosts: []string{},
+ LoadBalancedHost: nil,
+ Password: "",
+ Port: new(int32(2123456789)),
+ Uri: nil,
+ Username: "",
+ },
+ },
+ },
+ Model{
+ Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)),
+ CredentialId: types.StringValue("cid"),
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ Region: types.StringValue(testRegion),
+ Host: types.StringValue(""),
+ Hosts: types.ListValueMust(types.StringType, []attr.Value{}),
+ LoadBalancedHost: types.StringNull(),
+ Password: types.StringValue(""),
+ Port: types.Int32Value(2123456789),
+ Uri: types.StringNull(),
+ Username: types.StringValue(""),
+ RotateWhenChanged: types.MapNull(types.StringType),
+ },
+ true,
+ },
+ {
+ "nil_response",
+ Model{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ RotateWhenChanged: types.MapNull(types.StringType),
+ },
+ nil,
+ Model{},
+ false,
+ },
+ {
+ "no_resource_id",
+ Model{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ RotateWhenChanged: types.MapNull(types.StringType),
+ },
+ &valkey.CredentialsResponse{},
+ Model{},
+ false,
+ },
+ {
+ "nil_raw_credential",
+ Model{
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ RotateWhenChanged: types.MapNull(types.StringType),
+ },
+ &valkey.CredentialsResponse{
+ Id: "cid",
+ },
+ Model{},
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ err := mapFields(context.Background(), tt.input, &tt.state, testRegion)
+ if !tt.isValid && err == nil {
+ t.Fatalf("Should have failed")
+ }
+ if tt.isValid && err != nil {
+ t.Fatalf("Should not have failed: %v", err)
+ }
+ if tt.isValid {
+ diff := cmp.Diff(tt.state, tt.expected)
+ if diff != "" {
+ t.Fatalf("Data does not match: %s", diff)
+ }
+ }
+ })
+ }
+}
diff --git a/stackit/internal/services/valkey/instance/datasource.go b/stackit/internal/services/valkey/instance/datasource.go
new file mode 100644
index 000000000..40a95f62a
--- /dev/null
+++ b/stackit/internal/services/valkey/instance/datasource.go
@@ -0,0 +1,315 @@
+package valkey
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
+ valkeyUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/valkey/utils"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "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/terraform-provider-stackit/stackit/internal/core"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ valkey "github.com/stackitcloud/stackit-sdk-go/services/valkey/v2api"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+ _ datasource.DataSource = &instanceDataSource{}
+)
+
+// NewInstanceDataSource is a helper function to simplify the provider implementation.
+func NewInstanceDataSource() datasource.DataSource {
+ return &instanceDataSource{}
+}
+
+// instanceDataSource is the data source implementation.
+type instanceDataSource struct {
+ client *valkey.APIClient
+ providerData core.ProviderData
+}
+
+// Metadata returns the data source type name.
+func (r *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_valkey_instance"
+}
+
+// Configure adds the provider configured client to the data source.
+func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ var ok bool
+ r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
+ if !ok {
+ return
+ }
+
+ apiClient := valkeyUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ r.client = apiClient
+ tflog.Info(ctx, "Valkey instance client configured")
+}
+
+// Schema defines the schema for the data source.
+func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ descriptions := map[string]string{
+ "main": "Valkey instance data source schema. Must have a `region` specified in the provider configuration.",
+ "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`region`,`instance_id`\".",
+ "instance_id": "ID of the Valkey instance.",
+ "project_id": "STACKIT Project ID to which the instance is associated.",
+ "name": "Instance name.",
+ "version": "The service version.",
+ "plan_name": "The selected plan name.",
+ "plan_id": "The selected plan ID.",
+ "region": "The resource region. If not defined, the provider region is used.",
+ }
+
+ parametersDescriptions := map[string]string{
+ "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.",
+ "down_after_milliseconds": "The number of milliseconds after which the instance is considered down.",
+ "enable_monitoring": "Enable monitoring.",
+ "failover_timeout": "The failover timeout in milliseconds.",
+ "graphite": "Graphite server URL (host and port). If set, monitoring with Graphite will be enabled.",
+ "lazyfree_lazy_eviction": "The lazy eviction enablement (yes or no).",
+ "lazyfree_lazy_expire": "The lazy expire enablement (yes or no).",
+ "lua_time_limit": "The Lua time limit.",
+ "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.",
+ "maxclients": "The maximum number of clients.",
+ "maxmemory_policy": "The policy to handle the maximum memory (volatile-lru, noeviction, etc).",
+ "maxmemory_samples": "The maximum memory samples.",
+ "metrics_frequency": "The frequency in seconds at which metrics are emitted.",
+ "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key.",
+ "min_replicas_to_write": "The amount of connected replicas that are required for the primary to accept write operations. It can be set to 0 to disable it.",
+ "min_replicas_max_lag": "The minimum replicas maximum lag.",
+ "monitoring_instance_id": "The ID of the STACKIT monitoring instance.",
+ "notify_keyspace_events": "The notify keyspace events.",
+ "repl_backlog_size": "The replication backlog size for the cluster.",
+ "snapshot": "The snapshot configuration.",
+ "syslog": "List of syslog servers to send logs to.",
+ }
+
+ resp.Schema = schema.Schema{
+ Description: descriptions["main"],
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: descriptions["id"],
+ Computed: true,
+ },
+ "instance_id": schema.StringAttribute{
+ Description: descriptions["instance_id"],
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "project_id": schema.StringAttribute{
+ Description: descriptions["project_id"],
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "name": schema.StringAttribute{
+ Description: descriptions["name"],
+ Computed: true,
+ },
+ "version": schema.StringAttribute{
+ Description: descriptions["version"],
+ Computed: true,
+ },
+ "plan_name": schema.StringAttribute{
+ Description: descriptions["plan_name"],
+ Computed: true,
+ },
+ "plan_id": schema.StringAttribute{
+ Description: descriptions["plan_id"],
+ Computed: true,
+ },
+ "parameters": schema.SingleNestedAttribute{
+ Attributes: map[string]schema.Attribute{
+ "sgw_acl": schema.StringAttribute{
+ Description: parametersDescriptions["sgw_acl"],
+ Computed: true,
+ },
+ "down_after_milliseconds": schema.Int32Attribute{
+ Description: parametersDescriptions["down_after_milliseconds"],
+ Computed: true,
+ },
+ "enable_monitoring": schema.BoolAttribute{
+ Description: parametersDescriptions["enable_monitoring"],
+ Computed: true,
+ },
+ "failover_timeout": schema.Int32Attribute{
+ Description: parametersDescriptions["failover_timeout"],
+ Computed: true,
+ },
+ "graphite": schema.StringAttribute{
+ Description: parametersDescriptions["graphite"],
+ Computed: true,
+ },
+ "lazyfree_lazy_eviction": schema.StringAttribute{
+ Description: parametersDescriptions["lazyfree_lazy_eviction"],
+ Computed: true,
+ },
+ "lazyfree_lazy_expire": schema.StringAttribute{
+ Description: parametersDescriptions["lazyfree_lazy_expire"],
+ Computed: true,
+ },
+ "lua_time_limit": schema.Int32Attribute{
+ Description: parametersDescriptions["lua_time_limit"],
+ Computed: true,
+ },
+ "max_disk_threshold": schema.Int32Attribute{
+ Description: parametersDescriptions["max_disk_threshold"],
+ Computed: true,
+ },
+ "maxclients": schema.Int32Attribute{
+ Description: parametersDescriptions["maxclients"],
+ Computed: true,
+ },
+ "maxmemory_policy": schema.StringAttribute{
+ Description: parametersDescriptions["maxmemory_policy"],
+ Computed: true,
+ },
+ "maxmemory_samples": schema.Int32Attribute{
+ Description: parametersDescriptions["maxmemory_samples"],
+ Computed: true,
+ },
+ "metrics_frequency": schema.Int32Attribute{
+ Description: parametersDescriptions["metrics_frequency"],
+ Computed: true,
+ },
+ "metrics_prefix": schema.StringAttribute{
+ Description: parametersDescriptions["metrics_prefix"],
+ Computed: true,
+ },
+ "min_replicas_max_lag": schema.Int32Attribute{
+ Description: parametersDescriptions["min_replicas_max_lag"],
+ Computed: true,
+ },
+ "min_replicas_to_write": schema.Int32Attribute{
+ Description: parametersDescriptions["min_replicas_to_write"],
+ Computed: true,
+ },
+ "monitoring_instance_id": schema.StringAttribute{
+ Description: parametersDescriptions["monitoring_instance_id"],
+ Computed: true,
+ },
+ "notify_keyspace_events": schema.StringAttribute{
+ Description: parametersDescriptions["notify_keyspace_events"],
+ Computed: true,
+ },
+ "repl_backlog_size": schema.StringAttribute{
+ Description: parametersDescriptions["repl_backlog_size"],
+ Computed: true,
+ },
+ "snapshot": schema.StringAttribute{
+ Description: parametersDescriptions["snapshot"],
+ Computed: true,
+ },
+ "syslog": schema.ListAttribute{
+ ElementType: types.StringType,
+ Description: parametersDescriptions["syslog"],
+ Computed: true,
+ },
+ },
+ Computed: true,
+ },
+ "cf_guid": schema.StringAttribute{
+ Description: descriptions["cf_guid"],
+ Computed: true,
+ },
+ "cf_space_guid": schema.StringAttribute{
+ Description: descriptions["cf_space_guid"],
+ Computed: true,
+ },
+ "dashboard_url": schema.StringAttribute{
+ Description: descriptions["dashboard_url"],
+ Computed: true,
+ },
+ "image_url": schema.StringAttribute{
+ Description: descriptions["image_url"],
+ Computed: true,
+ },
+ "cf_organization_guid": schema.StringAttribute{
+ Description: descriptions["cf_organization_guid"],
+ Computed: true,
+ },
+ "region": schema.StringAttribute{
+ Optional: true,
+ // must be computed to allow for storing the override value from the provider
+ Computed: true,
+ Description: descriptions["region"],
+ },
+ },
+ }
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (r *instanceDataSource) 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()
+ instanceId := model.InstanceId.ValueString()
+ region := r.providerData.GetRegionWithOverride(model.Region)
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "instance_id", instanceId)
+ ctx = tflog.SetField(ctx, "region", region)
+
+ instanceResp, err := r.client.DefaultAPI.GetInstance(ctx, projectId, region, instanceId).Execute()
+ if err != nil {
+ utils.LogError(
+ ctx,
+ &resp.Diagnostics,
+ err,
+ "Reading instance",
+ fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceId, projectId),
+ map[int]string{
+ http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId),
+ http.StatusGone: fmt.Sprintf("Instance %q is gone.", instanceId),
+ },
+ )
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ err = mapFields(instanceResp, &model, region)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err))
+ return
+ }
+
+ // Compute and store values not present in the API response
+ err = loadPlanNameAndVersion(ctx, r.client, &model, region)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err))
+ return
+ }
+
+ // Set refreshed state
+ diags = resp.State.Set(ctx, &model)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ tflog.Info(ctx, "Valkey instance read")
+}
diff --git a/stackit/internal/services/valkey/instance/resource.go b/stackit/internal/services/valkey/instance/resource.go
new file mode 100644
index 000000000..7546142cd
--- /dev/null
+++ b/stackit/internal/services/valkey/instance/resource.go
@@ -0,0 +1,951 @@
+package valkey
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "slices"
+ "strings"
+
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
+
+ valkeyUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/valkey/utils"
+
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+
+ "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/validate"
+
+ "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/types"
+ "github.com/stackitcloud/stackit-sdk-go/core/oapierror"
+ valkey "github.com/stackitcloud/stackit-sdk-go/services/valkey/v2api"
+ "github.com/stackitcloud/stackit-sdk-go/services/valkey/v2api/wait"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+ _ resource.Resource = &instanceResource{}
+ _ resource.ResourceWithConfigure = &instanceResource{}
+ _ resource.ResourceWithImportState = &instanceResource{}
+)
+
+type Model struct {
+ Id types.String `tfsdk:"id"` // needed by TF
+ InstanceId types.String `tfsdk:"instance_id"`
+ ProjectId types.String `tfsdk:"project_id"`
+ Region types.String `tfsdk:"region"`
+ CfGuid types.String `tfsdk:"cf_guid"`
+ CfSpaceGuid types.String `tfsdk:"cf_space_guid"`
+ DashboardUrl types.String `tfsdk:"dashboard_url"`
+ ImageUrl types.String `tfsdk:"image_url"`
+ Name types.String `tfsdk:"name"`
+ CfOrganizationGuid types.String `tfsdk:"cf_organization_guid"`
+ Parameters types.Object `tfsdk:"parameters"`
+ Version types.String `tfsdk:"version"`
+ PlanName types.String `tfsdk:"plan_name"`
+ PlanId types.String `tfsdk:"plan_id"`
+}
+
+// Struct corresponding to DataSourceModel.Parameters
+type parametersModel struct {
+ SgwAcl types.String `tfsdk:"sgw_acl"`
+ DownAfterMilliseconds types.Int32 `tfsdk:"down_after_milliseconds"`
+ EnableMonitoring types.Bool `tfsdk:"enable_monitoring"`
+ FailoverTimeout types.Int32 `tfsdk:"failover_timeout"`
+ Graphite types.String `tfsdk:"graphite"`
+ LazyfreeLazyEviction types.String `tfsdk:"lazyfree_lazy_eviction"`
+ LazyfreeLazyExpire types.String `tfsdk:"lazyfree_lazy_expire"`
+ LuaTimeLimit types.Int32 `tfsdk:"lua_time_limit"`
+ MaxDiskThreshold types.Int32 `tfsdk:"max_disk_threshold"`
+ Maxclients types.Int32 `tfsdk:"maxclients"`
+ MaxmemoryPolicy types.String `tfsdk:"maxmemory_policy"`
+ MaxmemorySamples types.Int32 `tfsdk:"maxmemory_samples"`
+ MetricsFrequency types.Int32 `tfsdk:"metrics_frequency"`
+ MetricsPrefix types.String `tfsdk:"metrics_prefix"`
+ MinReplicasToWrite types.Int32 `tfsdk:"min_replicas_to_write"`
+ MinReplicasMaxLag types.Int32 `tfsdk:"min_replicas_max_lag"`
+ MonitoringInstanceId types.String `tfsdk:"monitoring_instance_id"`
+ NotifyKeyspaceEvents types.String `tfsdk:"notify_keyspace_events"`
+ ReplBacklogSize types.String `tfsdk:"repl_backlog_size"`
+ Snapshot types.String `tfsdk:"snapshot"`
+ Syslog types.List `tfsdk:"syslog"`
+}
+
+// Types corresponding to parametersModel
+var parametersTypes = map[string]attr.Type{
+ "sgw_acl": basetypes.StringType{},
+ "down_after_milliseconds": basetypes.Int32Type{},
+ "enable_monitoring": basetypes.BoolType{},
+ "failover_timeout": basetypes.Int32Type{},
+ "graphite": basetypes.StringType{},
+ "lazyfree_lazy_eviction": basetypes.StringType{},
+ "lazyfree_lazy_expire": basetypes.StringType{},
+ "lua_time_limit": basetypes.Int32Type{},
+ "max_disk_threshold": basetypes.Int32Type{},
+ "maxclients": basetypes.Int32Type{},
+ "maxmemory_policy": basetypes.StringType{},
+ "maxmemory_samples": basetypes.Int32Type{},
+ "metrics_frequency": basetypes.Int32Type{},
+ "metrics_prefix": basetypes.StringType{},
+ "min_replicas_to_write": basetypes.Int32Type{},
+ "min_replicas_max_lag": basetypes.Int32Type{},
+ "monitoring_instance_id": basetypes.StringType{},
+ "notify_keyspace_events": basetypes.StringType{},
+ "repl_backlog_size": basetypes.StringType{},
+ "snapshot": basetypes.StringType{},
+ "syslog": basetypes.ListType{ElemType: types.StringType},
+}
+
+// NewInstanceResource is a helper function to simplify the provider implementation.
+func NewInstanceResource() resource.Resource {
+ return &instanceResource{}
+}
+
+// instanceResource is the resource implementation.
+type instanceResource struct {
+ client *valkey.APIClient
+ providerData core.ProviderData
+}
+
+// Metadata returns the resource type name.
+func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_valkey_instance"
+}
+
+// Configure adds the provider configured client to the resource.
+func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ var ok bool
+ r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
+ if !ok {
+ return
+ }
+
+ apiClient := valkeyUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ r.client = apiClient
+ tflog.Info(ctx, "Valkey instance client configured")
+}
+
+// Schema defines the schema for the resource.
+func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ descriptions := map[string]string{
+ "main": "Valkey instance resource schema. Must have a `region` specified in the provider configuration.",
+ "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`\".",
+ "instance_id": "ID of the Valkey instance.",
+ "project_id": "STACKIT project ID to which the instance is associated.",
+ "name": "Instance name.",
+ "version": "The service version.",
+ "plan_name": "The selected plan name.",
+ "plan_id": "The selected plan ID.",
+ "parameters": "Configuration parameters. Please note that removing a previously configured field from your Terraform configuration won't replace its value in the API. To update a previously configured field, explicitly set a new value for it.",
+ "region": "The resource region. If not defined, the provider region is used.",
+ }
+
+ parametersDescriptions := map[string]string{
+ "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.",
+ "down_after_milliseconds": "The number of milliseconds after which the instance is considered down.",
+ "enable_monitoring": "Enable monitoring.",
+ "failover_timeout": "The failover timeout in milliseconds.",
+ "graphite": "Graphite server URL (host and port). If set, monitoring with Graphite will be enabled.",
+ "lazyfree_lazy_eviction": "The lazy eviction enablement (yes or no).",
+ "lazyfree_lazy_expire": "The lazy expire enablement (yes or no).",
+ "lua_time_limit": "The Lua time limit.",
+ "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.",
+ "maxclients": "The maximum number of clients.",
+ "maxmemory_policy": "The policy to handle the maximum memory (volatile-lru, noeviction, etc).",
+ "maxmemory_samples": "The maximum memory samples.",
+ "metrics_frequency": "The frequency in seconds at which metrics are emitted.",
+ "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key.",
+ "min_replicas_to_write": "The amount of connected replicas that are required for the primary to accept write operations. It can be set to 0 to disable it.",
+ "min_replicas_max_lag": "The minimum replicas maximum lag.",
+ "monitoring_instance_id": "The ID of the STACKIT monitoring instance.",
+ "notify_keyspace_events": "The notify keyspace events.",
+ "repl_backlog_size": "The replication backlog size for the cluster.",
+ "snapshot": "The snapshot configuration.",
+ "syslog": "List of syslog servers to send logs to.",
+ }
+
+ resp.Schema = schema.Schema{
+ Description: descriptions["main"],
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: descriptions["id"],
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "instance_id": schema.StringAttribute{
+ Description: descriptions["instance_id"],
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "project_id": schema.StringAttribute{
+ Description: descriptions["project_id"],
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "name": schema.StringAttribute{
+ Description: descriptions["name"],
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ },
+ "version": schema.StringAttribute{
+ Description: descriptions["version"],
+ Required: true,
+ },
+ "plan_name": schema.StringAttribute{
+ Description: descriptions["plan_name"],
+ Required: true,
+ },
+ "plan_id": schema.StringAttribute{
+ Description: descriptions["plan_id"],
+ Computed: true,
+ },
+ "parameters": schema.SingleNestedAttribute{
+ Description: descriptions["parameters"],
+ Attributes: map[string]schema.Attribute{
+ "sgw_acl": schema.StringAttribute{
+ Description: parametersDescriptions["sgw_acl"],
+ Optional: true,
+ Computed: true,
+ },
+ "down_after_milliseconds": schema.Int32Attribute{
+ Description: parametersDescriptions["down_after_milliseconds"],
+ Optional: true,
+ Computed: true,
+ },
+ "enable_monitoring": schema.BoolAttribute{
+ Description: parametersDescriptions["enable_monitoring"],
+ Optional: true,
+ Computed: true,
+ },
+ "failover_timeout": schema.Int32Attribute{
+ Description: parametersDescriptions["failover_timeout"],
+ Optional: true,
+ Computed: true,
+ },
+ "graphite": schema.StringAttribute{
+ Description: parametersDescriptions["graphite"],
+ Optional: true,
+ Computed: true,
+ },
+ "lazyfree_lazy_eviction": schema.StringAttribute{
+ Description: parametersDescriptions["lazyfree_lazy_eviction"],
+ Optional: true,
+ Computed: true,
+ },
+ "lazyfree_lazy_expire": schema.StringAttribute{
+ Description: parametersDescriptions["lazyfree_lazy_expire"],
+ Optional: true,
+ Computed: true,
+ },
+ "lua_time_limit": schema.Int32Attribute{
+ Description: parametersDescriptions["lua_time_limit"],
+ Optional: true,
+ Computed: true,
+ },
+ "max_disk_threshold": schema.Int32Attribute{
+ Description: parametersDescriptions["max_disk_threshold"],
+ Optional: true,
+ Computed: true,
+ },
+ "maxclients": schema.Int32Attribute{
+ Description: parametersDescriptions["maxclients"],
+ Optional: true,
+ Computed: true,
+ },
+ "maxmemory_policy": schema.StringAttribute{
+ Description: parametersDescriptions["maxmemory_policy"],
+ Optional: true,
+ Computed: true,
+ },
+ "maxmemory_samples": schema.Int32Attribute{
+ Description: parametersDescriptions["maxmemory_samples"],
+ Optional: true,
+ Computed: true,
+ },
+ "metrics_frequency": schema.Int32Attribute{
+ Description: parametersDescriptions["metrics_frequency"],
+ Optional: true,
+ Computed: true,
+ },
+ "metrics_prefix": schema.StringAttribute{
+ Description: parametersDescriptions["metrics_prefix"],
+ Optional: true,
+ Computed: true,
+ },
+ "min_replicas_max_lag": schema.Int32Attribute{
+ Description: parametersDescriptions["min_replicas_max_lag"],
+ Optional: true,
+ Computed: true,
+ },
+ "min_replicas_to_write": schema.Int32Attribute{
+ Description: parametersDescriptions["min_replicas_to_write"],
+ Optional: true,
+ Computed: true,
+ },
+ "monitoring_instance_id": schema.StringAttribute{
+ Description: parametersDescriptions["monitoring_instance_id"],
+ Optional: true,
+ Computed: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "notify_keyspace_events": schema.StringAttribute{
+ Description: parametersDescriptions["notify_keyspace_events"],
+ Optional: true,
+ Computed: true,
+ },
+ "repl_backlog_size": schema.StringAttribute{
+ Description: parametersDescriptions["repl_backlog_size"],
+ Optional: true,
+ Computed: true,
+ },
+ "snapshot": schema.StringAttribute{
+ Description: parametersDescriptions["snapshot"],
+ Optional: true,
+ Computed: true,
+ },
+ "syslog": schema.ListAttribute{
+ Description: parametersDescriptions["syslog"],
+ ElementType: types.StringType,
+ Optional: true,
+ Computed: true,
+ },
+ },
+ Optional: true,
+ Computed: true,
+ },
+ "cf_guid": schema.StringAttribute{
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "cf_space_guid": schema.StringAttribute{
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "dashboard_url": schema.StringAttribute{
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "image_url": schema.StringAttribute{
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "cf_organization_guid": schema.StringAttribute{
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "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(),
+ },
+ },
+ },
+ }
+}
+
+// Create creates the resource and sets the initial Terraform state.
+func (r *instanceResource) 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)
+
+ var parameters *parametersModel
+ if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) {
+ parameters = ¶metersModel{}
+ diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ }
+
+ err := r.loadPlanId(ctx, &model)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err))
+ return
+ }
+
+ // Generate API request body from model
+ payload, err := toCreatePayload(&model, parameters)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err))
+ return
+ }
+ // Create new instance
+ createResp, err := r.client.DefaultAPI.CreateInstance(ctx, projectId, region).CreateInstancePayload(*payload).Execute()
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ if createResp.InstanceId == "" {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", "Got empty instance id")
+ return
+ }
+ instanceId := createResp.InstanceId
+ // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler
+ ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
+ "project_id": projectId,
+ "instance_id": instanceId,
+ "region": region,
+ })
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId).WaitWithContext(ctx)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err))
+ return
+ }
+
+ // Map response body to schema
+ err = mapFields(waitResp, &model, region)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", 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, "Valkey instance created")
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (r *instanceResource) 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()
+ instanceId := model.InstanceId.ValueString()
+ if instanceId == "" {
+ // Resource not yet created; ID is unknown.
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ region := r.providerData.GetRegionWithOverride(model.Region)
+
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "instance_id", instanceId)
+ ctx = tflog.SetField(ctx, "region", region)
+
+ instanceResp, err := r.client.DefaultAPI.GetInstance(ctx, projectId, region, instanceId).Execute()
+ if err != nil {
+ var oapiErr *oapierror.GenericOpenAPIError
+ if errors.As(err, &oapiErr) && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusGone) {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ // Map response body to schema
+ err = mapFields(instanceResp, &model, region)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err))
+ return
+ }
+
+ // Compute and store values not present in the API response
+ err = loadPlanNameAndVersion(ctx, r.client, &model, region)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err))
+ return
+ }
+
+ // Set refreshed state
+ diags = resp.State.Set(ctx, model)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ tflog.Info(ctx, "Valkey instance read")
+}
+
+// Update updates the resource and sets the updated Terraform state on success.
+func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // 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()
+ instanceId := model.InstanceId.ValueString()
+ region := r.providerData.GetRegionWithOverride(model.Region)
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "instance_id", instanceId)
+ ctx = tflog.SetField(ctx, "region", region)
+
+ var parameters *parametersModel
+ if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) {
+ parameters = ¶metersModel{}
+ diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ }
+
+ err := r.loadPlanId(ctx, &model)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err))
+ return
+ }
+
+ // Generate API request body from model
+ payload, err := toUpdatePayload(&model, parameters)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err))
+ return
+ }
+ // Update existing instance
+ err = r.client.DefaultAPI.PartialUpdateInstance(ctx, projectId, instanceId, region).PartialUpdateInstancePayload(*payload).Execute()
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId).WaitWithContext(ctx)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err))
+ return
+ }
+
+ // Map response body to schema
+ err = mapFields(waitResp, &model, region)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", 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, "Valkey instance updated")
+}
+
+// Delete deletes the resource and removes the Terraform state on success.
+func (r *instanceResource) 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
+ }
+
+ ctx = core.InitProviderContext(ctx)
+
+ projectId := model.ProjectId.ValueString()
+ instanceId := model.InstanceId.ValueString()
+ region := r.providerData.GetRegionWithOverride(model.Region)
+ ctx = tflog.SetField(ctx, "project_id", projectId)
+ ctx = tflog.SetField(ctx, "instance_id", instanceId)
+ ctx = tflog.SetField(ctx, "region", region)
+
+ // Delete existing instance
+ err := r.client.DefaultAPI.DeleteInstance(ctx, projectId, region, instanceId).Execute()
+ if err != nil {
+ var oapiErr *oapierror.GenericOpenAPIError
+ if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err))
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ _, err = wait.DeleteInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId).WaitWithContext(ctx)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err))
+ return
+ }
+ tflog.Info(ctx, "Valkey instance deleted")
+}
+
+// ImportState imports a resource into the Terraform state on success.
+// The expected format of the resource import identifier is: project_id,region,instance_id
+func (r *instanceResource) 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 instance",
+ fmt.Sprintf("Expected import identifier with format: [project_id],[region],[instance_id] Got: %q", req.ID),
+ )
+ return
+ }
+
+ ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
+ "project_id": idParts[0],
+ "region": idParts[1],
+ "instance_id": idParts[2],
+ })
+ tflog.Info(ctx, "Valkey instance state imported")
+}
+
+func mapFields(instance *valkey.Instance, model *Model, region string) error {
+ if instance == nil {
+ return fmt.Errorf("response input is nil")
+ }
+ if model == nil {
+ return fmt.Errorf("model input is nil")
+ }
+
+ var instanceId string
+ if model.InstanceId.ValueString() != "" {
+ instanceId = model.InstanceId.ValueString()
+ } else if instance.InstanceId != nil {
+ instanceId = *instance.InstanceId
+ } else {
+ return fmt.Errorf("instance id not present")
+ }
+
+ model.Region = types.StringValue(region)
+ model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, instanceId)
+ model.InstanceId = types.StringValue(instanceId)
+ model.PlanId = types.StringValue(instance.PlanId)
+ model.CfGuid = types.StringValue(instance.CfGuid)
+ model.CfSpaceGuid = types.StringValue(instance.CfSpaceGuid)
+ model.DashboardUrl = types.StringValue(instance.DashboardUrl)
+ model.ImageUrl = types.StringValue(instance.ImageUrl)
+ model.Name = types.StringValue(instance.Name)
+ model.CfOrganizationGuid = types.StringValue(instance.CfOrganizationGuid)
+
+ if instance.Parameters == nil {
+ model.Parameters = types.ObjectNull(parametersTypes)
+ } else {
+ parameters, err := mapParameters(instance.Parameters)
+ if err != nil {
+ return fmt.Errorf("mapping parameters: %w", err)
+ }
+ model.Parameters = parameters
+ }
+ return nil
+}
+
+func mapParameters(params map[string]any) (types.Object, error) {
+ attributes := map[string]attr.Value{}
+ for attribute := range parametersTypes {
+ var valueInterface any
+ var ok bool
+
+ // This replacement is necessary because Terraform does not allow hyphens in attribute names
+ // And the API uses hyphens in some of the attribute names, which would cause a mismatch
+ // The following attributes have hyphens in the API but underscores in the schema
+ hyphenAttributes := []string{
+ "down_after_milliseconds",
+ "failover_timeout",
+ "lazyfree_lazy_eviction",
+ "lazyfree_lazy_expire",
+ "lua_time_limit",
+ "maxmemory_policy",
+ "maxmemory_samples",
+ "notify_keyspace_events",
+ "min_replicas_to_write",
+ "repl_backlog_size",
+ }
+ if slices.Contains(hyphenAttributes, attribute) {
+ alteredAttribute := strings.ReplaceAll(attribute, "_", "-")
+ valueInterface, ok = params[alteredAttribute]
+ } else {
+ valueInterface, ok = params[attribute]
+ }
+ if !ok {
+ // All fields are optional, so this is ok
+ // Set the value as nil, will be handled accordingly
+ valueInterface = nil
+ }
+
+ var value attr.Value
+ switch parametersTypes[attribute].(type) {
+ default:
+ return types.ObjectNull(parametersTypes), fmt.Errorf("found unexpected attribute type '%T'", parametersTypes[attribute])
+ case basetypes.StringType:
+ if valueInterface == nil {
+ value = types.StringNull()
+ } else {
+ valueString, ok := valueInterface.(string)
+ if !ok {
+ return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as string", attribute, valueInterface)
+ }
+ value = types.StringValue(valueString)
+ }
+ case basetypes.BoolType:
+ if valueInterface == nil {
+ value = types.BoolNull()
+ } else {
+ valueBool, ok := valueInterface.(bool)
+ if !ok {
+ return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as bool", attribute, valueInterface)
+ }
+ value = types.BoolValue(valueBool)
+ }
+ case basetypes.Int32Type:
+ if valueInterface == nil {
+ value = types.Int32Null()
+ } else {
+ // This may be int64, int32, int or float64
+ // We try to assert all 4
+ var valueInt32 int32
+ switch temp := valueInterface.(type) {
+ default:
+ return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as int", attribute, valueInterface)
+ case int32:
+ valueInt32 = temp
+ case float64:
+ valueInt32 = int32(temp)
+ }
+ value = types.Int32Value(valueInt32)
+ }
+ case basetypes.ListType: // Assumed to be a list of strings
+ if valueInterface == nil {
+ value = types.ListNull(types.StringType)
+ } else {
+ // This may be []string{} or []interface{}
+ // We try to assert all 2
+ var valueList []attr.Value
+ switch temp := valueInterface.(type) {
+ default:
+ return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as array of interface", attribute, valueInterface)
+ case []string:
+ for _, x := range temp {
+ valueList = append(valueList, types.StringValue(x))
+ }
+ case []any:
+ for _, x := range temp {
+ xString, ok := x.(string)
+ if !ok {
+ return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' with element '%s' of type %T, failed to assert as string", attribute, x, x)
+ }
+ valueList = append(valueList, types.StringValue(xString))
+ }
+ }
+ temp2, diags := types.ListValue(types.StringType, valueList)
+ if diags.HasError() {
+ return types.ObjectNull(parametersTypes), fmt.Errorf("failed to map %s: %w", attribute, core.DiagsToError(diags))
+ }
+ value = temp2
+ }
+ }
+ attributes[attribute] = value
+ }
+
+ output, diags := types.ObjectValue(parametersTypes, attributes)
+ if diags.HasError() {
+ return types.ObjectNull(parametersTypes), fmt.Errorf("failed to create object: %w", core.DiagsToError(diags))
+ }
+ return output, nil
+}
+
+func toCreatePayload(model *Model, parameters *parametersModel) (*valkey.CreateInstancePayload, error) {
+ if model == nil {
+ return nil, fmt.Errorf("nil model")
+ }
+
+ payloadParams, err := toInstanceParams(parameters)
+ if err != nil {
+ return nil, fmt.Errorf("converting parameters: %w", err)
+ }
+
+ return &valkey.CreateInstancePayload{
+ InstanceName: model.Name.ValueString(),
+ Parameters: payloadParams,
+ PlanId: model.PlanId.ValueString(),
+ }, nil
+}
+
+func toUpdatePayload(model *Model, parameters *parametersModel) (*valkey.PartialUpdateInstancePayload, error) {
+ if model == nil {
+ return nil, fmt.Errorf("nil model")
+ }
+
+ payloadParams, err := toInstanceParams(parameters)
+ if err != nil {
+ return nil, fmt.Errorf("converting parameters: %w", err)
+ }
+
+ return &valkey.PartialUpdateInstancePayload{
+ Parameters: payloadParams,
+ PlanId: conversion.StringValueToPointer(model.PlanId),
+ }, nil
+}
+
+func toInstanceParams(parameters *parametersModel) (*valkey.InstanceParameters, error) {
+ if parameters == nil {
+ return nil, nil
+ }
+ payloadParams := &valkey.InstanceParameters{}
+
+ payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl)
+ payloadParams.DownAfterMilliseconds = conversion.Int32ValueToPointer(parameters.DownAfterMilliseconds)
+ payloadParams.EnableMonitoring = conversion.BoolValueToPointer(parameters.EnableMonitoring)
+ payloadParams.FailoverTimeout = conversion.Int32ValueToPointer(parameters.FailoverTimeout)
+ payloadParams.Graphite = conversion.StringValueToPointer(parameters.Graphite)
+ payloadParams.LazyfreeLazyEviction = conversion.StringValueToEnumPointer[valkey.InstanceParametersLazyfreeLazyEviction](parameters.LazyfreeLazyEviction)
+ payloadParams.LazyfreeLazyExpire = conversion.StringValueToEnumPointer[valkey.InstanceParametersLazyfreeLazyExpire](parameters.LazyfreeLazyExpire)
+ payloadParams.LuaTimeLimit = conversion.Int32ValueToPointer(parameters.LuaTimeLimit)
+ payloadParams.MaxDiskThreshold = conversion.Int32ValueToPointer(parameters.MaxDiskThreshold)
+ payloadParams.Maxclients = conversion.Int32ValueToPointer(parameters.Maxclients)
+ payloadParams.MaxmemoryPolicy = conversion.StringValueToEnumPointer[valkey.InstanceParametersMaxmemoryPolicy](parameters.MaxmemoryPolicy)
+ payloadParams.MaxmemorySamples = conversion.Int32ValueToPointer(parameters.MaxmemorySamples)
+ payloadParams.MetricsFrequency = conversion.Int32ValueToPointer(parameters.MetricsFrequency)
+ payloadParams.MetricsPrefix = conversion.StringValueToPointer(parameters.MetricsPrefix)
+ payloadParams.MinReplicasToWrite = conversion.Int32ValueToPointer(parameters.MinReplicasToWrite)
+ payloadParams.MinReplicasMaxLag = conversion.Int32ValueToPointer(parameters.MinReplicasMaxLag)
+ payloadParams.MonitoringInstanceId = conversion.StringValueToPointer(parameters.MonitoringInstanceId)
+ payloadParams.NotifyKeyspaceEvents = conversion.StringValueToPointer(parameters.NotifyKeyspaceEvents)
+ payloadParams.ReplBacklogSize = conversion.StringValueToPointer(parameters.ReplBacklogSize)
+ payloadParams.Snapshot = conversion.StringValueToPointer(parameters.Snapshot)
+
+ var err error
+ payloadParams.Syslog, err = conversion.StringListToSlice(parameters.Syslog)
+ if err != nil {
+ return nil, fmt.Errorf("converting syslog: %w", err)
+ }
+
+ return payloadParams, nil
+}
+
+func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error {
+ projectId := model.ProjectId.ValueString()
+ region := r.providerData.GetRegionWithOverride(model.Region)
+ res, err := r.client.DefaultAPI.ListOfferings(ctx, projectId, region).Execute()
+ if err != nil {
+ return fmt.Errorf("getting Valkey offerings: %w", err)
+ }
+
+ version := model.Version.ValueString()
+ planName := model.PlanName.ValueString()
+ availableVersions := ""
+ availablePlanNames := ""
+ isValidVersion := false
+ for _, offer := range res.Offerings {
+ if !strings.EqualFold(offer.Version, version) {
+ availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, offer.Version)
+ continue
+ }
+ isValidVersion = true
+
+ for _, plan := range offer.Plans {
+ if plan.Name == "" {
+ continue
+ }
+ if strings.EqualFold(plan.Name, planName) && plan.Id != "" {
+ model.PlanId = types.StringValue(plan.Id)
+ return nil
+ }
+ availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, plan.Name)
+ }
+ }
+
+ if !isValidVersion {
+ return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions)
+ }
+ return fmt.Errorf("couldn't find plan_name '%s' for version %s, available names are: %s", planName, version, availablePlanNames)
+}
+
+func loadPlanNameAndVersion(ctx context.Context, client *valkey.APIClient, model *Model, region string) error {
+ projectId := model.ProjectId.ValueString()
+ planId := model.PlanId.ValueString()
+ res, err := client.DefaultAPI.ListOfferings(ctx, projectId, region).Execute()
+ if err != nil {
+ return fmt.Errorf("getting Valkey offerings: %w", err)
+ }
+
+ for _, offer := range res.Offerings {
+ for _, plan := range offer.Plans {
+ if strings.EqualFold(plan.Id, planId) && plan.Id != "" {
+ model.PlanName = types.StringValue(plan.Name)
+ model.Version = types.StringValue(offer.Version)
+ return nil
+ }
+ }
+ }
+
+ return fmt.Errorf("couldn't find plan_name and version for plan_id '%s'", planId)
+}
diff --git a/stackit/internal/services/valkey/instance/resource_test.go b/stackit/internal/services/valkey/instance/resource_test.go
new file mode 100644
index 000000000..affae44ec
--- /dev/null
+++ b/stackit/internal/services/valkey/instance/resource_test.go
@@ -0,0 +1,370 @@
+package valkey
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ valkey "github.com/stackitcloud/stackit-sdk-go/services/valkey/v2api"
+)
+
+var fixtureModelParameters = types.ObjectValueMust(parametersTypes, map[string]attr.Value{
+ "sgw_acl": types.StringValue("acl"),
+ "down_after_milliseconds": types.Int32Value(10),
+ "enable_monitoring": types.BoolValue(true),
+ "failover_timeout": types.Int32Value(10),
+ "graphite": types.StringValue("1.1.1.1:91"),
+ "lazyfree_lazy_eviction": types.StringValue("no"),
+ "lazyfree_lazy_expire": types.StringValue("no"),
+ "lua_time_limit": types.Int32Value(10),
+ "max_disk_threshold": types.Int32Value(100),
+ "maxclients": types.Int32Value(10),
+ "maxmemory_policy": types.StringValue("volatile-lru"),
+ "maxmemory_samples": types.Int32Value(10),
+ "metrics_frequency": types.Int32Value(10),
+ "metrics_prefix": types.StringValue("prefix"),
+ "min_replicas_max_lag": types.Int32Value(10),
+ "min_replicas_to_write": types.Int32Value(1),
+ "monitoring_instance_id": types.StringValue("mid"),
+ "notify_keyspace_events": types.StringValue("events"),
+ "repl_backlog_size": types.StringValue("1mb"),
+ "snapshot": types.StringValue("snapshot"),
+ "syslog": types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("syslog"),
+ types.StringValue("syslog2"),
+ }),
+})
+
+var fixtureInstanceParameters = valkey.InstanceParameters{
+ SgwAcl: new("acl"),
+ DownAfterMilliseconds: new(int32(10)),
+ EnableMonitoring: new(true),
+ FailoverTimeout: new(int32(10)),
+ Graphite: new("1.1.1.1:91"),
+ LazyfreeLazyEviction: new(valkey.INSTANCEPARAMETERSLAZYFREELAZYEVICTION_NO),
+ LazyfreeLazyExpire: new(valkey.INSTANCEPARAMETERSLAZYFREELAZYEXPIRE_NO),
+ LuaTimeLimit: new(int32(10)),
+ MaxDiskThreshold: new(int32(100)),
+ Maxclients: new(int32(10)),
+ MaxmemoryPolicy: new(valkey.INSTANCEPARAMETERSMAXMEMORYPOLICY_VOLATILE_LRU),
+ MaxmemorySamples: new(int32(10)),
+ MetricsFrequency: new(int32(10)),
+ MetricsPrefix: new("prefix"),
+ MinReplicasMaxLag: new(int32(10)),
+ MinReplicasToWrite: new(int32(1)),
+ MonitoringInstanceId: new("mid"),
+ NotifyKeyspaceEvents: new("events"),
+ ReplBacklogSize: new("1mb"),
+ Snapshot: new("snapshot"),
+ Syslog: []string{"syslog", "syslog2"},
+}
+
+func TestMapFields(t *testing.T) {
+ const testRegion = "eu01"
+ tests := []struct {
+ description string
+ input *valkey.Instance
+ expected Model
+ isValid bool
+ }{
+ {
+ "default_values",
+ &valkey.Instance{},
+ Model{
+ Id: types.StringValue(fmt.Sprintf("pid,%s,iid", testRegion)),
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ Region: types.StringValue(testRegion),
+ PlanId: types.StringValue(""),
+ Name: types.StringValue(""),
+ CfGuid: types.StringValue(""),
+ CfSpaceGuid: types.StringValue(""),
+ DashboardUrl: types.StringValue(""),
+ ImageUrl: types.StringValue(""),
+ CfOrganizationGuid: types.StringValue(""),
+ Parameters: types.ObjectNull(parametersTypes),
+ },
+ true,
+ },
+ {
+ "simple_values",
+ &valkey.Instance{
+ PlanId: "plan",
+ CfGuid: "cf",
+ CfSpaceGuid: "space",
+ DashboardUrl: "dashboard",
+ ImageUrl: "image",
+ InstanceId: new("iid"),
+ Name: "name",
+ CfOrganizationGuid: "org",
+ Parameters: map[string]any{
+ "sgw_acl": "acl",
+ "down-after-milliseconds": int32(10),
+ "enable_monitoring": true,
+ "failover-timeout": int32(10),
+ "graphite": "1.1.1.1:91",
+ "lazyfree-lazy-eviction": "no",
+ "lazyfree-lazy-expire": "no",
+ "lua-time-limit": int32(10),
+ "max_disk_threshold": int32(100),
+ "maxclients": int32(10),
+ "maxmemory-policy": "volatile-lru",
+ "maxmemory-samples": int32(10),
+ "metrics_frequency": int32(10),
+ "metrics_prefix": "prefix",
+ "min_replicas_max_lag": int32(10),
+ "min-replicas-to-write": int32(1),
+ "monitoring_instance_id": "mid",
+ "notify-keyspace-events": "events",
+ "repl-backlog-size": "1mb",
+ "snapshot": "snapshot",
+ "syslog": []string{"syslog", "syslog2"},
+ },
+ },
+ Model{
+ Id: types.StringValue(fmt.Sprintf("pid,%s,iid", testRegion)),
+ InstanceId: types.StringValue("iid"),
+ ProjectId: types.StringValue("pid"),
+ Region: types.StringValue(testRegion),
+ PlanId: types.StringValue("plan"),
+ Name: types.StringValue("name"),
+ CfGuid: types.StringValue("cf"),
+ CfSpaceGuid: types.StringValue("space"),
+ DashboardUrl: types.StringValue("dashboard"),
+ ImageUrl: types.StringValue("image"),
+ CfOrganizationGuid: types.StringValue("org"),
+ Parameters: fixtureModelParameters,
+ },
+ true,
+ },
+ {
+ "nil_response",
+ nil,
+ Model{},
+ false,
+ },
+ {
+ "no_resource_id",
+ &valkey.Instance{},
+ Model{},
+ false,
+ },
+ {
+ "wrong_param_types_1",
+ &valkey.Instance{
+ Parameters: map[string]any{
+ "sgw_acl": true,
+ },
+ },
+ Model{},
+ false,
+ },
+ {
+ "wrong_param_types_2",
+ &valkey.Instance{
+ Parameters: map[string]any{
+ "sgw_acl": 1,
+ },
+ },
+ Model{},
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ state := &Model{
+ ProjectId: tt.expected.ProjectId,
+ InstanceId: tt.expected.InstanceId,
+ }
+ err := mapFields(tt.input, state, testRegion)
+ 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 *valkey.CreateInstancePayload
+ isValid bool
+ }{
+ {
+ "default_values",
+ &Model{},
+ &valkey.CreateInstancePayload{},
+ true,
+ },
+ {
+ "simple_values",
+ &Model{
+ Name: types.StringValue("name"),
+ PlanId: types.StringValue("plan"),
+ Parameters: fixtureModelParameters,
+ },
+ &valkey.CreateInstancePayload{
+ InstanceName: "name",
+ Parameters: &fixtureInstanceParameters,
+ PlanId: "plan",
+ },
+ true,
+ },
+ {
+ "null_fields_and_int_conversions",
+ &Model{
+ Name: types.StringValue(""),
+ PlanId: types.StringValue(""),
+ Parameters: fixtureModelParameters,
+ },
+ &valkey.CreateInstancePayload{
+ InstanceName: "",
+ Parameters: &fixtureInstanceParameters,
+ PlanId: "",
+ },
+ true,
+ },
+ {
+ "nil_model",
+ nil,
+ nil,
+ false,
+ },
+ {
+ "nil_parameters",
+ &Model{
+ Name: types.StringValue("name"),
+ PlanId: types.StringValue("plan"),
+ },
+ &valkey.CreateInstancePayload{
+ InstanceName: "name",
+ PlanId: "plan",
+ },
+ true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ var parameters *parametersModel
+ if tt.input != nil {
+ if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) {
+ parameters = ¶metersModel{}
+ diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{})
+ if diags.HasError() {
+ t.Fatalf("Error converting parameters: %v", diags.Errors())
+ }
+ }
+ }
+ output, err := toCreatePayload(tt.input, parameters)
+ 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)
+ if diff != "" {
+ t.Fatalf("Data does not match: %s", diff)
+ }
+ }
+ })
+ }
+}
+
+func TestToUpdatePayload(t *testing.T) {
+ tests := []struct {
+ description string
+ input *Model
+ expected *valkey.PartialUpdateInstancePayload
+ isValid bool
+ }{
+ {
+ "default_values",
+ &Model{},
+ &valkey.PartialUpdateInstancePayload{},
+ true,
+ },
+ {
+ "simple_values",
+ &Model{
+ PlanId: types.StringValue("plan"),
+ Parameters: fixtureModelParameters,
+ },
+ &valkey.PartialUpdateInstancePayload{
+ Parameters: &fixtureInstanceParameters,
+ PlanId: new("plan"),
+ },
+ true,
+ },
+ {
+ "null_fields_and_int_conversions",
+ &Model{
+ PlanId: types.StringValue(""),
+ Parameters: fixtureModelParameters,
+ },
+ &valkey.PartialUpdateInstancePayload{
+ Parameters: &fixtureInstanceParameters,
+ PlanId: new(""),
+ },
+ true,
+ },
+ {
+ "nil_model",
+ nil,
+ nil,
+ false,
+ },
+ {
+ "nil_parameters",
+ &Model{
+ PlanId: types.StringValue("plan"),
+ },
+ &valkey.PartialUpdateInstancePayload{
+ PlanId: new("plan"),
+ },
+ true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ var parameters *parametersModel
+ if tt.input != nil {
+ if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) {
+ parameters = ¶metersModel{}
+ diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{})
+ if diags.HasError() {
+ t.Fatalf("Error converting parameters: %v", diags.Errors())
+ }
+ }
+ }
+ output, err := toUpdatePayload(tt.input, parameters)
+ 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)
+ if diff != "" {
+ t.Fatalf("Data does not match: %s", diff)
+ }
+ }
+ })
+ }
+}
diff --git a/stackit/internal/services/valkey/utils/util.go b/stackit/internal/services/valkey/utils/util.go
new file mode 100644
index 000000000..a3e7aa8f6
--- /dev/null
+++ b/stackit/internal/services/valkey/utils/util.go
@@ -0,0 +1,30 @@
+package utils
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/stackitcloud/stackit-sdk-go/core/config"
+ valkey "github.com/stackitcloud/stackit-sdk-go/services/valkey/v2api"
+
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
+)
+
+func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *valkey.APIClient {
+ apiClientConfigOptions := []config.ConfigurationOption{
+ config.WithCustomAuth(providerData.RoundTripper),
+ utils.UserAgentConfigOption(providerData.Version),
+ }
+ if providerData.ValkeyCustomEndpoint != "" {
+ apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ValkeyCustomEndpoint))
+ }
+ apiClient, err := valkey.NewAPIClient(apiClientConfigOptions...)
+ if err != nil {
+ core.LogAndAddError(ctx, diags, "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 nil
+ }
+
+ return apiClient
+}
diff --git a/stackit/internal/services/valkey/utils/util_test.go b/stackit/internal/services/valkey/utils/util_test.go
new file mode 100644
index 000000000..761315cd0
--- /dev/null
+++ b/stackit/internal/services/valkey/utils/util_test.go
@@ -0,0 +1,94 @@
+package utils
+
+import (
+ "context"
+ "os"
+ "reflect"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients"
+ "github.com/stackitcloud/stackit-sdk-go/core/config"
+ valkey "github.com/stackitcloud/stackit-sdk-go/services/valkey/v2api"
+
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
+)
+
+const (
+ testVersion = "1.2.3"
+ testCustomEndpoint = "https://valkey-custom-endpoint.api.stackit.cloud"
+)
+
+func TestConfigureClient(t *testing.T) {
+ /* mock authentication by setting service account token env variable */
+ os.Clearenv()
+ err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val")
+ if err != nil {
+ t.Errorf("error setting env variable: %v", err)
+ }
+
+ type args struct {
+ providerData *core.ProviderData
+ }
+ tests := []struct {
+ name string
+ args args
+ wantErr bool
+ expected *valkey.APIClient
+ }{
+ {
+ name: "default endpoint",
+ args: args{
+ providerData: &core.ProviderData{
+ Version: testVersion,
+ },
+ },
+ expected: func() *valkey.APIClient {
+ apiClient, err := valkey.NewAPIClient(
+ utils.UserAgentConfigOption(testVersion),
+ )
+ if err != nil {
+ t.Errorf("error configuring client: %v", err)
+ }
+ return apiClient
+ }(),
+ wantErr: false,
+ },
+ {
+ name: "custom endpoint",
+ args: args{
+ providerData: &core.ProviderData{
+ Version: testVersion,
+ ValkeyCustomEndpoint: testCustomEndpoint,
+ },
+ },
+ expected: func() *valkey.APIClient {
+ apiClient, err := valkey.NewAPIClient(
+ utils.UserAgentConfigOption(testVersion),
+ config.WithEndpoint(testCustomEndpoint),
+ )
+ if err != nil {
+ t.Errorf("error configuring client: %v", err)
+ }
+ return apiClient
+ }(),
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx := context.Background()
+ diags := diag.Diagnostics{}
+
+ actual := ConfigureClient(ctx, tt.args.providerData, &diags)
+ if diags.HasError() != tt.wantErr {
+ t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr)
+ }
+
+ if !reflect.DeepEqual(actual, tt.expected) {
+ t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected)
+ }
+ })
+ }
+}
diff --git a/stackit/internal/services/valkey/valkey_acc_test.go b/stackit/internal/services/valkey/valkey_acc_test.go
new file mode 100644
index 000000000..7f797375c
--- /dev/null
+++ b/stackit/internal/services/valkey/valkey_acc_test.go
@@ -0,0 +1,326 @@
+package valkey_test
+
+import (
+ "context"
+ "fmt"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+ "github.com/stackitcloud/stackit-sdk-go/core/utils"
+ valkey "github.com/stackitcloud/stackit-sdk-go/services/valkey/v2api"
+ "github.com/stackitcloud/stackit-sdk-go/services/valkey/v2api/wait"
+
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
+)
+
+// Instance resource data
+var instanceResource = map[string]string{
+ "project_id": testutil.ProjectId,
+ "name": testutil.ResourceNameWithDateTime("valkey"),
+ "plan_id": "65778323-a25f-4cc8-ae87-b17fb051cba1",
+ "plan_name": "stackit-keyvalue-1.4.10-single",
+ "version": "8",
+ "sgw_acl_invalid": "1.2.3.4/4",
+ "sgw_acl_valid": "192.168.0.0/16",
+ "sgw_acl_valid2": "10.10.10.0/24",
+}
+
+func parametersConfig(params map[string]string) string {
+ nonStringParams := []string{
+ "down_after_milliseconds",
+ "enable_monitoring",
+ "failover_timeout",
+ "lua_time_limit",
+ "max_disk_threshold",
+ "maxclients",
+ "maxmemory_samples",
+ "metrics_frequency",
+ "min_replicas_max_lag",
+ "min_replicas_to_write",
+ "syslog",
+ }
+ var parameters strings.Builder
+ parameters.WriteString("parameters = {")
+ for k, v := range params {
+ if utils.Contains(nonStringParams, k) {
+ parameters.WriteString(fmt.Sprintf("%s = %s\n", k, v))
+ } else {
+ parameters.WriteString(fmt.Sprintf("%s = %q\n", k, v))
+ }
+ }
+ parameters.WriteString("\n}")
+ return parameters.String()
+}
+
+func resourceConfig(params map[string]string) string {
+ return fmt.Sprintf(`
+ %s
+
+ resource "stackit_valkey_instance" "instance" {
+ project_id = "%s"
+ name = "%s"
+ plan_name = "%s"
+ version = "%s"
+ %s
+ }
+
+ %s
+ `,
+ testutil.NewConfigBuilder().BuildProviderConfig(),
+ instanceResource["project_id"],
+ instanceResource["name"],
+ instanceResource["plan_name"],
+ instanceResource["version"],
+ parametersConfig(params),
+ resourceConfigCredential(),
+ )
+}
+
+func resourceConfigCredential() string {
+ return `
+ resource "stackit_valkey_credential" "credential" {
+ project_id = stackit_valkey_instance.instance.project_id
+ instance_id = stackit_valkey_instance.instance.instance_id
+ }
+ `
+}
+
+func TestAccValkeyResource(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
+ CheckDestroy: testAccCheckValkeyDestroy,
+ Steps: []resource.TestStep{
+ // Creation fail
+ {
+ Config: resourceConfig(map[string]string{"sgw_acl": instanceResource["sgw_acl_invalid"]}),
+ ExpectError: regexp.MustCompile(`.*sgw_acl is invalid.*`),
+ },
+ // Creation
+ {
+ Config: resourceConfig(map[string]string{
+ "sgw_acl": instanceResource["sgw_acl_valid"],
+ "down_after_milliseconds": "10000",
+ "enable_monitoring": "false",
+ "failover_timeout": "30000",
+ "graphite": "graphite.example.com:2003",
+ "lazyfree_lazy_eviction": "no",
+ "lazyfree_lazy_expire": "no",
+ "lua_time_limit": "5000",
+ "max_disk_threshold": "80",
+ "maxclients": "10000",
+ "maxmemory_policy": "volatile-lru",
+ "maxmemory_samples": "5",
+ "metrics_frequency": "10",
+ "metrics_prefix": "prefix",
+ "min_replicas_max_lag": "15",
+ "min_replicas_to_write": "10",
+ "notify_keyspace_events": "Ex",
+ "repl_backlog_size": "1mb",
+ "syslog": `["syslog.example.com:123"]`,
+ }),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ // Instance data
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "project_id", instanceResource["project_id"]),
+ resource.TestCheckResourceAttrSet("stackit_valkey_instance.instance", "instance_id"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "plan_id", instanceResource["plan_id"]),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "plan_name", instanceResource["plan_name"]),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "version", instanceResource["version"]),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "name", instanceResource["name"]),
+
+ // Instance Params data
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.sgw_acl", instanceResource["sgw_acl_valid"]),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.down_after_milliseconds", "10000"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.enable_monitoring", "false"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.failover_timeout", "30000"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.graphite", "graphite.example.com:2003"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.lazyfree_lazy_eviction", "no"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.lazyfree_lazy_expire", "no"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.lua_time_limit", "5000"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.max_disk_threshold", "80"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.maxclients", "10000"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.maxmemory_policy", "volatile-lru"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.maxmemory_samples", "5"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.metrics_frequency", "10"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.metrics_prefix", "prefix"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.min_replicas_max_lag", "15"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.min_replicas_to_write", "10"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.notify_keyspace_events", "Ex"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.repl_backlog_size", "1mb"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.syslog.#", "1"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.syslog.0", "syslog.example.com:123"),
+
+ // Credential data
+ resource.TestCheckResourceAttrPair(
+ "stackit_valkey_credential.credential", "project_id",
+ "stackit_valkey_instance.instance", "project_id",
+ ),
+ resource.TestCheckResourceAttrPair(
+ "stackit_valkey_credential.credential", "instance_id",
+ "stackit_valkey_instance.instance", "instance_id",
+ ),
+ resource.TestCheckResourceAttrSet("stackit_valkey_credential.credential", "credential_id"),
+ resource.TestCheckResourceAttrSet("stackit_valkey_credential.credential", "host"),
+ ),
+ },
+ // data source
+ {
+ Config: fmt.Sprintf(`
+ %s
+
+ data "stackit_valkey_instance" "instance" {
+ project_id = stackit_valkey_instance.instance.project_id
+ instance_id = stackit_valkey_instance.instance.instance_id
+ }
+
+ data "stackit_valkey_credential" "credential" {
+ project_id = stackit_valkey_credential.credential.project_id
+ instance_id = stackit_valkey_credential.credential.instance_id
+ credential_id = stackit_valkey_credential.credential.credential_id
+ }`,
+ resourceConfig(nil),
+ ),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ // Instance data
+ resource.TestCheckResourceAttr("data.stackit_valkey_instance.instance", "project_id", instanceResource["project_id"]),
+ resource.TestCheckResourceAttrPair("stackit_valkey_instance.instance", "instance_id",
+ "data.stackit_valkey_credential.credential", "instance_id"),
+ resource.TestCheckResourceAttrPair("data.stackit_valkey_instance.instance", "instance_id",
+ "data.stackit_valkey_credential.credential", "instance_id"),
+ resource.TestCheckResourceAttr("data.stackit_valkey_instance.instance", "plan_id", instanceResource["plan_id"]),
+ resource.TestCheckResourceAttr("data.stackit_valkey_instance.instance", "name", instanceResource["name"]),
+ resource.TestCheckResourceAttrSet("data.stackit_valkey_instance.instance", "parameters.sgw_acl"),
+
+ // Credentials data
+ resource.TestCheckResourceAttr("data.stackit_valkey_credential.credential", "project_id", instanceResource["project_id"]),
+ resource.TestCheckResourceAttrSet("data.stackit_valkey_credential.credential", "credential_id"),
+ resource.TestCheckResourceAttrSet("data.stackit_valkey_credential.credential", "host"),
+ resource.TestCheckResourceAttrSet("data.stackit_valkey_credential.credential", "port"),
+ resource.TestCheckResourceAttrSet("data.stackit_valkey_credential.credential", "uri"),
+ resource.TestCheckResourceAttrSet("data.stackit_valkey_credential.credential", "load_balanced_host"),
+ ),
+ },
+ // Import
+ {
+ ResourceName: "stackit_valkey_instance.instance",
+ ImportStateIdFunc: func(s *terraform.State) (string, error) {
+ r, ok := s.RootModule().Resources["stackit_valkey_instance.instance"]
+ if !ok {
+ return "", fmt.Errorf("couldn't find resource stackit_valkey_instance.instance")
+ }
+ region, ok := r.Primary.Attributes["region"]
+ if !ok {
+ return "", fmt.Errorf("couldn't find attribute region")
+ }
+ instanceId, ok := r.Primary.Attributes["instance_id"]
+ if !ok {
+ return "", fmt.Errorf("couldn't find attribute instance_id")
+ }
+ return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, instanceId), nil
+ },
+ ImportState: true,
+ ImportStateVerify: true,
+ },
+ {
+ ResourceName: "stackit_valkey_credential.credential",
+ ImportStateIdFunc: func(s *terraform.State) (string, error) {
+ r, ok := s.RootModule().Resources["stackit_valkey_credential.credential"]
+ if !ok {
+ return "", fmt.Errorf("couldn't find resource stackit_valkey_credential.credential")
+ }
+ region, ok := r.Primary.Attributes["region"]
+ if !ok {
+ return "", fmt.Errorf("couldn't find attribute region")
+ }
+ instanceId, ok := r.Primary.Attributes["instance_id"]
+ if !ok {
+ return "", fmt.Errorf("couldn't find attribute instance_id")
+ }
+ credentialId, ok := r.Primary.Attributes["credential_id"]
+ if !ok {
+ return "", fmt.Errorf("couldn't find attribute credential_id")
+ }
+ return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, region, instanceId, credentialId), nil
+ },
+ ImportState: true,
+ ImportStateVerify: true,
+ },
+ // Update
+ {
+ Config: resourceConfig(map[string]string{"sgw_acl": instanceResource["sgw_acl_valid2"]}),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ // Instance data
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "project_id", instanceResource["project_id"]),
+ resource.TestCheckResourceAttrSet("stackit_valkey_instance.instance", "instance_id"),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "plan_id", instanceResource["plan_id"]),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "plan_name", instanceResource["plan_name"]),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "version", instanceResource["version"]),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "name", instanceResource["name"]),
+ resource.TestCheckResourceAttr("stackit_valkey_instance.instance", "parameters.sgw_acl", instanceResource["sgw_acl_valid2"]),
+ ),
+ },
+ // Deletion is done by the framework implicitly
+ },
+ })
+}
+
+func checkInstanceDeleteSuccess(i *valkey.Instance) bool {
+ if i.LastOperation.Type != valkey.INSTANCELASTOPERATIONTYPE_DELETE {
+ return false
+ }
+
+ if i.LastOperation.Type == valkey.INSTANCELASTOPERATIONTYPE_DELETE {
+ if i.LastOperation.State != "succeeded" {
+ return false
+ } else if strings.Contains(i.LastOperation.Description, "DeleteFailed") || strings.Contains(i.LastOperation.Description, "failed") {
+ return false
+ }
+ }
+ return true
+}
+
+func testAccCheckValkeyDestroy(s *terraform.State) error {
+ ctx := context.Background()
+ client, err := valkey.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.ValkeyCustomEndpoint, false)...)
+ if err != nil {
+ return fmt.Errorf("creating client: %w", err)
+ }
+
+ instancesToDestroy := []string{}
+ for _, rs := range s.RootModule().Resources {
+ if rs.Type != "stackit_valkey_instance" {
+ continue
+ }
+ // instance terraform ID: "[project_id],[region],[instance_id]"
+ instanceId := strings.Split(rs.Primary.ID, core.Separator)[2]
+ instancesToDestroy = append(instancesToDestroy, instanceId)
+ }
+
+ instancesResp, err := client.DefaultAPI.ListInstances(ctx, testutil.ProjectId, "eu01").Execute()
+ if err != nil {
+ return fmt.Errorf("getting instancesResp: %w", err)
+ }
+
+ instances := instancesResp.Instances
+ for i := range instances {
+ if instances[i].InstanceId == nil {
+ continue
+ }
+ if utils.Contains(instancesToDestroy, *instances[i].InstanceId) {
+ if !checkInstanceDeleteSuccess(&instances[i]) {
+ err := client.DefaultAPI.DeleteInstance(ctx, testutil.ProjectId, "eu01", *instances[i].InstanceId).Execute()
+ if err != nil {
+ return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].InstanceId, err)
+ }
+ _, err = wait.DeleteInstanceWaitHandler(ctx, client.DefaultAPI, testutil.ProjectId, "eu01", *instances[i].InstanceId).WaitWithContext(ctx)
+ if err != nil {
+ return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].InstanceId, err)
+ }
+ }
+ }
+ }
+ return nil
+}
diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go
index 512a08641..45216cbc3 100644
--- a/stackit/internal/testutil/testutil.go
+++ b/stackit/internal/testutil/testutil.go
@@ -105,6 +105,7 @@ var (
IntakeCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_INTAKE_CUSTOM_ENDPOINT", providerName: "intake_custom_endpoint"}
TelemetryRouterCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_TELEMETRYROUTER_CUSTOM_ENDPOINT", providerName: "telemetryrouter_custom_endpoint"}
TelemetryLinkCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_TELEMETRYLINK_CUSTOM_ENDPOINT", providerName: "telemetrylink_custom_endpoint"}
+ ValkeyCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_VALKEY_CUSTOM_ENDPOINT", providerName: "valkey_custom_endpoint"}
allCustomEndpoints = []customEndpointConfig{
ALBCustomEndpoint,
@@ -142,6 +143,7 @@ var (
SKECustomEndpoint,
TelemetryRouterCustomEndpoint,
TelemetryLinkCustomEndpoint,
+ ValkeyCustomEndpoint,
}
)
diff --git a/stackit/provider.go b/stackit/provider.go
index e436c837f..a8b8f2e83 100644
--- a/stackit/provider.go
+++ b/stackit/provider.go
@@ -137,6 +137,8 @@ import (
telemetryRouterAccessToken "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetryrouter/accesstoken"
telemetryRouterDestination "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetryrouter/destination"
telemetryRouterInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetryrouter/instance"
+ valkeyCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/valkey/credential"
+ valkeyInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/valkey/instance"
vpnConnection "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/connection"
vpnGateway "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/gateway"
vpnGatewayStatus "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/gateway_status"
@@ -220,6 +222,7 @@ type providerModel struct {
TelemetryLinkCustomEndpoint types.String `tfsdk:"telemetrylink_custom_endpoint"`
TelemetryRouterCustomEndpoint types.String `tfsdk:"telemetryrouter_custom_endpoint"`
TokenCustomEndpoint types.String `tfsdk:"token_custom_endpoint"`
+ ValkeyCustomEndpoint types.String `tfsdk:"valkey_custom_endpoint"`
VpnCustomEndpoint types.String `tfsdk:"vpn_custom_endpoint"`
OIDCTokenRequestURL types.String `tfsdk:"oidc_request_url"`
OIDCTokenRequestToken types.String `tfsdk:"oidc_request_token"`
@@ -281,6 +284,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro
"telemetrylink_custom_endpoint": "Custom endpoint for the Telemetry Link service",
"telemetryrouter_custom_endpoint": "Custom endpoint for the Telemetry Router service",
"token_custom_endpoint": "Custom endpoint for the token API, which is used to request access tokens when using the key flow",
+ "valkey_custom_endpoint": "Custom endpoint for the Key Value Store service",
"vpn_custom_endpoint": "Custom endpoint for the VPN service",
"enable_beta_resources": "Enable beta resources. Default is false.",
"experiments": fmt.Sprintf("Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: %v", strings.Join(features.AvailableExperiments, ", ")),
@@ -497,6 +501,10 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro
Optional: true,
Description: descriptions["telemetrylink_custom_endpoint"],
},
+ "valkey_custom_endpoint": schema.StringAttribute{
+ Optional: true,
+ Description: descriptions["valkey_custom_endpoint"],
+ },
"vpn_custom_endpoint": schema.StringAttribute{
Optional: true,
Description: descriptions["vpn_custom_endpoint"],
@@ -591,6 +599,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest,
setStringField(providerConfig.SqlServerFlexCustomEndpoint, func(v string) { providerData.SQLServerFlexCustomEndpoint = v })
setStringField(providerConfig.TelemetryRouterCustomEndpoint, func(v string) { providerData.TelemetryRouterCustomEndpoint = v })
setStringField(providerConfig.TelemetryLinkCustomEndpoint, func(v string) { providerData.TelemetryLinkCustomEndpoint = v })
+ setStringField(providerConfig.ValkeyCustomEndpoint, func(v string) { providerData.ValkeyCustomEndpoint = v })
setStringField(providerConfig.VpnCustomEndpoint, func(v string) { providerData.VpnCustomEndpoint = v })
if !(providerConfig.Experiments.IsUnknown() || providerConfig.Experiments.IsNull()) {
@@ -771,6 +780,8 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
telemetryRouterInstance.NewTelemetryRouterInstanceDataSource,
telemetryRouterDestination.NewTelemetryRouterDestinationDataSource,
telemetryLink.NewTelemetryLinkDataSource,
+ valkeyInstance.NewInstanceDataSource,
+ valkeyCredential.NewCredentialDataSource,
vpnGateway.NewVPNGatewayDataSource,
vpnGatewayStatus.NewVPNGatewayStatusDataSource,
vpnConnection.NewVPNConnectionDataSource,
@@ -883,6 +894,8 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
telemetryRouterInstance.NewTelemetryRouterInstanceResource,
telemetryRouterDestination.NewTelemetryRouterDestinationResource,
telemetryLink.NewTelemetryLinkResource,
+ valkeyInstance.NewInstanceResource,
+ valkeyCredential.NewCredentialResource,
vpnConnection.NewVpnConnectionResource,
vpnGateway.NewGatewayResource,
}
diff --git a/stackit/testdata/provider-all-attributes.tf b/stackit/testdata/provider-all-attributes.tf
index 6f8067792..f421bf50b 100644
--- a/stackit/testdata/provider-all-attributes.tf
+++ b/stackit/testdata/provider-all-attributes.tf
@@ -33,6 +33,7 @@ provider "stackit" {
ske_custom_endpoint = "https://ske.api.stackit.cloud"
service_enablement_custom_endpoint = "https://service-enablement.api.stackit.cloud"
token_custom_endpoint = "https://token.api.stackit.cloud"
+ valkey_custom_endpoint = "https://valkey.api.stackit.cloud"
enable_beta_resources = "true"
}