package nodepatcher import ( "context" "fmt" "slices" "strings" "maps" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "k8s.io/apimachinery/pkg/util/validation" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/edge/capabilities" "edge-infra.dev/pkg/k8s/runtime/controller/reconcile" "edge-infra.dev/pkg/sds/controlplaneguardian/identifier" v1ien "edge-infra.dev/pkg/sds/ien/k8s/apis/v1" "edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/config" nodemeta "edge-infra.dev/pkg/sds/ien/node" ) type NodePatcher struct{} // Updates the host node with labels and ownerReferences generated from the IEN Spec func (nodePatcherPlugin NodePatcher) Reconcile(ctx context.Context, ienode *v1ien.IENode, conf config.Config) (reconcile.Result, error) { node, err := conf.GetHostNode(ctx) if err != nil { return reconcile.ResultRequeue, err } idf, err := identifier.FromClient(ctx, conf.GetClient()) if client.IgnoreNotFound(err) != nil { return reconcile.ResultRequeue, err } nodeUpdated := node.DeepCopy() if err := setNodeLabels(nodeUpdated, ienode, idf); err != nil { return reconcile.ResultRequeue, err } setNodeOwnerReferences(nodeUpdated, ienode, idf) if err := conf.GetClient().Patch(ctx, nodeUpdated, client.StrategicMergeFrom(node)); err != nil { return reconcile.ResultRequeue, err } return reconcile.ResultSuccess, nil } // Sets the labels on the hostsystem node from the IEN Spec func setNodeLabels(node *corev1.Node, ienode *v1ien.IENode, idf *identifier.ControlPlaneIdentifier) error { labels := node.GetLabels() if err := validateCustomNodeLabels(ienode); err != nil { return err } // delete custom and edge capability labels so only the current custom labels on the ienode cr // are applied to the node https://github.com/ncr-swt-retail/edge-roadmap/issues/9425 maps.DeleteFunc(labels, func(key string, _ string) bool { edgeCap := slices.ContainsFunc(capabilities.EdgeAutomatedCapabilityLabels, func(edgeLabel *model.LabelInput) bool { return edgeLabel != nil && edgeLabel.Key == key }) return strings.HasPrefix(key, v1ien.CustomNodePrefix) || edgeCap }) maps.Copy(labels, ienode.CustomLabels()) addNodeLabels(ienode, labels, idf) node.SetLabels(labels) return nil } // Validate the custom labels on the hostsystem node from the IEN Spec func validateCustomNodeLabels(ienode *v1ien.IENode) error { customLabels := ienode.CustomLabels() for key, value := range customLabels { keyErrs := validation.IsQualifiedName(key) if len(keyErrs) > 0 { return fmt.Errorf("Invalid label key %s: %s", key, strings.Join(keyErrs, "; ")) } valueErrs := validation.IsValidLabelValue(value) if len(valueErrs) > 0 { return fmt.Errorf("Invalid label value %s: %s", value, strings.Join(valueErrs, "; ")) } } return nil } // Creates the node labels from the IEN spec to apply to the host node func addNodeLabels(ienode *v1ien.IENode, labels map[string]string, idf *identifier.ControlPlaneIdentifier) { if ienode.GetName() != "" { labels[nodemeta.HostnameLabel] = ienode.GetName() } if string(ienode.Spec.Class) != "" { labels[nodemeta.ClassLabel] = string(ienode.Spec.Class) } if ienode.Spec.Lane != "" { labels[nodemeta.LaneLabel] = ienode.Spec.Lane } role := getRole(ienode, idf) if role != "" { labels[nodemeta.RoleLabel] = role } // ensures the control plane nodes are marked if role == string(v1ien.ControlPlane) { labels[nodemeta.ControlPlaneLabel] = "" delete(labels, nodemeta.WorkerClassLabel) } if role == string(v1ien.Worker) { labels[nodemeta.WorkerClassLabel] = string(ienode.Spec.Class) delete(labels, nodemeta.ControlPlaneLabel) } } // getRole returns the role of the IENode based on the identifier. If the // identifier is nil, the role is taken from the IENode spec. func getRole(ienode *v1ien.IENode, idf *identifier.ControlPlaneIdentifier) string { if idf == nil || idf.ControlPlane == "" { return string(ienode.Spec.Role) } if idf.ControlPlane == ienode.GetName() { return string(v1ien.ControlPlane) } return string(v1ien.Worker) } // Sets the ownerRefernces on k8s node to be owned by the IENode document. This only applies to worker nodes. func setNodeOwnerReferences(node *corev1.Node, ienode *v1ien.IENode, idf *identifier.ControlPlaneIdentifier) { if getRole(ienode, idf) == string(v1ien.Worker) { node.SetOwnerReferences([]metav1.OwnerReference{createOwnerReferenceToIEN(ienode)}) return } // remove old owner references on existing control plane nodes ownerReferences := []metav1.OwnerReference{} for _, or := range node.ObjectMeta.OwnerReferences { if or.Kind == v1ien.IENodeGVK.Kind && or.Name == ienode.Name { continue } ownerReferences = append(ownerReferences, or) } node.OwnerReferences = ownerReferences } func createOwnerReferenceToIEN(ienode *v1ien.IENode) metav1.OwnerReference { return metav1.OwnerReference{ APIVersion: ienode.APIVersion, Kind: ienode.Kind, Name: ienode.GetName(), UID: ienode.GetUID(), } }