Skip to content

Commit 507eee8

Browse files
authored
Merge pull request kubernetes#127092 from munnerz/pod-topology
KEP-4742: Copy topology labels from Node objects to Pods upon binding/scheduling
2 parents 61572d7 + 8cfb9ad commit 507eee8

File tree

10 files changed

+733
-8
lines changed

10 files changed

+733
-8
lines changed

pkg/controlplane/apiserver/samples/generic/server/admission_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"k8s.io/kubernetes/plugin/pkg/admission/limitranger"
2525
"k8s.io/kubernetes/plugin/pkg/admission/network/defaultingressclass"
2626
"k8s.io/kubernetes/plugin/pkg/admission/nodetaint"
27+
"k8s.io/kubernetes/plugin/pkg/admission/podtopologylabels"
2728
podpriority "k8s.io/kubernetes/plugin/pkg/admission/priority"
2829
"k8s.io/kubernetes/plugin/pkg/admission/runtimeclass"
2930
"k8s.io/kubernetes/plugin/pkg/admission/security/podsecurity"
@@ -42,6 +43,7 @@ var intentionallyOffPlugins = sets.New[string](
4243
runtimeclass.PluginName, // RuntimeClass
4344
defaultingressclass.PluginName, // DefaultIngressClass
4445
podsecurity.PluginName, // PodSecurity
46+
podtopologylabels.PluginName, // PodTopologyLabels
4547
)
4648

4749
func TestDefaultOffAdmissionPlugins(t *testing.T) {

pkg/features/kube_features.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,20 @@ const (
964964
// restore the old behavior. Please file issues if you hit issues and have to use this Feature Gate.
965965
// The Feature Gate will be locked to true and then removed in +2 releases (1.35) if there are no bug reported
966966
DisableCPUQuotaWithExclusiveCPUs featuregate.Feature = "DisableCPUQuotaWithExclusiveCPUs"
967+
968+
// owner: @munnerz
969+
// kep: https://kep.k8s.io/4742
970+
// alpha: v1.33
971+
//
972+
// Enables the PodTopologyLabelsAdmission admission plugin that mutates `pod/binding`
973+
// requests by copying the `topology.k8s.io/{zone,region}` labels from the assigned
974+
// Node object (in the Binding being admitted) onto the Binding
975+
// so that it can be persisted onto the Pod object when the Pod is being scheduled.
976+
// This allows workloads running in pods to understand the topology information of their assigned node.
977+
// Enabling this feature also permits external schedulers to set labels on pods in an atomic
978+
// operation when scheduling a Pod by setting the `metadata.labels` field on the submitted Binding,
979+
// similar to how `metadata.annotations` behaves.
980+
PodTopologyLabelsAdmission featuregate.Feature = "PodTopologyLabelsAdmission"
967981
)
968982

969983
// defaultVersionedKubernetesFeatureGates consists of all known Kubernetes-specific feature keys with VersionedSpecs.
@@ -1571,6 +1585,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
15711585
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // GA in 1.30; remove in 1.32
15721586
},
15731587

1588+
PodTopologyLabelsAdmission: {
1589+
{Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Alpha},
1590+
},
1591+
15741592
PortForwardWebsockets: {
15751593
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
15761594
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},

pkg/kubeapiserver/options/plugins.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import (
4646
"k8s.io/kubernetes/plugin/pkg/admission/nodetaint"
4747
"k8s.io/kubernetes/plugin/pkg/admission/podnodeselector"
4848
"k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction"
49+
"k8s.io/kubernetes/plugin/pkg/admission/podtopologylabels"
4950
podpriority "k8s.io/kubernetes/plugin/pkg/admission/priority"
5051
"k8s.io/kubernetes/plugin/pkg/admission/runtimeclass"
5152
"k8s.io/kubernetes/plugin/pkg/admission/security/podsecurity"
@@ -93,6 +94,7 @@ var AllOrderedPlugins = []string{
9394
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
9495
defaultingressclass.PluginName, // DefaultIngressClass
9596
denyserviceexternalips.PluginName, // DenyServiceExternalIPs
97+
podtopologylabels.PluginName, // PodTopologyLabels
9698

9799
// new admission plugins should generally be inserted above here
98100
// webhook, resourcequota, and deny plugins must go at the end
@@ -138,6 +140,7 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
138140
certsigning.Register(plugins)
139141
ctbattest.Register(plugins)
140142
certsubjectrestriction.Register(plugins)
143+
podtopologylabels.Register(plugins)
141144
}
142145

143146
// DefaultOffAdmissionPlugins get admission plugins off by default for kube-apiserver.
@@ -162,6 +165,7 @@ func DefaultOffAdmissionPlugins() sets.Set[string] {
162165
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
163166
defaultingressclass.PluginName, // DefaultIngressClass
164167
podsecurity.PluginName, // PodSecurity
168+
podtopologylabels.PluginName, // PodTopologyLabels, only active when feature gate PodTopologyLabelsAdmission is enabled.
165169
mutatingadmissionpolicy.PluginName, // Mutatingadmissionpolicy, only active when feature gate MutatingAdmissionpolicy is enabled
166170
validatingadmissionpolicy.PluginName, // ValidatingAdmissionPolicy, only active when feature gate ValidatingAdmissionPolicy is enabled
167171
)

pkg/registry/core/pod/storage/storage.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ import (
3333
"k8s.io/apiserver/pkg/storage"
3434
storeerr "k8s.io/apiserver/pkg/storage/errors"
3535
"k8s.io/apiserver/pkg/util/dryrun"
36+
utilfeature "k8s.io/apiserver/pkg/util/feature"
3637
policyclient "k8s.io/client-go/kubernetes/typed/policy/v1"
3738
podutil "k8s.io/kubernetes/pkg/api/pod"
3839
api "k8s.io/kubernetes/pkg/apis/core"
3940
"k8s.io/kubernetes/pkg/apis/core/validation"
41+
kubefeatures "k8s.io/kubernetes/pkg/features"
4042
"k8s.io/kubernetes/pkg/kubelet/client"
4143
"k8s.io/kubernetes/pkg/printers"
4244
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
@@ -191,7 +193,7 @@ func (r *BindingREST) Create(ctx context.Context, name string, obj runtime.Objec
191193
}
192194
}
193195

194-
err = r.assignPod(ctx, binding.UID, binding.ResourceVersion, binding.Name, binding.Target.Name, binding.Annotations, dryrun.IsDryRun(options.DryRun))
196+
err = r.assignPod(ctx, binding.UID, binding.ResourceVersion, binding.Name, binding.Target.Name, binding.Annotations, binding.Labels, dryrun.IsDryRun(options.DryRun))
195197
out = &metav1.Status{Status: metav1.StatusSuccess}
196198
return
197199
}
@@ -203,10 +205,10 @@ func (r *BindingREST) PreserveRequestObjectMetaSystemFieldsOnSubresourceCreate()
203205
return true
204206
}
205207

206-
// setPodHostAndAnnotations sets the given pod's host to 'machine' if and only if
207-
// the pod is unassigned and merges the provided annotations with those of the pod.
208+
// setPodNodeAndMetadata sets the given pod's nodeName to 'machine' if and only if
209+
// the pod is unassigned, and merges the provided annotations and labels with those of the pod.
208210
// Returns the current state of the pod, or an error.
209-
func (r *BindingREST) setPodHostAndAnnotations(ctx context.Context, podUID types.UID, podResourceVersion, podID, machine string, annotations map[string]string, dryRun bool) (finalPod *api.Pod, err error) {
211+
func (r *BindingREST) setPodNodeAndMetadata(ctx context.Context, podUID types.UID, podResourceVersion, podID, machine string, annotations, labels map[string]string, dryRun bool) (finalPod *api.Pod, err error) {
210212
podKey, err := r.store.KeyFunc(ctx, podID)
211213
if err != nil {
212214
return nil, err
@@ -245,6 +247,11 @@ func (r *BindingREST) setPodHostAndAnnotations(ctx context.Context, podUID types
245247
for k, v := range annotations {
246248
pod.Annotations[k] = v
247249
}
250+
// Copy all labels from the Binding over to the Pod object, overwriting
251+
// any existing labels set on the Pod.
252+
if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.PodTopologyLabelsAdmission) {
253+
copyLabelsWithOverwriting(pod, labels)
254+
}
248255
podutil.UpdatePodCondition(&pod.Status, &api.PodCondition{
249256
Type: api.PodScheduled,
250257
Status: api.ConditionTrue,
@@ -255,9 +262,23 @@ func (r *BindingREST) setPodHostAndAnnotations(ctx context.Context, podUID types
255262
return finalPod, err
256263
}
257264

265+
func copyLabelsWithOverwriting(pod *api.Pod, labels map[string]string) {
266+
if len(labels) == 0 {
267+
// nothing to do
268+
return
269+
}
270+
if pod.Labels == nil {
271+
pod.Labels = make(map[string]string)
272+
}
273+
// Iterate over the binding's labels and copy them across to the Pod.
274+
for k, v := range labels {
275+
pod.Labels[k] = v
276+
}
277+
}
278+
258279
// assignPod assigns the given pod to the given machine.
259-
func (r *BindingREST) assignPod(ctx context.Context, podUID types.UID, podResourceVersion, podID string, machine string, annotations map[string]string, dryRun bool) (err error) {
260-
if _, err = r.setPodHostAndAnnotations(ctx, podUID, podResourceVersion, podID, machine, annotations, dryRun); err != nil {
280+
func (r *BindingREST) assignPod(ctx context.Context, podUID types.UID, podResourceVersion, podID string, machine string, annotations, labels map[string]string, dryRun bool) (err error) {
281+
if _, err = r.setPodNodeAndMetadata(ctx, podUID, podResourceVersion, podID, machine, annotations, labels, dryRun); err != nil {
261282
err = storeerr.InterpretGetError(err, api.Resource("pods"), podID)
262283
err = storeerr.InterpretUpdateError(err, api.Resource("pods"), podID)
263284
if _, ok := err.(*errors.StatusError); !ok {

pkg/registry/core/pod/storage/storage_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ import (
4242
apiserverstorage "k8s.io/apiserver/pkg/storage"
4343
storeerr "k8s.io/apiserver/pkg/storage/errors"
4444
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
45+
utilfeature "k8s.io/apiserver/pkg/util/feature"
46+
featuregatetesting "k8s.io/component-base/featuregate/testing"
4547
podtest "k8s.io/kubernetes/pkg/api/pod/testing"
4648
api "k8s.io/kubernetes/pkg/apis/core"
49+
kubefeatures "k8s.io/kubernetes/pkg/features"
4750
"k8s.io/kubernetes/pkg/registry/registrytest"
4851
"k8s.io/kubernetes/pkg/securitycontext"
4952
)
@@ -676,12 +679,14 @@ func TestEtcdCreateWithContainersNotFound(t *testing.T) {
676679
t.Fatalf("unexpected error: %v", err)
677680
}
678681

682+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, kubefeatures.PodTopologyLabelsAdmission, true)
679683
// Suddenly, a wild scheduler appears:
680684
_, err = bindingStorage.Create(ctx, "foo", &api.Binding{
681685
ObjectMeta: metav1.ObjectMeta{
682686
Namespace: metav1.NamespaceDefault,
683687
Name: "foo",
684-
Annotations: map[string]string{"label1": "value1"},
688+
Annotations: map[string]string{"annotation1": "value1"},
689+
Labels: map[string]string{"label1": "label-value1"},
685690
},
686691
Target: api.ObjectReference{Name: "machine"},
687692
}, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
@@ -695,9 +700,12 @@ func TestEtcdCreateWithContainersNotFound(t *testing.T) {
695700
}
696701
pod := obj.(*api.Pod)
697702

698-
if !(pod.Annotations != nil && pod.Annotations["label1"] == "value1") {
703+
if !(pod.Annotations != nil && pod.Annotations["annotation1"] == "value1") {
699704
t.Fatalf("Pod annotations don't match the expected: %v", pod.Annotations)
700705
}
706+
if !(pod.Labels != nil && pod.Labels["label1"] == "label-value1") {
707+
t.Fatalf("Pod labels don't match the expected: %v", pod.Labels)
708+
}
701709
}
702710

703711
func TestEtcdCreateWithConflict(t *testing.T) {

0 commit comments

Comments
 (0)