Skip to content

Commit e849dcf

Browse files
authored
feat: add topologyspreadconstraints for service (#100)
1 parent d22e4a1 commit e849dcf

8 files changed

Lines changed: 235 additions & 4 deletions

File tree

modules/service/common.k

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import container as c
22
import secret as sec
3+
import topologyspreadconstraint as tp
34
import kam.v1.workload as wl
45

56
schema WorkloadBase(wl.Workload):
@@ -30,6 +31,9 @@ schema WorkloadBase(wl.Workload):
3031
# The number of containers that should be ran.
3132
replicas?: int
3233

34+
# TopologySpreadConstraint describes how a group of pods ought to spread across topology domains.
35+
topologySpreadConstraints?: {str:tp.TopologySpreadConstraint}
36+
3337
###### Other metadata info
3438
# Labels and annotations can be used to attach arbitrary metadata as key-value pairs to resources.
3539
labels?: {str:str}

modules/service/kcl.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "service"
3-
version = "0.2.0"
3+
version = "0.2.1"
44

55
[dependencies]
66
kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0" }

modules/service/src/service.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ func (svc *Service) Generate(_ context.Context, request *module.GeneratorRequest
5959
return nil, err
6060
}
6161

62+
topologySpreadConstraints := handleTopologySpreadConstraints(svc.TopologySpreadConstraints)
63+
6264
res := make([]kusionv1.Resource, 0)
6365
// Create ConfigMap objects based on the App's configuration.
6466
for _, cm := range configMaps {
@@ -89,8 +91,9 @@ func (svc *Service) Generate(_ context.Context, request *module.GeneratorRequest
8991
Annotations: annotations,
9092
},
9193
Spec: corev1.PodSpec{
92-
Containers: containers,
93-
Volumes: volumes,
94+
TopologySpreadConstraints: topologySpreadConstraints,
95+
Containers: containers,
96+
Volumes: volumes,
9497
},
9598
}
9699

modules/service/src/type.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package main
22

3-
import "gopkg.in/yaml.v2"
3+
import (
4+
"gopkg.in/yaml.v2"
5+
corev1 "k8s.io/api/core/v1"
6+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7+
)
48

59
const (
610
BuiltinModulePrefix = ""
@@ -10,6 +14,49 @@ const (
1014
TypeTCP = BuiltinModulePrefix + ProbePrefix + "Tcp"
1115
)
1216

17+
// LabelSelector is a label query over a set of resources.
18+
type LabelSelector struct {
19+
// matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
20+
// map is equivalent to an element of matchExpressions, whose key field is "key", the
21+
// operator is "In", and the values array contains only "value".
22+
MatchLabels map[string]string `yaml:"matchLabels,omitempty" json:"matchLabels,omitempty"`
23+
// matchExpressions is a list of label selector requirements.
24+
MatchExpressions []LabelSelectorRequirement `yaml:"matchExpressions,omitempty" json:"matchExpressions,omitempty"`
25+
}
26+
27+
// LabelSelectorRequirement is a selector that contains values, a key, and an operator that relates the key and values.
28+
type LabelSelectorRequirement struct {
29+
// key is the label key that the selector applies to.
30+
Key string `yaml:"key" json:"key"`
31+
// operator represents a key's relationship to a set of values.
32+
// Valid operators are In, NotIn, Exists and DoesNotExist.
33+
Operator metav1.LabelSelectorOperator `yaml:"operator" json:"operator"`
34+
// values is an array of string values. If the operator is In or NotIn,
35+
// the values array must be non-empty. If the operator is Exists or DoesNotExist,
36+
// the values array must be empty. This array is replaced during a strategic merge patch.
37+
Values []string `yaml:"values,omitempty" json:"values,omitempty"`
38+
}
39+
40+
// TopologySpreadConstraint specifies how to spread matching pods among the given topology.
41+
type TopologySpreadConstraint struct {
42+
// MaxSkew describes the degree to which pods may be unevenly distributed.
43+
MaxSkew int32 `yaml:"maxSkew" json:"maxSkew"`
44+
// TopologyKey is the key of node labels.
45+
TopologyKey string `yaml:"topologyKey" json:"topologyKey"`
46+
// WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint.
47+
WhenUnsatisfiable corev1.UnsatisfiableConstraintAction `yaml:"whenUnsatisfiable" json:"whenUnsatisfiable"`
48+
// LabelSelector is used to find matching pods.
49+
LabelSelector *LabelSelector `yaml:"labelSelector,omitempty" json:"labelSelector,omitempty"`
50+
// MinDomains indicates a minimum number of eligible domains.
51+
MinDomains *int32 `yaml:"minDomains,omitempty" json:"minDomains,omitempty"`
52+
// NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector when calculating pod topology spread skew.
53+
NodeAffinityPolicy *corev1.NodeInclusionPolicy `yaml:"nodeAffinityPolicy,omitempty" json:"nodeAffinityPolicy,omitempty"`
54+
// NodeTaintsPolicy indicates how we will treat node taints when calculating pod topology spread skew.
55+
NodeTaintsPolicy *corev1.NodeInclusionPolicy `yaml:"nodeTaintsPolicy,omitempty" json:"nodeTaintsPolicy,omitempty"`
56+
// MatchLabelKeys is a set of pod label keys to select the pods over which spreading will be calculated.
57+
MatchLabelKeys []string `yaml:"matchLabelKeys,omitempty" json:"matchLabelKeys,omitempty"`
58+
}
59+
1360
// Container describes how the App's tasks are expected to be run.
1461
type Container struct {
1562
// Image to run for this container
@@ -179,6 +226,9 @@ type Base struct {
179226
// Labels and Annotations can be used to attach arbitrary metadata as key-value pairs to resources.
180227
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
181228
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
229+
// TopologySpreadConstraints describes how a group of pods ought to spread across topology domains.
230+
// Scheduler will schedule pods in a way which abides by the constraints. All topologySpreadConstraints are ANDed.
231+
TopologySpreadConstraints map[string]TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty" yaml:"topologySpreadConstraints,omitempty"`
182232
}
183233

184234
type ServiceType string

modules/service/src/workload_base.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,3 +449,39 @@ func parseSecretReference(ref string) (result secretReference, _ bool, _ error)
449449

450450
return result, true, nil
451451
}
452+
453+
func handleTopologySpreadConstraints(tps map[string]TopologySpreadConstraint) []corev1.TopologySpreadConstraint {
454+
var topologySpreadConstraints []corev1.TopologySpreadConstraint
455+
if len(tps) == 0 {
456+
return nil
457+
}
458+
459+
for _, v := range tps {
460+
tp := corev1.TopologySpreadConstraint{
461+
MaxSkew: v.MaxSkew,
462+
TopologyKey: v.TopologyKey,
463+
WhenUnsatisfiable: v.WhenUnsatisfiable,
464+
MinDomains: v.MinDomains,
465+
NodeAffinityPolicy: v.NodeAffinityPolicy,
466+
NodeTaintsPolicy: v.NodeTaintsPolicy,
467+
MatchLabelKeys: v.MatchLabelKeys,
468+
}
469+
470+
if v.LabelSelector != nil {
471+
var matchExpressions []metav1.LabelSelectorRequirement
472+
for _, m := range v.LabelSelector.MatchExpressions {
473+
matchExpressions = append(matchExpressions, metav1.LabelSelectorRequirement{
474+
Key: m.Key,
475+
Operator: m.Operator,
476+
Values: m.Values,
477+
})
478+
}
479+
tp.LabelSelector = &metav1.LabelSelector{
480+
MatchLabels: v.LabelSelector.MatchLabels,
481+
MatchExpressions: matchExpressions,
482+
}
483+
}
484+
topologySpreadConstraints = append(topologySpreadConstraints, tp)
485+
}
486+
return topologySpreadConstraints
487+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
schema LabelSelector:
2+
""" A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed.
3+
An empty label selector matches all objects. A null label selector matches no objects.
4+
"""
5+
6+
# MatchExpressions is a list of label selector requirements. The requirements are ANDed.
7+
matchExpressions?: [LabelSelectorRequirement]
8+
9+
# matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions,
10+
# whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
11+
matchLabels?: {str:str}
12+
13+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
schema LabelSelectorRequirement:
2+
""" A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.
3+
"""
4+
5+
# Key is the label key that the selector applies to.
6+
key: str
7+
8+
# Operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.
9+
operator: str
10+
11+
# Values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator
12+
# is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.
13+
values?: [str]
14+
15+
check:
16+
operator in ["In", "NotIn", "Exists", "DoesNotExist"], "operator value is invalid"
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
schema TopologySpreadConstraint:
2+
""" TopologySpreadConstraint describes how a group of pods ought to spread across topology domains.
3+
Scheduler will schedule pods in a way which abides by the constraints. All topologySpreadConstraints are ANDed.
4+
5+
Attributes
6+
----------
7+
maxSkew: int, default is Undefined, required.
8+
MaxSkew describes the degree to which pods may be unevenly distributed. When whenUnsatisfiable=DoNotSchedule,it is the
9+
maximum permitted difference between the number of matching pods in the target topology and the global minimum. The global
10+
minimum is the minimum number of matching pods in an eligible domain or zero if the number of eligible domains is less than
11+
MinDomains. For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same labelSelector spread as 2/2/1: In
12+
this case, the global minimum is 1. | zone1 | zone2 | zone3 | | P P | P P | P | - if MaxSkew is 1, incoming pod can only be
13+
scheduled to zone3 to become 2/2/2; scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) violate MaxSkew(1).
14+
- if MaxSkew is 2, incoming pod can be scheduled onto any zone. When whenUnsatisfiable=ScheduleAnyway, it is used to give higher
15+
precedence to topologies that satisfy it. It's a required field. Default value is 1 and 0 is not allowed.
16+
topologyKey: str, default is Undefined, required.
17+
TopologyKey is the key of node labels. Nodes that have a label with this key and identical values are considered to be in the same
18+
topology. We consider each <key, value> as a "bucket", and try to put balanced number of pods into each bucket. We define a domain
19+
as a particular instance of a topology. Also, we define an eligible domain as a domain whose nodes meet the requirements of nodeAffinityPolicy
20+
and nodeTaintsPolicy. e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. And, if TopologyKey is
21+
"topology.kubernetes.io/zone", each zone is a domain of that topology. It's a required field.
22+
whenUnsatisfiable: str, default is Undefined, required.
23+
WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - DoNotSchedule (default) tells the
24+
scheduler not to schedule it. - ScheduleAnyway tells the scheduler to schedule the pod in any location, but giving higher precedence
25+
to topologies that would help reduce the skew. A constraint is considered "Unsatisfiable" for an incoming pod if and only if every
26+
possible node assignment for that pod would violate "MaxSkew" on some topology. For example, in a 3-zone cluster, MaxSkew is set to 1,
27+
and pods with the same labelSelector spread as 3/1/1: | zone1 | zone2 | zone3 | | P P P | P | P | If WhenUnsatisfiable is set to DoNotSchedule,
28+
incoming pod can only be scheduled to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies MaxSkew(1). In other words,
29+
the cluster can still be imbalanced, but scheduler won't make it more imbalanced. It's a required field.
30+
Possible enum values:
31+
- `"DoNotSchedule"` instructs the scheduler not to schedule the pod when constraints are not satisfied.
32+
- `"ScheduleAnyway"` instructs the scheduler to schedule the pod even if constraints are not satisfied.
33+
labelSelector: LabelSelector, default is Undefined, optional.
34+
LabelSelector is used to find matching pods. Pods that match this label selector are counted to determine the number of
35+
pods in their corresponding topology domain.
36+
matchLabelKeys: [str], default is Undefined, optional.
37+
MatchLabelKeys is a set of pod label keys to select the pods over which spreading will be calculated. The keys are used
38+
to lookup values from the incoming pod labels, those key-value labels are ANDed with labelSelector to select the group
39+
of existing pods over which spreading will be calculated for the incoming pod. Keys that don't exist in the incoming pod
40+
labels will be ignored. A null or empty list means only match against labelSelector.
41+
minDomains: int, default is Undefined, optional.
42+
MinDomains indicates a minimum number of eligible domains. When the number of eligible domains with matching topology keys is
43+
less than minDomains, Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. And when
44+
the number of eligible domains with matching topology keys equals or greater than minDomains, this value has no effect on scheduling.
45+
As a result, when the number of eligible domains is less than minDomains, scheduler won't schedule more than maxSkew Pods to those
46+
domains. If value is nil, the constraint behaves as if MinDomains is equal to 1. Valid values are integers greater than 0. When value
47+
is not nil, WhenUnsatisfiable must be DoNotSchedule.
48+
For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same labelSelector spread as
49+
2/2/2: | zone1 | zone2 | zone3 | | P P | P P | P P | The number of domains is less than 5(MinDomains), so "global minimum" is treated
50+
as 0. In this situation, new pod with the same labelSelector cannot be scheduled, because computed skew will be 3(3 - 0) if new Pod is
51+
scheduled to any of the three zones, it will violate MaxSkew.
52+
This is a beta field and requires the MinDomainsInPodTopologySpread feature gate to be enabled (enabled by default).
53+
nodeAffinityPolicy: str, default is Undefined, optional.
54+
NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector when calculating pod topology spread skew. Options are:
55+
- Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - Ignore: nodeAffinity/nodeSelector are ignored.
56+
All nodes are included in the calculations.
57+
If this value is nil, the behavior is equivalent to the Honor policy. This is a beta-level feature default enabled by the
58+
NodeInclusionPolicyInPodTopologySpread feature flag.
59+
nodeTaintsPolicy: str, default is Undefined, optional.
60+
NodeTaintsPolicy indicates how we will treat node taints when calculating pod topology spread skew. Options are: - Honor: nodes without
61+
taints, along with tainted nodes for which the incoming pod has a toleration, are included. - Ignore: node taints are ignored. All nodes
62+
are included.
63+
If this value is nil, the behavior is equivalent to the Ignore policy. This is a beta-level feature default enabled by the
64+
NodeInclusionPolicyInPodTopologySpread feature flag.
65+
66+
Examples
67+
--------
68+
import catalog.workload.topologyspreadconstraint as tp
69+
70+
topologyspreadconstraint = tp.TopologySpreadConstraint {
71+
maxSkew: 1
72+
topologyKey: "kubernetes.io/hostname"
73+
whenUnsatisfiable: "DoNotSchedule"
74+
}
75+
"""
76+
77+
# MaxSkew describes the degree to which pods may be unevenly distributed.
78+
# Must be greater than 0.
79+
maxSkew: int
80+
81+
# TopologyKey is the key of node labels.
82+
topologyKey: str
83+
84+
# WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint.
85+
whenUnsatisfiable: str
86+
87+
# LabelSelector is used to find matching pods.
88+
labelSelector?: LabelSelector
89+
90+
# MatchLabelKeys is a set of pod label keys to select the pods over which spreading will be calculated.
91+
matchLabelKeys?: [str]
92+
93+
# MinDomains indicates a minimum number of eligible domains.
94+
minDomains?: int
95+
96+
# NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector when calculating pod topology spread skew.
97+
nodeAffinityPolicy?: str
98+
99+
# NodeTaintsPolicy indicates how we will treat node taints when calculating pod topology spread skew.
100+
nodeTaintsPolicy?: str
101+
102+
check:
103+
maxSkew > 0, "maxSkew must be greater than 0"
104+
whenUnsatisfiable in ["DoNotSchedule", "ScheduleAnyway"], "whenUnsatisfiable value is invalid"
105+
labelSelector if matchLabelKeys, "matchLabelKeys can't be set when labelSelector isn't set"
106+
minDomains > 0 if minDomains != Undefined, "minDomains must be greater than 0"
107+
nodeAffinityPolicy in ["Ignore", "Honor"] if nodeAffinityPolicy, "nodeAffinityPolicy value is invalid"
108+
nodeTaintsPolicy in ["Ignore", "Honor"] if nodeTaintsPolicy, "nodeTaintsPolicy value is invalid"
109+

0 commit comments

Comments
 (0)