mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-01-04 23:17:50 +00:00
implement Node affinity and NodeSelector
This commit is contained in:
@@ -97,6 +97,10 @@ func ValidateAnnotations(annotations map[string]string, fldPath *field.Path) fie
|
||||
if totalSize > (int64)(totalAnnotationSizeLimitB) {
|
||||
allErrs = append(allErrs, field.TooLong(fldPath, "", totalAnnotationSizeLimitB))
|
||||
}
|
||||
|
||||
if annotations[api.AffinityAnnotationKey] != "" {
|
||||
allErrs = append(allErrs, ValidateAffinityInPodAnnotations(annotations, fldPath)...)
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
@@ -1346,6 +1350,102 @@ func ValidatePodSpec(spec *api.PodSpec, fldPath *field.Path) field.ErrorList {
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateNodeSelectorRequirement tests that the specified NodeSelectorRequirement fields has valid data
|
||||
func ValidateNodeSelectorRequirement(rq api.NodeSelectorRequirement, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
switch rq.Operator {
|
||||
case api.NodeSelectorOpIn, api.NodeSelectorOpNotIn:
|
||||
if len(rq.Values) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("values"), "must be specified when `operator` is 'In' or 'NotIn'"))
|
||||
}
|
||||
case api.NodeSelectorOpExists, api.NodeSelectorOpDoesNotExist:
|
||||
if len(rq.Values) > 0 {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("values"), "may not be specified when `operator` is 'Exists' or 'DoesNotExist'"))
|
||||
}
|
||||
|
||||
case api.NodeSelectorOpGt, api.NodeSelectorOpLt:
|
||||
if len(rq.Values) != 1 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("values"), "must be specified single value when `operator` is 'Lt' or 'Gt'"))
|
||||
}
|
||||
default:
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("operator"), rq.Operator, "not a valid selector operator"))
|
||||
}
|
||||
allErrs = append(allErrs, ValidateLabelName(rq.Key, fldPath.Child("key"))...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateNodeSelectorTerm tests that the specified node selector term has valid data
|
||||
func ValidateNodeSelectorTerm(term api.NodeSelectorTerm, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
if len(term.MatchExpressions) == 0 {
|
||||
return append(allErrs, field.Required(fldPath.Child("matchExpressions"), "must have at least one node selector requirement"))
|
||||
}
|
||||
for j, req := range term.MatchExpressions {
|
||||
allErrs = append(allErrs, ValidateNodeSelectorRequirement(req, fldPath.Child("matchExpressions").Index(j))...)
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateNodeSelector tests that the specified nodeSelector fields has valid data
|
||||
func ValidateNodeSelector(nodeSelector *api.NodeSelector, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
termFldPath := fldPath.Child("nodeSelectorTerms")
|
||||
if len(nodeSelector.NodeSelectorTerms) == 0 {
|
||||
return append(allErrs, field.Required(termFldPath, "must have at least one node selector term"))
|
||||
}
|
||||
|
||||
for i, term := range nodeSelector.NodeSelectorTerms {
|
||||
allErrs = append(allErrs, ValidateNodeSelectorTerm(term, termFldPath.Index(i))...)
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidatePreferredSchedulingTerms tests that the specified SoftNodeAffinity fields has valid data
|
||||
func ValidatePreferredSchedulingTerms(terms []api.PreferredSchedulingTerm, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
for i, term := range terms {
|
||||
if term.Weight <= 0 || term.Weight > 100 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Index(i).Child("weight"), term.Weight, "must be in the range 1-100"))
|
||||
}
|
||||
|
||||
allErrs = append(allErrs, ValidateNodeSelectorTerm(term.Preference, fldPath.Index(i).Child("preference"))...)
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateAffinityInPodAnnotations tests that the serialized Affinity in Pod.Annotations has valid data
|
||||
func ValidateAffinityInPodAnnotations(annotations map[string]string, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
affinity, err := api.GetAffinityFromPodAnnotations(annotations)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, api.AffinityAnnotationKey, err.Error()))
|
||||
return allErrs
|
||||
}
|
||||
|
||||
if affinity.NodeAffinity != nil {
|
||||
na := affinity.NodeAffinity
|
||||
if na.RequiredDuringSchedulingRequiredDuringExecution != nil {
|
||||
allErrs = append(allErrs, ValidateNodeSelector(na.RequiredDuringSchedulingRequiredDuringExecution, fldPath.Child("requiredDuringSchedulingRequiredDuringExecution"))...)
|
||||
}
|
||||
|
||||
if na.RequiredDuringSchedulingIgnoredDuringExecution != nil {
|
||||
allErrs = append(allErrs, ValidateNodeSelector(na.RequiredDuringSchedulingIgnoredDuringExecution, fldPath.Child("requiredDuringSchedulingIgnoredDuringExecution"))...)
|
||||
}
|
||||
|
||||
if len(na.PreferredDuringSchedulingIgnoredDuringExecution) > 0 {
|
||||
allErrs = append(allErrs, ValidatePreferredSchedulingTerms(na.PreferredDuringSchedulingIgnoredDuringExecution, fldPath.Child("preferredDuringSchedulingIgnoredDuringExecution"))...)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidatePodSecurityContext test that the specified PodSecurityContext has valid data.
|
||||
func ValidatePodSecurityContext(securityContext *api.PodSecurityContext, spec *api.PodSpec, specPath, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
@@ -1550,6 +1550,12 @@ func TestValidatePodSpec(t *testing.T) {
|
||||
RestartPolicy: api.RestartPolicyAlways,
|
||||
DNSPolicy: api.DNSClusterFirst,
|
||||
},
|
||||
{ // Populate Affinity.
|
||||
Volumes: []api.Volume{{Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}},
|
||||
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
|
||||
RestartPolicy: api.RestartPolicyAlways,
|
||||
DNSPolicy: api.DNSClusterFirst,
|
||||
},
|
||||
}
|
||||
for i := range successCases {
|
||||
if errs := ValidatePodSpec(&successCases[i], field.NewPath("field")); len(errs) != 0 {
|
||||
@@ -1712,6 +1718,50 @@ func TestValidatePod(t *testing.T) {
|
||||
NodeName: "foobar",
|
||||
},
|
||||
},
|
||||
{ // Serialized affinity requirements in annotations.
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "123",
|
||||
Namespace: "ns",
|
||||
Annotations: map[string]string{
|
||||
api.AffinityAnnotationKey: `
|
||||
{"nodeAffinity": {
|
||||
"requiredDuringSchedulingRequiredDuringExecution": {
|
||||
"nodeSelectorTerms": [{
|
||||
"matchExpressions": [{
|
||||
"key": "key1",
|
||||
"operator": "Exists"
|
||||
}]
|
||||
}]
|
||||
},
|
||||
"requiredDuringSchedulingIgnoredDuringExecution": {
|
||||
"nodeSelectorTerms": [{
|
||||
"matchExpressions": [{
|
||||
"key": "key2",
|
||||
"operator": "In",
|
||||
"values": ["value1", "value2"]
|
||||
}]
|
||||
}]
|
||||
},
|
||||
"preferredDuringSchedulingIgnoredDuringExecution": [
|
||||
{
|
||||
"weight": 10,
|
||||
"preference": {"matchExpressions": [
|
||||
{
|
||||
"key": "foo",
|
||||
"operator": "In", "values": ["bar"]
|
||||
}
|
||||
]}
|
||||
}
|
||||
]
|
||||
}}`,
|
||||
},
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
|
||||
RestartPolicy: api.RestartPolicyAlways,
|
||||
DNSPolicy: api.DNSClusterFirst,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, pod := range successCases {
|
||||
if errs := ValidatePod(&pod); len(errs) != 0 {
|
||||
@@ -1756,6 +1806,111 @@ func TestValidatePod(t *testing.T) {
|
||||
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
|
||||
},
|
||||
},
|
||||
"invalid json of affinity in pod annotations": {
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "123",
|
||||
Namespace: "ns",
|
||||
Annotations: map[string]string{
|
||||
api.AffinityAnnotationKey: `
|
||||
{"nodeAffinity": {
|
||||
"requiredDuringSchedulingRequiredDuringExecution": {
|
||||
"nodeSelectorTerms": [{
|
||||
`,
|
||||
},
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
|
||||
RestartPolicy: api.RestartPolicyAlways,
|
||||
DNSPolicy: api.DNSClusterFirst,
|
||||
},
|
||||
},
|
||||
"invalid node selector requirement in affinity in pod annotations, operator can't be null": {
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "123",
|
||||
Namespace: "ns",
|
||||
Annotations: map[string]string{
|
||||
api.AffinityAnnotationKey: `
|
||||
{"nodeAffinity": {"requiredDuringSchedulingRequiredDuringExecution": {
|
||||
"nodeSelectorTerms": [{
|
||||
"matchExpressions": [{
|
||||
"key": "key1",
|
||||
}]
|
||||
}]
|
||||
}}}`,
|
||||
},
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
|
||||
RestartPolicy: api.RestartPolicyAlways,
|
||||
DNSPolicy: api.DNSClusterFirst,
|
||||
},
|
||||
},
|
||||
"invalid preferredSchedulingTerm in affinity in pod annotations, weight should be in range 1-100": {
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "123",
|
||||
Namespace: "ns",
|
||||
Annotations: map[string]string{
|
||||
api.AffinityAnnotationKey: `
|
||||
{"nodeAffinity": {"preferredDuringSchedulingIgnoredDuringExecution": [
|
||||
{
|
||||
"weight": 199,
|
||||
"preference": {"matchExpressions": [
|
||||
{
|
||||
"key": "foo",
|
||||
"operator": "In",
|
||||
"values": ["bar"]
|
||||
}
|
||||
]}
|
||||
}
|
||||
]}}`,
|
||||
},
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
|
||||
RestartPolicy: api.RestartPolicyAlways,
|
||||
DNSPolicy: api.DNSClusterFirst,
|
||||
},
|
||||
},
|
||||
"invalid requiredDuringSchedulingRequiredDuringExecution node selector, nodeSelectorTerms must have at least one term": {
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "123",
|
||||
Namespace: "ns",
|
||||
Annotations: map[string]string{
|
||||
api.AffinityAnnotationKey: `
|
||||
{"nodeAffinity": {
|
||||
"requiredDuringSchedulingRequiredDuringExecution": {
|
||||
"nodeSelectorTerms": []
|
||||
},
|
||||
}}`,
|
||||
},
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
|
||||
RestartPolicy: api.RestartPolicyAlways,
|
||||
DNSPolicy: api.DNSClusterFirst,
|
||||
},
|
||||
},
|
||||
"invalid requiredDuringSchedulingRequiredDuringExecution node selector term, matchExpressions must have at least one node selector requirement": {
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "123",
|
||||
Namespace: "ns",
|
||||
Annotations: map[string]string{
|
||||
api.AffinityAnnotationKey: `
|
||||
{"nodeAffinity": {
|
||||
"requiredDuringSchedulingRequiredDuringExecution": {
|
||||
"nodeSelectorTerms": [{
|
||||
"matchExpressions": []
|
||||
}]
|
||||
},
|
||||
}}`,
|
||||
},
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
|
||||
RestartPolicy: api.RestartPolicyAlways,
|
||||
DNSPolicy: api.DNSClusterFirst,
|
||||
},
|
||||
},
|
||||
}
|
||||
for k, v := range errorCases {
|
||||
if errs := ValidatePod(&v); len(errs) == 0 {
|
||||
|
||||
Reference in New Issue
Block a user