@@ -47,6 +47,14 @@ const (
4747 FieldApplyThresholdStrategyPercentageType = "PERCENTAGE"
4848 FieldApplyThresholdStrategyDefaultAdaptiveType = "DEFAULT_ADAPTIVE"
4949 FieldApplyThresholdStrategyCustomAdaptiveType = "CUSTOM_ADAPTIVE"
50+ FieldAssignmentRules = "assignment_rules"
51+ )
52+
53+ const (
54+ K8sLabelInOperator = "In"
55+ K8sLabelNotInOperator = "NotIn"
56+ K8sLabelExistsOperator = "Exists"
57+ K8sLabelDoesNotExistOperator = "DoesNotExist"
5058)
5159
5260var (
@@ -72,6 +80,63 @@ func resourceWorkloadScalingPolicy() *schema.Resource {
7280 Description : "CAST AI cluster id" ,
7381 ValidateDiagFunc : validation .ToDiagFunc (validation .IsUUID ),
7482 },
83+ FieldAssignmentRules : {
84+ Type : schema .TypeList ,
85+ Optional : true ,
86+ Description : "Allows defining conditions for automatically assigning workloads to this scaling policy." ,
87+ Elem : & schema.Resource {
88+ Schema : map [string ]* schema.Schema {
89+ "rules" : {
90+ Type : schema .TypeList ,
91+ Required : true ,
92+ Elem : & schema.Resource {
93+ Schema : map [string ]* schema.Schema {
94+ "namespace" : {
95+ Type : schema .TypeList ,
96+ Optional : true ,
97+ MaxItems : 1 ,
98+ Description : "Allows assigning a scaling policy based on the workload's namespace." ,
99+ Elem : & schema.Resource {
100+ Schema : map [string ]* schema.Schema {
101+ "names" : {
102+ Type : schema .TypeList ,
103+ Optional : true ,
104+ Description : "Defines matching by namespace names." ,
105+ Elem : & schema.Schema {Type : schema .TypeString },
106+ },
107+ // TODO(WOOP-714): enable label expressions
108+ //"labels_expressions": k8sLabelExpressionsSchema(),
109+ },
110+ },
111+ },
112+ "workload" : {
113+ Type : schema .TypeList ,
114+ Optional : true ,
115+ MaxItems : 1 ,
116+ Description : "Allows assigning a scaling policy based on the workload's metadata." ,
117+ Elem : & schema.Resource {
118+ Schema : map [string ]* schema.Schema {
119+ "gvk" : {
120+ Type : schema .TypeList ,
121+ Optional : true ,
122+ Description : `Group, version, and kind for Kubernetes resources. Format: kind[.version][.group].
123+ It can be either:
124+ - only kind, e.g. "Deployment"
125+ - group and kind: e.g."Deployment.apps"
126+ - group, version and kind: e.g."Deployment.v1.apps"` ,
127+
128+ Elem : & schema.Schema {Type : schema .TypeString },
129+ },
130+ "labels_expressions" : k8sLabelExpressionsSchema (),
131+ },
132+ },
133+ },
134+ },
135+ },
136+ },
137+ },
138+ },
139+ },
75140 "name" : {
76141 Type : schema .TypeString ,
77142 Required : true ,
@@ -200,6 +265,37 @@ func resourceWorkloadScalingPolicy() *schema.Resource {
200265 }
201266}
202267
268+ func k8sLabelExpressionsSchema () * schema.Schema {
269+ return & schema.Schema {
270+ Type : schema .TypeList ,
271+ Optional : true ,
272+ Description : "Defines matching by label selector requirements." ,
273+ Elem : & schema.Resource {
274+ Schema : map [string ]* schema.Schema {
275+ "key" : {
276+ Type : schema .TypeString ,
277+ Required : true ,
278+ Description : "The label key to match." ,
279+ },
280+ "operator" : {
281+ Type : schema .TypeString ,
282+ Required : true ,
283+ Description : "The operator to use for matching the label." ,
284+ ValidateDiagFunc : validation .ToDiagFunc (validation .StringInSlice ([]string {
285+ K8sLabelInOperator , K8sLabelNotInOperator , K8sLabelExistsOperator , K8sLabelDoesNotExistOperator ,
286+ }, false )),
287+ },
288+ "values" : {
289+ Type : schema .TypeList ,
290+ Optional : true ,
291+ Description : "A list of values to match against the label key. Allowed for `In` and `NotIn` operators." ,
292+ Elem : & schema.Schema {Type : schema .TypeString },
293+ },
294+ },
295+ },
296+ }
297+ }
298+
203299func workloadScalingPolicyResourceSchema (resource , function string , overhead , minRecommended float64 ) * schema.Resource {
204300 return & schema.Resource {
205301 Schema : map [string ]* schema.Schema {
@@ -414,6 +510,12 @@ func resourceWorkloadScalingPolicyCreate(ctx context.Context, d *schema.Resource
414510
415511 req .RecommendationPolicies .AntiAffinity = toAntiAffinity (toSection (d , "anti_affinity" ))
416512
513+ ar , err := toAssignmentRules (toSection (d , FieldAssignmentRules ))
514+ if err != nil {
515+ return diag .FromErr (err )
516+ }
517+ req .AssignmentRules = ar
518+
417519 create , err := client .WorkloadOptimizationAPICreateWorkloadScalingPolicyWithResponse (ctx , clusterID , req )
418520 if err != nil {
419521 return diag .FromErr (err )
@@ -489,6 +591,10 @@ func resourceWorkloadScalingPolicyRead(ctx context.Context, d *schema.ResourceDa
489591 return diag .FromErr (fmt .Errorf ("setting anti-affinity: %w" , err ))
490592 }
491593
594+ if err := d .Set (FieldAssignmentRules , toAssignmentRulesMap (getResourceFrom (d , FieldAssignmentRules ), sp .AssignmentRules )); err != nil {
595+ return diag .FromErr (fmt .Errorf ("setting assignment rules: %w" , err ))
596+ }
597+
492598 return nil
493599}
494600
@@ -511,6 +617,7 @@ func resourceWorkloadScalingPolicyUpdate(ctx context.Context, d *schema.Resource
511617 "memory_event" ,
512618 "anti_affinity" ,
513619 FieldConfidence ,
620+ FieldAssignmentRules ,
514621 ) {
515622 tflog .Info (ctx , "scaling policy up to date" )
516623 return nil
@@ -526,9 +633,15 @@ func resourceWorkloadScalingPolicyUpdate(ctx context.Context, d *schema.Resource
526633 if err != nil {
527634 return diag .FromErr (err )
528635 }
636+ ar , err := toAssignmentRules (toSection (d , FieldAssignmentRules ))
637+ if err != nil {
638+ return diag .FromErr (err )
639+ }
640+
529641 req := sdk.WorkloadOptimizationAPIUpdateWorkloadScalingPolicyJSONRequestBody {
530- Name : d .Get ("name" ).(string ),
531- ApplyType : sdk .WorkloadoptimizationV1ApplyType (d .Get ("apply_type" ).(string )),
642+ Name : d .Get ("name" ).(string ),
643+ ApplyType : sdk .WorkloadoptimizationV1ApplyType (d .Get ("apply_type" ).(string )),
644+ AssignmentRules : ar ,
532645 RecommendationPolicies : sdk.WorkloadoptimizationV1RecommendationPolicies {
533646 ManagementOption : sdk .WorkloadoptimizationV1ManagementOption (d .Get ("management_option" ).(string )),
534647 Cpu : cpu ,
@@ -1058,3 +1171,214 @@ func getWorkloadScalingPolicyByName(ctx context.Context, client sdk.ClientWithRe
10581171 }
10591172 return nil , fmt .Errorf ("policy with name %q not found" , name )
10601173}
1174+
1175+ func toAssignmentRules (in map [string ]any ) (* []sdk.WorkloadoptimizationV1ScalingPolicyAssignmentRule , error ) {
1176+ if len (in ) == 0 {
1177+ return nil , nil
1178+ }
1179+
1180+ rules , ok := in ["rules" ].([]any )
1181+ if ! ok || len (rules ) == 0 {
1182+ return & []sdk.WorkloadoptimizationV1ScalingPolicyAssignmentRule {}, nil
1183+ }
1184+
1185+ result := make ([]sdk.WorkloadoptimizationV1ScalingPolicyAssignmentRule , len (rules ))
1186+ for i , rule := range rules {
1187+ ruleMap := rule .(map [string ]any )
1188+
1189+ nsRules , err := toNamespaceAssignmentRule (ruleMap )
1190+ if err != nil {
1191+ return nil , fmt .Errorf ("field assignment_rules[%d].namespace: %w" , i , err )
1192+ }
1193+
1194+ wRules , err := toWorkloadAssignmentRule (ruleMap )
1195+ if err != nil {
1196+ return nil , fmt .Errorf ("field assignment_rules[%d].workload: %w" , i , err )
1197+ }
1198+ if nsRules == nil && wRules == nil {
1199+ continue
1200+ }
1201+ result [i ] = sdk.WorkloadoptimizationV1ScalingPolicyAssignmentRule {
1202+ Workload : wRules ,
1203+ Namespace : nsRules ,
1204+ }
1205+ }
1206+
1207+ return & result , nil
1208+ }
1209+
1210+ func toNamespaceAssignmentRule (ruleMap map [string ]any ) (* sdk.WorkloadoptimizationV1KubernetesNamespaceMatcher , error ) {
1211+ namespaceMap := getFirstElem (ruleMap , "namespace" )
1212+ if namespaceMap == nil {
1213+ return nil , nil
1214+ }
1215+
1216+ namespaceMatcher := & sdk.WorkloadoptimizationV1KubernetesNamespaceMatcher {}
1217+
1218+ if names := readOptionalValue [[]any ](namespaceMap , "names" ); names != nil {
1219+ namespaceMatcher .Names = lo .ToPtr (toStringList (* names ))
1220+ }
1221+
1222+ labels , err := toKubernetesLabelExpressionMatcher (namespaceMap )
1223+ if err != nil {
1224+ return nil , err
1225+ }
1226+ namespaceMatcher .LabelsExpressions = labels
1227+
1228+ return namespaceMatcher , nil
1229+ }
1230+
1231+ func toKubernetesLabelExpressionMatcher (namespaceMap map [string ]any ) (* []sdk.WorkloadoptimizationV1KubernetesLabelExpressionMatcher , error ) {
1232+ exprs := readOptionalValue [[]any ](namespaceMap , "labels_expressions" )
1233+ if exprs == nil {
1234+ return nil , nil
1235+ }
1236+ expressions := make ([]sdk.WorkloadoptimizationV1KubernetesLabelExpressionMatcher , len (* exprs ))
1237+ for j , expr := range * exprs {
1238+ exprMap := expr .(map [string ]any )
1239+ key , err := mustGetValue [string ](exprMap , "key" )
1240+ if err != nil {
1241+ return nil , err
1242+ }
1243+
1244+ operator , err := mustGetValue [string ](exprMap , "operator" )
1245+ if err != nil {
1246+ return nil , err
1247+ }
1248+
1249+ expressions [j ] = sdk.WorkloadoptimizationV1KubernetesLabelExpressionMatcher {
1250+ Key : * key ,
1251+ Operator : toLabelSelectorOperator (* operator ),
1252+ }
1253+
1254+ values := readOptionalValue [[]any ](exprMap , "values" )
1255+ if values != nil {
1256+ expressions [j ].Values = toStringList (* values )
1257+ }
1258+ }
1259+ return & expressions , nil
1260+ }
1261+
1262+ func toWorkloadAssignmentRule (ruleMap map [string ]any ) (* sdk.WorkloadoptimizationV1KubernetesWorkloadMatcher , error ) {
1263+ workloadMap := getFirstElem (ruleMap , "workload" )
1264+ if workloadMap == nil {
1265+ return nil , nil
1266+ }
1267+
1268+ workloadMatcher := & sdk.WorkloadoptimizationV1KubernetesWorkloadMatcher {}
1269+
1270+ if gvk := readOptionalValue [[]any ](workloadMap , "gvk" ); gvk != nil {
1271+ workloadMatcher .Gvk = lo .ToPtr (toStringList (* gvk ))
1272+ }
1273+
1274+ labels , err := toKubernetesLabelExpressionMatcher (workloadMap )
1275+ if err != nil {
1276+ return nil , err
1277+ }
1278+ workloadMatcher .LabelsExpressions = labels
1279+
1280+ return workloadMatcher , nil
1281+ }
1282+
1283+ func getFirstElem (in map [string ]any , key string ) map [string ]any {
1284+ list , ok := in [key ].([]any )
1285+ if ! ok || len (list ) == 0 {
1286+ return nil
1287+ }
1288+ elem , ok := list [0 ].(map [string ]any )
1289+ if ! ok {
1290+ return nil
1291+ }
1292+ return elem
1293+ }
1294+
1295+ func toLabelSelectorOperator (in string ) sdk.WorkloadoptimizationV1KubernetesLabelSelectorOperator {
1296+ switch in {
1297+ case K8sLabelInOperator :
1298+ return sdk .KUBERNETESLABELSELECTOROPIN
1299+ case K8sLabelNotInOperator :
1300+ return sdk .KUBERNETESLABELSELECTOROPNOTIN
1301+ case K8sLabelExistsOperator :
1302+ return sdk .KUBERNETESLABELSELECTOROPEXISTS
1303+ case K8sLabelDoesNotExistOperator :
1304+ return sdk .KUBERNETESLABELSELECTOROPDOESNOTEXIST
1305+ }
1306+ return sdk .KUBERNETESLABELSELECTOROPUNSPECIFIED
1307+ }
1308+
1309+ func toAssignmentRulesMap (previous map [string ]any , rules * []sdk.WorkloadoptimizationV1ScalingPolicyAssignmentRule ) []any {
1310+ if rules == nil {
1311+ return nil
1312+ }
1313+
1314+ if len (* rules ) == 0 && len (previous ) == 0 {
1315+ return nil
1316+ }
1317+
1318+ result := make ([]map [string ]any , len (* rules ))
1319+ for i , rule := range * rules {
1320+ ruleMap := make (map [string ]any )
1321+
1322+ if rule .Namespace != nil {
1323+ namespaceMap := make (map [string ]any )
1324+
1325+ if rule .Namespace .Names != nil {
1326+ namespaceMap ["names" ] = * rule .Namespace .Names
1327+ }
1328+
1329+ if rule .Namespace .LabelsExpressions != nil && len (* rule .Namespace .LabelsExpressions ) > 0 {
1330+ namespaceMap ["labels_expressions" ] = toK8sLabelsExpressionsMap (* rule .Namespace .LabelsExpressions )
1331+ }
1332+
1333+ ruleMap ["namespace" ] = []map [string ]any {namespaceMap }
1334+ }
1335+
1336+ if rule .Workload != nil {
1337+ workloadMap := make (map [string ]any )
1338+
1339+ if rule .Workload .Gvk != nil && len (* rule .Workload .Gvk ) > 0 {
1340+ workloadMap ["gvk" ] = * rule .Workload .Gvk
1341+ }
1342+
1343+ if rule .Workload .LabelsExpressions != nil && len (* rule .Workload .LabelsExpressions ) > 0 {
1344+ workloadMap ["labels_expressions" ] = toK8sLabelsExpressionsMap (* rule .Workload .LabelsExpressions )
1345+ }
1346+
1347+ ruleMap ["workload" ] = []map [string ]any {workloadMap }
1348+ }
1349+
1350+ result [i ] = ruleMap
1351+ }
1352+
1353+ return []any {
1354+ map [string ]any {
1355+ "rules" : result ,
1356+ },
1357+ }
1358+ }
1359+
1360+ func toK8sLabelsExpressionsMap (in []sdk.WorkloadoptimizationV1KubernetesLabelExpressionMatcher ) []map [string ]any {
1361+ expressions := make ([]map [string ]any , len (in ))
1362+ for j , expr := range in {
1363+ expressions [j ] = map [string ]any {
1364+ "key" : expr .Key ,
1365+ "operator" : labelSelectorOperatorMap (expr .Operator ),
1366+ "values" : expr .Values ,
1367+ }
1368+ }
1369+ return expressions
1370+ }
1371+
1372+ func labelSelectorOperatorMap (in sdk.WorkloadoptimizationV1KubernetesLabelSelectorOperator ) string {
1373+ switch in {
1374+ case sdk .KUBERNETESLABELSELECTOROPIN :
1375+ return K8sLabelInOperator
1376+ case sdk .KUBERNETESLABELSELECTOROPNOTIN :
1377+ return K8sLabelNotInOperator
1378+ case sdk .KUBERNETESLABELSELECTOROPEXISTS :
1379+ return K8sLabelExistsOperator
1380+ case sdk .KUBERNETESLABELSELECTOROPDOESNOTEXIST :
1381+ return K8sLabelDoesNotExistOperator
1382+ }
1383+ return "unspecified"
1384+ }
0 commit comments