diff --git a/docs/resources/node_policy.md b/docs/resources/node_policy.md
index b652d57..f060171 100644
--- a/docs/resources/node_policy.md
+++ b/docs/resources/node_policy.md
@@ -108,10 +108,9 @@ resource "devzero_node_policy" "aws_comprehensive" {
# Disruption policy for cost optimization
disruption = {
- consolidate_after = "15m"
- consolidation_policy = "WhenEmptyOrUnderutilized"
- expire_after = "720h" # 30 days
- ttl_seconds_after_empty = 300 # 5 minutes
+ consolidate_after = "15m"
+ consolidation_policy = "WhenEmptyOrUnderutilized"
+ expire_after = "720h" # 30 days
budgets = [
{
diff --git a/docs/resources/node_policy_target.md b/docs/resources/node_policy_target.md
index 29073c3..6e991d6 100644
--- a/docs/resources/node_policy_target.md
+++ b/docs/resources/node_policy_target.md
@@ -13,21 +13,19 @@ Attaches a node policy to specific clusters. Node policy targets determine which
## Example Usage
```terraform
-# Prerequisites - need cluster and node policy resources
+# Prerequisites
resource "devzero_cluster" "production" {
name = "production-cluster"
}
-resource "devzero_node_policy" "general" {
- name = "general-purpose"
- node_pool_name = "general-pool"
- node_class_name = "general-class"
+resource "devzero_node_policy" "standard_nodes" {
+ name = "standard-nodes"
}
# Minimal example - only required attributes
resource "devzero_node_policy_target" "minimal" {
name = "production-clusters"
- policy_id = devzero_node_policy.general.id
+ policy_id = devzero_node_policy.standard_nodes.id
cluster_ids = [devzero_cluster.production.id]
# Defaults applied automatically:
@@ -37,13 +35,12 @@ resource "devzero_node_policy_target" "minimal" {
# Comprehensive example - all attributes
resource "devzero_node_policy_target" "comprehensive" {
- name = "production-general-target"
- description = "Applies general purpose node policy to production clusters"
- policy_id = devzero_node_policy.general.id
+ name = "cluster-nodes"
+ description = "Applies standard node policy to production clusters"
+ policy_id = devzero_node_policy.standard_nodes.id
enabled = true
cluster_ids = [
devzero_cluster.production.id,
- # Add more cluster IDs as needed
]
}
@@ -63,7 +60,7 @@ resource "devzero_cluster" "eu_west" {
resource "devzero_node_policy_target" "multi_cluster" {
name = "all-production-clusters"
description = "Apply cost optimization policy to all production clusters"
- policy_id = devzero_node_policy.general.id
+ policy_id = devzero_node_policy.standard_nodes.id
enabled = true
cluster_ids = [
devzero_cluster.us_east.id,
@@ -76,7 +73,7 @@ resource "devzero_node_policy_target" "multi_cluster" {
resource "devzero_node_policy_target" "disabled" {
name = "staging-clusters"
description = "Temporarily disabled while testing new policy"
- policy_id = devzero_node_policy.general.id
+ policy_id = devzero_node_policy.standard_nodes.id
enabled = false # Target exists but is not active
cluster_ids = [devzero_cluster.production.id]
}
diff --git a/docs/resources/workload_policy.md b/docs/resources/workload_policy.md
index 9560170..ab7c417 100644
--- a/docs/resources/workload_policy.md
+++ b/docs/resources/workload_policy.md
@@ -13,64 +13,51 @@ Configures DevZero workload recommendation policies, including triggers, scaling
## Example Usage
```terraform
-# Only required attributes
-resource "devzero_workload_policy" "workload_policy" {
- name = "terraform-example"
+# Minimal — only required attributes
+resource "devzero_workload_policy" "minimal" {
+ name = "cost-saving-policy"
action_triggers = ["on_detection", "on_schedule"]
}
-# All attributes
-resource "devzero_workload_policy" "workload_policy" {
- name = "terraform-example"
- description = "some description"
- action_triggers = ["on_detection", "on_schedule"]
- cron_schedule = "*/15 * * * *" # Every 15th minute
- detection_triggers = ["pod_creation", "pod_update"]
- loopback_period_seconds = 3600 # 1 hour
- startup_period_seconds = 60 # 1 minute
- live_migration_enabled = true
- scheduler_plugins = ["dz-scheduler"]
- defragmentation_schedule = "*/15 * * * *"
+# Full example — values kept in sync with the Pulumi provider
+resource "devzero_workload_policy" "cost_saving" {
+ name = "cost-saving-policy"
+ description = "Rightsize non-critical workloads"
+ action_triggers = ["on_detection", "on_schedule"]
+ cron_schedule = "*/15 * * * *" # every 15 min; required when "on_schedule" is set
+ detection_triggers = ["pod_creation", "pod_update"] # used when "on_detection" is set
+ loopback_period_seconds = 86400 # 1 day — lookback window for usage data
+ cooldown_minutes = 300 # 5 hours between successive scale-down actions
+ min_data_points = 20 # min samples before any recommendation
+ min_change_percent = 0.2 # apply only if change > 20%
cpu_vertical_scaling = {
- enabled = true
- min_request = 1000
- max_request = 2000
- overhead_multiplier = 0.05 # 5%
- limits_adjustment_enabled = true
+ enabled = true
+ target_percentile = 0.75 # P75 of observed usage
+ min_request = 25 # millicores; hard floor
+ max_scale_up_percent = 1000 # max % increase per step
+ max_scale_down_percent = 1 # max % decrease per step
+ min_data_points = 20 # min CPU samples before recommendation
+ adjust_req_even_if_not_set = true # set requests even if workload has none
+ limits_removal_enabled = true # strip CPU limits (cycles compress safely)
}
memory_vertical_scaling = {
- enabled = true
- min_request = 1000
- max_request = 2000
- overhead_multiplier = 0.05 # 5%
- limits_adjustment_enabled = true
+ enabled = true
+ target_percentile = 1 # P100 — guard against OOMKills
+ min_request = 134217728 # 128 MiB in bytes; hard floor
+ max_scale_up_percent = 1000 # max % increase per step
+ max_scale_down_percent = 1 # max % decrease per step
+ overhead_multiplier = 0.3 # extra headroom over the recommendation
+ limits_adjustment_enabled = true # adjust limits alongside requests
+ limit_multiplier = 1 # limits = request × this
+ min_data_points = 20 # min memory samples before recommendation
+ adjust_req_even_if_not_set = true # set requests even if workload has none
+ limits_removal_enabled = false # memory limits removal not supported
}
- gpu_vertical_scaling = {
- enabled = true
- min_request = 1000
- max_request = 2000
- overhead_multiplier = 0.05 # 5%
- limits_adjustment_enabled = false
- }
-
- gpu_vram_vertical_scaling = {
- enabled = true
- min_request = 1000
- max_request = 2000
- overhead_multiplier = 0.05 # 5%
- limits_adjustment_enabled = false
- }
-
- horizontal_scaling = {
- enabled = true
- min_replicas = 1
- max_replicas = 2
- overhead_multiplier = 0.05 # 5%
- limits_adjustment_enabled = true
- }
+ enable_pmax_protection = true # guard against spike-induced OOMKills
+ pmax_ratio_threshold = 3 # raise requests when peak is 3× the recommendation
}
```
@@ -91,6 +78,7 @@ resource "devzero_workload_policy" "workload_policy" {
- `description` (String) Free-form description of the policy to help others understand its intent and scope.
- `detection_triggers` (List of String) Detection triggers for when to apply the workload policy. Only one of `pod_creation` or `pod_update` is allowed.The `pod_creation` trigger is used to apply the workload policy when a pod is created.The `pod_update` trigger is used to apply the workload policy when a pod is updated.
- `drift_delta_percent` (Number) Percentage drift from baseline that triggers VPA refresh
+- `enable_pmax_protection` (Boolean) When true, the recommender raises requests to cover observed peak usage when the peak-to-recommendation ratio exceeds `pmax_ratio_threshold`. Default: false.
- `gpu_vertical_scaling` (Attributes) GPU vertical scaling options (see [below for nested schema](#nestedatt--gpu_vertical_scaling))
- `gpu_vram_vertical_scaling` (Attributes) GPU VRAM vertical scaling options (see [below for nested schema](#nestedatt--gpu_vram_vertical_scaling))
- `horizontal_scaling` (Attributes) Horizontal scaling options (see [below for nested schema](#nestedatt--horizontal_scaling))
@@ -101,6 +89,7 @@ resource "devzero_workload_policy" "workload_policy" {
- `min_change_percent` (Number) Global minimum change threshold for applying recommendations
- `min_data_points` (Number) Global minimum data points required for recommendations
- `min_vpa_window_data_points` (Number) Minimum data points in VPA analysis window
+- `pmax_ratio_threshold` (Number) Peak-to-recommendation ratio above which pmax protection activates. Example: 3.0 — triggers when peak is 3× the recommendation. Default: 3.0.
- `scheduler_plugins` (List of String) Kubernetes scheduler plugins to activate
- `stability_cv_max` (Number) Maximum coefficient of variation to consider stable
- `startup_period_seconds` (Number) Startup period seconds of the workload policy. The startup period is the period of time to ignore resource usage data after the workload is started.
@@ -114,9 +103,11 @@ resource "devzero_workload_policy" "workload_policy" {
Optional:
+- `adjust_req_even_if_not_set` (Boolean) When true, the recommender will suggest resource requests even if the workload currently has none set. Default: false.
- `enabled` (Boolean) Enable or disable vertical scaling for this resource. When disabled, vertical recommendations will not be applied.
- `limit_multiplier` (Number) How much higher limits should be vs requests (e.g., 2.0 = 2x the request).
- `limits_adjustment_enabled` (Boolean) Allow recommender to adjust container limits as well as requests. When disabled, only requests are modified.
+- `limits_removal_enabled` (Boolean) When true, the recommender will remove resource limits from workloads (CPU axis only — memory limits removal is not supported). Default: false.
- `max_request` (Number) Upper bound for container resource requests (e.g., CPU millicores or memory bytes) considered by the recommender.
- `max_scale_down_percent` (Number) Maximum percent to scale down in one step
- `max_scale_up_percent` (Number) Maximum percent to scale up in one step
@@ -131,9 +122,11 @@ Optional:
Optional:
+- `adjust_req_even_if_not_set` (Boolean) When true, the recommender will suggest resource requests even if the workload currently has none set. Default: false.
- `enabled` (Boolean) Enable or disable vertical scaling for this resource. When disabled, vertical recommendations will not be applied.
- `limit_multiplier` (Number) How much higher limits should be vs requests (e.g., 2.0 = 2x the request).
- `limits_adjustment_enabled` (Boolean) Allow recommender to adjust container limits as well as requests. When disabled, only requests are modified.
+- `limits_removal_enabled` (Boolean) When true, the recommender will remove resource limits from workloads (CPU axis only — memory limits removal is not supported). Default: false.
- `max_request` (Number) Upper bound for container resource requests (e.g., CPU millicores or memory bytes) considered by the recommender.
- `max_scale_down_percent` (Number) Maximum percent to scale down in one step
- `max_scale_up_percent` (Number) Maximum percent to scale up in one step
@@ -148,9 +141,11 @@ Optional:
Optional:
+- `adjust_req_even_if_not_set` (Boolean) When true, the recommender will suggest resource requests even if the workload currently has none set. Default: false.
- `enabled` (Boolean) Enable or disable vertical scaling for this resource. When disabled, vertical recommendations will not be applied.
- `limit_multiplier` (Number) How much higher limits should be vs requests (e.g., 2.0 = 2x the request).
- `limits_adjustment_enabled` (Boolean) Allow recommender to adjust container limits as well as requests. When disabled, only requests are modified.
+- `limits_removal_enabled` (Boolean) When true, the recommender will remove resource limits from workloads (CPU axis only — memory limits removal is not supported). Default: false.
- `max_request` (Number) Upper bound for container resource requests (e.g., CPU millicores or memory bytes) considered by the recommender.
- `max_scale_down_percent` (Number) Maximum percent to scale down in one step
- `max_scale_up_percent` (Number) Maximum percent to scale up in one step
@@ -179,9 +174,11 @@ Optional:
Optional:
+- `adjust_req_even_if_not_set` (Boolean) When true, the recommender will suggest resource requests even if the workload currently has none set. Default: false.
- `enabled` (Boolean) Enable or disable vertical scaling for this resource. When disabled, vertical recommendations will not be applied.
- `limit_multiplier` (Number) How much higher limits should be vs requests (e.g., 2.0 = 2x the request).
- `limits_adjustment_enabled` (Boolean) Allow recommender to adjust container limits as well as requests. When disabled, only requests are modified.
+- `limits_removal_enabled` (Boolean) When true, the recommender will remove resource limits from workloads (CPU axis only — memory limits removal is not supported). Default: false.
- `max_request` (Number) Upper bound for container resource requests (e.g., CPU millicores or memory bytes) considered by the recommender.
- `max_scale_down_percent` (Number) Maximum percent to scale down in one step
- `max_scale_up_percent` (Number) Maximum percent to scale up in one step
diff --git a/docs/resources/workload_policy_target.md b/docs/resources/workload_policy_target.md
index 667a93c..5b34bc8 100644
--- a/docs/resources/workload_policy_target.md
+++ b/docs/resources/workload_policy_target.md
@@ -13,39 +13,44 @@ Defines which workloads a policy applies to by selecting namespaces, workloads,
## Example Usage
```terraform
-resource "devzero_cluster" "cluster" {
- name = "terraform-example"
+resource "devzero_cluster" "production" {
+ name = "production-cluster"
}
-resource "devzero_workload_policy" "workload_policy" {
- name = "terraform-example"
+resource "devzero_workload_policy" "cost_saving" {
+ name = "cost-saving-policy"
}
-# Only required attributes
-resource "devzero_workload_policy_target" "workload_policy_target" {
- name = "terraform-example"
- policy_id = devzero_workload_policy.workload_policy.id
- cluster_ids = [devzero_cluster.cluster.id]
+# Minimal — only required attributes
+resource "devzero_workload_policy_target" "minimal" {
+ name = "production-target"
+ policy_id = devzero_workload_policy.cost_saving.id
+ cluster_ids = [devzero_cluster.production.id]
}
# All attributes
-resource "devzero_workload_policy_target" "workload_policy_target" {
+resource "devzero_workload_policy_target" "full" {
name = "terraform-example"
description = "some description"
- policy_id = devzero_workload_policy.workload_policy.id
- cluster_ids = [devzero_cluster.cluster.id]
+ policy_id = devzero_workload_policy.cost_saving.id
+ cluster_ids = [devzero_cluster.production.id]
priority = 1
enabled = true
workload_names = ["workload-1", "workload-2"] # Empty list means all workloads
node_group_names = ["node-group-1", "node-group-2"] # Empty list means all node groups
- kind_filter = ["Deployment", "ReplicaSet"] # Empty list means all kinds
+ kind_filter = ["Deployment", "StatefulSet"] # Empty list means all kinds
name_pattern = {
pattern = "terraform-example"
flags = "i"
}
+ namespace_pattern = {
+ pattern = "^prod-"
+ flags = "i" # case-insensitive
+ }
+
namespace_selector = {
match_labels = {
app = "terraform-example"
@@ -75,6 +80,7 @@ resource "devzero_workload_policy_target" "workload_policy_target" {
- `enabled` (Boolean) Enable or disable this target. When disabled, the associated policy will not apply to the selected workloads.
- `kind_filter` (List of String) Restrict matching to specific Kubernetes kinds. Allowed values: `Pod`, `Job`, `Deployment`, `StatefulSet`, `DaemonSet`, `ReplicaSet`, `CronJob`, `ReplicationController`, `Rollout`.
- `name_pattern` (Attributes) Regex to match workload names. Useful to target rollouts or name conventions (e.g., `^api-.*`). (see [below for nested schema](#nestedatt--name_pattern))
+- `namespace_pattern` (Attributes) Regex to match namespace names. Useful when namespaces follow a naming convention (e.g., `^prod-`). (see [below for nested schema](#nestedatt--namespace_pattern))
- `namespace_selector` (Attributes) Select namespaces by labels. Uses the same semantics as Kubernetes label selectors. (see [below for nested schema](#nestedatt--namespace_selector))
- `node_group_names` (List of String) Restrict matching to specific node groups by name
- `priority` (Number) Evaluation priority among multiple targets. Higher values take precedence when multiple targets overlap.
@@ -94,6 +100,15 @@ Optional:
- `pattern` (String) Regular expression applied to workload names. Uses RE2 syntax. Example: `^api-(staging|prod)-.*$`.
+
+### Nested Schema for `namespace_pattern`
+
+Optional:
+
+- `flags` (String) Regex flags to modify matching behavior. Supported: `i` (case-insensitive), `m` (multi-line).
+- `pattern` (String) Regular expression applied to workload names. Uses RE2 syntax. Example: `^api-(staging|prod)-.*$`.
+
+
### Nested Schema for `namespace_selector`
diff --git a/examples/resources/devzero_node_policy/resource.tf b/examples/resources/devzero_node_policy/resource.tf
index 49a7e94..d2e673b 100644
--- a/examples/resources/devzero_node_policy/resource.tf
+++ b/examples/resources/devzero_node_policy/resource.tf
@@ -93,10 +93,9 @@ resource "devzero_node_policy" "aws_comprehensive" {
# Disruption policy for cost optimization
disruption = {
- consolidate_after = "15m"
- consolidation_policy = "WhenEmptyOrUnderutilized"
- expire_after = "720h" # 30 days
- ttl_seconds_after_empty = 300 # 5 minutes
+ consolidate_after = "15m"
+ consolidation_policy = "WhenEmptyOrUnderutilized"
+ expire_after = "720h" # 30 days
budgets = [
{
diff --git a/examples/resources/devzero_node_policy_target/resource.tf b/examples/resources/devzero_node_policy_target/resource.tf
index 32737f3..df09938 100644
--- a/examples/resources/devzero_node_policy_target/resource.tf
+++ b/examples/resources/devzero_node_policy_target/resource.tf
@@ -1,18 +1,16 @@
-# Prerequisites - need cluster and node policy resources
+# Prerequisites
resource "devzero_cluster" "production" {
name = "production-cluster"
}
-resource "devzero_node_policy" "general" {
- name = "general-purpose"
- node_pool_name = "general-pool"
- node_class_name = "general-class"
+resource "devzero_node_policy" "standard_nodes" {
+ name = "standard-nodes"
}
# Minimal example - only required attributes
resource "devzero_node_policy_target" "minimal" {
name = "production-clusters"
- policy_id = devzero_node_policy.general.id
+ policy_id = devzero_node_policy.standard_nodes.id
cluster_ids = [devzero_cluster.production.id]
# Defaults applied automatically:
@@ -22,13 +20,12 @@ resource "devzero_node_policy_target" "minimal" {
# Comprehensive example - all attributes
resource "devzero_node_policy_target" "comprehensive" {
- name = "production-general-target"
- description = "Applies general purpose node policy to production clusters"
- policy_id = devzero_node_policy.general.id
+ name = "cluster-nodes"
+ description = "Applies standard node policy to production clusters"
+ policy_id = devzero_node_policy.standard_nodes.id
enabled = true
cluster_ids = [
devzero_cluster.production.id,
- # Add more cluster IDs as needed
]
}
@@ -48,7 +45,7 @@ resource "devzero_cluster" "eu_west" {
resource "devzero_node_policy_target" "multi_cluster" {
name = "all-production-clusters"
description = "Apply cost optimization policy to all production clusters"
- policy_id = devzero_node_policy.general.id
+ policy_id = devzero_node_policy.standard_nodes.id
enabled = true
cluster_ids = [
devzero_cluster.us_east.id,
@@ -61,7 +58,7 @@ resource "devzero_node_policy_target" "multi_cluster" {
resource "devzero_node_policy_target" "disabled" {
name = "staging-clusters"
description = "Temporarily disabled while testing new policy"
- policy_id = devzero_node_policy.general.id
+ policy_id = devzero_node_policy.standard_nodes.id
enabled = false # Target exists but is not active
cluster_ids = [devzero_cluster.production.id]
}
diff --git a/examples/resources/devzero_workload_policy/resource.tf b/examples/resources/devzero_workload_policy/resource.tf
index 92699c7..f74b9c5 100644
--- a/examples/resources/devzero_workload_policy/resource.tf
+++ b/examples/resources/devzero_workload_policy/resource.tf
@@ -1,59 +1,46 @@
-# Only required attributes
-resource "devzero_workload_policy" "workload_policy" {
- name = "terraform-example"
+# Minimal — only required attributes
+resource "devzero_workload_policy" "minimal" {
+ name = "cost-saving-policy"
action_triggers = ["on_detection", "on_schedule"]
}
-# All attributes
-resource "devzero_workload_policy" "workload_policy" {
- name = "terraform-example"
- description = "some description"
- action_triggers = ["on_detection", "on_schedule"]
- cron_schedule = "*/15 * * * *" # Every 15th minute
- detection_triggers = ["pod_creation", "pod_update"]
- loopback_period_seconds = 3600 # 1 hour
- startup_period_seconds = 60 # 1 minute
- live_migration_enabled = true
- scheduler_plugins = ["dz-scheduler"]
- defragmentation_schedule = "*/15 * * * *"
+# Full example — values kept in sync with the Pulumi provider
+resource "devzero_workload_policy" "cost_saving" {
+ name = "cost-saving-policy"
+ description = "Rightsize non-critical workloads"
+ action_triggers = ["on_detection", "on_schedule"]
+ cron_schedule = "*/15 * * * *" # every 15 min; required when "on_schedule" is set
+ detection_triggers = ["pod_creation", "pod_update"] # used when "on_detection" is set
+ loopback_period_seconds = 86400 # 1 day — lookback window for usage data
+ cooldown_minutes = 300 # 5 hours between successive scale-down actions
+ min_data_points = 20 # min samples before any recommendation
+ min_change_percent = 0.2 # apply only if change > 20%
cpu_vertical_scaling = {
- enabled = true
- min_request = 1000
- max_request = 2000
- overhead_multiplier = 0.05 # 5%
- limits_adjustment_enabled = true
+ enabled = true
+ target_percentile = 0.75 # P75 of observed usage
+ min_request = 25 # millicores; hard floor
+ max_scale_up_percent = 1000 # max % increase per step
+ max_scale_down_percent = 1 # max % decrease per step
+ min_data_points = 20 # min CPU samples before recommendation
+ adjust_req_even_if_not_set = true # set requests even if workload has none
+ limits_removal_enabled = true # strip CPU limits (cycles compress safely)
}
memory_vertical_scaling = {
- enabled = true
- min_request = 1000
- max_request = 2000
- overhead_multiplier = 0.05 # 5%
- limits_adjustment_enabled = true
+ enabled = true
+ target_percentile = 1 # P100 — guard against OOMKills
+ min_request = 134217728 # 128 MiB in bytes; hard floor
+ max_scale_up_percent = 1000 # max % increase per step
+ max_scale_down_percent = 1 # max % decrease per step
+ overhead_multiplier = 0.3 # extra headroom over the recommendation
+ limits_adjustment_enabled = true # adjust limits alongside requests
+ limit_multiplier = 1 # limits = request × this
+ min_data_points = 20 # min memory samples before recommendation
+ adjust_req_even_if_not_set = true # set requests even if workload has none
+ limits_removal_enabled = false # memory limits removal not supported
}
- gpu_vertical_scaling = {
- enabled = true
- min_request = 1000
- max_request = 2000
- overhead_multiplier = 0.05 # 5%
- limits_adjustment_enabled = false
- }
-
- gpu_vram_vertical_scaling = {
- enabled = true
- min_request = 1000
- max_request = 2000
- overhead_multiplier = 0.05 # 5%
- limits_adjustment_enabled = false
- }
-
- horizontal_scaling = {
- enabled = true
- min_replicas = 1
- max_replicas = 2
- overhead_multiplier = 0.05 # 5%
- limits_adjustment_enabled = true
- }
+ enable_pmax_protection = true # guard against spike-induced OOMKills
+ pmax_ratio_threshold = 3 # raise requests when peak is 3× the recommendation
}
\ No newline at end of file
diff --git a/examples/resources/devzero_workload_policy_target/resource.tf b/examples/resources/devzero_workload_policy_target/resource.tf
index 43a0fd6..cc09f24 100644
--- a/examples/resources/devzero_workload_policy_target/resource.tf
+++ b/examples/resources/devzero_workload_policy_target/resource.tf
@@ -1,36 +1,41 @@
-resource "devzero_cluster" "cluster" {
- name = "terraform-example"
+resource "devzero_cluster" "production" {
+ name = "production-cluster"
}
-resource "devzero_workload_policy" "workload_policy" {
- name = "terraform-example"
+resource "devzero_workload_policy" "cost_saving" {
+ name = "cost-saving-policy"
}
-# Only required attributes
-resource "devzero_workload_policy_target" "workload_policy_target" {
- name = "terraform-example"
- policy_id = devzero_workload_policy.workload_policy.id
- cluster_ids = [devzero_cluster.cluster.id]
+# Minimal — only required attributes
+resource "devzero_workload_policy_target" "minimal" {
+ name = "production-target"
+ policy_id = devzero_workload_policy.cost_saving.id
+ cluster_ids = [devzero_cluster.production.id]
}
# All attributes
-resource "devzero_workload_policy_target" "workload_policy_target" {
+resource "devzero_workload_policy_target" "full" {
name = "terraform-example"
description = "some description"
- policy_id = devzero_workload_policy.workload_policy.id
- cluster_ids = [devzero_cluster.cluster.id]
+ policy_id = devzero_workload_policy.cost_saving.id
+ cluster_ids = [devzero_cluster.production.id]
priority = 1
enabled = true
workload_names = ["workload-1", "workload-2"] # Empty list means all workloads
node_group_names = ["node-group-1", "node-group-2"] # Empty list means all node groups
- kind_filter = ["Deployment", "ReplicaSet"] # Empty list means all kinds
+ kind_filter = ["Deployment", "StatefulSet"] # Empty list means all kinds
name_pattern = {
pattern = "terraform-example"
flags = "i"
}
+ namespace_pattern = {
+ pattern = "^prod-"
+ flags = "i" # case-insensitive
+ }
+
namespace_selector = {
match_labels = {
app = "terraform-example"
@@ -42,4 +47,4 @@ resource "devzero_workload_policy_target" "workload_policy_target" {
app = "terraform-example"
}
}
-}
\ No newline at end of file
+}
diff --git a/internal/provider/workload_policy.go b/internal/provider/workload_policy.go
index a503f36..8cc5f08 100644
--- a/internal/provider/workload_policy.go
+++ b/internal/provider/workload_policy.go
@@ -65,6 +65,8 @@ type WorkloadPolicyResourceModel struct {
DriftDeltaPercent types.Float32 `tfsdk:"drift_delta_percent"`
MinVpaWindowDataPoints types.Int32 `tfsdk:"min_vpa_window_data_points"`
CooldownMinutes types.Int32 `tfsdk:"cooldown_minutes"`
+ EnablePmaxProtection types.Bool `tfsdk:"enable_pmax_protection"`
+ PmaxRatioThreshold types.Float32 `tfsdk:"pmax_ratio_threshold"`
}
type VerticalScalingOptions struct {
@@ -78,6 +80,8 @@ type VerticalScalingOptions struct {
MaxScaleDownPercent types.Float32 `tfsdk:"max_scale_down_percent"`
LimitMultiplier types.Float32 `tfsdk:"limit_multiplier"`
MinDataPoints types.Int32 `tfsdk:"min_data_points"`
+ AdjustReqEvenIfNotSet types.Bool `tfsdk:"adjust_req_even_if_not_set"`
+ LimitsRemovalEnabled types.Bool `tfsdk:"limits_removal_enabled"`
}
type HorizontalScalingOptions struct {
@@ -156,6 +160,20 @@ func (r *WorkloadPolicyResource) Schema(ctx context.Context, req resource.Schema
Computed: true,
Default: int32default.StaticInt32(15),
},
+ "adjust_req_even_if_not_set": schema.BoolAttribute{
+ Description: "Recommend requests even when the workload has no existing requests set",
+ MarkdownDescription: "When true, the recommender will suggest resource requests even if the workload currently has none set. Default: false.",
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(false),
+ },
+ "limits_removal_enabled": schema.BoolAttribute{
+ Description: "Actively remove resource limits from workloads",
+ MarkdownDescription: "When true, the recommender will remove resource limits from workloads (CPU axis only — memory limits removal is not supported). Default: false.",
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(false),
+ },
}
}
@@ -382,6 +400,20 @@ func (r *WorkloadPolicyResource) Schema(ctx context.Context, req resource.Schema
Computed: true,
Default: int32default.StaticInt32(30),
},
+ "enable_pmax_protection": schema.BoolAttribute{
+ Description: "Raise requests to cover observed peak usage when the peak/recommendation ratio exceeds pmax_ratio_threshold",
+ MarkdownDescription: "When true, the recommender raises requests to cover observed peak usage when the peak-to-recommendation ratio exceeds `pmax_ratio_threshold`. Default: false.",
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(false),
+ },
+ "pmax_ratio_threshold": schema.Float32Attribute{
+ Description: "Peak-to-recommendation ratio above which pmax protection activates",
+ MarkdownDescription: "Peak-to-recommendation ratio above which pmax protection activates. Example: 3.0 — triggers when peak is 3× the recommendation. Default: 3.0.",
+ Optional: true,
+ Computed: true,
+ Default: float32default.StaticFloat32(3.0),
+ },
},
}
}
@@ -593,6 +625,8 @@ func (m *WorkloadPolicyResourceModel) toProto(ctx context.Context, diags *diag.D
DriftDeltaPercent: m.DriftDeltaPercent.ValueFloat32Pointer(),
MinVpaWindowDataPoints: m.MinVpaWindowDataPoints.ValueInt32Pointer(),
CooldownMinutes: m.CooldownMinutes.ValueInt32Pointer(),
+ EnablePmaxProtection: m.EnablePmaxProtection.ValueBool(),
+ PmaxRatioThreshold: m.PmaxRatioThreshold.ValueFloat32Pointer(),
}
}
@@ -676,6 +710,10 @@ func (m *WorkloadPolicyResourceModel) fromProto(policy *apiv1.WorkloadRecommenda
if policy.CooldownMinutes != nil {
m.CooldownMinutes = types.Int32Value(*policy.CooldownMinutes)
}
+ m.EnablePmaxProtection = types.BoolValue(policy.EnablePmaxProtection)
+ if policy.PmaxRatioThreshold != nil {
+ m.PmaxRatioThreshold = types.Float32Value(*policy.PmaxRatioThreshold)
+ }
}
func (o *VerticalScalingOptions) toProto() *apiv1.VerticalScalingOptimizationTarget {
@@ -693,6 +731,8 @@ func (o *VerticalScalingOptions) toProto() *apiv1.VerticalScalingOptimizationTar
MaxScaleDownPercent: o.MaxScaleDownPercent.ValueFloat32Pointer(),
LimitMultiplier: o.LimitMultiplier.ValueFloat32Pointer(),
MinDataPoints: o.MinDataPoints.ValueInt32Pointer(),
+ AdjustReqEvenIfNotSet: o.AdjustReqEvenIfNotSet.ValueBool(),
+ LimitsRemovalEnabled: o.LimitsRemovalEnabled.ValueBool(),
}
}
@@ -733,6 +773,8 @@ func (o *VerticalScalingOptions) fromProto(target *apiv1.VerticalScalingOptimiza
if target.MinDataPoints != nil {
o.MinDataPoints = types.Int32Value(*target.MinDataPoints)
}
+ o.AdjustReqEvenIfNotSet = types.BoolValue(target.AdjustReqEvenIfNotSet)
+ o.LimitsRemovalEnabled = types.BoolValue(target.LimitsRemovalEnabled)
}
func (o *HorizontalScalingOptions) toProto() *apiv1.HorizontalScalingOptimizationTarget {
diff --git a/internal/provider/workload_policy_target.go b/internal/provider/workload_policy_target.go
index b0a17af..acf6640 100644
--- a/internal/provider/workload_policy_target.go
+++ b/internal/provider/workload_policy_target.go
@@ -50,6 +50,7 @@ type WorkloadPolicyTargetResourceModel struct {
WorkloadSelector *LabelSelector `tfsdk:"workload_selector"`
KindFilter types.List `tfsdk:"kind_filter"`
NamePattern *RegexPattern `tfsdk:"name_pattern"`
+ NamespacePattern *RegexPattern `tfsdk:"namespace_pattern"`
WorkloadNames types.List `tfsdk:"workload_names"`
NodeGroupNames types.List `tfsdk:"node_group_names"`
ClusterIds types.List `tfsdk:"cluster_ids"`
@@ -201,6 +202,12 @@ func (r *WorkloadPolicyTargetResource) Schema(ctx context.Context, req resource.
Optional: true,
Attributes: regexPatternAttributes,
},
+ "namespace_pattern": schema.SingleNestedAttribute{
+ Description: "Regex to match namespace names",
+ MarkdownDescription: "Regex to match namespace names. Useful when namespaces follow a naming convention (e.g., `^prod-`).",
+ Optional: true,
+ Attributes: regexPatternAttributes,
+ },
"workload_names": schema.ListAttribute{
Description: "Explicit list of workload names to include",
MarkdownDescription: "Explicit list of workload names to include",
@@ -302,6 +309,7 @@ func (r *WorkloadPolicyTargetResource) Create(ctx context.Context, req resource.
WorkloadSelector: workloadSelector,
KindFilter: kindFilters,
NamePattern: data.NamePattern.toProto(),
+ NamespacePattern: data.NamespacePattern.toProto(),
WorkloadNames: workloadNames,
NodeGroupNames: nodeGroupNames,
ClusterIds: clusterIds,
@@ -416,6 +424,7 @@ func (r *WorkloadPolicyTargetResource) Update(ctx context.Context, req resource.
WorkloadSelector: workloadSelector,
KindFilter: kindFilters,
NamePattern: data.NamePattern.toProto(),
+ NamespacePattern: data.NamespacePattern.toProto(),
WorkloadNames: workloadNames,
NodeGroupNames: nodeGroupNames,
ClusterIds: clusterIds,
@@ -631,6 +640,7 @@ func (m *WorkloadPolicyTargetResourceModel) fromProto(target *apiv1.WorkloadPoli
m.WorkloadSelector.fromProto(target.WorkloadSelector)
m.KindFilter = types.ListValueMust(types.StringType, fromKindFilter(target.KindFilter))
m.NamePattern.fromProto(target.NamePattern)
+ m.NamespacePattern.fromProto(target.NamespacePattern)
m.WorkloadNames = types.ListValueMust(types.StringType, fromStringList(target.WorkloadNames))
m.NodeGroupNames = types.ListValueMust(types.StringType, fromStringList(target.NodeGroupNames))
m.ClusterIds = types.ListValueMust(types.StringType, fromStringList(target.ClusterIds))
diff --git a/internal/provider/workload_policy_target_test.go b/internal/provider/workload_policy_target_test.go
index af5d066..29006b7 100644
--- a/internal/provider/workload_policy_target_test.go
+++ b/internal/provider/workload_policy_target_test.go
@@ -307,6 +307,51 @@ func TestWorkloadPolicyTargetResourceModel(t *testing.T) {
}
})
+ // Test NamespacePattern toProto
+ t.Run("NamespacePattern_ToProto", func(t *testing.T) {
+ pattern := &RegexPattern{
+ Pattern: types.StringValue("^prod-"),
+ Flags: types.StringValue("i"),
+ }
+
+ proto := pattern.toProto()
+ if proto == nil {
+ t.Fatal("Expected non-nil proto")
+ }
+ if proto.Pattern != "^prod-" {
+ t.Errorf("Expected pattern '^prod-', got %s", proto.Pattern)
+ }
+ if proto.Flags != "i" {
+ t.Errorf("Expected flags 'i', got %s", proto.Flags)
+ }
+ })
+
+ // Test NamespacePattern nil toProto
+ t.Run("NamespacePattern_NilToProto", func(t *testing.T) {
+ var pattern *RegexPattern
+ proto := pattern.toProto()
+ if proto != nil {
+ t.Errorf("Expected nil proto for nil NamespacePattern, got %v", proto)
+ }
+ })
+
+ // Test NamespacePattern fromProto
+ t.Run("NamespacePattern_FromProto", func(t *testing.T) {
+ protoPattern := &apiv1.RegexPattern{
+ Pattern: "^prod-",
+ Flags: "i",
+ }
+
+ pattern := &RegexPattern{}
+ pattern.fromProto(protoPattern)
+ if pattern.Pattern.ValueString() != "^prod-" {
+ t.Errorf("Expected pattern '^prod-', got %s", pattern.Pattern.ValueString())
+ }
+ if pattern.Flags.ValueString() != "i" {
+ t.Errorf("Expected flags 'i', got %s", pattern.Flags.ValueString())
+ }
+ })
+
// Test LabelSelector with empty collections returns null
t.Run("LabelSelector_EmptyCollectionsToNull", func(t *testing.T) {
// Create a proto selector with empty match labels and expressions
@@ -454,4 +499,8 @@ func validateTargetSchema(t *testing.T, schema schema.Schema) {
if _, exists := schema.Attributes["name_pattern"]; !exists {
t.Error("name_pattern attribute not found")
}
+
+ if _, exists := schema.Attributes["namespace_pattern"]; !exists {
+ t.Error("namespace_pattern attribute not found")
+ }
}
diff --git a/internal/provider/workload_policy_test.go b/internal/provider/workload_policy_test.go
index 41872a5..bc1246c 100644
--- a/internal/provider/workload_policy_test.go
+++ b/internal/provider/workload_policy_test.go
@@ -4,6 +4,7 @@ import (
"context"
"testing"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -44,6 +45,8 @@ func TestWorkloadPolicyResourceModel(t *testing.T) {
MaxScaleDownPercent: types.Float32Value(25.0),
LimitMultiplier: types.Float32Value(2.0),
MinDataPoints: types.Int32Value(10),
+ AdjustReqEvenIfNotSet: types.BoolValue(true),
+ LimitsRemovalEnabled: types.BoolValue(true),
}
// Test toProto
@@ -81,6 +84,26 @@ func TestWorkloadPolicyResourceModel(t *testing.T) {
if *proto.MinDataPoints != 10 {
t.Errorf("Expected MinDataPoints to be 10, got %d", *proto.MinDataPoints)
}
+ if !proto.AdjustReqEvenIfNotSet {
+ t.Error("Expected AdjustReqEvenIfNotSet to be true")
+ }
+ if !proto.LimitsRemovalEnabled {
+ t.Error("Expected LimitsRemovalEnabled to be true")
+ }
+ })
+
+ // Test VerticalScalingOptions new fields default to false
+ t.Run("VerticalScalingOptionsDefaults", func(t *testing.T) {
+ opts := &VerticalScalingOptions{
+ Enabled: types.BoolValue(true),
+ }
+ proto := opts.toProto()
+ if proto.AdjustReqEvenIfNotSet {
+ t.Error("Expected AdjustReqEvenIfNotSet to default to false")
+ }
+ if proto.LimitsRemovalEnabled {
+ t.Error("Expected LimitsRemovalEnabled to default to false")
+ }
})
// Test HorizontalScalingOptions
@@ -123,6 +146,35 @@ func TestWorkloadPolicyResourceModel(t *testing.T) {
}
})
+ // Test pmax protection fields on WorkloadPolicyResourceModel
+ t.Run("PmaxProtection", func(t *testing.T) {
+ ctx := context.Background()
+ var diagnostics diag.Diagnostics
+
+ model := &WorkloadPolicyResourceModel{
+ Name: types.StringValue("test"),
+ Description: types.StringValue(""),
+ CronSchedule: types.StringValue("*/15 * * * *"),
+ DefragmentationSchedule: types.StringValue("*/15 * * * *"),
+ ActionTriggers: types.ListValueMust(types.StringType, nil),
+ DetectionTriggers: types.ListValueMust(types.StringType, nil),
+ SchedulerPlugins: types.ListValueMust(types.StringType, nil),
+ EnablePmaxProtection: types.BoolValue(true),
+ PmaxRatioThreshold: types.Float32Value(3.0),
+ }
+
+ proto := model.toProto(ctx, &diagnostics, "team-123")
+ if proto == nil {
+ t.Fatal("Expected non-nil proto")
+ }
+ if !proto.EnablePmaxProtection {
+ t.Error("Expected EnablePmaxProtection to be true")
+ }
+ if proto.PmaxRatioThreshold == nil || *proto.PmaxRatioThreshold != 3.0 {
+ t.Errorf("Expected PmaxRatioThreshold to be 3.0, got %v", proto.PmaxRatioThreshold)
+ }
+ })
+
// Test HPAMetric conversions
t.Run("HPAMetricConversions", func(t *testing.T) {
opts := &HorizontalScalingOptions{}
@@ -160,11 +212,11 @@ func TestWorkloadPolicyResourceModel(t *testing.T) {
})
}
-func validateSchema(t *testing.T, schema schema.Schema) {
+func validateSchema(t *testing.T, s schema.Schema) {
// Validate required attributes
requiredAttrs := []string{"name", "action_triggers"}
for _, attr := range requiredAttrs {
- if _, exists := schema.Attributes[attr]; !exists {
+ if _, exists := s.Attributes[attr]; !exists {
t.Errorf("Required attribute %s not found in schema", attr)
}
}
@@ -172,7 +224,7 @@ func validateSchema(t *testing.T, schema schema.Schema) {
// Validate computed attributes
computedAttrs := []string{"id"}
for _, attr := range computedAttrs {
- if attrSchema, exists := schema.Attributes[attr]; exists {
+ if attrSchema, exists := s.Attributes[attr]; exists {
if !attrSchema.IsComputed() {
t.Errorf("Attribute %s should be computed", attr)
}
@@ -180,11 +232,27 @@ func validateSchema(t *testing.T, schema schema.Schema) {
}
// Validate nested attributes exist
- if _, exists := schema.Attributes["cpu_vertical_scaling"]; !exists {
+ if _, exists := s.Attributes["cpu_vertical_scaling"]; !exists {
t.Error("cpu_vertical_scaling attribute not found")
}
- if _, exists := schema.Attributes["horizontal_scaling"]; !exists {
+ if _, exists := s.Attributes["horizontal_scaling"]; !exists {
t.Error("horizontal_scaling attribute not found")
}
+
+ // Validate pmax fields exist at top level
+ for _, attr := range []string{"enable_pmax_protection", "pmax_ratio_threshold"} {
+ if _, exists := s.Attributes[attr]; !exists {
+ t.Errorf("Expected top-level attribute %q not found in schema", attr)
+ }
+ }
+
+ // Validate new vertical scaling fields exist inside cpu_vertical_scaling
+ if cpuVs, ok := s.Attributes["cpu_vertical_scaling"].(schema.SingleNestedAttribute); ok {
+ for _, attr := range []string{"adjust_req_even_if_not_set", "limits_removal_enabled"} {
+ if _, exists := cpuVs.Attributes[attr]; !exists {
+ t.Errorf("Expected vertical_scaling attribute %q not found in cpu_vertical_scaling", attr)
+ }
+ }
+ }
}