@@ -49,6 +49,13 @@ const (
4949 FieldApplyThresholdStrategyCustomAdaptiveType = "CUSTOM_ADAPTIVE"
5050)
5151
52+ const (
53+ K8sLabelInOperator = "In"
54+ K8sLabelNotInOperator = "NotIn"
55+ K8sLabelExistsOperator = "Exists"
56+ K8sLabelDoesNotExistOperator = "DoesNotExist"
57+ )
58+
5259var (
5360 k8sNameRegex = regexp .MustCompile ("^[a-z0-9A-Z][a-z0-9A-Z._-]{0,61}[a-z0-9A-Z]$" )
5461)
@@ -72,6 +79,73 @@ func resourceWorkloadScalingPolicy() *schema.Resource {
7279 Description : "CAST AI cluster id" ,
7380 ValidateDiagFunc : validation .ToDiagFunc (validation .IsUUID ),
7481 },
82+ "assignment_rules" : {
83+ Type : schema .TypeList ,
84+ Optional : true ,
85+ Description : "Allows defining conditions for automatically assigning workloads to this scaling policy." ,
86+ Elem : & schema.Resource {
87+ Schema : map [string ]* schema.Schema {
88+ "rules" : {
89+ Type : schema .TypeList ,
90+ Optional : true ,
91+ Elem : & schema.Resource {
92+ Schema : map [string ]* schema.Schema {
93+ "namespace" : {
94+ Type : schema .TypeList ,
95+ Optional : true ,
96+ MaxItems : 1 ,
97+ Description : "Allows assigning a scaling policy based on the workload's namespace." ,
98+ Elem : & schema.Resource {
99+ Schema : map [string ]* schema.Schema {
100+ "all" : { // TODO: sync with Sandra about that field.
101+ Type : schema .TypeBool ,
102+ Optional : true ,
103+ Description : "Defines matching all namespaces. Cannot be set together with other matchers." ,
104+ },
105+ "names" : {
106+ Type : schema .TypeList ,
107+ Optional : true ,
108+ Description : "Defines matching by namespace names." ,
109+ Elem : & schema.Schema {Type : schema .TypeString },
110+ },
111+ // TODO(https://castai.atlassian.net/browse/WOOP-714): enable label expressions
112+ //"labels_expressions": k8sLabelExpressionsSchema(),
113+ },
114+ },
115+ },
116+ "workload" : {
117+ Type : schema .TypeList ,
118+ Optional : true ,
119+ MaxItems : 1 ,
120+ Description : "Allows assigning a scaling policy based on the workload's metadata." ,
121+ Elem : & schema.Resource {
122+ Schema : map [string ]* schema.Schema {
123+ "all" : { // TODO: sync with Sandra about that field.
124+ Type : schema .TypeBool ,
125+ Optional : true ,
126+ Description : "Defines matching all workloads. Cannot be set together with other matchers." ,
127+ },
128+ "gvk" : {
129+ Type : schema .TypeList ,
130+ Optional : true ,
131+ Description : `Group, version, and kind for Kubernetes resources. Format: kind[.version][.group].
132+ It can be either:
133+ - only kind, e.g. "Deployment"
134+ - group and kind: e.g."Deployment.apps"
135+ - group, version and kind: e.g."Deployment.v1.apps"` ,
136+
137+ Elem : & schema.Schema {Type : schema .TypeString },
138+ },
139+ "labels_expressions" : k8sLabelExpressionsSchema (),
140+ },
141+ },
142+ },
143+ },
144+ },
145+ },
146+ },
147+ },
148+ },
75149 "name" : {
76150 Type : schema .TypeString ,
77151 Required : true ,
@@ -200,6 +274,37 @@ func resourceWorkloadScalingPolicy() *schema.Resource {
200274 }
201275}
202276
277+ func k8sLabelExpressionsSchema () * schema.Schema {
278+ return & schema.Schema {
279+ Type : schema .TypeList ,
280+ Optional : true ,
281+ Description : "Defines matching by label selector requirements." ,
282+ Elem : & schema.Resource {
283+ Schema : map [string ]* schema.Schema {
284+ "key" : {
285+ Type : schema .TypeString ,
286+ Required : true ,
287+ Description : "The label key to match." ,
288+ },
289+ "operator" : {
290+ Type : schema .TypeString ,
291+ Required : true ,
292+ Description : "The operator to use for matching the label." ,
293+ ValidateDiagFunc : validation .ToDiagFunc (validation .StringInSlice ([]string {
294+ K8sLabelInOperator , K8sLabelNotInOperator , K8sLabelExistsOperator , K8sLabelDoesNotExistOperator ,
295+ }, false )),
296+ },
297+ "values" : {
298+ Type : schema .TypeList ,
299+ Optional : true ,
300+ Description : "A list of values to match against the label key. Allowed for `In` and `NotIn` operators." ,
301+ Elem : & schema.Schema {Type : schema .TypeString },
302+ },
303+ },
304+ },
305+ }
306+ }
307+
203308func workloadScalingPolicyResourceSchema (resource , function string , overhead , minRecommended float64 ) * schema.Resource {
204309 return & schema.Resource {
205310 Schema : map [string ]* schema.Schema {
@@ -414,6 +519,12 @@ func resourceWorkloadScalingPolicyCreate(ctx context.Context, d *schema.Resource
414519
415520 req .RecommendationPolicies .AntiAffinity = toAntiAffinity (toSection (d , "anti_affinity" ))
416521
522+ ar , err := toAssignmentRules (toSection (d , "assignment_rules" ))
523+ if err != nil {
524+ return diag .FromErr (err )
525+ }
526+ req .AssignmentRules = ar
527+
417528 create , err := client .WorkloadOptimizationAPICreateWorkloadScalingPolicyWithResponse (ctx , clusterID , req )
418529 if err != nil {
419530 return diag .FromErr (err )
@@ -489,6 +600,10 @@ func resourceWorkloadScalingPolicyRead(ctx context.Context, d *schema.ResourceDa
489600 return diag .FromErr (fmt .Errorf ("setting anti-affinity: %w" , err ))
490601 }
491602
603+ if err := d .Set ("assignment_rules" , toAssignmentRulesMap (sp .AssignmentRules )); err != nil {
604+ return diag .FromErr (fmt .Errorf ("setting assignment rules: %w" , err ))
605+ }
606+
492607 return nil
493608}
494609
@@ -511,6 +626,7 @@ func resourceWorkloadScalingPolicyUpdate(ctx context.Context, d *schema.Resource
511626 "memory_event" ,
512627 "anti_affinity" ,
513628 FieldConfidence ,
629+ "assignment_rules" ,
514630 ) {
515631 tflog .Info (ctx , "scaling policy up to date" )
516632 return nil
@@ -526,9 +642,15 @@ func resourceWorkloadScalingPolicyUpdate(ctx context.Context, d *schema.Resource
526642 if err != nil {
527643 return diag .FromErr (err )
528644 }
645+ ar , err := toAssignmentRules (toSection (d , "assignment_rules" ))
646+ if err != nil {
647+ return diag .FromErr (err )
648+ }
649+
529650 req := sdk.WorkloadOptimizationAPIUpdateWorkloadScalingPolicyJSONRequestBody {
530- Name : d .Get ("name" ).(string ),
531- ApplyType : sdk .WorkloadoptimizationV1ApplyType (d .Get ("apply_type" ).(string )),
651+ Name : d .Get ("name" ).(string ),
652+ ApplyType : sdk .WorkloadoptimizationV1ApplyType (d .Get ("apply_type" ).(string )),
653+ AssignmentRules : ar ,
532654 RecommendationPolicies : sdk.WorkloadoptimizationV1RecommendationPolicies {
533655 ManagementOption : sdk .WorkloadoptimizationV1ManagementOption (d .Get ("management_option" ).(string )),
534656 Cpu : cpu ,
@@ -1058,3 +1180,222 @@ func getWorkloadScalingPolicyByName(ctx context.Context, client sdk.ClientWithRe
10581180 }
10591181 return nil , fmt .Errorf ("policy with name %q not found" , name )
10601182}
1183+
1184+ func toAssignmentRules (in map [string ]any ) (* []sdk.WorkloadoptimizationV1ScalingPolicyAssignmentRule , error ) {
1185+ if in == nil || len (in ) == 0 {
1186+ return nil , nil
1187+ }
1188+
1189+ rules := in ["rules" ].([]any )
1190+ if len (rules ) == 0 {
1191+ return & []sdk.WorkloadoptimizationV1ScalingPolicyAssignmentRule {}, nil
1192+ }
1193+
1194+ result := make ([]sdk.WorkloadoptimizationV1ScalingPolicyAssignmentRule , len (rules ))
1195+ for i , rule := range rules {
1196+ ruleMap := rule .(map [string ]any )
1197+
1198+ nsRules , err := toNamespaceAssignmentRule (ruleMap )
1199+ if err != nil {
1200+ return nil , fmt .Errorf ("field assignment_rules[%d].namespace: %w" , i , err )
1201+ }
1202+
1203+ wRules , err := toWorkloadAssignmentRule (ruleMap )
1204+ if err != nil {
1205+ return nil , fmt .Errorf ("field assignment_rules[%d].workload: %w" , i , err )
1206+ }
1207+ if nsRules == nil && wRules == nil {
1208+ continue
1209+ }
1210+ result [i ] = sdk.WorkloadoptimizationV1ScalingPolicyAssignmentRule {
1211+ Workload : wRules ,
1212+ Namespace : nsRules ,
1213+ }
1214+ }
1215+
1216+ return & result , nil
1217+ }
1218+
1219+ func toNamespaceAssignmentRule (ruleMap map [string ]any ) (* sdk.WorkloadoptimizationV1KubernetesNamespaceMatcher , error ) {
1220+ namespaceMap := getFirstElem (ruleMap , "namespace" )
1221+ if namespaceMap == nil {
1222+ return nil , nil
1223+ }
1224+
1225+ namespaceMatcher := & sdk.WorkloadoptimizationV1KubernetesNamespaceMatcher {}
1226+
1227+ if all := readOptionalValue [bool ](namespaceMap , "all" ); all != nil && * all { //TODO: need to fix backend API to allow 'false'
1228+ namespaceMatcher .All = all
1229+ }
1230+
1231+ if names := readOptionalValue [[]any ](namespaceMap , "names" ); names != nil {
1232+ namespaceMatcher .Names = lo .ToPtr (toStringList (* names ))
1233+ }
1234+
1235+ labels , err := toKubernetesLabelExpressionMatcher (namespaceMap )
1236+ if err != nil {
1237+ return nil , err
1238+ }
1239+ namespaceMatcher .LabelsExpressions = labels
1240+
1241+ return namespaceMatcher , nil
1242+ }
1243+
1244+ func toKubernetesLabelExpressionMatcher (namespaceMap map [string ]any ) (* []sdk.WorkloadoptimizationV1KubernetesLabelExpressionMatcher , error ) {
1245+ exprs := readOptionalValue [[]any ](namespaceMap , "labels_expressions" )
1246+ if exprs == nil {
1247+ return nil , nil
1248+ }
1249+ expressions := make ([]sdk.WorkloadoptimizationV1KubernetesLabelExpressionMatcher , len (* exprs ))
1250+ for j , expr := range * exprs {
1251+ exprMap := expr .(map [string ]any )
1252+ key , err := mustGetValue [string ](exprMap , "key" )
1253+ if err != nil {
1254+ return nil , err
1255+ }
1256+
1257+ operator , err := mustGetValue [string ](exprMap , "operator" )
1258+ if err != nil {
1259+ return nil , err
1260+ }
1261+
1262+ expressions [j ] = sdk.WorkloadoptimizationV1KubernetesLabelExpressionMatcher {
1263+ Key : * key ,
1264+ Operator : toLabelSelectorOperator (* operator ),
1265+ }
1266+
1267+ values := readOptionalValue [[]any ](exprMap , "values" )
1268+ if values != nil {
1269+ expressions [j ].Values = toStringList (* values )
1270+ }
1271+ }
1272+ return & expressions , nil
1273+ }
1274+
1275+ func toWorkloadAssignmentRule (ruleMap map [string ]any ) (* sdk.WorkloadoptimizationV1KubernetesWorkloadMatcher , error ) {
1276+ workloadMap := getFirstElem (ruleMap , "workload" )
1277+ if workloadMap == nil {
1278+ return nil , nil
1279+ }
1280+
1281+ workloadMatcher := & sdk.WorkloadoptimizationV1KubernetesWorkloadMatcher {}
1282+
1283+ if all := readOptionalValue [bool ](workloadMap , "all" ); all != nil && * all { //TODO: need to fix backend API to allow 'false'
1284+ workloadMatcher .All = all
1285+ }
1286+
1287+ if gvk := readOptionalValue [[]any ](workloadMap , "gvk" ); gvk != nil {
1288+ workloadMatcher .Gvk = lo .ToPtr (toStringList (* gvk ))
1289+ }
1290+
1291+ labels , err := toKubernetesLabelExpressionMatcher (workloadMap )
1292+ if err != nil {
1293+ return nil , err
1294+ }
1295+ workloadMatcher .LabelsExpressions = labels
1296+
1297+ return workloadMatcher , nil
1298+ }
1299+
1300+ func getFirstElem (in map [string ]any , key string ) map [string ]any {
1301+ val , ok := in [key ]
1302+ if ! ok || val == nil || len (val .([]any )) == 0 || val .([]any )[0 ] == nil {
1303+ return nil
1304+ }
1305+ return val .([]any )[0 ].(map [string ]any )
1306+ }
1307+
1308+ func toLabelSelectorOperator (in string ) sdk.WorkloadoptimizationV1KubernetesLabelSelectorOperator {
1309+ switch in {
1310+ case K8sLabelInOperator :
1311+ return sdk .KUBERNETESLABELSELECTOROPIN
1312+ case K8sLabelNotInOperator :
1313+ return sdk .KUBERNETESLABELSELECTOROPNOTIN
1314+ case K8sLabelExistsOperator :
1315+ return sdk .KUBERNETESLABELSELECTOROPEXISTS
1316+ case K8sLabelDoesNotExistOperator :
1317+ return sdk .KUBERNETESLABELSELECTOROPDOESNOTEXIST
1318+ }
1319+ return sdk .KUBERNETESLABELSELECTOROPUNSPECIFIED
1320+ }
1321+
1322+ func toAssignmentRulesMap (rules * []sdk.WorkloadoptimizationV1ScalingPolicyAssignmentRule ) []any {
1323+ if rules == nil {
1324+ return nil
1325+ }
1326+
1327+ result := make ([]map [string ]any , len (* rules ))
1328+ for i , rule := range * rules {
1329+ ruleMap := make (map [string ]any )
1330+
1331+ if rule .Namespace != nil {
1332+ namespaceMap := make (map [string ]any )
1333+
1334+ if rule .Namespace .All != nil {
1335+ namespaceMap ["all" ] = * rule .Namespace .All
1336+ }
1337+
1338+ if rule .Namespace .Names != nil {
1339+ namespaceMap ["names" ] = * rule .Namespace .Names
1340+ }
1341+
1342+ if rule .Namespace .LabelsExpressions != nil && len (* rule .Namespace .LabelsExpressions ) > 0 {
1343+ namespaceMap ["labels_expressions" ] = toK8sLabelsExpressionsMap (* rule .Namespace .LabelsExpressions )
1344+ }
1345+
1346+ ruleMap ["namespace" ] = []map [string ]any {namespaceMap }
1347+ }
1348+
1349+ if rule .Workload != nil {
1350+ workloadMap := make (map [string ]any )
1351+
1352+ if rule .Workload .All != nil {
1353+ workloadMap ["all" ] = * rule .Workload .All
1354+ }
1355+
1356+ if rule .Workload .Gvk != nil && len (* rule .Workload .Gvk ) > 0 {
1357+ workloadMap ["gvk" ] = * rule .Workload .Gvk
1358+ }
1359+
1360+ if rule .Workload .LabelsExpressions != nil && len (* rule .Workload .LabelsExpressions ) > 0 {
1361+ workloadMap ["labels_expressions" ] = toK8sLabelsExpressionsMap (* rule .Workload .LabelsExpressions )
1362+ }
1363+
1364+ ruleMap ["workload" ] = []map [string ]any {workloadMap }
1365+ }
1366+
1367+ result [i ] = ruleMap
1368+ }
1369+
1370+ return []any {
1371+ map [string ]any {
1372+ "rules" : result ,
1373+ },
1374+ }
1375+ }
1376+
1377+ func toK8sLabelsExpressionsMap (in []sdk.WorkloadoptimizationV1KubernetesLabelExpressionMatcher ) []map [string ]any {
1378+ expressions := make ([]map [string ]any , len (in ))
1379+ for j , expr := range in {
1380+ expressions [j ] = map [string ]any {
1381+ "key" : expr .Key ,
1382+ "operator" : labelSelectorOperatorMap (expr .Operator ),
1383+ "values" : expr .Values ,
1384+ }
1385+ }
1386+ return expressions
1387+ }
1388+
1389+ func labelSelectorOperatorMap (in sdk.WorkloadoptimizationV1KubernetesLabelSelectorOperator ) string {
1390+ switch in {
1391+ case sdk .KUBERNETESLABELSELECTOROPIN :
1392+ return K8sLabelInOperator
1393+ case sdk .KUBERNETESLABELSELECTOROPNOTIN :
1394+ return K8sLabelNotInOperator
1395+ case sdk .KUBERNETESLABELSELECTOROPEXISTS :
1396+ return K8sLabelExistsOperator
1397+ case sdk .KUBERNETESLABELSELECTOROPDOESNOTEXIST :
1398+ return K8sLabelDoesNotExistOperator
1399+ }
1400+ return "unspecified"
1401+ }
0 commit comments