Skip to content

Commit

Permalink
Add support to reconcile labels from Machines to Nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
enxebre committed Oct 11, 2022
1 parent 4d8c88f commit bd6a5e9
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 0 deletions.
16 changes: 16 additions & 0 deletions api/v1beta1/machine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,22 @@ const (
// This annotation can be set on BootstrapConfig or Machine objects. The value set on the Machine object takes precedence.
// This annotation can only be used on Control Plane Machines.
MachineCertificatesExpiryDateAnnotation = "machine.cluster.x-k8s.io/certificates-expiry"

// NodeRoleLabelDomain is one of the CAPI managed Node label domains.
NodeRoleLabelDomain = "node-role.kubernetes.io/"
// NodeRestrictionLabelDomain is one of the CAPI managed Node label domains.
NodeRestrictionLabelDomain = "node-restriction.kubernetes.io/"
// ManagedNodeLabelDomain is one of the CAPI managed domain Node label domains.
ManagedNodeLabelDomain = "node.cluster.x-k8s.io/"
)

var (
// ManagedNodeLabelDomains is a slice with all the CAPI managed Node label domains.
ManagedNodeLabelDomains = []string{
NodeRoleLabelDomain,
NodeRestrictionLabelDomain,
ManagedNodeLabelDomain,
}
)

// ANCHOR: MachineSpec
Expand Down
61 changes: 61 additions & 0 deletions internal/controllers/machine/machine_controller_noderef.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package machine
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"strings"

"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -116,6 +118,14 @@ func (r *Reconciler) reconcileNode(ctx context.Context, cluster *clusterv1.Clust
}
}

if reconcileManagedLabels(machine, node) {
options := []client.PatchOption{
client.FieldOwner("MachineController"),
}
nodePatch := unstructuredNode(node)
return ctrl.Result{}, remoteClient.Patch(ctx, nodePatch, client.Apply, options...)
}

// Do the remaining node health checks, then set the node health to true if all checks pass.
status, message := summarizeNodeConditions(node)
if status == corev1.ConditionFalse {
Expand All @@ -131,6 +141,57 @@ func (r *Reconciler) reconcileNode(ctx context.Context, cluster *clusterv1.Clust
return ctrl.Result{}, nil
}

// unstructuredNode returns a raw unstructured from a Node.
func unstructuredNode(node *corev1.Node) *unstructured.Unstructured {
obj := &unstructured.Unstructured{}
obj.SetAPIVersion(node.String())
obj.SetKind(node.Kind)
obj.SetName(node.Name)
obj.SetUID(node.UID)
obj.SetLabels(node.Labels)
return obj
}

func reconcileManagedLabels(machine *clusterv1.Machine, node *corev1.Node) bool {
// Reconcile managed Labels.
var labelsHaveChanged bool
desiredLabels := getManagedLabels(machine.Labels)
currentNodeLabels := getManagedLabels(node.Labels)

// Reconcile desired labels.
for desiredKey, desiredValue := range desiredLabels {
if currentValue, ok := currentNodeLabels[desiredKey]; !ok || desiredValue != currentValue {
node.Labels[desiredKey] = desiredValue
labelsHaveChanged = true
}
}

// Delete current labels which are not desired anymore.
for currentKey := range currentNodeLabels {
if _, ok := desiredLabels[currentKey]; !ok {
labelsHaveChanged = true
delete(node.Labels, currentKey)
}
}

return labelsHaveChanged
}

// getManagedLabels gets a map[string]string and returns another map[string]string
// filtering out labels not managed by CAPI.
func getManagedLabels(labels map[string]string) map[string]string {
managedLabels := make(map[string]string)
for key, value := range labels {
for _, managedDomain := range clusterv1.ManagedNodeLabelDomains {
if strings.Contains(key, managedDomain) {
managedLabels[key] = value
}
}
}

return managedLabels
}

// summarizeNodeConditions summarizes a Node's conditions and returns the summary of condition statuses and concatenate failed condition messages:
// if there is at least 1 semantically-negative condition, summarized status = False;
// if there is at least 1 semantically-positive condition when there is 0 semantically negative condition, summarized status = True;
Expand Down
129 changes: 129 additions & 0 deletions internal/controllers/machine/machine_controller_noderef_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,132 @@ func TestSummarizeNodeConditions(t *testing.T) {
})
}
}

func TestGetManagedLabels(t *testing.T) {
// Create managedLabels map from known managed prefixes.
managedLabels := map[string]string{}
for _, prefix := range clusterv1.ManagedNodeLabelDomains {
managedLabels[prefix] = ""
}
managedLabels["custom-prefix."+clusterv1.NodeRestrictionLabelDomain] = ""

// Append arbitrary labels.
allLabels := map[string]string{
"foo": "",
"bar": "",
}
for k, v := range managedLabels {
allLabels[k] = v
}

g := NewWithT(t)
got := getManagedLabels(allLabels)
g.Expect(got).To(BeEquivalentTo(managedLabels))
}

func TestReconcileManagedLabels(t *testing.T) {
allLabels := map[string]string{
"foo": "",
"bar": "",
}
for _, prefix := range clusterv1.ManagedNodeLabelDomains {
allLabels[prefix] = ""
}

testCases := []struct {
name string
node *corev1.Node
machine *clusterv1.Machine
expectedLabels map[string]string
expected bool
}{
{
name: "when Node is missing have wanted managed labels they should be added",
node: &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"foo": "A",
clusterv1.ManagedNodeLabelDomain: "anything",
},
},
},
machine: &clusterv1.Machine{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
clusterv1.NodeRoleLabelDomain: "",
clusterv1.NodeRestrictionLabelDomain: "",
clusterv1.ManagedNodeLabelDomain: "stamp",
"foo": "B",
},
},
},
expectedLabels: map[string]string{
clusterv1.NodeRoleLabelDomain: "",
clusterv1.NodeRestrictionLabelDomain: "",
clusterv1.ManagedNodeLabelDomain: "stamp",
"foo": "A",
},
expected: true,
},
{
name: "when Node is have unwanted managed labels they should be removed",
node: &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
clusterv1.NodeRestrictionLabelDomain: "anything",
clusterv1.ManagedNodeLabelDomain: "anything",
"foo": "A",
},
},
},
machine: &clusterv1.Machine{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
clusterv1.NodeRoleLabelDomain: "anything",
"foo": "B",
},
},
},
expectedLabels: map[string]string{
clusterv1.NodeRoleLabelDomain: "anything",
"foo": "A",
},
expected: true,
},
{
name: "when all managed labels match nothing is needed",
node: &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
clusterv1.NodeRestrictionLabelDomain: "anything",
clusterv1.ManagedNodeLabelDomain: "anything",
"foo": "A",
},
},
},
machine: &clusterv1.Machine{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
clusterv1.NodeRestrictionLabelDomain: "anything",
clusterv1.ManagedNodeLabelDomain: "anything",
"foo": "B",
},
},
},
expectedLabels: map[string]string{
clusterv1.NodeRestrictionLabelDomain: "anything",
clusterv1.ManagedNodeLabelDomain: "anything",
"foo": "A",
},
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
g := NewWithT(t)
needsUpdated := reconcileManagedLabels(tc.machine, tc.node)
g.Expect(tc.expected).To(Equal(needsUpdated))
g.Expect(tc.expectedLabels).To(BeEquivalentTo(tc.node.Labels))
})
}
}

0 comments on commit bd6a5e9

Please sign in to comment.