Skip to content

Commit 597f443

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 a196406 commit 597f443

File tree

3 files changed

+72
-25
lines changed

3 files changed

+72
-25
lines changed

pkg/apis/v1/labels.go

+51-23
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,14 @@ 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

7671
// WellKnownLabels are labels that belong to the RestrictedLabelDomains but allowed.
@@ -109,33 +104,66 @@ func IsRestrictedLabel(key string) error {
109104
if WellKnownLabels.Has(key) {
110105
return nil
111106
}
112-
if IsRestrictedNodeLabel(key) {
107+
108+
labelDomain := GetLabelDomain(key)
109+
for restrictedLabelDomain := range RestrictedLabelDomains {
110+
if strings.HasSuffix(labelDomain, restrictedLabelDomain) {
111+
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))
112+
}
113+
}
114+
115+
if RestrictedLabels.Has(key) {
113116
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))
114117
}
118+
115119
return nil
116120
}
117121

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 {
122+
// IsKubeletLabel returns true if the label key is one that kubelets are allowed to set on their own Node object.
123+
// 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
124+
// but karpenter also restricts the known labels to be passed to kubelet. Only the kubeletLabelNamespaces are allowed.
125+
func IsKubeletLabel(key string) bool {
122126
if WellKnownLabels.Has(key) {
123-
return true
127+
return false
124128
}
125-
labelDomain := GetLabelDomain(key)
126-
for exceptionLabelDomain := range LabelDomainExceptions {
127-
if strings.HasSuffix(labelDomain, exceptionLabelDomain) {
128-
return false
129-
}
129+
130+
if !isKubernetesLabel(key) {
131+
return true
130132
}
131-
for restrictedLabelDomain := range RestrictedLabelDomains {
132-
if strings.HasSuffix(labelDomain, restrictedLabelDomain) {
133+
134+
namespace := getLabelNamespace(key)
135+
for allowedNamespace := range kubeletLabelNamespaces {
136+
if namespace == allowedNamespace || strings.HasSuffix(namespace, "."+allowedNamespace) {
133137
return true
134138
}
135139
}
136-
return RestrictedLabels.Has(key)
140+
141+
return false
142+
}
143+
144+
func isKubernetesLabel(key string) bool {
145+
namespace := getLabelNamespace(key)
146+
if namespace == "kubernetes.io" || strings.HasSuffix(namespace, ".kubernetes.io") {
147+
return true
148+
}
149+
if namespace == "k8s.io" || strings.HasSuffix(namespace, ".k8s.io") {
150+
return true
151+
}
152+
return false
137153
}
138154

155+
func getLabelNamespace(key string) string {
156+
if parts := strings.SplitN(key, "/", 2); len(parts) == 2 {
157+
return parts[0]
158+
}
159+
return ""
160+
}
161+
162+
var kubeletLabelNamespaces = sets.NewString(
163+
v1.LabelNamespaceSuffixKubelet,
164+
v1.LabelNamespaceSuffixNode,
165+
)
166+
139167
func GetLabelDomain(key string) string {
140168
if parts := strings.SplitN(key, "/", 2); len(parts) == 2 {
141169
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 + "/kubelet-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

+11-1
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,17 @@ func (r Requirements) Intersects(requirements Requirements) (errs error) {
306306
func (r Requirements) Labels() map[string]string {
307307
labels := map[string]string{}
308308
for key, requirement := range r {
309-
if !v1.IsRestrictedNodeLabel(key) {
309+
if value := requirement.Any(); value != "" {
310+
labels[key] = value
311+
}
312+
}
313+
return labels
314+
}
315+
316+
func (r Requirements) KubeletLabels() map[string]string {
317+
labels := map[string]string{}
318+
for key, requirement := range r {
319+
if v1.IsKubeletLabel(key) {
310320
if value := requirement.Any(); value != "" {
311321
labels[key] = value
312322
}

0 commit comments

Comments
 (0)