Skip to content

Commit

Permalink
Add name hashing for long MS names
Browse files Browse the repository at this point in the history
Signed-off-by: killianmuldoon <kmuldoon@vmware.com>
  • Loading branch information
killianmuldoon committed Dec 13, 2022
1 parent f58a06c commit e6881e3
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 25 deletions.
5 changes: 5 additions & 0 deletions api/v1beta1/machine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,17 @@ const (
ExcludeNodeDrainingAnnotation = "machine.cluster.x-k8s.io/exclude-node-draining"

// MachineSetLabelName is the label set on machines if they're controlled by MachineSet.
// Note: The value of this label may be a hash if the MachineSet name is longer than 63 characters.
MachineSetLabelName = "cluster.x-k8s.io/set-name"

// ExcludeWaitForNodeVolumeDetachAnnotation annotation explicitly skips the waiting for node volume detaching if set.
ExcludeWaitForNodeVolumeDetachAnnotation = "machine.cluster.x-k8s.io/exclude-wait-for-node-volume-detach"

// MachineDeploymentLabelName is the label set on machines if they're controlled by MachineDeployment.
MachineDeploymentLabelName = "cluster.x-k8s.io/deployment-name"

// MachineControlPlaneNameLabel is the label set on machines if they're controlled by a ControlPlane.
// Note: The value of this label may be a hash if the control plane name is longer than 63 characters.
MachineControlPlaneNameLabel = "cluster.x-k8s.io/control-plane-name"

// PreDrainDeleteHookAnnotationPrefix annotation specifies the prefix we
Expand Down
6 changes: 4 additions & 2 deletions api/v1beta1/machineset_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"

capilabels "sigs.k8s.io/cluster-api/internal/labels"
"sigs.k8s.io/cluster-api/util/version"
)

Expand Down Expand Up @@ -64,8 +65,9 @@ func (m *MachineSet) Default() {
}

if len(m.Spec.Selector.MatchLabels) == 0 && len(m.Spec.Selector.MatchExpressions) == 0 {
m.Spec.Selector.MatchLabels[MachineSetLabelName] = m.Name
m.Spec.Template.Labels[MachineSetLabelName] = m.Name
// Note: MustFormatValue is used here as the value of this label will be a hash if the MachineSet name is longer than 63 characters.
m.Spec.Selector.MatchLabels[MachineSetLabelName] = capilabels.MustFormatValue(m.Name)
m.Spec.Template.Labels[MachineSetLabelName] = capilabels.MustFormatValue(m.Name)
}

if m.Spec.Template.Spec.Version != nil && !strings.HasPrefix(*m.Spec.Template.Spec.Version, "v") {
Expand Down
4 changes: 3 additions & 1 deletion controlplane/kubeadm/internal/cluster_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package internal
import (
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
capilabels "sigs.k8s.io/cluster-api/internal/labels"
)

// ControlPlaneMachineLabelsForCluster returns a set of labels to add to a control plane machine for this specific cluster.
Expand All @@ -34,6 +35,7 @@ func ControlPlaneMachineLabelsForCluster(kcp *controlplanev1.KubeadmControlPlane
// Always force these labels over the ones coming from the spec.
labels[clusterv1.ClusterLabelName] = clusterName
labels[clusterv1.MachineControlPlaneLabelName] = ""
labels[clusterv1.MachineControlPlaneNameLabel] = kcp.Name
// Note: MustFormatValue is used here as the label value can be a hash if the control plane name is longer than 63 characters.
labels[clusterv1.MachineControlPlaneNameLabel] = capilabels.MustFormatValue(kcp.Name)
return labels
}
7 changes: 5 additions & 2 deletions controlplane/kubeadm/internal/controllers/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal"
expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
"sigs.k8s.io/cluster-api/feature"
"sigs.k8s.io/cluster-api/internal/labels"
"sigs.k8s.io/cluster-api/util"
"sigs.k8s.io/cluster-api/util/annotations"
"sigs.k8s.io/cluster-api/util/collections"
Expand Down Expand Up @@ -338,8 +339,10 @@ func (r *KubeadmControlPlaneReconciler) reconcile(ctx context.Context, cluster *
// NOTE: cluster.x-k8s.io/control-plane is already set at this stage (it is used when reading controlPlane.Machines).
for i := range controlPlane.Machines {
machine := controlPlane.Machines[i]
if value, ok := machine.Labels[clusterv1.MachineControlPlaneNameLabel]; !ok || value != kcp.Name {
machine.Labels[clusterv1.MachineControlPlaneNameLabel] = kcp.Name
// Note: MustEqualValue and MustFormatValue is used here as the label value can be a hash if the control plane
// name is longer than 63 characters.
if value, ok := machine.Labels[clusterv1.MachineControlPlaneNameLabel]; !ok || !labels.MustEqualValue(kcp.Name, value) {
machine.Labels[clusterv1.MachineControlPlaneNameLabel] = labels.MustFormatValue(kcp.Name)
}
}

Expand Down
27 changes: 14 additions & 13 deletions docs/book/src/reference/labels_and_annotations.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
**Supported Labels:**


| Label | Note |
|:--------|:--------|
| cluster.x-k8s.io/cluster-name| It is set on machines linked to a cluster and external objects(bootstrap and infrastructure providers). |
| topology.cluster.x-k8s.io/owned| It is set on all the object which are managed as part of a ClusterTopology. |
|topology.cluster.x-k8s.io/deployment-name | It is set on the generated MachineDeployment objects to track the name of the MachineDeployment topology it represents. |
| cluster.x-k8s.io/provider| It is set on components in the provider manifest. The label allows one to easily identify all the components belonging to a provider. The clusterctl tool uses this label for implementing provider's lifecycle operations. |
| cluster.x-k8s.io/watch-filter | It can be applied to any Cluster API object. Controllers which allow for selective reconciliation may check this label and proceed with reconciliation of the object only if this label and a configured value is present. |
| cluster.x-k8s.io/interruptible| It is used to mark the nodes that run on interruptible instances. |
|cluster.x-k8s.io/control-plane | It is set on machines or related objects that are part of a control plane. |
| cluster.x-k8s.io/set-name| It is set on machines if they're controlled by MachineSet. |
| cluster.x-k8s.io/deployment-name| It is set on machines if they're controlled by a MachineDeployment. |
| machine-template-hash| It is applied to Machines in a MachineDeployment containing the hash of the template. |
| Label | Note |
|:--------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| cluster.x-k8s.io/cluster-name | It is set on machines linked to a cluster and external objects(bootstrap and infrastructure providers). |
| topology.cluster.x-k8s.io/owned | It is set on all the object which are managed as part of a ClusterTopology. |
| topology.cluster.x-k8s.io/deployment-name | It is set on the generated MachineDeployment objects to track the name of the MachineDeployment topology it represents. |
| cluster.x-k8s.io/provider | It is set on components in the provider manifest. The label allows one to easily identify all the components belonging to a provider. The clusterctl tool uses this label for implementing provider's lifecycle operations. |
| cluster.x-k8s.io/watch-filter | It can be applied to any Cluster API object. Controllers which allow for selective reconciliation may check this label and proceed with reconciliation of the object only if this label and a configured value is present. |
| cluster.x-k8s.io/interruptible | It is used to mark the nodes that run on interruptible instances. |
| cluster.x-k8s.io/control-plane | It is set on machines or related objects that are part of a control plane. |
| cluster.x-k8s.io/set-name | It is set on machines if they're controlled by MachineSet. The value of this label may be a hash if the MachineSet name is longer than 63 characters. |
| cluster.x-k8s.io/control-plane-name | It is set on machines if they're controlled by a contorl plane. The value of this label may be a hash if the control plane name is longer than 63 characters. |
| cluster.x-k8s.io/deployment-name | It is set on machines if they're controlled by a MachineDeployment. |
| machine-template-hash | It is applied to Machines in a MachineDeployment containing the hash of the template. |
<br>


Expand Down Expand Up @@ -44,4 +45,4 @@
| machinedeployment.clusters.x-k8s.io/max-replicas | It is the maximum replicas a deployment can have at a given point, which is machinedeployment.spec.replicas + maxSurge. Used by the underlying machine sets to estimate their proportions in case the deployment has surge replicas. |
| controlplane.cluster.x-k8s.io/skip-coredns | It explicitly skips reconciling CoreDNS if set. |
|controlplane.cluster.x-k8s.io/skip-kube-proxy | It explicitly skips reconciling kube-proxy if set.|
| controlplane.cluster.x-k8s.io/kubeadm-cluster-configuration| It is a machine annotation that stores the json-marshalled string of KCP ClusterConfiguration. This annotation is used to detect any changes in ClusterConfiguration and trigger machine rollout in KCP.|
| controlplane.cluster.x-k8s.io/kubeadm-cluster-configuration| It is a machine annotation that stores the json-marshalled string of KCP ClusterConfiguration. This annotation is used to detect any changes in ClusterConfiguration and trigger machine rollout in KCP.|
17 changes: 10 additions & 7 deletions internal/controllers/machineset/machineset_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"sigs.k8s.io/cluster-api/controllers/noderefutil"
"sigs.k8s.io/cluster-api/controllers/remote"
"sigs.k8s.io/cluster-api/internal/controllers/machine"
capilabels "sigs.k8s.io/cluster-api/internal/labels"
"sigs.k8s.io/cluster-api/util"
"sigs.k8s.io/cluster-api/util/annotations"
"sigs.k8s.io/cluster-api/util/collections"
Expand Down Expand Up @@ -287,7 +288,8 @@ func (r *Reconciler) reconcile(ctx context.Context, cluster *clusterv1.Cluster,
mdNameOnMachineSet, mdNameSetOnMachineSet := machineSet.Labels[clusterv1.MachineDeploymentLabelName]
mdNameOnMachine := machine.Labels[clusterv1.MachineDeploymentLabelName]

if msName, ok := machine.Labels[clusterv1.MachineSetLabelName]; ok && msName == machineSet.Name &&
// Note: MustEqualValue is used here as the value of this label will be a hash if the MachineSet name is longer than 63 characters.
if msNameLabelValue, ok := machine.Labels[clusterv1.MachineSetLabelName]; ok && capilabels.MustEqualValue(machineSet.Name, msNameLabelValue) &&
(!mdNameSetOnMachineSet || mdNameOnMachineSet == mdNameOnMachine) {
// Continue if the MachineSet name label is already set correctly and
// either the MachineDeployment name label is not set on the MachineSet or
Expand All @@ -299,8 +301,9 @@ func (r *Reconciler) reconcile(ctx context.Context, cluster *clusterv1.Cluster,
if err != nil {
return ctrl.Result{}, errors.Wrapf(err, "failed to apply %s label to Machine %q", clusterv1.MachineSetLabelName, machine.Name)
}
machine.Labels[clusterv1.MachineSetLabelName] = machineSet.Name
// Propagate the MachineDeploymentLabelName from MachineSet to Machine if it is set on the MachineSet.
// Note: MustFormatValue is used here as the value of this label will be a hash if the MachineSet name is longer than 63 characters.
machine.Labels[clusterv1.MachineSetLabelName] = capilabels.MustFormatValue(machineSet.Name)
// Propagate the MachineDeploymentNameLabel from MachineSet to Machine if it is set on the MachineSet.
if mdNameSetOnMachineSet {
machine.Labels[clusterv1.MachineDeploymentLabelName] = mdNameOnMachineSet
}
Expand Down Expand Up @@ -540,10 +543,10 @@ func (r *Reconciler) getNewMachine(machineSet *clusterv1.MachineSet) *clusterv1.
machine.Labels[k] = v
}

// Enforce that the MachineSetLabelName label is set
// Note: the MachineSetLabelName is added by the default webhook to MachineSet.spec.template.labels if a spec.selector is empty.
machine.Labels[clusterv1.MachineSetLabelName] = machineSet.Name
// Propagate the MachineDeploymentLabelName from MachineSet to Machine if it exists.
// Enforce that the MachineSetNameLabel label is set
// Note: the MachineSetNameLabel is added by the default webhook to MachineSet.spec.template.labels if a spec.selector is empty.
machine.Labels[clusterv1.MachineSetLabelName] = capilabels.MustFormatValue(machineSet.Name)
// Propagate the MachineDeploymentNameLabel from MachineSet to Machine if it exists.
if mdName, ok := machineSet.Labels[clusterv1.MachineDeploymentLabelName]; ok {
machine.Labels[clusterv1.MachineDeploymentLabelName] = mdName
}
Expand Down
51 changes: 51 additions & 0 deletions internal/labels/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
Copyright 2021 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 labels contains functions to validate and compare values used in Kubernetes labels.
package labels

import (
"encoding/base64"
"hash/fnv"

"k8s.io/apimachinery/pkg/util/validation"
)

// MustFormatValue returns the passed inputLabelValue if it meets the standards for a Kubernetes label value.
// If the name is not a valid label value this function returns a hash which meets the requirements.
func MustFormatValue(str string) string {
// a valid Kubernetes label value must:
// - be less than 64 characters long.
// - be an empty string OR consist of alphanumeric characters, '-', '_' or '.'.
// - start and end with an alphanumeric character
if len(validation.IsValidLabelValue(str)) == 0 {
return str
}
hasher := fnv.New32a()
_, err := hasher.Write([]byte(str))
if err != nil {
// At time of writing the implementation of fnv's Write function can never return an error.
// If this changes in a future go version this function will panic.
panic(err)
}
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hasher.Sum(nil))
}

// MustEqualValue returns true if the actualLabelValue equals either the inputLabelValue or the hashed
// value of the inputLabelValue.
func MustEqualValue(str, labelValue string) bool {
return labelValue == MustFormatValue(str)
}
90 changes: 90 additions & 0 deletions internal/labels/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
Copyright 2021 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 labels

import (
"testing"

"github.com/onsi/gomega"
)

func TestNameLabelValue(t *testing.T) {
g := gomega.NewWithT(t)
tests := []struct {
name string
machineSetName string
want string
}{
{
name: "return the name if it's less than 63 characters",
machineSetName: "machineSetName",
want: "machineSetName",
},
{
name: "return for a name with more than 63 characters",
machineSetName: "machineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetName",
want: "FR_ghQ",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := MustFormatValue(tt.machineSetName)
g.Expect(got).To(gomega.Equal(tt.want))
})
}
}

func TestMustMatchLabelValueForName(t *testing.T) {
g := gomega.NewWithT(t)
tests := []struct {
name string
machineSetName string
labelValue string
want bool
}{
{
name: "match labels when MachineSet name is short",
machineSetName: "ms1",
labelValue: "ms1",
want: true,
},
{
name: "don't match different labels when MachineSet name is short",
machineSetName: "ms1",
labelValue: "notMS1",
want: false,
},
{
name: "don't match labels when MachineSet name is long",
machineSetName: "machineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetName",
labelValue: "Nx4RdE",
want: false,
},
{
name: "match labels when MachineSet name is long",
machineSetName: "machineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetNamemachineSetName",
labelValue: "FR_ghQ",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := MustEqualValue(tt.machineSetName, tt.labelValue)
g.Expect(got).To(gomega.Equal(tt.want))
})
}
}

0 comments on commit e6881e3

Please sign in to comment.