Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 95 additions & 1 deletion castai/resource_workload_scaling_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const (
maxExponentValue = 1.
minExponentValue = 0.
defaultApplyType = "IMMEDIATE"

// CPU stall defaults
defaultCPUStallMinPressuredPodPct = 50.0
defaultCPUStallThresholdPct = 10.0
)

const (
Expand Down Expand Up @@ -66,6 +70,10 @@ const (
FieldApplyThresholdStrategyDefaultAdaptiveType = "DEFAULT_ADAPTIVE"
FieldApplyThresholdStrategyCustomAdaptiveType = "CUSTOM_ADAPTIVE"
FieldAssignmentRules = "assignment_rules"
FieldAnomalyDetection = "anomaly_detection"
FieldAnomalyDetectionCpuPressure = "cpu_pressure"
FieldCpuStallThresholdPercentage = "cpu_stall_threshold_percentage"
FieldMinPressuredPodPercentage = "min_pressured_pod_percentage"
)

const (
Expand Down Expand Up @@ -346,6 +354,41 @@ It can be either:
},
},
},
FieldAnomalyDetection: {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Description: "Defines anomaly detection settings for the scaling policy.",
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
return suppressAnomalyDetectionDefaultValueDiff(old, new, d)
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
FieldAnomalyDetectionCpuPressure: {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Description: "Configures CPU pressure anomaly detection thresholds.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
FieldCpuStallThresholdPercentage: {
Type: schema.TypeFloat,
Required: true,
Description: "Percentage of time (0-100) that a pod must experience CPU pressure to be considered under pressure.",
ValidateDiagFunc: validation.ToDiagFunc(validation.FloatBetween(0, 100)),
},
FieldMinPressuredPodPercentage: {
Type: schema.TypeFloat,
Required: true,
Description: "Percentage (0-100) of pods that must be experiencing pressure for the detector to trigger.",
ValidateDiagFunc: validation.ToDiagFunc(validation.FloatBetween(0, 100)),
},
},
},
},
},
},
},
},
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(createTimeout),
Expand Down Expand Up @@ -638,6 +681,8 @@ func resourceWorkloadScalingPolicyCreate(ctx context.Context, d *schema.Resource

req.RecommendationPolicies.Jvm = toJvm(toSection(d, FieldJVM))

req.RecommendationPolicies.AnomalyDetection = toAnomalyDetection(toSection(d, FieldAnomalyDetection))

req.RecommendationPolicies.ExcludedContainers = toExcludedContainers(d)

ar, err := toAssignmentRules(toSection(d, FieldAssignmentRules))
Expand Down Expand Up @@ -781,7 +826,9 @@ func fetchScalingPolicy(ctx context.Context, d *schema.ResourceData, meta any) (
if err := d.Set(FieldJVM, toJvmMap(sp.RecommendationPolicies.Jvm)); err != nil {
return nil, fmt.Errorf("setting jvm: %w", err)
}

if err := d.Set(FieldAnomalyDetection, toAnomalyDetectionMap(sp.RecommendationPolicies.AnomalyDetection)); err != nil {
return nil, fmt.Errorf("setting anomaly detection: %w", err)
}
if err := d.Set(FieldAssignmentRules, toAssignmentRulesMap(getResourceFrom(d, FieldAssignmentRules), sp.AssignmentRules)); err != nil {
return nil, fmt.Errorf("setting assignment rules: %w", err)
}
Expand Down Expand Up @@ -820,6 +867,7 @@ func updateScalingPolicy(ctx context.Context, d *schema.ResourceData, meta any)
FieldPredictiveScaling,
FieldRolloutBehavior,
FieldJVM,
FieldAnomalyDetection,
FieldExcludedContainers,
) {
tflog.Info(ctx, "scaling policy up to date")
Expand Down Expand Up @@ -857,6 +905,7 @@ func updateScalingPolicy(ctx context.Context, d *schema.ResourceData, meta any)
PredictiveScaling: toPredictiveScaling(toSection(d, FieldPredictiveScaling)),
RolloutBehavior: toRolloutBehavior(toSection(d, FieldRolloutBehavior)),
Jvm: toJvm(toSection(d, FieldJVM)),
AnomalyDetection: toAnomalyDetection(toSection(d, FieldAnomalyDetection)),
ExcludedContainers: toExcludedContainers(d),
},
}
Expand Down Expand Up @@ -1118,6 +1167,17 @@ func suppressMemoryEventApplyTypeDefaultValueDiff(oldValue, newValue string, d *
return oldValue == newValue
}

func suppressAnomalyDetectionDefaultValueDiff(oldValue, newValue string, d *schema.ResourceData) bool {
if isEmpty(newValue) {
cpuStallThreshold := d.Get(fmt.Sprintf("%s.0.%s.0.%s", FieldAnomalyDetection, FieldAnomalyDetectionCpuPressure, FieldCpuStallThresholdPercentage))
minPressuredPodPct := d.Get(fmt.Sprintf("%s.0.%s.0.%s", FieldAnomalyDetection, FieldAnomalyDetectionCpuPressure, FieldMinPressuredPodPercentage))
// Suppress diff if the API-returned values equal the defaults (meaning no explicit config is needed)
return cpuStallThreshold == defaultCPUStallThresholdPct && minPressuredPodPct == defaultCPUStallMinPressuredPodPct
}

return oldValue == newValue
}

func isEmpty(value string) bool {
return value == "" || value == "0"
}
Expand Down Expand Up @@ -1492,6 +1552,40 @@ func toRolloutBehaviorMap(s *sdk.WorkloadoptimizationV1RolloutBehaviorSettings)
return []map[string]any{m}
}

func toAnomalyDetection(m map[string]any) *sdk.WorkloadoptimizationV1AnomalyDetectionSettings {
if len(m) == 0 {
return nil
}
result := &sdk.WorkloadoptimizationV1AnomalyDetectionSettings{}
if cpuPressure := getFirstElem(m, FieldAnomalyDetectionCpuPressure); cpuPressure != nil {
result.CpuPressure = &sdk.WorkloadoptimizationV1CPUPressureSettings{
// schema already handles type validation, so casting is safe
CpuStallThresholdPercentage: cpuPressure[FieldCpuStallThresholdPercentage].(float64),
MinPressuredPodPercentage: cpuPressure[FieldMinPressuredPodPercentage].(float64),
}
}
return result
}

func toAnomalyDetectionMap(s *sdk.WorkloadoptimizationV1AnomalyDetectionSettings) []map[string]any {
if s == nil {
return nil
}
m := map[string]any{}
if s.CpuPressure != nil {
m[FieldAnomalyDetectionCpuPressure] = []map[string]any{
{
FieldCpuStallThresholdPercentage: s.CpuPressure.CpuStallThresholdPercentage,
FieldMinPressuredPodPercentage: s.CpuPressure.MinPressuredPodPercentage,
},
}
}
if len(m) == 0 {
return nil
}
return []map[string]any{m}
}

func toJvm(m map[string]any) *sdk.WorkloadoptimizationV1JVMSettings {
if len(m) == 0 {
return nil
Expand Down
84 changes: 84 additions & 0 deletions castai/resource_workload_scaling_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ func TestAccGKE_ResourceWorkloadScalingPolicy(t *testing.T) {
// Requires workload-autoscaler from v0.35.3
resource.TestCheckResourceAttr(resourceName, "rollout_behavior.0.type", "NO_DISRUPTION"),
resource.TestCheckResourceAttr(resourceName, "jvm.0.memory.0.optimization", "true"),
resource.TestCheckResourceAttr(resourceName, "anomaly_detection.0.cpu_pressure.0.cpu_stall_threshold_percentage", "50"),
resource.TestCheckResourceAttr(resourceName, "anomaly_detection.0.cpu_pressure.0.min_pressured_pod_percentage", "30"),
),
},
},
Expand Down Expand Up @@ -393,6 +395,12 @@ func scalingPolicyConfigUpdated(clusterName, projectID, name string) string {
confidence {
threshold = 0.6
}
anomaly_detection {
cpu_pressure {
cpu_stall_threshold_percentage = 50
min_pressured_pod_percentage = 30
}
}
jvm {
memory {
optimization = true
Expand Down Expand Up @@ -837,6 +845,82 @@ func Test_toRolloutBehaviorMap(t *testing.T) {
}
}

func Test_toAnomalyDetection(t *testing.T) {
tests := map[string]struct {
args map[string]any
exp *sdk.WorkloadoptimizationV1AnomalyDetectionSettings
}{
"should return nil on empty map": {
args: map[string]any{},
exp: nil,
},
"should return anomaly detection settings with cpu_pressure": {
args: map[string]any{
FieldAnomalyDetectionCpuPressure: []any{
map[string]any{
FieldCpuStallThresholdPercentage: float64(50),
FieldMinPressuredPodPercentage: float64(30),
},
},
},
exp: &sdk.WorkloadoptimizationV1AnomalyDetectionSettings{
CpuPressure: &sdk.WorkloadoptimizationV1CPUPressureSettings{
CpuStallThresholdPercentage: 50,
MinPressuredPodPercentage: 30,
},
},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
r := require.New(t)
got := toAnomalyDetection(tt.args)
r.Equal(tt.exp, got)
})
}
}

func Test_toAnomalyDetectionMap(t *testing.T) {
tests := map[string]struct {
args *sdk.WorkloadoptimizationV1AnomalyDetectionSettings
exp []map[string]any
}{
"should return nil for nil input": {
args: nil,
exp: nil,
},
"should return anomaly detection map with cpu_pressure": {
args: &sdk.WorkloadoptimizationV1AnomalyDetectionSettings{
CpuPressure: &sdk.WorkloadoptimizationV1CPUPressureSettings{
CpuStallThresholdPercentage: 50,
MinPressuredPodPercentage: 30,
},
},
exp: []map[string]any{
{
FieldAnomalyDetectionCpuPressure: []map[string]any{
{
FieldCpuStallThresholdPercentage: float64(50),
FieldMinPressuredPodPercentage: float64(30),
},
},
},
},
},
"should return nil for empty settings": {
args: &sdk.WorkloadoptimizationV1AnomalyDetectionSettings{},
exp: nil,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
r := require.New(t)
got := toAnomalyDetectionMap(tt.args)
r.Equal(tt.exp, got)
})
}
}

func Test_toJvm(t *testing.T) {
tests := map[string]struct {
args map[string]any
Expand Down
24 changes: 24 additions & 0 deletions docs/resources/workload_scaling_policy.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ resource "castai_workload_scaling_policy" "services" {
rollout_behavior {
type = "NO_DISRUPTION"
}
anomaly_detection {
cpu_pressure {
cpu_stall_threshold_percentage = 50
min_pressured_pod_percentage = 30
}
}
jvm {
memory {
optimization = true
Expand Down
Loading