diff --git a/api/v1alpha3/common_types.go b/api/v1alpha3/common_types.go index 96c128d6d821..d72d9ef5ec78 100644 --- a/api/v1alpha3/common_types.go +++ b/api/v1alpha3/common_types.go @@ -31,6 +31,21 @@ const ( // tool uses this label for implementing provider's lifecycle operations. ProviderLabelName = "cluster.x-k8s.io/provider" + // ClusterNameAnnotation is the annotation set on nodes identifying the name of the cluster the node belongs to. + ClusterNameAnnotation = "cluster.x-k8s.io/cluster-name" + + // ClusterNamespaceAnnotation is the annotation set on nodes identifying the namespace of the cluster the node belongs to. + ClusterNamespaceAnnotation = "cluster.x-k8s.io/cluster-namespace" + + // MachineAnnotation is the annotation set on nodes identifying the machine the node belongs to. + MachineAnnotation = "cluster.x-k8s.io/machine" + + // OwnerKindAnnotation is the annotation set on nodes identifying the owner kind. + OwnerKindAnnotation = "cluster.x-k8s.io/owner-kind" + + // OwnerNameAnnotation is the annotation set on nodes identifying the owner name. + OwnerNameAnnotation = "cluster.x-k8s.io/owner-name" + // PausedAnnotation is an annotation that can be applied to any Cluster API // object to prevent a controller from processing a resource. // diff --git a/controllers/machine_controller_noderef.go b/controllers/machine_controller_noderef.go index 82587540cb7a..17bcf401f20a 100644 --- a/controllers/machine_controller_noderef.go +++ b/controllers/machine_controller_noderef.go @@ -19,6 +19,9 @@ package controllers import ( "context" "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/cluster-api/util/annotations" + "sigs.k8s.io/cluster-api/util/patch" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -84,6 +87,27 @@ func (r *MachineReconciler) reconcileNode(ctx context.Context, cluster *clusterv r.recorder.Event(machine, corev1.EventTypeNormal, "SuccessfulSetNodeRef", machine.Status.NodeRef.Name) } + // Reconcile node annotations. + patchHelper, err := patch.NewHelper(node, remoteClient) + if err != nil { + return ctrl.Result{}, err + } + desired := map[string]string{ + clusterv1.ClusterNameAnnotation: machine.Spec.ClusterName, + clusterv1.ClusterNamespaceAnnotation: machine.GetNamespace(), + clusterv1.MachineAnnotation: machine.Name, + } + if owner := metav1.GetControllerOfNoCopy(machine); owner != nil { + desired[clusterv1.OwnerKindAnnotation] = owner.Kind + desired[clusterv1.OwnerNameAnnotation] = owner.Name + } + if annotations.AddAnnotations(node, desired) { + if err := patchHelper.Patch(ctx, node); err != nil { + logger.V(2).Info("Failed patch node to set annotations", "err", err, "node name", node.Name) + return ctrl.Result{}, err + } + } + // 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 { diff --git a/exp/controllers/machinepool_controller_noderef.go b/exp/controllers/machinepool_controller_noderef.go index df28952bc849..d34680fffa9e 100644 --- a/exp/controllers/machinepool_controller_noderef.go +++ b/exp/controllers/machinepool_controller_noderef.go @@ -19,6 +19,8 @@ package controllers import ( "context" "fmt" + "sigs.k8s.io/cluster-api/util/annotations" + "sigs.k8s.io/cluster-api/util/patch" "time" ctrl "sigs.k8s.io/controller-runtime" @@ -100,6 +102,31 @@ func (r *MachinePoolReconciler) reconcileNodeRefs(ctx context.Context, cluster * logger.Info("Set MachinePools's NodeRefs", "noderefs", mp.Status.NodeRefs) r.recorder.Event(mp, apicorev1.EventTypeNormal, "SuccessfulSetNodeRefs", fmt.Sprintf("%+v", mp.Status.NodeRefs)) + // Reconcile node annotations. + for _, nodeRef := range nodeRefsResult.references { + node := &corev1.Node{} + if err := clusterClient.Get(ctx, client.ObjectKey{Name: nodeRef.Name}, node); err != nil { + logger.V(2).Info("Failed to get Node, skipping setting annotations", "err", err, "nodeRef.Name", nodeRef.Name) + continue + } + patchHelper, err := patch.NewHelper(node, clusterClient) + if err != nil { + return ctrl.Result{}, err + } + desired := map[string]string{ + clusterv1.ClusterNameAnnotation: mp.Spec.ClusterName, + clusterv1.ClusterNamespaceAnnotation: mp.GetNamespace(), + clusterv1.OwnerKindAnnotation: mp.Kind, + clusterv1.OwnerNameAnnotation: mp.Name, + } + if annotations.AddAnnotations(node, desired) { + if err := patchHelper.Patch(ctx, node); err != nil { + logger.V(2).Info("Failed patch node to set annotations", "err", err, "node name", node.Name) + return ctrl.Result{}, err + } + } + } + if mp.Status.Replicas != mp.Status.ReadyReplicas || len(nodeRefsResult.references) != int(mp.Status.ReadyReplicas) { r.Log.Info("NodeRefs != ReadyReplicas", "NodeRefs", len(nodeRefsResult.references), "ReadyReplicas", mp.Status.ReadyReplicas) conditions.MarkFalse(mp, expv1.ReplicasReadyCondition, expv1.WaitingForReplicasReadyReason, clusterv1.ConditionSeverityInfo, "") diff --git a/util/annotations/helpers_test.go b/util/annotations/helpers_test.go new file mode 100644 index 000000000000..533d1b282ac6 --- /dev/null +++ b/util/annotations/helpers_test.go @@ -0,0 +1,122 @@ +/* +Copyright 2020 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 annotations + +import ( + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" +) + +func TestAddAnnotations(t *testing.T) { + g := NewWithT(t) + + var testcases = []struct { + name string + obj metav1.Object + input map[string]string + expected map[string]string + changed bool + }{ + { + name: "should return false if no changes are made", + obj: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "foo": "bar", + }, + }, + Spec: corev1.NodeSpec{}, + Status: corev1.NodeStatus{}, + }, + input: map[string]string{ + "foo": "bar", + }, + expected: map[string]string{ + "foo": "bar", + }, + changed: false, + }, + { + name: "should do nothing if no annotations are provided", + obj: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "foo": "bar", + }, + }, + Spec: corev1.NodeSpec{}, + Status: corev1.NodeStatus{}, + }, + input: map[string]string{}, + expected: map[string]string{ + "foo": "bar", + }, + changed: false, + }, + { + name: "should return true if annotations are added", + obj: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "foo": "bar", + }, + }, + Spec: corev1.NodeSpec{}, + Status: corev1.NodeStatus{}, + }, + input: map[string]string{ + "thing1": "thing2", + "buzz": "blah", + }, + expected: map[string]string{ + "foo": "bar", + "thing1": "thing2", + "buzz": "blah", + }, + changed: true, + }, + { + name: "should return true if annotations are changed", + obj: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "foo": "bar", + }, + }, + Spec: corev1.NodeSpec{}, + Status: corev1.NodeStatus{}, + }, + input: map[string]string{ + "foo": "buzz", + }, + expected: map[string]string{ + "foo": "buzz", + }, + changed: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + res := AddAnnotations(tc.obj, tc.input) + g.Expect(res).To(Equal(tc.changed)) + g.Expect(tc.obj.GetAnnotations()).To(Equal(tc.expected)) + }) + } +}