Skip to content

Commit c91b821

Browse files
committed
Allow karpenter to set arbitrary k8s labels on NodeClaim/Nodes
nodeClaim Labels are the source for core karpenter to sync Node Labels in a centralized fashion. Some bootstrap userdata provider implementations also consume this labels and pass them through kubelet self setting. That results in a coupling between a centralized and a kubelet self setting approach. This coupling results in conflicting criteria for validations and degraded UX. This PR removes the coupling. As a consequence, Node Labels are not unncessarily restricted for the centralized sync anymore. This better empowers administrators reducing the reliance on self setting kubelet and minimizing the risk of a Node steering privileged workloads to itself. A subset of labels, excluding those disallowed by the node restriction admission, is now stored in json within the "karpenter.sh/kubelet-labels" NodeClaim annotation. This enables bootstrap userdata providers to continue utilizing the labels if needed.
1 parent 380bcc9 commit c91b821

File tree

3 files changed

+102
-30
lines changed

3 files changed

+102
-30
lines changed

pkg/apis/v1/labels.go

+72-25
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,17 @@ const (
5858
)
5959

6060
var (
61-
// RestrictedLabelDomains are either prohibited by the kubelet or reserved by karpenter
61+
// RestrictedLabelDomains are reserved by karpenter
6262
RestrictedLabelDomains = sets.New(
63-
"kubernetes.io",
64-
"k8s.io",
6563
apis.Group,
6664
)
6765

68-
// LabelDomainExceptions are sub-domains of the RestrictedLabelDomains but allowed because
69-
// they are not used in a context where they may be passed as argument to kubelet.
70-
LabelDomainExceptions = sets.New(
71-
"kops.k8s.io",
72-
v1.LabelNamespaceSuffixNode,
73-
v1.LabelNamespaceNodeRestriction,
66+
K8sLabelDomains = sets.New(
67+
"kubernetes.io",
68+
"k8s.io",
7469
)
7570

76-
// WellKnownLabels are labels that belong to the RestrictedLabelDomains but allowed.
71+
// WellKnownLabels are labels that belong to the RestrictedLabelDomains or K8sLabelDomains but allowed.
7772
// Karpenter is aware of these labels, and they can be used to further narrow down
7873
// the range of the corresponding values by either nodepool or pods.
7974
WellKnownLabels = sets.New(
@@ -104,38 +99,90 @@ var (
10499
}
105100
)
106101

107-
// IsRestrictedLabel returns an error if the label is restricted.
102+
// IsRestrictedLabel is used for runtime validation of requirements.
103+
// Returns an error if the label is restricted. E.g. using .karpenter.sh suffix.
108104
func IsRestrictedLabel(key string) error {
109105
if WellKnownLabels.Has(key) {
110106
return nil
111107
}
112-
if IsRestrictedNodeLabel(key) {
108+
109+
labelDomain := GetLabelDomain(key)
110+
for restrictedLabelDomain := range RestrictedLabelDomains {
111+
if labelDomain == restrictedLabelDomain || strings.HasSuffix(labelDomain, "."+restrictedLabelDomain) {
112+
return fmt.Errorf("label %s is restricted; specify a well known label: %v, or a custom label that does not use a restricted domain: %v", key, sets.List(WellKnownLabels), sets.List(RestrictedLabelDomains))
113+
}
114+
}
115+
116+
if RestrictedLabels.Has(key) {
113117
return fmt.Errorf("label %s is restricted; specify a well known label: %v, or a custom label that does not use a restricted domain: %v", key, sets.List(WellKnownLabels), sets.List(RestrictedLabelDomains))
114118
}
119+
115120
return nil
116121
}
117122

118-
// IsRestrictedNodeLabel returns true if a node label should not be injected by Karpenter.
119-
// They are either known labels that will be injected by cloud providers,
120-
// or label domain managed by other software (e.g., kops.k8s.io managed by kOps).
121-
func IsRestrictedNodeLabel(key string) bool {
123+
// IsValidLabelToSync returns true if the label key is allowed to be synced to the Node object centrally by Karpenter.
124+
func IsValidToSyncCentrallyLabel(key string) bool {
125+
// TODO(enxebre): consider this to be configurable with runtime flag.
126+
notValidToSyncLabel := sets.New(
127+
v1.LabelHostname,
128+
v1.LabelArchStable,
129+
v1.LabelOSStable,
130+
v1.LabelInstanceTypeStable,
131+
v1.LabelWindowsBuild,
132+
)
133+
134+
if notValidToSyncLabel.Has(key) {
135+
return false
136+
}
137+
138+
return true
139+
}
140+
141+
// IsKubeletLabel returns true if the label key is one that kubelets are allowed to set on their own Node object.
142+
// This function is similar the one used by the node restriction admission https://github.com/kubernetes/kubernetes/blob/e319c541f144e9bee6160f1dd8671638a9029f4c/staging/src/k8s.io/kubelet/pkg/apis/well_known_labels.go#L67
143+
// but karpenter also restricts the known labels to be passed to kubelet. Only the kubeletLabelNamespaces are allowed.
144+
func IsKubeletLabel(key string) bool {
122145
if WellKnownLabels.Has(key) {
123-
return true
146+
return false
124147
}
125-
labelDomain := GetLabelDomain(key)
126-
for exceptionLabelDomain := range LabelDomainExceptions {
127-
if strings.HasSuffix(labelDomain, exceptionLabelDomain) {
128-
return false
129-
}
148+
149+
if !isKubernetesLabel(key) {
150+
return true
130151
}
131-
for restrictedLabelDomain := range RestrictedLabelDomains {
132-
if strings.HasSuffix(labelDomain, restrictedLabelDomain) {
152+
153+
namespace := getLabelNamespace(key)
154+
for allowedNamespace := range kubeletLabelNamespaces {
155+
if namespace == allowedNamespace || strings.HasSuffix(namespace, "."+allowedNamespace) {
133156
return true
134157
}
135158
}
136-
return RestrictedLabels.Has(key)
159+
160+
return false
161+
}
162+
163+
func isKubernetesLabel(key string) bool {
164+
namespace := getLabelNamespace(key)
165+
if namespace == "kubernetes.io" || strings.HasSuffix(namespace, ".kubernetes.io") {
166+
return true
167+
}
168+
if namespace == "k8s.io" || strings.HasSuffix(namespace, ".k8s.io") {
169+
return true
170+
}
171+
return false
137172
}
138173

174+
func getLabelNamespace(key string) string {
175+
if parts := strings.SplitN(key, "/", 2); len(parts) == 2 {
176+
return parts[0]
177+
}
178+
return ""
179+
}
180+
181+
var kubeletLabelNamespaces = sets.NewString(
182+
v1.LabelNamespaceSuffixKubelet,
183+
v1.LabelNamespaceSuffixNode,
184+
)
185+
139186
func GetLabelDomain(key string) string {
140187
if parts := strings.SplitN(key, "/", 2); len(parts) == 2 {
141188
return parts[0]

pkg/controllers/nodeclaim/lifecycle/launch.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package lifecycle
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"errors"
2223
"fmt"
2324

@@ -28,6 +29,7 @@ import (
2829
"sigs.k8s.io/controller-runtime/pkg/log"
2930
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3031

32+
"sigs.k8s.io/karpenter/pkg/apis"
3133
v1 "sigs.k8s.io/karpenter/pkg/apis/v1"
3234
"sigs.k8s.io/karpenter/pkg/cloudprovider"
3335
"sigs.k8s.io/karpenter/pkg/events"
@@ -127,7 +129,14 @@ func PopulateNodeClaimDetails(nodeClaim, retrieved *v1.NodeClaim) *v1.NodeClaim
127129
scheduling.NewNodeSelectorRequirementsWithMinValues(nodeClaim.Spec.Requirements...).Labels(), // Single-value requirement resolved labels
128130
nodeClaim.Labels, // User-defined labels
129131
)
130-
nodeClaim.Annotations = lo.Assign(nodeClaim.Annotations, retrieved.Annotations)
132+
133+
kubeletLabels := scheduling.NewNodeSelectorRequirementsWithMinValues(nodeClaim.Spec.Requirements...).KubeletLabels()
134+
json, err := json.Marshal(kubeletLabels)
135+
if err != nil {
136+
panic(err)
137+
}
138+
kubeletLabelsAnnotation := map[string]string{apis.Group + "/node-restricted-labels": string(json)}
139+
nodeClaim.Annotations = lo.Assign(nodeClaim.Annotations, retrieved.Annotations, kubeletLabelsAnnotation)
131140
nodeClaim.Status.ProviderID = retrieved.Status.ProviderID
132141
nodeClaim.Status.ImageID = retrieved.Status.ImageID
133142
nodeClaim.Status.Allocatable = retrieved.Status.Allocatable

pkg/scheduling/requirements.go

+20-4
Original file line numberDiff line numberDiff line change
@@ -303,13 +303,29 @@ func (r Requirements) Intersects(requirements Requirements) (errs error) {
303303
return errs
304304
}
305305

306+
// Labels filters out labels from the requirements that are not allowed to be synced centrally by Karpenter to the Node.
306307
func (r Requirements) Labels() map[string]string {
307308
labels := map[string]string{}
308309
for key, requirement := range r {
309-
if !v1.IsRestrictedNodeLabel(key) {
310-
if value := requirement.Any(); value != "" {
311-
labels[key] = value
312-
}
310+
if !v1.IsValidToSyncCentrallyLabel(key) {
311+
continue
312+
}
313+
if value := requirement.Any(); value != "" {
314+
labels[key] = value
315+
}
316+
}
317+
return labels
318+
}
319+
320+
// KubeletLabels filters out labels from the requirements that will be rejected by the node restriction admission.
321+
func (r Requirements) KubeletLabels() map[string]string {
322+
labels := map[string]string{}
323+
for key, requirement := range r {
324+
if !v1.IsKubeletLabel(key) {
325+
continue
326+
}
327+
if value := requirement.Any(); value != "" {
328+
labels[key] = value
313329
}
314330
}
315331
return labels

0 commit comments

Comments
 (0)