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) + } + } + } }