Skip to content

Commit 69f7e87

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/node-restricted-labels" NodeClaim annotation. This enables bootstrap userdata providers to continue utilizing the labels if needed.
1 parent 1c467ba commit 69f7e87

File tree

3 files changed

+87
-30
lines changed

3 files changed

+87
-30
lines changed

pkg/apis/v1/labels.go

+53-24
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,72 @@ 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) {
113-
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))
108+
109+
labelDomain := GetLabelDomain(key)
110+
for restrictedLabelDomain := range RestrictedLabelDomains {
111+
if labelDomain == restrictedLabelDomain || strings.HasSuffix(labelDomain, "."+restrictedLabelDomain) {
112+
return fmt.Errorf("Using label %s is not allowed as it might interfere with the internal provisioning logic; 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) {
117+
return fmt.Errorf("Using label %s is not allowed as it might interfere with the internal provisioning logic; 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 := WellKnownLabels
127+
128+
return !notValidToSyncLabel.Has(key)
129+
}
130+
131+
// IsKubeletLabel returns true if the label key is one that kubelets are allowed to set on their own Node object.
132+
// 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
133+
// but karpenter also restricts the known labels to be passed to kubelet. Only the kubeletLabelNamespaces are allowed.
134+
func IsKubeletLabel(key string) bool {
122135
if WellKnownLabels.Has(key) {
136+
return false
137+
}
138+
139+
if !isKubernetesLabel(key) {
123140
return true
124141
}
125-
labelDomain := GetLabelDomain(key)
126-
for exceptionLabelDomain := range LabelDomainExceptions {
127-
if strings.HasSuffix(labelDomain, exceptionLabelDomain) {
128-
return false
142+
143+
namespace := GetLabelDomain(key)
144+
for allowedNamespace := range kubeletLabelNamespaces {
145+
if namespace == allowedNamespace || strings.HasSuffix(namespace, "."+allowedNamespace) {
146+
return true
129147
}
130148
}
131-
for restrictedLabelDomain := range RestrictedLabelDomains {
132-
if strings.HasSuffix(labelDomain, restrictedLabelDomain) {
149+
150+
return false
151+
}
152+
153+
func isKubernetesLabel(key string) bool {
154+
for k8sDomain := range K8sLabelDomains {
155+
if key == k8sDomain || strings.HasSuffix(key, "."+k8sDomain) {
133156
return true
134157
}
135158
}
136-
return RestrictedLabels.Has(key)
159+
160+
return false
137161
}
138162

163+
var kubeletLabelNamespaces = sets.NewString(
164+
v1.LabelNamespaceSuffixKubelet,
165+
v1.LabelNamespaceSuffixNode,
166+
)
167+
139168
func GetLabelDomain(key string) string {
140169
if parts := strings.SplitN(key, "/", 2); len(parts) == 2 {
141170
return parts[0]

pkg/controllers/nodeclaim/lifecycle/launch.go

+13-2
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"
@@ -124,10 +126,19 @@ func PopulateNodeClaimDetails(nodeClaim, retrieved *v1.NodeClaim) *v1.NodeClaim
124126
// or the static nodeClaim labels
125127
nodeClaim.Labels = lo.Assign(
126128
retrieved.Labels, // CloudProvider-resolved labels
127-
scheduling.NewNodeSelectorRequirementsWithMinValues(nodeClaim.Spec.Requirements...).Labels(), // Single-value requirement resolved labels
129+
scheduling.NewNodeSelectorRequirementsWithMinValues(nodeClaim.Spec.Requirements...).Labels(), // Single-value requirement resolved labels that are synced to the Node object centrally by Karpenter.
128130
nodeClaim.Labels, // User-defined labels
129131
)
130-
nodeClaim.Annotations = lo.Assign(nodeClaim.Annotations, retrieved.Annotations)
132+
133+
// We store labels compliant with the node restriction admission as an annotation on the NodeClaim
134+
// so that bootstrap provider implementation can use them for kubelet labels.
135+
kubeletLabels := scheduling.NewNodeSelectorRequirementsWithMinValues(nodeClaim.Spec.Requirements...).KubeletLabels()
136+
json, err := json.Marshal(kubeletLabels)
137+
if err != nil {
138+
panic(err)
139+
}
140+
kubeletLabelsAnnotation := map[string]string{apis.Group + "/node-restricted-labels": string(json)}
141+
nodeClaim.Annotations = lo.Assign(nodeClaim.Annotations, retrieved.Annotations, kubeletLabelsAnnotation)
131142
nodeClaim.Status.ProviderID = retrieved.Status.ProviderID
132143
nodeClaim.Status.ImageID = retrieved.Status.ImageID
133144
nodeClaim.Status.Allocatable = retrieved.Status.Allocatable

pkg/scheduling/requirements.go

+21-4
Original file line numberDiff line numberDiff line change
@@ -303,13 +303,30 @@ 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+
// Bootstrap implementations can choose to pass the resulting list to the kubelet.
322+
func (r Requirements) KubeletLabels() map[string]string {
323+
labels := map[string]string{}
324+
for key, requirement := range r {
325+
if !v1.IsKubeletLabel(key) {
326+
continue
327+
}
328+
if value := requirement.Any(); value != "" {
329+
labels[key] = value
313330
}
314331
}
315332
return labels

0 commit comments

Comments
 (0)