Skip to content

Commit

Permalink
Add support for setting cpu&mem limit strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
mszostok committed Dec 16, 2024
1 parent c7f19ff commit ea98d6a
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 32 deletions.
83 changes: 79 additions & 4 deletions castai/resource_workload_scaling_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"github.com/castai/terraform-provider-castai/castai/sdk"
)

const minResourceMultiplierValue = 1.0

var (
k8sNameRegex = regexp.MustCompile("^[a-z0-9A-Z][a-z0-9A-Z._-]{0,61}[a-z0-9A-Z]$")
)
Expand Down Expand Up @@ -51,7 +53,7 @@ func resourceWorkloadScalingPolicy() *schema.Resource {
"apply_type": {
Type: schema.TypeString,
Required: true,
Description: `Recommendation apply type.
Description: `Recommendation apply type.
- IMMEDIATE - pods are restarted immediately when new recommendation is generated.
- DEFERRED - pods are not restarted and recommendation values are applied during natural restarts only (new deployment, etc.)`,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"IMMEDIATE", "DEFERRED"}, false)),
Expand Down Expand Up @@ -203,6 +205,37 @@ func workloadScalingPolicyResourceSchema(function string, overhead, minRecommend
Optional: true,
Description: "Max values for the recommendation, applies to every container. For memory - this is in MiB, for CPU - this is in cores.",
},
"limit": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Description: "Resource limit settings",
Elem: workloadScalingPolicyResourceLimitSchema(),
},
},
}
}

func workloadScalingPolicyResourceLimitSchema() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"type": {
Type: schema.TypeString,
Required: true,
Description: `Defines limit strategy type.
- NONE - removes the resource limit even if it was specified in the workload spec.
- MULTIPLIER - used to calculate the resource limit. The final value is determined by multiplying the resource request by the specified factor.`,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"NONE", "MULTIPLIER"}, true)), // FIXME: any reason to be strict about it?
DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool { // FIXME:
return strings.EqualFold(oldValue, newValue)
},
},
"multiplier": {
Type: schema.TypeFloat,
Optional: true,
Description: "Multiplier used to calculate the resource limit. It must be defined for the MULTIPLIER strategy.",
ValidateDiagFunc: validation.ToDiagFunc(validation.FloatAtLeast(minResourceMultiplierValue)),
},
},
}
}
Expand Down Expand Up @@ -375,19 +408,22 @@ func resourceWorkloadScalingPolicyDiff(_ context.Context, d *schema.ResourceDiff
cpu := toWorkloadScalingPolicies(d.Get("cpu").([]interface{})[0].(map[string]interface{}))
memory := toWorkloadScalingPolicies(d.Get("memory").([]interface{})[0].(map[string]interface{}))

if err := validateArgs(cpu, "cpu"); err != nil {
if err := validateResourcePolicy(cpu, "cpu"); err != nil {
return err
}
return validateArgs(memory, "memory")
return validateResourcePolicy(memory, "memory")
}

func validateArgs(r sdk.WorkloadoptimizationV1ResourcePolicies, res string) error {
func validateResourcePolicy(r sdk.WorkloadoptimizationV1ResourcePolicies, res string) error {
if r.Function == "QUANTILE" && len(r.Args) == 0 {
return fmt.Errorf("field %q: QUANTILE function requires args to be provided", res)
}
if r.Function == "MAX" && len(r.Args) > 0 {
return fmt.Errorf("field %q: MAX function doesn't accept any args", res)
}

// TODO: validate type and multiplier, once API is updated

return nil
}

Expand Down Expand Up @@ -449,10 +485,33 @@ func toWorkloadScalingPolicies(obj map[string]interface{}) sdk.Workloadoptimizat
if v, ok := obj["max"].(float64); ok && v > 0 {
out.Max = lo.ToPtr(v)
}
if v, ok := obj["limit"].([]any); ok {
out.Limit = toWorkloadResourceLimit(v[0].(map[string]any))
}

return out
}

func toWorkloadResourceLimit(obj map[string]any) *sdk.WorkloadoptimizationV1ResourceLimitStrategy {
if len(obj) == 0 {
return nil
}

out := &sdk.WorkloadoptimizationV1ResourceLimitStrategy{}
// TODO: validate type and multiplier, once API is updated
if v, ok := obj["type"].(string); ok {
switch v {
case "NONE":
out.None = lo.ToPtr(true)
case "MULTIPLIER":
if v, ok := obj["multiplier"].(float64); ok && v > 0 {
out.Multiplier = lo.ToPtr(v)
}
}
}
return out
}

func toWorkloadScalingPoliciesMap(p sdk.WorkloadoptimizationV1ResourcePolicies) []map[string]interface{} {
m := map[string]interface{}{
"function": p.Function,
Expand All @@ -467,6 +526,22 @@ func toWorkloadScalingPoliciesMap(p sdk.WorkloadoptimizationV1ResourcePolicies)
m["look_back_period_seconds"] = int(*p.LookBackPeriodSeconds)
}

// TODO: change casting, once API is updated
if p.Limit != nil {
if p.Limit.None != nil {
m["limit"] = []map[string]any{
{"type": "NONE"},
}
} else if p.Limit.Multiplier != nil {
m["limit"] = []map[string]any{
{
"type": "MULTIPLIER",
"multiplier": *p.Limit.Multiplier,
},
}
}
}

return []map[string]interface{}{m}
}

Expand Down
11 changes: 7 additions & 4 deletions castai/resource_workload_scaling_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func scalingPolicyConfigUpdated(clusterName, projectID, name string) string {
}
memory_event {
apply_type = "DEFERRED"
}
}
anti_affinity {
consider_anti_affinity = true
}
Expand Down Expand Up @@ -187,7 +187,7 @@ func testAccCheckScalingPolicyDestroy(s *terraform.State) error {
return nil
}

func Test_validateArgs(t *testing.T) {
func Test_validateResourcePolicy(t *testing.T) {
tests := map[string]struct {
args sdk.WorkloadoptimizationV1ResourcePolicies
wantErr bool
Expand All @@ -211,11 +211,14 @@ func Test_validateArgs(t *testing.T) {
},
wantErr: true,
},
"should return error when no value is specified for the multiplier strategy": {},
"should return error when the value is lower than 1 for the multiplier strategy": {},
"should return error when a value is specified for the none strategy": {},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
if err := validateArgs(tt.args, ""); (err != nil) != tt.wantErr {
t.Errorf("validateArgs() error = %v, wantErr %v", err, tt.wantErr)
if err := validateResourcePolicy(tt.args, ""); (err != nil) != tt.wantErr {
t.Errorf("validateResourcePolicy() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
Expand Down
Loading

0 comments on commit ea98d6a

Please sign in to comment.