Repository: kubernetes/kubernetes (language: go)

Issue:
Consider exposing node labels via downward api
**What keywords did you search in Kubernetes issues before filing this one?** (If you have found any duplicates, you should instead reply there.): downward api node labels

---

**Is this a BUG REPORT or FEATURE REQUEST?** (choose one): Feature request

<!--
If this is a BUG REPORT, please:
  - Fill in as much of the template below as you can.  If you leave out
    information, we can't help you as well.

If this is a FEATURE REQUEST, please:
  - Describe *in detail* the feature/behavior/change you'd like to see.

In both cases, be ready for followup questions, and please respond in a timely
manner.  If we can't reproduce a bug or think a feature already exists, we
might close your issue.  If we're wrong, PLEASE feel free to reopen it and
explain why.
-->

I think the downward API should support exposing the node labels on which the pod it is running. While I understand that the Pod should not be coupled too much with the node it is scheduled to. But I think there are certain scenarios that require the pod  to have more information about it's host.

- ***failure-zones*** like rack names on premise or availability zones in cloud
- ***instance-types*** for adapting thread settings per instance type
- other labels like local ssd storage available 

This helps applications that are cloud-native to understand where they are located. While this could be achieved using API calls to the node object, I think it should be supported by the downward API. A node object would expose way more information that should not be available to any pod. In OpenShift's default configuration there is access to the Node objects prevented by default.

@mattbates @smarterclayton @bprashanth  


Discussion / hints:
There *could* be security implications for some types of clusters (exposing information the admin doesn't want the pod to have, like the exact core architecture).  That said, for most deployments it doesn't seem completely unreasonable.  A per node setting might decide whether to expose it or to return an empty map.
This was previously explored in #25957
/sig node
Issues go stale after 90d of inactivity.
Mark the issue as fresh with `/remove-lifecycle stale`.
Stale issues rot after an additional 30d of inactivity and eventually close.

Prevent issues from auto-closing with an `/lifecycle frozen` comment.

If this issue is safe to close now please do so with `/close`.

Send feedback to sig-testing, kubernetes/test-infra and/or `@fejta`.
/lifecycle stale
Stale issues rot after 30d of inactivity.
Mark the issue as fresh with `/remove-lifecycle rotten`.
Rotten issues close after an additional 30d of inactivity.

If this issue is safe to close now please do so with `/close`.

Send feedback to sig-testing, kubernetes/test-infra and/or [fejta](https://github.com/fejta).
/lifecycle rotten
/remove-lifecycle stale
Rotten issues close after 30d of inactivity.
Reopen the issue with `/reopen`.
Mark the issue as fresh with `/remove-lifecycle rotten`.

Send feedback to sig-testing, kubernetes/test-infra and/or [fejta](https://github.com/fejta).
/close
/remove-lifecycle rotten
Can somebody please re-open this issue? I think this is a valid concern. I don't think that node labels and annotations are sensitive but if people disagree, maybe whitelisting some labels would be an option.

My use case is setting labels for node group detection in the autoscaler. If I could use the downward API, I wouldn't have to hardcode (or template) the manifests.
/reopen
@kargakis: you can't re-open an issue/PR unless you authored it or you are assigned to it.

<details>

In response to [this](https://github.com/kubernetes/kubernetes/issues/40610#issuecomment-379237173):

>/reopen


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/devel/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
Issues go stale after 90d of inactivity.
Mark the issue as fresh with `/remove-lifecycle stale`.
Stale issues rot after an additional 30d of inactivity and eventually close.

If this issue is safe to close now please do so with `/close`.

Send feedback to sig-testing, kubernetes/test-infra and/or [fejta](https://github.com/fejta).
/lifecycle stale
/remove-lifecycle stale
/lifecycle freeze
Is this still being considered? I'm currently trying to find a sensible way to retrieve the zone a pod is scheduled in for rack awareness, as per the original use case.
We're looking at doing this for a per-zone load balancer, and per-region load balancer on a single deployment. Our use case is that customers must be able to bind at layer 4 to multiple failure domains. This could be achieved with 3x deployments (one per zone) but it would be much nicer to only have to worry about a single deployment.
I think this should be brought up in a sig-node meeting, otherwise I doubt this will get attention and ultimately get closed off by @fejta-bot 
Issues go stale after 90d of inactivity.
Mark the issue as fresh with `/remove-lifecycle stale`.
Stale issues rot after an additional 30d of inactivity and eventually close.

If this issue is safe to close now please do so with `/close`.

Send feedback to sig-testing, kubernetes/test-infra and/or [fejta](https://github.com/fejta).
/lifecycle stale
/remove-lifecycle stale
I am in favor of something like this.  The real problem here is that most people seem to want this for topology information and that information is somewhat arbitrary.  Hostname/Zone/Region are codified, so they get the most attention, but there is some demand for other topology.  That leads me to assert that the common API is labels and not something special for topology info.
would folks be comfortable with kubelet having a configurable whitelist of node-labels that can be consumed by pods via the downward-api. I am constantly hitting snags that having this feature would have unblocked or prevented weird workarounds
Can we talk about concrete needs?  What snags are you hitting?

I have heard concrete need for topology info, which is stored as labels and is indistinguishable from other labels.  That (rightly) sets off alarms for security-conscious people.  Do we need to first-class topology?  I'm less confident in that.

The last thing we really want is another configuration knob that admins have to know about to be secure...
Our use case is being able to inform elasticsearch pods about topology so that we can make sure shards get placed across failure domains (eg racks).  Currently, we have to contort ourselves through some awkward initContainers to get those details into elasticsearch configs.
ACK.  Another +1 for topology.  I am not sure the right answer.  Moving
topology to a distinct struct would make it easy to copy that info into
pods, but there's a lot of momentum around labels already, so we'd have to
do both for the indefinite future.

As far as I know, nobody is signed up to try to push that yet.

On Tue, Jun 11, 2019 at 4:55 PM Erik Stephens <notifications@github.com>
wrote:

> Our use case is being able to inform elasticsearch pods about topology so
> that we can make sure shards get placed across failure domains (eg racks).
> Currently, we have to contort ourselves through some awkward initContainers
> to get those details into elasticsearch configs.
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/kubernetes/kubernetes/issues/40610?email_source=notifications&email_token=ABKWAVF4QT5PYDADVUJRZNTP2A3QJA5CNFSM4C57YMFKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXO2YJA#issuecomment-501066788>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABKWAVFPKZP7W747YAJANKTP2A3QJANCNFSM4C57YMFA>
> .
>

> Can we talk about concrete needs? What snags are you hitting?

Sorry for not providing an helpful case :) All my snags have all been topology related, app needs to know what region/az or datacenter it is deployed to.

> As far as I know, nobody is signed up to try to push that yet.

Where do I get to sign up for this :) I don't mind pushing it if I get guidance
My use case is for the EC2 instance ID (which is automatically added as a label by the cloud provider) to be added as an environment variable for a container. Since I lacked this feature I had to provide a workaround (granted my pod access to the AWS metadata FUSE we use at my company https://github.com/jszwedko/ec2-metadatafs) that I wasn't really keen on. Having pod labels available in the downward API just makes sense honestly, I don't know why we need to introduce brand new features for this :D
> Where do I get to sign up for this :) I don't mind pushing it if I get guidance

You sign up by saying "I'd like to tackle this". :)

As for guidance, the real problem is that I don't know what the right design is.  The way I would usually start such an effort is with a brainstorm doc - try to pin down the kernel of the requirements expressed here and then dream up 10 different ways we could solve it, no judgement on good/bad ideas.  Then write the most aggressive pros/cons you can for each.  Then we discuss.

e.g. 

1) we could add node labels as a downward API "fake" field path
1b) we could make new syntax for field paths that follow object ref pointers
2) we could literally copy node labels to pods
3) we could add a new node topology block and copy THAT into pods
4) we could add a link-local metadata API for use at run-time
5) we could add a new type of volume/envFrom source that is node-metadata
...
> You sign up by saying "I'd like to tackle this". :)

I'd like to tackle this :)

Thanks for all of this context, I'll start writing some doc and share as soon I have something
Like @erik-stephens failure domain awareness would be a prime motivator for this: in our case Ceph.
For folks interested myself and @thockin had a brainstorm in this doc https://docs.google.com/document/d/1fH64mEfZH597luup-ZBfBNkiTVjfoFYGEa-G1G_TM6A/edit#, I wrote something that covered the last idea (which is Tim's preference): https://github.com/bjhaid/node-labels-to-files
Thanks @bjhaid, this helps but for my use-case (elasticsearch), I will still need another initContainer in order to get those values into a form that elasticsearch will read (eg write updated config, inject environment variables).  Maybe I can use something like [envdir](https://cr.yp.to/daemontools/envdir.html).  Some soft goals I'm aiming for:

1. Minimal changes to vendor's image
1. Use default entrypoint & command
1. Minimal initContainers

Chiming in here in case others on this thread have ideas.  I was hoping there was a stock/std linux way to do what `envdir` does but I couldn't find.
we need this for 2 user cases:
- nodes zone info in pod labels, so service selector can deliver traffic inside the same zone instead of doing cross zone requests
- environment variable with node zone info, so cluster based apps can have the correct info. It can also be used to request the correct in-zone dns and add the correct labels to monitoring ( like /metrics) and logs 

While we can use a initcontainer to set this, it require giving pods `patch` permissions to affected pods and so opening a security hole to allow some hacker to change kubernetes setup


My proposal is to have a node "flag"/label/annotations that would enable label propagation and what labels are allowed to be propagated (say: `node-labels-propagation: false` (default) , `true` (all labels) , `'{"failure-domain.beta.kubernetes.io/zone","failure-domain.beta.kubernetes.io/region"}' `)

- This allow the admin to control the info node leak to pods.

Then in the pods, it would be required to request what info they want, with two annotation, one for labels and another for environment ( say: `pod-labels-from-nodes-labels: false, true , '{"...","..."}' ` for label copy and `pod-enviroment-from-nodes-labels: false, true, '{..}' ` for environment). If needed, another option for a volume with the info (`pod-volume-from-node-labels: ...`)

- This allow the pod admin to request only what is needed and ignore any other label that he doesn't need
Issues go stale after 90d of inactivity.
Mark the issue as fresh with `/remove-lifecycle stale`.
Stale issues rot after an additional 30d of inactivity and eventually close.

If this issue is safe to close now please do so with `/close`.

Send feedback to sig-testing, kubernetes/test-infra and/or [fejta](https://github.com/fejta).
/lifecycle stale
I'm not sure, that `fejta-bot` is useful. Why every one subscriber must get this message, just because no body take care about issues and hope that they are automagically get closed by time?

/remove-lifecycle stale
@Bessonov tell that @fejta 
I've just stopped filling issues I don't have the resources to fix right away.
One similar use case from istio: to support locality load balancing, we need to watch all nodes of the cluster to acquire its topology labels such as`[topology.kubernetes.io/region, topology.kubernetes.io/zone]`. We just need these locality info, but we have to get all the nodes, which is a waste of lot of memory. It would be better if k8s provide a way to allow setting these topology info directly into pod.
+1 for this as well

We would like access to this information in order for a highly available data store service to know which `[topology.kubernetes.io/region, topology.kubernetes.io/zone]` the data store processes are running. Without this information, we could end up in a scenario where data is stored in only a single region or zone. Upon that region becoming unavailable, it's possible the data store service would be 0% available. 
```
node-labels-propagation: false (default) , true (all labels) ...
```
I think when node label is allowed to propagate, 'all labels' should be avoided since there may be information which admin doesn't want to reveal.
Listing specific labels to propagate would serve the use cases.
It is a bit sad seeing this feature, with so many practical use cases listed above, left unanswered for so long.
As a fully owned, single tenant Kubernetes user, I take care of both nodes AND pods, and I've been needing this feature for a long time.
It it utterly pointless to set topology information on nodes if that information can not be propagated to pods to implement intelligent, topology-aware deployments. Besides, pod affinity/antiaffinity rules already rely on node labels, and that is not considered a security concern.

I have never considered hiding nodes' labels a security feature.

I am wondering if we can tackle the security concerns by adding a clause that the user creating (or the serviceaccount associated with) a pod that requests node's information via one of the methods proposed by @bjhaid in the linked google doc, needs to have the `get node` permission? 
I'm another +1 for topology info. [Topology spread constraints](https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/) let you easily spread pods across different regions/racks etc, but there's no (easy) way for the pod to know where it is actually scheduled, so the application can't make smart decisions based on topology. Even making only `topology.kubernetes.io` labels available would be an improvement.
Issues go stale after 90d of inactivity.
Mark the issue as fresh with `/remove-lifecycle stale`.
Stale issues rot after an additional 30d of inactivity and eventually close.

If this issue is safe to close now please do so with `/close`.

Send feedback to sig-testing, kubernetes/test-infra and/or [fejta](https://github.com/fejta).
/lifecycle stale
/remove-lifecycle stale
/remove-lifecycle stale
Another solid use-case is in a cluster-per-environment setup where a node label can be added (e.g. in gke or eks you could add a label `cluster-environment: staging` to all nodes in a cluster) and then that can be provided to workloads via the downward api to behave appropriately according to business logic.
This obviously doesn't fit with a `topology` structure, so I might propose that it is maybe slightly too narrow in scope.  Something perhaps to consider would be a format for "exposable" labels.  Perhaps something along the lines of `user/.*` being exposable.
Already regretting this comment just because of the notifications I'll get from fejta forever as it languishes.
I want to have node zone name in envronment variable to use some latency-critical services strongly in the same zone (for example: `mongos`).

`mongos` is a separate service from mongodb, through which all requests in a sharded configuration routed (app <-> mongos <-> mongodb). Since these are database queries, each hop has a significant impact on overall performance.

In bare hardware we deploy and use mongos in the same host (`localhost`) for reduce traffic and latency.

In Kubernetes envrinoment we try to:
 - Reproduce localhost topology by DaemonSet deploy. This does not work well due to very different resource consumption from node to node and service to service. We have had situations where mongod created more than 50% of the load on the node.
 - Deploy mongos to same pod. This leads to too large RAM and CPU overhead.
 - Deploy separate mongos Service to every zone. This solution looks very ugly, as the application has to execute a lot of code to get the zone from the node's metadata. Moreover, this code is different in each language. If it was possible to add a zone to an environment variable, it would be much easier (for example: Spring in Java expand environment variables in properties out of the box).
Hi,

we need to propagate the node labels
failure-domain.beta.kubernetes.io/region
failure-domain.beta.kubernetes.io/zone
topology.kubernetes.io/region
topology.kubernetes.io/zone
to our pods because we need the information to distribute elasticsearch shards correctly and more safely.

So it would be great if this possibility could be implemented.
Given EndpointSlice is removing `topology`, this becomes even more important, as the only source of region/zone is now the node.
> Given EndpointSlice is removing topology

Any links? @howardjohn 
https://opensource.googleblog.com/2020/11/kubernetes-efficient-multi-zone.html, https://github.com/kubernetes/enhancements/tree/master/keps/sig-network/2030-endpointslice-subsetting, and https://github.com/kubernetes/enhancements/blob/master/keps/sig-network/2086-service-internal-traffic-policy/README.md

After thinking about it a bit more I don't think it is directly related to this issue though (needing to know the topology of all endpoints vs your own node labels) but this would still help in many other cases
@steffeneichler

This is what I had to do to implement rack-awareness for elasticsearch in kubernetes:

Add rack awareness in elasticsearch.yml:
```yaml
cluster.routing.allocation.awareness.attributes: rack
```

Label my nodes with their rack-information:
```bash
kubectl label node my-node topology.kubernetes.io/rack=L101B09
```

Extract node information via kubectl, and export them to elasticsearch via env variable: (I stripped pod affinity, ports, probes, volumes... for brevity)
```yaml
apiVersion: apps/v1
kind: StatefulSet
spec:
  serviceName: elasticsearch
  template:
    spec:
      serviceAccountName: elasticsearch

      initContainers:
      - name: get-rack
        image: kubectl
        command:
        - bash
        - -c
        - |
          set -ex
          RACK=$(kubectl get node $NODE_NAME -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/rack}')
          echo "$RACK" > /tmp/es-rack
        env:
        - name: NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        volumeMounts:
        - name: tmp
          mountPath: /tmp/

      containers:
      - name: elasticsearch
        image: elasticsearch
        command:
        - bash
        - -c
        - |
          set -ex
          env "node.attr.rack=$(</tmp/es-rack)" /usr/local/bin/docker-entrypoint.sh eswrapper
        env:
        - name: node.name
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: node.attr.node
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        volumeMounts:
        - name: tmp
          mountPath: /tmp/
      volumes:
      - name: tmp
        emptyDir: {}
```

Aaaaand of course you need RBAC to allow the serviceaccount to extract node information:
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: elasticsearch
  namespace: elastic
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: elasticsearch-get-nodes
  namespace: elastic
rules:
- apiGroups: [""]
  resources: [nodes]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: elasticsearch-get-nodes
  namespace: elastic
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: elasticsearch-get-nodes
subjects:
- kind: ServiceAccount
  name: elasticsearch
  namespace: elastic
```

Yes, it is this ugly. I actually could have done it all in the main container by creating an image with both elasticsearch and kubectl.
Hi @Feder1co5oave,

I found this way too. But this is a very ugly way. The best way would be to propagate the node topoloy information to the pods like the nodename.
is this ever going to be addressed? perhaps a label for nodes prefaced with an indicator for downwardapi. e.g. dapi-region, dapi-az, etc. This way all of the node labels aren't exposed, but ones the admin wants are.
Wow, this has been going for a long time.  I would really like to have a clear picture of how my pods are balanced between availability zones.  I can't imagine I am the only one operating kubernetes clusters in multiple failover zones who would benefit from this perspective.  Can anyone explain to me at a high level how region/AZ labels on metrics can be used to compromise the cluster or it's workload?  I don't understand the objection here, there are clear benefits to the proposal.
The simple answer is that it will get implemented when someone steps up to design and implement it.  There are hundreds and hundreds of these issues - really decent ideas - and they all need someone to step up to drive it.

I know that's not what you wanted to hear, but this project has a finite number of (mostly volunteer!) contributors with finite hours per week.  If you have some time, why not write a KEP?  It's not hard, but it needs some follow-thru.

In the interim, you can always run `kubectl get ... | jq ...` in a container in your pod, and save the output to a file on an emptydir volume.


Absolutely, I do understand how community contributions work. I've read through two issues here with somewhat unsubstantiated privacy concerns, my hope was to get those concerns overtly documented here to help such contributions move forward.
What about the solution https://github.com/kubernetes/kubernetes/issues/93999 ?
@s97712: I'm not sure how #93999 could help here to fix the problem. One thing is the node label and #93999 is about nodeSecret or configmap.
> @s97712: I'm not sure how #93999 could help here to fix the problem. One thing is the node label and #93999 is about nodeSecret or configmap.

Although the names are different, they are all data in essence.
> 
> 
> > @s97712: I'm not sure how #93999 could help here to fix the problem. One thing is the node label and #93999 is about nodeSecret or configmap.
> 
> Although the names are different, they are all data in essence.

Everything is data. A node label is an existing node property. The task here is to find a way to expose this existing information to pod in the same way as the node name. #93999 is a request for new kind of information.
Issues go stale after 90d of inactivity.
Mark the issue as fresh with `/remove-lifecycle stale`.
Stale issues rot after an additional 30d of inactivity and eventually close.

If this issue is safe to close now please do so with `/close`.

Send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).
/lifecycle stale
/remove-lifecycle stale
/kind feature
in my point of view these node labels should be available by default as env variable...
> 
> 
> in my point of view these node labels should be available by default as env variable...

that would be great.
What about implementing it as a new kind of volume (e.g. nodeProperties) that a pod spec could require?
kubelet would be in charge of representing the requested node properties (in the volumes entry definition included in the Pod spec) as files, "mounting" it in the pod's container's specified path; those requested node properties (i.e. labels) configured in kubelet as "restricted" would be exposed to the pod's container as empty files.

I think this is a cleaner solution than using the downward API (as that was thought for exposing to a pod's container some pod's fields, but not fields from another API object).

Hi! I am also seeking for this feature. It might be useful for reducing latency. Imagine a situation when we host each kafka node and k8s worker node in separate AZ. Then with additional metadata pod should pick the closest destination to get a better network performance. I believe its a bit complicated, but lots of design patterns will benefit from this enhancement
The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue or PR as fresh with `/remove-lifecycle stale`
- Mark this issue or PR as rotten with `/lifecycle rotten`
- Close this issue or PR with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
/remove-lifecycle stale
>Imagine a situation when we host each kafka node and k8s worker node in separate AZ. Then with additional metadata pod should pick the closest destination to get a better network performance.

We're in exactly this situation and it's not just performance, but cost. Cross-AZ Kafka traffic is serious money at scale. Right now we're relying on GET requests to the node metadata API, but that's going to get locked down for compliance. A pattern for exposing certain fields and not others would satisfy both needs nicely.
The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue or PR as fresh with `/remove-lifecycle stale`
- Mark this issue or PR as rotten with `/lifecycle rotten`
- Close this issue or PR with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
/remove-lifecycle stale
The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue or PR as fresh with `/remove-lifecycle stale`
- Mark this issue or PR as rotten with `/lifecycle rotten`
- Close this issue or PR with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
/remove-lifecycle stale
Should this issue be closed (as Won't-Fix) because [the comments](https://github.com/kubernetes/kubernetes/pull/25957#issuecomment-228041766) in the most recently attempted implementation (#25957) read as irreconcilable to me. Leaving this issue open seems deceptive when the intent is to not implement this. 
It reads to me that a PR that allowed cluster admins to selective choose which metadata is made available to the downward API would address the security concerns in https://github.com/kubernetes/kubernetes/pull/25957#issuecomment-228041766
I think its reasonable to open a KEP for this proposal for a specific set of allowed node labels for usage in downward API.  Labels have been better normalized since this issue was originally opened and expanding labels for particular use cases could be useful.  Allowing all labels though may prove problematic for users that label nodes with security sensitive information that is not to be exposed to the workload.
Having access to a selected 'allow-listed' set of node labels through the downward API would address my use-case as well as long as the following [(Well-Known)](https://kubernetes.io/docs/reference/labels-annotations-taints/) labels would be part of this set:

[topology.kubernetes.io/zone](https://kubernetes.io/docs/reference/labels-annotations-taints/#topologykubernetesiozone)
[topology.kubernetes.io/region](https://kubernetes.io/docs/reference/labels-annotations-taints/#topologykubernetesioregion)

I understand and agree that exposing all node labels is definitely not wanted due to the concerns mentioned by @derekwaynecarr

Has a KEP been created for this already?
Injecting topology awareness into pods can be accomplished with [KubeMod](https://github.com/kubemod/kubemod).

Here's a KubeMod ModRule which intercepts the event of any pod being scheduled on a node, then copies the node's `topology.kubernetes.io/region` and `topology.kubernetes.io/zone` labels into the pod's labels before the pod's containers are created.

```yaml
apiVersion: api.kubemod.io/v1beta1
kind: ModRule
metadata:
  name: inject-topology-labels
  namespace: kubemod-system
spec:
  type: Patch
  targetNamespaceRegex: ".*"
  admissionOperations:
    - UPDATE

  match:
    # Match pods...
    - select: '$.kind'
      matchValue: 'Pod'
    # ...which have access to the node's manifest through the synthetic ref injected by KubeMod.
    - select: '$.syntheticRefs.node.metadata.labels'

  patch:
    # Grab the node's region and zone and put them in the pod's corresponding labels.
    - op: add
      path: /metadata/labels/topology.kubernetes.io~1region
      value: '"{{ index .Target.syntheticRefs.node.metadata.labels "topology.kubernetes.io/region"}}"'
    - op: add
      path: /metadata/labels/topology.kubernetes.io~1zone
      value: '"{{ index .Target.syntheticRefs.node.metadata.labels "topology.kubernetes.io/zone"}}"'
``` 

Having KubeMod and the above ModRule deployed to your cluster, transparently stamps out all pods with their node's topology labels whenever they get scheduled on a node. This makes topology information available to the pod's containers through the Kubernetes Downward API.

For example, the following `Deployment` will use the region and availability zone injected by KubeMod into its pods, to pass as variables into the pods’ containers:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  template:
    metadata:
      annotations:
        # Tell KubeMod to make node metadata available to pod ModRules.
        ref.kubemod.io/inject-node-ref: "true"
    spec:
      containers:
      - name: my-container
        image: my-container-image:v1.2.3
        env:
          - name: TOPOLOGY_REGION
            valueFrom:
              fieldRef:
                # The following pod label will be injected by ModRule inject-topology-labels.
                fieldPath: metadata.labels['topology.kubernetes.io/region']
          - name: TOPOLOGY_ZONE
            valueFrom:
              fieldRef:
                # The following pod label will be injected by ModRule inject-topology-labels.
                fieldPath: metadata.labels['topology.kubernetes.io/zone']
```
6 years now, and there is still no way to access topology labels from the downward API. Wild.
https://github.com/kubernetes/kubernetes/issues/40610#issuecomment-1231988005

KEPs are welcome!
I was thinking about how to leverage topology information from within an MPI application.

In MPI, you could reduce inter-zone communication by using communicators. The function looks like this:

```
MPI_Comm_split(
	MPI_Comm comm,
	int color,
	int key,
	MPI_Comm* newcomm)
```

`color` matches nicely with node label :smiley: 

But it's hard to currently get this information into the Pod. So I would also welcome a KEP that addresses this :)

/label wg-batch

@alculquicondor: The label(s) `/label wg-batch
` cannot be applied. These labels are supported: `api-review, tide/merge-method-merge, tide/merge-method-rebase, tide/merge-method-squash, team/katacoda, refactor, official-cve-feed`. Is this label configured under `labels -> additional_labels` or `labels -> restricted_labels` in `plugin.yaml`?

<details>

In response to [this](https://github.com/kubernetes/kubernetes/issues/40610#issuecomment-1531695983):

>I was thinking about how to leverage topology information from within an MPI application.
>
>In MPI, you could reduce inter-zone communication by using communicators. The function looks like this:
>
>```
>MPI_Comm_split(
>	MPI_Comm comm,
>	int color,
>	int key,
>	MPI_Comm* newcomm)
>```
>
>`color` matches nicely with node label :smiley: 
>
>But it's hard to currently get this information into the Pod. So I would also welcome a KEP that addresses this :)
>
>/label wg-batch
>


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
/wg batch
cc @marquiz 
/cc 
could this be _partially_ addressed with https://kubernetes-sigs.github.io/node-feature-discovery/v0.13/usage/custom-resources.html#nodefeature ? 
Not really. The Pod could always query the kubernetes API to get Node labels. Reading a NodeFeatures CRD has the same problem. The point of the downward API is that the Pod doesn't have to do that: the information is readily available to the Pod as an environment variable or a file.
we could drop that CR per node, so it can be consumed by pods, preventing pods from having to access the node it self. WDYT? 
I can think in something like the `client-go` https://github.com/kubernetes/client-go/blob/master/rest/config.go#L511 
but to retrieve the NFD `NodeFeature` struck from the node 
The idea is that a workload doesn't need to know about the Kubernetes API, Node or CRD. The information should be available via environment variables, so you don't have to change your application or add a dependency on a kubernetes client.
The problem with downward API across resources is that the pod's SA might
intentionally NOT be allowed to read nodes.

Why not do this as a sidecar and an emptyDir volume?  We now have a kubectl
container image - just run that as an initcontainer, save the info you want
to a file, done.

If your pod doesn't have permission, it will fail.

On Tue, May 2, 2023, 12:45 PM Aldo Culquicondor ***@***.***>
wrote:

> The idea is that a workload doesn't need to know about the Kubernetes API,
> Node or CRD. The information should be available via environment variables,
> so you don't have to change your application or add a dependency on a
> kubernetes client.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/kubernetes/kubernetes/issues/40610#issuecomment-1532052078>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABKWAVEINKYCAGZ72JWXS3DXEFP6PANCNFSM4C57YMFA>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

It's totally understood, that the info is available in the Kubernetes API and that one could use an init container with some  Kubernetes API client to fetch it. This issue is about providing the info, just like https://github.com/kubernetes/kubernetes/issues/40610#issuecomment-1532052078 said, without adding extra components / using API clients. To me the sole purpose of the downward API is that NOT have to use an API client for rather common details (such as the pods requests and limits) or `spec.nodeName` and having to fiddle with the right policy to allow it.

And in a distributed environment (running applications across multiple nodes and also failure domains) it's not far fetched to also expose this info to the workload to e.g. replicate it's data accordingly. 

The discussion should be more about which info should not be exposed or needs further filtering and how this could be achieved without exploding into complexity hell, beating the purpose of this "elegant" solution of using the existing downloadAPI.
> Why not do this as a sidecar and an emptyDir volume?  We now have a kubectl
> container image - just run that as an initcontainer, save the info you want
> to a file, done.

That sounds useful. If we pair it with #116993, then I think it's a reasonable UX.

> The problem with downward API across resources is that the pod's SA might
intentionally NOT be allowed to read nodes.

The kubelet could potentially check this permission at pod startup time.
It's important to understand that this project has hundreds of large
features in flight, and thousands of open bugs and PRs. While it may seem
like an easy, obvious feature, it requires thinking about, testing, code
review, docs, etc.

Every feature we add has a cost, both to our ability to in get some other
things done and in complexity of the system.

Features/bugs which have viable workarounds are generally less critical
than those without.

As always, if you think this is important, a KEP or even a doc is the right
place to start proposing a design and evaluating the risks.

As a compromise, one thing we could probably do safely is copy the topology
labels (topology.kubernetes.io/*) from node to pod (making sure the pod
does not already set them itself) at scheduling time. To do more than that
would require more analysis.

On Wed, May 3, 2023, 5:18 AM Aldo Culquicondor ***@***.***>
wrote:

> Why not do this as a sidecar and an emptyDir volume? We now have a kubectl
> container image - just run that as an initcontainer, save the info you want
> to a file, done.
>
> That sounds useful. If we pair it with #116993
> <https://github.com/kubernetes/kubernetes/issues/116993>, then I think
> it's a reasonable UX.
>
> The problem with downward API across resources is that the pod's SA might
> intentionally NOT be allowed to read nodes.
>
> The kubelet could potentially check this permission at pod startup time.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/kubernetes/kubernetes/issues/40610#issuecomment-1532924946>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABKWAVDBPBD3IHWKWD5MEI3XEJEHPANCNFSM4C57YMFA>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

/sig scheduling

> As a compromise, one thing we could probably do safely is copy the topology
> labels (topology.kubernetes.io/*) from node to pod (making sure the pod
> does not already set them itself) at scheduling time. To do more than that
> would require more analysis.

I would welcome this approach. I can bring it up to the SIG scheduling meeting today.
Note: I'm not saying I'll implement this. If the SIG agrees that this is a doable approach, I'll leave it up to others to write a KEP and implementation.
Even this simple mode needs some thought. If a user sets a topology label
(they shouldn't but we don't stop them) should we override it or leave it?

Do we copy all such labels or just the well-known ones?

Do we copy labels for pods that manually set nodeName (no scheduling)?

Doel we copy labels onto mirror pods?

Will this cause problems for systems which enumerate pod labels to derive
identity (e.g. Cilium)?

Do we need to update those if the labels change on the node?  Topology
should not change, but we don't prevent it...

Just off the top of my head :)

On Thu, May 4, 2023, 6:40 AM Aldo Culquicondor ***@***.***>
wrote:

> Note: I'm not saying I'll implement this. If the SIG agrees that this is a
> doable approach, I'll leave it up to others to write a KEP and
> implementation.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/kubernetes/kubernetes/issues/40610#issuecomment-1534802610>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABKWAVG43G2M2SAGEAJMCDDXEOWWJANCNFSM4C57YMFA>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

We didn't have enough time to discuss during the meeting, but the SIG scheduling leads are aware of it.

My take is that we only copy well-known labels, including:
- topology.kubernetes.io/region
- topology.kubernetes.io/zone

The rest of the questions can probably be discussed in the KEP pull request.
Automagically modifying pod labels is a bit scary to me, although it has the added benefit of free topology-based pod querying... How about adding a `NodeFieldRef *ObjectFieldSelector` field to `EnvVarSource` and `DownwardAPIVolumeFile` that is validated against an explicit allow-list of fields (which could grow in the future)?

e.g.:

- `metadata.name`
- `metadata.labels['topology.kubernetes.io/region']`
- `metadata.labels['topology.kubernetes.io/zone']`

That way, everything is very explicit and follows well-established patterns.
@thockin re: https://github.com/kubernetes/kubernetes/issues/40610#issuecomment-1534903777 I'm not sure what the issue is here, we'd just be exposing node labels via the downward API right? So who cares what they're set to? Just make them available. We already have a downward API for things like node name. These are only exposable as env vars or mounted files. So this doesn't change pod labels. https://kubernetes.io/docs/concepts/workloads/pods/downward-api/

I believe in the past week this conversation has gotten off track from the original request. I understand that every feature _is_ an extra complexity and extra maintenance burden, but in this case I believe that the turn in discussion has made this much more complex than necessary.

I believe the only thing discussed in the past few posts that we are going to need to worry about is what happens when someone updates a node label after a pod that is using the label via the downward API has already fetched it. However, I'm _pretty_ sure that there are things in the downward API that can change out without the pod having to be recreated, and that's why they're not allowed to be environment variables. So, wouldn't this fall under that case?

---

Anyway to everyone here who's suggested a KEP, I want to say this, with the caveat that I'm not someone very important, I just want to give my opinion on a phenomenon I've witnessed as a user and small-time contributor over the past 5 years; nothing I'm saying should be taken personally or is even written with a particular group of people in mind since I've seen this come from so many contributors that I can't keep track of them.

The KEP process is so overly bureaucratic and tedious that it's no wonder that people avoid engaging with it, especially for proposals that are pretty small in scope. KEPs make sense for big changes like adding or removing entire features, or breaking changes to existing features, or other efforts that would be wide-in-scope; however, asking people to make them for every little thing makes it seem–to a lot of people requesting smaller changes (like this one)–that KEPs exist primarily to prevent improvements from being made without sufficiently flagellating the people who dared to be so brazen as to ask for that improvement. I have noticed that a lot of issues I follow end up leading down the "make a KEP (talk to the hand)" path. Of course that is not their stated purpose but I am not alone in perceiving this as their de facto purpose, or at least, an intended side effect.

I sympathize with everyone's concerns for not wanting scope creep to end up making maintenance a nightmare, of course! I believe in this particular case we are overthinking ourselves into reasons why not to accept something as a valid feature request. Since it's possible to work around the lack of this feature, I agree it'd be a low priority feature and have only a moderate value add, so it's reasonable to say it should be left to a community contributor to implement when they feel like they have the time, but invoking the KEP process before they're allowed to try an implementation would be (IMO) bikeshedding, wasting not only their time but also the time of everyone reviewing the KEP.

But, that's just my 2 cents as a user and an extremely minor contributor who doesn't attend SIG meetings and doesn't know anyone here personally. Perhaps my opinion is born out of ignorance of the inner workings of the core kubernetes contributor community. Hopefully you can take it as just one perspective out of many.
> we'd just be exposing node labels via the downward API right

How confident are you that every cluster operator in the world is OK with this - that none of them have information that isn't meant for pods to see?

> We already have a downward API for things like node name

...which are are all present as fields on the Pod object.  Node labels are fundamentally different, and warrant some caution.

> KEPs exist primarily to prevent improvements from being made

Almost correct - KEPs exist primarily to prevent improvements from being made without thinking about some of the harder and less obvious issues that are easy to miss.  I know it feels like a lot of BS, but the project is HUGE.  Even I don't know where all the traps are any more.  Everything in the KEP questionairre is there because someone forgot to think about it and caused a problem.  KEPs are how we communicate the details to each other across time and space.

 > invoking the KEP process before they're allowed to try an implementation

You are free to try anything you like.  Go implement it, and come back with evidence that it was easy, that all the tests Just Work, and use that to inform your KEP.  100%.  Go forth and hack.

> Hopefully you can take it as just one perspective out of many.

Look, I hear you.  I myself have said "oh no, another KEP to write".  But the reality is that the potential blast-radius of doing something dumb is just SO high, and it's so easy to make a mistake.  Writing a KEP is not really that hard, but it does make you think about some things you might like to avoid thinking about until later.  Bolting tests and metrics and scale on at the end just doesn't work.

I encourage you to try it.  It's not so bad.
I'm interest with this issue, let me try to make it out, a KEP or a design doc as a startup.
/assign
Or could spec.nodeTopologyZone and spec.nodeTopologyRegion be added from the nodes topology.kubernetes.io well known labels? Then pods could consume using downward API like spec.nodeName today?
@kerthcet we need this feature. Does KEP in process? I want join this feature development.
On the way, when completed, you can take a review of that.
@kerthcet Sorry to interrupt. How is the progress now?
Just a wild idea: how about making this a Node Resource Interface (NRI) plugin, instead(?) That is, not make it part of Kubernetes but a runtime plugin that can be deployed by those who need it. (Note: NRI is quite new and isn't enabled by default in cri-o or containerd, yet).

Refs:
- https://github.com/containerd/nri
-https://github.com/containerd/containerd/blob/main/docs/NRI.md
- https://kccncna2022.sched.com/event/182JT
- https://github.com/containers/nri-plugins
The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
/remove-lifecycle stale
The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
/remove-lifecycle stale
/remove-lifecycle stale
> Just a wild idea: how about making this a Node Resource Interface (NRI) plugin, instead(?) That is, not make it part of Kubernetes but a runtime plugin that can be deployed by those who need it. (Note: NRI is quite new and isn't enabled by default in cri-o or containerd, yet).
> 
> Refs:
> 
> * https://github.com/containerd/nri
>   -https://github.com/containerd/containerd/blob/main/docs/NRI.md
> * https://kccncna2022.sched.com/event/182JT
> * https://github.com/containers/nri-plugins

If we agree on this approach as a community, I am more than happy to take/assign this myself and work to get it done (with help from @marquiz )

cc @simonswine @kerthcet 
Would that make the solution less portable? How easy is it to install an NRI plugin, specially in a cloud environment?
Anything that bypasses access-control should be a non-starter.  Doing it as NRI or doing it in Kubelet or doing it in apiserver are all identical in this aspect.

As said elsewhere, I think that copying topology labels from node->pod is totally reasonable, but has some corner cases to sort out.  Anything more than that needs to answer access control.

As a workaround: https://github.com/thockin/kubectl-sidecar
@thockin @SergeyKanzhelev we're working on a proof-of-concept solution to this issue and will have a PR for it - what's the best path forward to starting a discussion for approval? We'll plan on coming to the SIG node meetings or can coord via the Slack channel.
You can post a summary of the design and a link to a KEP here, to start?
> what's the best path forward to starting a discussion for approval? We'll plan on coming to the SIG node meetings or can coord via the Slack channel.

SIG Node meeting, slack, mailing list all the good ways to collect feedback. As @thockin said - design and KEP links would be a good starting point. Before going deep into implementation, we definitely need an agreement on overall design. Off the opt of my head, security was the concern. Another thing that is always a question with the mutable values is how end user will know which node value was used at a time of scheduling and what is expected behavior on container restart.

Also I was hoping that https://github.com/kubernetes/enhancements/issues/3721 will allow to have init container that will read whatever you want. Listing this as well as any other alternatives (like a webhook) with pros/cons would also help.
The design we're prototyping mimics the existing `metadata.labels` structure for both environment variables and downwardAPI volumes, but using a new `metadata.node.labels` FieldPath and doing the lookups from the *v1.Node object (for environment variables) or VolumeHost.GetNodeLabels() (for volumes) so I'd expect the behavior to match the pod label lookups, as far as mutability w/ the pod seeing updates as they happen. We'll submit a `enhancements` issue and start working on a KEP - excited to work with you all on this.
`metadata.node.labels` as a field path is weird -- it's not a real field.

https://github.com/kubernetes/kubernetes/issues/40610#issuecomment-2077964178

> I think that copying topology labels from node->pod is totally reasonable, but has some corner cases to sort out. Anything more than that needs to answer access control.

IOW - I don't think you can just expose ALL node labels.
I think we're open to alternate field paths if there are suggestions. I'm curious though with the concerns about access control - are there examples where node labels can be abused by pods or have contained otherwise sensitive information?
> `metadata.node.labels` as a field path is weird -- it's not a real field.
> 
> [#40610 (comment)](https://github.com/kubernetes/kubernetes/issues/40610#issuecomment-2077964178)
> 
> > I think that copying topology labels from node->pod is totally reasonable, but has some corner cases to sort out. Anything more than that needs to answer access control.
> 
> IOW - I don't think you can just expose ALL node labels.

Just to clarify i think he meant, `node.metadata.labels` to make it succinct with the actual spec. cc: @docandrew  
> `metadata.node.labels` as a field path is weird -- it's not a real field.
> 
> [#40610 (comment)](https://github.com/kubernetes/kubernetes/issues/40610#issuecomment-2077964178)
> 
> > I think that copying topology labels from node->pod is totally reasonable, but has some corner cases to sort out. Anything more than that needs to answer access control.
> 
> IOW - I don't think you can just expose ALL node labels.

RE: `I don't think you can just expose ALL node labels.` What would you think an appropriate way to handle exposing just a subset be? Maybe only certain FieldPaths?
> I'm curious though with the concerns about access control - are there examples where node labels can be abused by pods or have contained otherwise sensitive information?

Maybe I am over-blowi9ng it, but there are deployments where the average user can't read nodes.  I don't think we have the context to say whether it's OK for us to unilaterally publish information that the user was otherwise restricted from viewing.  If, by chance, there's some sensitive information in there - a tenant ID, a cloud provider ID, etc - and we expose it, I don't think that's OK.  I don't like living in fear, but I think we need to be cautious that we don't inadvertently expose our users.

> What would you think an appropriate way to handle exposing just a subset be? Maybe only certain FieldPaths?

I think I suggested elsewhere that we could copy `topology.k8s.io/*` from Node to Pod (as long as we handle collisions properly and document that there are not mutable after the fact) either when scheduling or during Kubelet pod-start.  The advantag I see in that is that downward API and projected volumes and so forth ALREADY WORK for labels.
> I think I suggested elsewhere that we could copy topology.k8s.io/* from Node to Pod (as long as we handle collisions properly and document that there are not mutable after the fact) either when scheduling or during Kubelet pod-start. The advantag I see in that is that downward API and projected volumes and so forth ALREADY WORK for labels.

What would prevent users from cloning any* label into `topology.k8s.io/*` ? still feels to me that exposing node labels via downward API will push a lot of customers to re-think their labels usage 


> > I think I suggested elsewhere that we could copy topology.k8s.io/* from Node to Pod (as long as we handle collisions properly and document that there are not mutable after the fact) either when scheduling or during Kubelet pod-start. The advantag I see in that is that downward API and projected volumes and so forth ALREADY WORK for labels.
> 
> What would prevent users from cloning any* label into `topology.k8s.io/*` ? still feels to me that exposing node labels via downward API will push a lot of customers to re-think their labels usage

Helper functions that utilize the existing functions, mainly, [`IsLegalSubscriptionAndQualifiedName`](https://github.com/cmwylie19/kubernetes/blob/0b17feb3c4f57a8d6e097020af36f929933c5b29/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go#L55) and [`SanitizeNodeLabels`](https://github.com/cmwylie19/kubernetes/blob/0b17feb3c4f57a8d6e097020af36f929933c5b29/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go#L39C6-L39C24). We would programmatically restrict the user from using illegal labels by the same means that are currently in the code base. This is more or less what we would be thinking, there is[ no official PR yet](https://github.com/kubernetes/kubernetes/compare/master...cmwylie19:kubernetes:node-labels-downward-api) because we have a [KEP](https://github.com/kubernetes/enhancements/pull/4747) PR right now.

Does this reduce your concerns? 
> > I think I suggested elsewhere that we could copy topology.k8s.io/* from Node to Pod (as long as we handle collisions properly and document that there are not mutable after the fact) either when scheduling or during Kubelet pod-start. The advantag I see in that is that downward API and projected volumes and so forth ALREADY WORK for labels.
> 
> What would prevent users from cloning any* label into `topology.k8s.io/*` ? still feels to me that exposing node labels via downward API will push a lot of customers to re-think their labels usage

also, not sure what you mean by cloning but in the event that you meant updating labels on a given node/pod there is no way to guarantee that the pod will have the latest label without a restart. In the KEP we discuss that we want to make that clear.
> What would prevent users from cloning any* label into topology.k8s.io/* ? 

If you are allowed to write to node labels, then go nuts. My point is that the average pod Creator may or may not be able to read node labels, and we should not assume that.
This issue is currently awaiting triage.

If a SIG or subproject determines this is a relevant issue, they will accept it by applying the `triage/accepted` label and provide further guidance.

The `triage/accepted` label can be added by org members by writing `/triage accepted` in a comment.


<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
I am create light tools: https://github.com/topologytopod/topologytopod, use webhook can copy all **topology** label to **Pod**.
Sorry for another notification caused, but we were also using another service at my prev company: https://github.com/danfromtitan/envars-from-node-labels quite successfully.
The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
/remove-lifecycle stale
We encountered a similar requirement to expose our node labels to our pods.

For those in need of an immediate solution:
I successfully collaborated with the [Kyverno](https://kyverno.io/) team to implement a robust [solution](https://kyverno.io/blog/2024/02/19/assigning-node-metadata-to-pods/)

Moving forward, I hope that Kubernetes will enhance the Downward API to support this requirement directly, thereby eliminating the need for external solutions like the one we developed with Kyverno.
Folks, if you have the expertise, please open a KEP. But note that the thread already has a lot of open questions that need to be answered in the KEP to move forward.
The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
/remove-lifecycle stale

For anyone still following this issue, we merged a proposal to move forward on this for v1.33 (alpha) https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/4742-node-topology-downward-api

TL;DR we are going to follow similar approach from @munnerz's PR https://github.com/kubernetes/kubernetes/pull/127092 with some modifcations. A built-in admission plugin `PodTopologyLabels` will copy **only** standardized node topology labels to Pods so it can be consumed via downward API using existing mechanism for labels. 

For exposing labels outside the standard topology labels, this will require mutating admission webhooks on `pods/binding`.  

While the KEP is merged / approved, we're still open to hearing alternatives / feedback on the proposed approach. 

Fix patch:
diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go
index ec9a8d2fd681b..ace72de0fea2c 100644
--- a/pkg/features/kube_features.go
+++ b/pkg/features/kube_features.go
@@ -964,6 +964,20 @@ const (
 	// restore the old behavior. Please file issues if you hit issues and have to use this Feature Gate.
 	// The Feature Gate will be locked to true and then removed in +2 releases (1.35) if there are no bug reported
 	DisableCPUQuotaWithExclusiveCPUs featuregate.Feature = "DisableCPUQuotaWithExclusiveCPUs"
+
+	// owner: @munnerz
+	// kep: https://kep.k8s.io/4742
+	// alpha: v1.33
+	//
+	// Enables the PodTopologyLabelsAdmission admission plugin that mutates `pod/binding`
+	// requests by copying the `topology.k8s.io/{zone,region}` labels from the assigned
+	// Node object (in the Binding being admitted) onto the Binding
+	// so that it can be persisted onto the Pod object when the Pod is being scheduled.
+	// This allows workloads running in pods to understand the topology information of their assigned node.
+	// Enabling this feature also permits external schedulers to set labels on pods in an atomic
+	// operation when scheduling a Pod by setting the `metadata.labels` field on the submitted Binding,
+	// similar to how `metadata.annotations` behaves.
+	PodTopologyLabelsAdmission featuregate.Feature = "PodTopologyLabelsAdmission"
 )
 
 // defaultVersionedKubernetesFeatureGates consists of all known Kubernetes-specific feature keys with VersionedSpecs.
@@ -1571,6 +1585,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
 		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // GA in 1.30; remove in 1.32
 	},
 
+	PodTopologyLabelsAdmission: {
+		{Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Alpha},
+	},
+
 	PortForwardWebsockets: {
 		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
 		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
diff --git a/pkg/kubeapiserver/options/plugins.go b/pkg/kubeapiserver/options/plugins.go
index e9f8c41b81475..0dccb55e2a48c 100644
--- a/pkg/kubeapiserver/options/plugins.go
+++ b/pkg/kubeapiserver/options/plugins.go
@@ -46,6 +46,7 @@ import (
 	"k8s.io/kubernetes/plugin/pkg/admission/nodetaint"
 	"k8s.io/kubernetes/plugin/pkg/admission/podnodeselector"
 	"k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction"
+	"k8s.io/kubernetes/plugin/pkg/admission/podtopologylabels"
 	podpriority "k8s.io/kubernetes/plugin/pkg/admission/priority"
 	"k8s.io/kubernetes/plugin/pkg/admission/runtimeclass"
 	"k8s.io/kubernetes/plugin/pkg/admission/security/podsecurity"
@@ -93,6 +94,7 @@ var AllOrderedPlugins = []string{
 	certsubjectrestriction.PluginName,       // CertificateSubjectRestriction
 	defaultingressclass.PluginName,          // DefaultIngressClass
 	denyserviceexternalips.PluginName,       // DenyServiceExternalIPs
+	podtopologylabels.PluginName,            // PodTopologyLabels
 
 	// new admission plugins should generally be inserted above here
 	// webhook, resourcequota, and deny plugins must go at the end
@@ -138,6 +140,7 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
 	certsigning.Register(plugins)
 	ctbattest.Register(plugins)
 	certsubjectrestriction.Register(plugins)
+	podtopologylabels.Register(plugins)
 }
 
 // DefaultOffAdmissionPlugins get admission plugins off by default for kube-apiserver.
@@ -162,6 +165,7 @@ func DefaultOffAdmissionPlugins() sets.Set[string] {
 		certsubjectrestriction.PluginName,       // CertificateSubjectRestriction
 		defaultingressclass.PluginName,          // DefaultIngressClass
 		podsecurity.PluginName,                  // PodSecurity
+		podtopologylabels.PluginName,            // PodTopologyLabels, only active when feature gate PodTopologyLabelsAdmission is enabled.
 		mutatingadmissionpolicy.PluginName,      // Mutatingadmissionpolicy, only active when feature gate MutatingAdmissionpolicy is enabled
 		validatingadmissionpolicy.PluginName,    // ValidatingAdmissionPolicy, only active when feature gate ValidatingAdmissionPolicy is enabled
 	)
diff --git a/pkg/registry/core/pod/storage/storage.go b/pkg/registry/core/pod/storage/storage.go
index 8aaa358d0856f..7323efe5faad9 100644
--- a/pkg/registry/core/pod/storage/storage.go
+++ b/pkg/registry/core/pod/storage/storage.go
@@ -33,10 +33,12 @@ import (
 	"k8s.io/apiserver/pkg/storage"
 	storeerr "k8s.io/apiserver/pkg/storage/errors"
 	"k8s.io/apiserver/pkg/util/dryrun"
+	utilfeature "k8s.io/apiserver/pkg/util/feature"
 	policyclient "k8s.io/client-go/kubernetes/typed/policy/v1"
 	podutil "k8s.io/kubernetes/pkg/api/pod"
 	api "k8s.io/kubernetes/pkg/apis/core"
 	"k8s.io/kubernetes/pkg/apis/core/validation"
+	kubefeatures "k8s.io/kubernetes/pkg/features"
 	"k8s.io/kubernetes/pkg/kubelet/client"
 	"k8s.io/kubernetes/pkg/printers"
 	printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
@@ -191,7 +193,7 @@ func (r *BindingREST) Create(ctx context.Context, name string, obj runtime.Objec
 		}
 	}
 
-	err = r.assignPod(ctx, binding.UID, binding.ResourceVersion, binding.Name, binding.Target.Name, binding.Annotations, dryrun.IsDryRun(options.DryRun))
+	err = r.assignPod(ctx, binding.UID, binding.ResourceVersion, binding.Name, binding.Target.Name, binding.Annotations, binding.Labels, dryrun.IsDryRun(options.DryRun))
 	out = &metav1.Status{Status: metav1.StatusSuccess}
 	return
 }
@@ -203,10 +205,10 @@ func (r *BindingREST) PreserveRequestObjectMetaSystemFieldsOnSubresourceCreate()
 	return true
 }
 
-// setPodHostAndAnnotations sets the given pod's host to 'machine' if and only if
-// the pod is unassigned and merges the provided annotations with those of the pod.
+// setPodNodeAndMetadata sets the given pod's nodeName to 'machine' if and only if
+// the pod is unassigned, and merges the provided annotations and labels with those of the pod.
 // Returns the current state of the pod, or an error.
-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) {
+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) {
 	podKey, err := r.store.KeyFunc(ctx, podID)
 	if err != nil {
 		return nil, err
@@ -245,6 +247,11 @@ func (r *BindingREST) setPodHostAndAnnotations(ctx context.Context, podUID types
 		for k, v := range annotations {
 			pod.Annotations[k] = v
 		}
+		// Copy all labels from the Binding over to the Pod object, overwriting
+		// any existing labels set on the Pod.
+		if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.PodTopologyLabelsAdmission) {
+			copyLabelsWithOverwriting(pod, labels)
+		}
 		podutil.UpdatePodCondition(&pod.Status, &api.PodCondition{
 			Type:   api.PodScheduled,
 			Status: api.ConditionTrue,
@@ -255,9 +262,23 @@ func (r *BindingREST) setPodHostAndAnnotations(ctx context.Context, podUID types
 	return finalPod, err
 }
 
+func copyLabelsWithOverwriting(pod *api.Pod, labels map[string]string) {
+	if len(labels) == 0 {
+		// nothing to do
+		return
+	}
+	if pod.Labels == nil {
+		pod.Labels = make(map[string]string)
+	}
+	// Iterate over the binding's labels and copy them across to the Pod.
+	for k, v := range labels {
+		pod.Labels[k] = v
+	}
+}
+
 // assignPod assigns the given pod to the given machine.
-func (r *BindingREST) assignPod(ctx context.Context, podUID types.UID, podResourceVersion, podID string, machine string, annotations map[string]string, dryRun bool) (err error) {
-	if _, err = r.setPodHostAndAnnotations(ctx, podUID, podResourceVersion, podID, machine, annotations, dryRun); err != nil {
+func (r *BindingREST) assignPod(ctx context.Context, podUID types.UID, podResourceVersion, podID string, machine string, annotations, labels map[string]string, dryRun bool) (err error) {
+	if _, err = r.setPodNodeAndMetadata(ctx, podUID, podResourceVersion, podID, machine, annotations, labels, dryRun); err != nil {
 		err = storeerr.InterpretGetError(err, api.Resource("pods"), podID)
 		err = storeerr.InterpretUpdateError(err, api.Resource("pods"), podID)
 		if _, ok := err.(*errors.StatusError); !ok {
diff --git a/plugin/pkg/admission/podtopologylabels/admission.go b/plugin/pkg/admission/podtopologylabels/admission.go
new file mode 100644
index 0000000000000..3b010df6fe632
--- /dev/null
+++ b/plugin/pkg/admission/podtopologylabels/admission.go
@@ -0,0 +1,243 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package podtopologylabels
+
+import (
+	"context"
+	"fmt"
+	"io"
+
+	"k8s.io/klog/v2"
+
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/apiserver/pkg/admission"
+	genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
+	"k8s.io/client-go/informers"
+	corev1listers "k8s.io/client-go/listers/core/v1"
+	"k8s.io/component-base/featuregate"
+	api "k8s.io/kubernetes/pkg/apis/core"
+	"k8s.io/kubernetes/pkg/features"
+)
+
+// PluginName is a string with the name of the plugin
+const PluginName = "PodTopologyLabels"
+
+// defaultConfig is the configuration used for the default instantiation of the plugin.
+// This configuration is used by kube-apiserver.
+// It is not exported to avoid any chance of accidentally mutating the variable.
+var defaultConfig = Config{
+	Labels: []string{"topology.k8s.io/zone", "topology.k8s.io/region"},
+}
+
+// Register registers a plugin
+func Register(plugins *admission.Plugins) {
+	plugins.Register(PluginName, func(_ io.Reader) (admission.Interface, error) {
+		plugin := NewPodTopologyPlugin(defaultConfig)
+		return plugin, nil
+	})
+}
+
+// Config contains configuration for instances of the podtopologylabels admission plugin.
+// This is not exposed as user-facing APIServer configuration, however can be used by
+// platform operators when building custom topology label plugins.
+type Config struct {
+	// Labels is set of explicit label keys to be copied from the Node object onto
+	// pod Binding objects during admission.
+	Labels []string
+}
+
+// NewPodTopologyPlugin initializes a Plugin
+func NewPodTopologyPlugin(c Config) *Plugin {
+	return &Plugin{
+		Handler: admission.NewHandler(admission.Create),
+		labels:  sets.New(c.Labels...),
+	}
+}
+
+type Plugin struct {
+	*admission.Handler
+
+	nodeLister corev1listers.NodeLister
+
+	// explicit labels to be copied to Pod objects being bound.
+	labels sets.Set[string]
+
+	enabled, inspectedFeatureGates bool
+}
+
+var _ admission.MutationInterface = &Plugin{}
+var _ genericadmissioninitializer.WantsExternalKubeInformerFactory = &Plugin{}
+var _ genericadmissioninitializer.WantsFeatures = &Plugin{}
+
+// InspectFeatureGates implements WantsFeatures.
+func (p *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
+	p.enabled = featureGates.Enabled(features.PodTopologyLabelsAdmission)
+	p.inspectedFeatureGates = true
+}
+
+func (p *Plugin) SetExternalKubeInformerFactory(factory informers.SharedInformerFactory) {
+	nodeInformer := factory.Core().V1().Nodes()
+	p.nodeLister = nodeInformer.Lister()
+	p.SetReadyFunc(nodeInformer.Informer().HasSynced)
+}
+
+func (p *Plugin) ValidateInitialization() error {
+	if p.nodeLister == nil {
+		return fmt.Errorf("nodeLister not set")
+	}
+	if !p.inspectedFeatureGates {
+		return fmt.Errorf("feature gates not inspected")
+	}
+	return nil
+}
+
+func (p *Plugin) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
+	if !p.enabled {
+		return nil
+	}
+	// check whether the request is for a Binding or a Pod spec update.
+	shouldAdmit, doAdmit, err := p.shouldAdmit(a)
+	if !shouldAdmit || err != nil {
+		// error is either nil and admit == false, or err is non-nil and should be returned.
+		return err
+	}
+	// we need to wait for our caches to warm
+	if !p.WaitForReady() {
+		return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
+	}
+	// run type specific admission
+	return doAdmit(ctx, a, o)
+}
+
+func (p *Plugin) admitBinding(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
+	binding := a.GetObject().(*api.Binding)
+	// other fields are not set by the default scheduler for the binding target, so only check the Kind.
+	if binding.Target.Kind != "Node" {
+		klog.V(6).Info("Skipping Pod being bound to non-Node object type", "target", binding.Target.GroupVersionKind())
+		return nil
+	}
+
+	labelsToCopy, err := p.topologyLabelsForNodeName(binding.Target.Name)
+	if err != nil {
+		return err
+	}
+	if len(labelsToCopy) == 0 {
+		// fast-path/short circuit if the node has no topology labels
+		return nil
+	}
+
+	// copy the topology labels into the Binding's labels, as these are copied from the Binding
+	// to the Pod object being bound within the podBinding registry/store.
+	if binding.Labels == nil {
+		binding.Labels = make(map[string]string)
+	}
+	mergeLabels(binding.Labels, labelsToCopy)
+
+	return nil
+}
+
+func (p *Plugin) admitPod(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
+	pod := a.GetObject().(*api.Pod)
+	if pod.Spec.NodeName == "" {
+		// pod has not been scheduled yet
+		return nil
+	}
+
+	// Determine the topology labels from the assigned node to be copied.
+	labelsToCopy, err := p.topologyLabelsForNodeName(pod.Spec.NodeName)
+	if err != nil {
+		return err
+	}
+	if len(labelsToCopy) == 0 {
+		// fast-path/short circuit if the node has no topology labels
+		return nil
+	}
+
+	// copy the topology labels into the Pod's labels.
+	if pod.Labels == nil {
+		pod.Labels = make(map[string]string)
+	}
+	// overwrite any existing labels on the pod
+	mergeLabels(pod.Labels, labelsToCopy)
+
+	return nil
+}
+
+func (p *Plugin) topologyLabelsForNodeName(nodeName string) (map[string]string, error) {
+	labels := make(map[string]string)
+	node, err := p.nodeLister.Get(nodeName)
+	if err != nil {
+		// Ignore NotFound errors to avoid risking breaking compatibility/behaviour.
+		if apierrors.IsNotFound(err) {
+			return labels, nil
+		}
+		return nil, err
+	}
+
+	for k, v := range node.Labels {
+		if !p.isTopologyLabel(k) {
+			continue
+		}
+		labels[k] = v
+	}
+
+	return labels, nil
+}
+
+// mergeLabels merges new into existing, overwriting existing keys.
+func mergeLabels(existing, new map[string]string) {
+	for k, v := range new {
+		existing[k] = v
+	}
+}
+
+func (p *Plugin) isTopologyLabel(key string) bool {
+	// First check explicit label keys.
+	if p.labels.Has(key) {
+		return true
+	}
+	return false
+}
+
+type admitFunc func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error)
+
+// shouldAdmit inspects the provided adminssion attributes to determine whether the request
+// requires admittance through this plugin.
+func (p *Plugin) shouldAdmit(a admission.Attributes) (bool, admitFunc, error) {
+	if a.GetResource().GroupResource() != api.Resource("pods") {
+		return false, nil, nil
+	}
+
+	switch a.GetSubresource() {
+	case "": // regular Pod endpoint
+		_, ok := a.GetObject().(*api.Pod)
+		if !ok {
+			return false, nil, fmt.Errorf("expected Pod but got %T", a.GetObject())
+		}
+		return true, p.admitPod, nil
+	case "binding":
+		_, ok := a.GetObject().(*api.Binding)
+		if !ok {
+			return false, nil, fmt.Errorf("expected Binding but got %s", a.GetKind().Kind)
+		}
+		return true, p.admitBinding, nil
+	default:
+		// Ignore all other sub-resources.
+		return false, nil, nil
+	}
+}
diff --git a/plugin/pkg/admission/podtopologylabels/doc.go b/plugin/pkg/admission/podtopologylabels/doc.go
new file mode 100644
index 0000000000000..56131f0b70abb
--- /dev/null
+++ b/plugin/pkg/admission/podtopologylabels/doc.go
@@ -0,0 +1,25 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package podtopologylabels is a plugin that mutates `pod/binding` requests
+// by copying the `topology.k8s.io/{zone,region}` labels from the assigned Node
+// object (in the Binding being admitted) onto the Binding so that it can be
+// persisted onto the Pod object when the Pod is being scheduled.
+// Requests for the regular `pods` resource that set the `spec.nodeName` will
+// also trigger the plugin to copy the labels as described.
+// If the binding target is NOT a Node object, no action is taken.
+// If the referenced Node object does not exist, no action is taken.
+package podtopologylabels // import "k8s.io/kubernetes/plugin/pkg/admission/podtopologylabels"


Test patch:
diff --git a/pkg/controlplane/apiserver/samples/generic/server/admission_test.go b/pkg/controlplane/apiserver/samples/generic/server/admission_test.go
index b85423d7c7f3a..a16a257840a31 100644
--- a/pkg/controlplane/apiserver/samples/generic/server/admission_test.go
+++ b/pkg/controlplane/apiserver/samples/generic/server/admission_test.go
@@ -24,6 +24,7 @@ import (
 	"k8s.io/kubernetes/plugin/pkg/admission/limitranger"
 	"k8s.io/kubernetes/plugin/pkg/admission/network/defaultingressclass"
 	"k8s.io/kubernetes/plugin/pkg/admission/nodetaint"
+	"k8s.io/kubernetes/plugin/pkg/admission/podtopologylabels"
 	podpriority "k8s.io/kubernetes/plugin/pkg/admission/priority"
 	"k8s.io/kubernetes/plugin/pkg/admission/runtimeclass"
 	"k8s.io/kubernetes/plugin/pkg/admission/security/podsecurity"
@@ -42,6 +43,7 @@ var intentionallyOffPlugins = sets.New[string](
 	runtimeclass.PluginName,                 // RuntimeClass
 	defaultingressclass.PluginName,          // DefaultIngressClass
 	podsecurity.PluginName,                  // PodSecurity
+	podtopologylabels.PluginName,            // PodTopologyLabels
 )
 
 func TestDefaultOffAdmissionPlugins(t *testing.T) {
diff --git a/pkg/registry/core/pod/storage/storage_test.go b/pkg/registry/core/pod/storage/storage_test.go
index 39177e5ff96ce..a2a0ac6c0a1ca 100644
--- a/pkg/registry/core/pod/storage/storage_test.go
+++ b/pkg/registry/core/pod/storage/storage_test.go
@@ -42,8 +42,11 @@ import (
 	apiserverstorage "k8s.io/apiserver/pkg/storage"
 	storeerr "k8s.io/apiserver/pkg/storage/errors"
 	etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
+	utilfeature "k8s.io/apiserver/pkg/util/feature"
+	featuregatetesting "k8s.io/component-base/featuregate/testing"
 	podtest "k8s.io/kubernetes/pkg/api/pod/testing"
 	api "k8s.io/kubernetes/pkg/apis/core"
+	kubefeatures "k8s.io/kubernetes/pkg/features"
 	"k8s.io/kubernetes/pkg/registry/registrytest"
 	"k8s.io/kubernetes/pkg/securitycontext"
 )
@@ -676,12 +679,14 @@ func TestEtcdCreateWithContainersNotFound(t *testing.T) {
 		t.Fatalf("unexpected error: %v", err)
 	}
 
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, kubefeatures.PodTopologyLabelsAdmission, true)
 	// Suddenly, a wild scheduler appears:
 	_, err = bindingStorage.Create(ctx, "foo", &api.Binding{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace:   metav1.NamespaceDefault,
 			Name:        "foo",
-			Annotations: map[string]string{"label1": "value1"},
+			Annotations: map[string]string{"annotation1": "value1"},
+			Labels:      map[string]string{"label1": "label-value1"},
 		},
 		Target: api.ObjectReference{Name: "machine"},
 	}, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
@@ -695,9 +700,12 @@ func TestEtcdCreateWithContainersNotFound(t *testing.T) {
 	}
 	pod := obj.(*api.Pod)
 
-	if !(pod.Annotations != nil && pod.Annotations["label1"] == "value1") {
+	if !(pod.Annotations != nil && pod.Annotations["annotation1"] == "value1") {
 		t.Fatalf("Pod annotations don't match the expected: %v", pod.Annotations)
 	}
+	if !(pod.Labels != nil && pod.Labels["label1"] == "label-value1") {
+		t.Fatalf("Pod labels don't match the expected: %v", pod.Labels)
+	}
 }
 
 func TestEtcdCreateWithConflict(t *testing.T) {
diff --git a/plugin/pkg/admission/podtopologylabels/admission_test.go b/plugin/pkg/admission/podtopologylabels/admission_test.go
new file mode 100644
index 0000000000000..956c2a934c27d
--- /dev/null
+++ b/plugin/pkg/admission/podtopologylabels/admission_test.go
@@ -0,0 +1,232 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package podtopologylabels
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+
+	corev1 "k8s.io/api/core/v1"
+	apiequality "k8s.io/apimachinery/pkg/api/equality"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apiserver/pkg/admission"
+	genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
+	admissiontesting "k8s.io/apiserver/pkg/admission/testing"
+	"k8s.io/apiserver/pkg/util/feature"
+	"k8s.io/client-go/informers"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/kubernetes/fake"
+	featuregatetesting "k8s.io/component-base/featuregate/testing"
+	api "k8s.io/kubernetes/pkg/apis/core"
+	kubefeatures "k8s.io/kubernetes/pkg/features"
+)
+
+// TestPodTopology verifies the pod topology admission plugin works as expected.
+func TestPodTopology(t *testing.T) {
+	tests := []struct {
+		name                  string               // name of the test case.
+		bindingTarget         *api.ObjectReference // target being bound to. Defaults to a valid node with the provided labels.
+		targetNodeLabels      map[string]string    // list of labels set on the node being bound to.
+		existingBindingLabels map[string]string    // list of labels that are set on the Binding prior to admission (aka by the client/scheduler)
+		expectedBindingLabels map[string]string    // list of labels that we expect to be set on the Binding after admission.
+		featureDisabled       bool                 // configure whether the SetPodTopologyLabels feature gate should be disabled.
+	}{
+		{
+			name: "copies topology.k8s.io/zone and region labels to binding labels",
+			targetNodeLabels: map[string]string{
+				"topology.k8s.io/zone":      "zone1",
+				"topology.k8s.io/region":    "region1",
+				"topology.k8s.io/arbitrary": "something",
+				"non-topology.k8s.io/label": "something", // verify we don't unexpectedly copy non topology.k8s.io labels.
+			},
+			expectedBindingLabels: map[string]string{
+				"topology.k8s.io/zone":   "zone1",
+				"topology.k8s.io/region": "region1",
+			},
+		},
+		{
+			name: "does not copy arbitrary topology labels",
+			targetNodeLabels: map[string]string{
+				"topology.k8s.io/zone":      "zone1",
+				"topology.k8s.io/arbitrary": "something",
+			},
+			expectedBindingLabels: map[string]string{
+				"topology.k8s.io/zone": "zone1",
+			},
+		},
+		{
+			name: "does not copy topology labels that use a subdomain",
+			targetNodeLabels: map[string]string{
+				"topology.k8s.io/region":   "region1",
+				"sub.topology.k8s.io/zone": "value",
+			},
+			expectedBindingLabels: map[string]string{
+				"topology.k8s.io/region": "region1",
+			},
+		},
+		{
+			name: "does not copy label keys that don't contain a / character",
+			targetNodeLabels: map[string]string{
+				"topology.k8s.io": "value",
+			},
+			existingBindingLabels: map[string]string{},
+		},
+		{
+			name: "overwrites existing topology labels",
+			existingBindingLabels: map[string]string{
+				"topology.k8s.io/zone": "oldValue",
+			},
+			targetNodeLabels: map[string]string{
+				"topology.k8s.io/zone": "newValue",
+			},
+			expectedBindingLabels: map[string]string{
+				"topology.k8s.io/zone": "newValue",
+			},
+		},
+		{
+			name: "does nothing if the SetPodTopologyLabels feature gate is disabled",
+			targetNodeLabels: map[string]string{
+				"topology.k8s.io/zone":   "zone1",
+				"topology.k8s.io/region": "region1",
+			},
+			expectedBindingLabels: map[string]string{},
+			featureDisabled:       true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			namespace := &corev1.Namespace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-ns"},
+			}
+			node := &corev1.Node{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:   "valid-test-node",
+					Labels: test.targetNodeLabels,
+				},
+			}
+
+			featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, kubefeatures.PodTopologyLabelsAdmission, !test.featureDisabled)
+
+			t.Run("using Pod directly (update)", func(t *testing.T) {
+				// set up the client and informers
+				mockClient := fake.NewSimpleClientset(namespace, node)
+				handler, informerFactory, err := newHandlerForTest(mockClient)
+				if err != nil {
+					t.Fatalf("unexpected error initializing handler: %v", err)
+				}
+				stopCh := make(chan struct{})
+				defer close(stopCh)
+				informerFactory.Start(stopCh)
+
+				oldPod := &api.Pod{
+					ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: namespace.Name, Labels: test.existingBindingLabels},
+					Spec:       api.PodSpec{},
+				}
+				pod := oldPod.DeepCopy()
+				pod.Spec.NodeName = node.Name
+
+				if err := admissiontesting.WithReinvocationTesting(t, handler).
+					Admit(context.TODO(), admission.NewAttributesRecord(pod, oldPod,
+						api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name,
+						api.Resource("pods").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{},
+						false, nil), nil); err != nil {
+					t.Errorf("failed running admission plugin: %v", err)
+				}
+			})
+
+			t.Run("using Pod directly (create)", func(t *testing.T) {
+				// set up the client and informers
+				mockClient := fake.NewSimpleClientset(namespace, node)
+				handler, informerFactory, err := newHandlerForTest(mockClient)
+				if err != nil {
+					t.Fatalf("unexpected error initializing handler: %v", err)
+				}
+				stopCh := make(chan struct{})
+				defer close(stopCh)
+				informerFactory.Start(stopCh)
+
+				pod := &api.Pod{
+					ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: namespace.Name, Labels: test.existingBindingLabels},
+					Spec: api.PodSpec{
+						NodeName: node.Name,
+					},
+				}
+				if err := admissiontesting.WithReinvocationTesting(t, handler).
+					Admit(context.TODO(), admission.NewAttributesRecord(pod, nil,
+						api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name,
+						api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.UpdateOptions{},
+						false, nil), nil); err != nil {
+					t.Errorf("failed running admission plugin: %v", err)
+				}
+			})
+
+			t.Run("using Binding subresource", func(t *testing.T) {
+				// Pod we bind during test cases.
+				existingPod := &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: namespace.Name},
+					Spec:       corev1.PodSpec{},
+				}
+				mockClient := fake.NewSimpleClientset(namespace, node, existingPod)
+				handler, informerFactory, err := newHandlerForTest(mockClient)
+				if err != nil {
+					t.Fatalf("unexpected error initializing handler: %v", err)
+				}
+				stopCh := make(chan struct{})
+				defer close(stopCh)
+				informerFactory.Start(stopCh)
+
+				// create and submit a Binding object
+				target := test.bindingTarget
+				if target == nil {
+					target = &api.ObjectReference{
+						Kind: "Node",
+						Name: node.Name,
+					}
+				}
+				binding := &api.Binding{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      existingPod.Name,
+						Namespace: existingPod.Namespace,
+						Labels:    test.existingBindingLabels,
+					},
+					Target: *target,
+				}
+				if err := admissiontesting.WithReinvocationTesting(t, handler).
+					Admit(context.TODO(), admission.NewAttributesRecord(binding, nil, api.Kind("Binding").WithVersion("version"), existingPod.Namespace, existingPod.Name, api.Resource("pods").WithVersion("version"), "binding", admission.Create, &metav1.CreateOptions{}, false, nil), nil); err != nil {
+					t.Errorf("failed running admission plugin: %v", err)
+				}
+				updatedBindingLabels := binding.Labels
+				if !apiequality.Semantic.DeepEqual(updatedBindingLabels, test.expectedBindingLabels) {
+					t.Errorf("Unexpected label values: %v", cmp.Diff(updatedBindingLabels, test.expectedBindingLabels))
+				}
+			})
+		})
+	}
+}
+
+// newHandlerForTest returns the admission controller configured for testing.
+func newHandlerForTest(c kubernetes.Interface) (*Plugin, informers.SharedInformerFactory, error) {
+	factory := informers.NewSharedInformerFactory(c, 5*time.Minute)
+	handler := NewPodTopologyPlugin(defaultConfig) // todo: write additional test cases with non-default config.
+	pluginInitializer := genericadmissioninitializer.New(c, nil, factory, nil, feature.DefaultFeatureGate, nil, nil)
+	pluginInitializer.Initialize(handler)
+	return handler, factory, admission.ValidateInitialization(handler)
+}
diff --git a/test/compatibility_lifecycle/reference/versioned_feature_list.yaml b/test/compatibility_lifecycle/reference/versioned_feature_list.yaml
index dd0ad06e1fc37..658daa9c738b6 100644
--- a/test/compatibility_lifecycle/reference/versioned_feature_list.yaml
+++ b/test/compatibility_lifecycle/reference/versioned_feature_list.yaml
@@ -1075,6 +1075,12 @@
     lockToDefault: true
     preRelease: GA
     version: "1.30"
+- name: PodTopologyLabelsAdmission
+  versionedSpecs:
+  - default: false
+    lockToDefault: false
+    preRelease: Alpha
+    version: "1.33"
 - name: PortForwardWebsockets
   versionedSpecs:
   - default: false
diff --git a/test/integration/pods/pods_test.go b/test/integration/pods/pods_test.go
index c02d3890d6d8f..bc77f0b04aa35 100644
--- a/test/integration/pods/pods_test.go
+++ b/test/integration/pods/pods_test.go
@@ -22,11 +22,15 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
+
 	v1 "k8s.io/api/core/v1"
 	rbacv1 "k8s.io/api/rbac/v1"
+	apiequality "k8s.io/apimachinery/pkg/api/equality"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/version"
 	utilfeature "k8s.io/apiserver/pkg/util/feature"
 	clientset "k8s.io/client-go/kubernetes"
 	typedv1 "k8s.io/client-go/kubernetes/typed/core/v1"
@@ -40,6 +44,168 @@ import (
 	"k8s.io/kubernetes/test/integration/framework"
 )
 
+func TestPodTopologyLabels(t *testing.T) {
+	tests := []podTopologyTestCase{
+		{
+			name: "zone and region topology labels copied from assigned Node",
+			targetNodeLabels: map[string]string{
+				"topology.k8s.io/zone":   "zone",
+				"topology.k8s.io/region": "region",
+			},
+			expectedPodLabels: map[string]string{
+				"topology.k8s.io/zone":   "zone",
+				"topology.k8s.io/region": "region",
+			},
+		},
+		{
+			name: "subdomains of topology.k8s.io are not copied",
+			targetNodeLabels: map[string]string{
+				"sub.topology.k8s.io/zone": "zone",
+				"topology.k8s.io/region":   "region",
+			},
+			expectedPodLabels: map[string]string{
+				"topology.k8s.io/region": "region",
+			},
+		},
+		{
+			name: "custom topology.k8s.io labels are not copied",
+			targetNodeLabels: map[string]string{
+				"topology.k8s.io/custom": "thing",
+				"topology.k8s.io/zone":   "zone",
+				"topology.k8s.io/region": "region",
+			},
+			expectedPodLabels: map[string]string{
+				"topology.k8s.io/zone":   "zone",
+				"topology.k8s.io/region": "region",
+			},
+		},
+		{
+			name: "labels from Bindings overwriting existing labels on Pod",
+			existingPodLabels: map[string]string{
+				"topology.k8s.io/zone":   "bad-zone",
+				"topology.k8s.io/region": "bad-region",
+				"topology.k8s.io/abc":    "123",
+			},
+			targetNodeLabels: map[string]string{
+				"topology.k8s.io/zone":   "zone",
+				"topology.k8s.io/region": "region",
+				"topology.k8s.io/abc":    "456", // this label isn't in (zone, region) so isn't copied
+			},
+			expectedPodLabels: map[string]string{
+				"topology.k8s.io/zone":   "zone",
+				"topology.k8s.io/region": "region",
+				"topology.k8s.io/abc":    "123",
+			},
+		},
+	}
+	// Enable the feature BEFORE starting the test server, as the admission plugin only checks feature gates
+	// on start up and not on each invocation at runtime.
+	featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.33"))
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodTopologyLabelsAdmission, true)
+	testPodTopologyLabels(t, tests)
+}
+
+func TestPodTopologyLabels_FeatureDisabled(t *testing.T) {
+	tests := []podTopologyTestCase{
+		{
+			name: "does nothing when the feature is not enabled",
+			targetNodeLabels: map[string]string{
+				"topology.k8s.io/zone":     "zone",
+				"topology.k8s.io/region":   "region",
+				"topology.k8s.io/custom":   "thing",
+				"sub.topology.k8s.io/zone": "zone",
+			},
+			expectedPodLabels: map[string]string{},
+		},
+	}
+	// Disable the feature BEFORE starting the test server, as the admission plugin only checks feature gates
+	// on start up and not on each invocation at runtime.
+	featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.33"))
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodTopologyLabelsAdmission, false)
+	testPodTopologyLabels(t, tests)
+}
+
+// podTopologyTestCase is defined outside of TestPodTopologyLabels to allow us to re-use the test implementation logic
+// between the feature enabled and feature disabled tests.
+// This will no longer be required once the feature gate graduates to GA/locked to being enabled.
+type podTopologyTestCase struct {
+	name              string
+	targetNodeLabels  map[string]string
+	existingPodLabels map[string]string
+	expectedPodLabels map[string]string
+}
+
+func testPodTopologyLabels(t *testing.T, tests []podTopologyTestCase) {
+	server := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
+	defer server.TearDownFn()
+	client := clientset.NewForConfigOrDie(server.ClientConfig)
+	ns := framework.CreateNamespaceOrDie(client, "pod-topology-labels", t)
+	defer framework.DeleteNamespaceOrDie(client, ns, t)
+
+	prototypePod := func() *v1.Pod {
+		return &v1.Pod{
+			ObjectMeta: metav1.ObjectMeta{
+				GenerateName: "pod-topology-test-",
+			},
+			Spec: v1.PodSpec{
+				Containers: []v1.Container{
+					{
+						Name:  "fake-name",
+						Image: "fakeimage",
+					},
+				},
+			},
+		}
+	}
+	prototypeNode := func() *v1.Node {
+		return &v1.Node{
+			ObjectMeta: metav1.ObjectMeta{
+				GenerateName: "podtopology-test-node-",
+			},
+		}
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			// Create the Node we are going to bind to.
+			node := prototypeNode()
+			// Set the labels on the Node we are going to create.
+			node.Labels = test.targetNodeLabels
+			ctx := context.Background()
+
+			var err error
+			if node, err = client.CoreV1().Nodes().Create(ctx, node, metav1.CreateOptions{}); err != nil {
+				t.Errorf("Failed to create node: %v", err)
+			}
+
+			pod := prototypePod()
+			pod.Labels = test.existingPodLabels
+			if pod, err = client.CoreV1().Pods(ns.Name).Create(ctx, pod, metav1.CreateOptions{}); err != nil {
+				t.Errorf("Failed to create pod: %v", err)
+			}
+
+			binding := &v1.Binding{
+				ObjectMeta: metav1.ObjectMeta{Name: pod.Name, Namespace: pod.Namespace},
+				Target: v1.ObjectReference{
+					Kind: "Node",
+					Name: node.Name,
+				},
+			}
+			if err := client.CoreV1().Pods(pod.Namespace).Bind(ctx, binding, metav1.CreateOptions{}); err != nil {
+				t.Errorf("Failed to bind pod to node: %v", err)
+			}
+
+			if pod, err = client.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{}); err != nil {
+				t.Errorf("Failed to fetch bound Pod: %v", err)
+			}
+
+			if !apiequality.Semantic.DeepEqual(pod.Labels, test.expectedPodLabels) {
+				t.Errorf("Unexpected label values: %v", cmp.Diff(pod.Labels, test.expectedPodLabels))
+			}
+		})
+	}
+}
+
 func TestPodUpdateActiveDeadlineSeconds(t *testing.T) {
 	// Disable ServiceAccount admission plugin as we don't have serviceaccount controller running.
 	server := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())