diff --git a/go.mod b/go.mod index fe3db92ebb..2792890305 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( k8s.io/api v0.21.0 k8s.io/apimachinery v0.21.0 k8s.io/client-go v12.0.0+incompatible + k8s.io/component-helpers v0.21.0 k8s.io/klog v1.0.0 k8s.io/klog/v2 v2.8.0 k8s.io/kubectl v0.21.0 diff --git a/go.sum b/go.sum index 0c750fdc6a..48ea819247 100644 --- a/go.sum +++ b/go.sum @@ -1444,6 +1444,7 @@ k8s.io/cluster-bootstrap v0.21.0/go.mod h1:rs7i1JpBCa56YNmnYxFJuoUghIwpMzDidY8Zm k8s.io/code-generator v0.21.0/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= k8s.io/component-base v0.21.0 h1:tLLGp4BBjQaCpS/KiuWh7m2xqvAdsxLm4ATxHSe5Zpg= k8s.io/component-base v0.21.0/go.mod h1:qvtjz6X0USWXbgmbfXR+Agik4RZ3jv2Bgr5QnZzdPYw= +k8s.io/component-helpers v0.21.0 h1:SoWLsd63LI5uwofcHVSO4jtlmZEJRycfwNBKU4eAGPQ= k8s.io/component-helpers v0.21.0/go.mod h1:tezqefP7lxfvJyR+0a+6QtVrkZ/wIkyMLK4WcQ3Cj8U= k8s.io/controller-manager v0.21.0/go.mod h1:Ohy0GRNRKPVjB8C8G+dV+4aPn26m8HYUI6ejloUBvUA= k8s.io/cri-api v0.21.0/go.mod h1:nJbXlTpXwYCYuGMR7v3PQb1Du4WOGj2I9085xMVjr3I= diff --git a/pkg/consts/namespaceMapping.go b/pkg/consts/namespaceMapping.go index 6b6482b3ae..edfbd2d625 100644 --- a/pkg/consts/namespaceMapping.go +++ b/pkg/consts/namespaceMapping.go @@ -13,4 +13,11 @@ const ( NamespaceMapControllerFinalizer = "namespacemap-controller.liqo.io/finalizer" // DocumentationURL is the URL to official Liqo Documentation. DocumentationURL = "https://doc.liqo.io/" + // DefaultNameNamespaceOffloading is the default name of NamespaceOffloading resources, every namespace that have + // to be offloaded with Liqo, must have a NamespaceOffloading resource with this name. + DefaultNameNamespaceOffloading = "offloading" + // EnablingLiqoLabel is necessary in order to allow Pods to be scheduled on remote clusters. + EnablingLiqoLabel = "liqo.io/enabled" + // EnablingLiqoLabelValue unique value allowed for EnablingLiqoLabel. + EnablingLiqoLabelValue = "true" ) diff --git a/pkg/liqo-controller-manager/namespaceOffloading-controller/doc.go b/pkg/liqo-controller-manager/namespaceOffloading-controller/doc.go new file mode 100644 index 0000000000..66a2011859 --- /dev/null +++ b/pkg/liqo-controller-manager/namespaceOffloading-controller/doc.go @@ -0,0 +1,3 @@ +// Package namespaceoffloadingctrl contains NamespaceOffloading Controller logic and some functions for adding +// DesiredMappings to NamespaceMaps Spec +package namespaceoffloadingctrl diff --git a/pkg/liqo-controller-manager/namespaceOffloading-controller/manage_desiredMappings.go b/pkg/liqo-controller-manager/namespaceOffloading-controller/manage_desiredMappings.go new file mode 100644 index 0000000000..472ce57c86 --- /dev/null +++ b/pkg/liqo-controller-manager/namespaceOffloading-controller/manage_desiredMappings.go @@ -0,0 +1,76 @@ +/* +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 namespaceoffloadingctrl + +import ( + "context" + + "k8s.io/klog" + "sigs.k8s.io/controller-runtime/pkg/client" + + mapsv1alpha1 "github.com/liqotech/liqo/apis/virtualKubelet/v1alpha1" +) + +// Removes right entry from one NamespaceMap. +func (r *NamespaceOffloadingReconciler) removeDesiredMapping(localName string, nm *mapsv1alpha1.NamespaceMap) error { + if _, ok := nm.Spec.DesiredMapping[localName]; ok { + delete(nm.Spec.DesiredMapping, localName) + if err := r.Update(context.TODO(), nm); err != nil { + klog.Errorf("%s --> Unable to update NamespaceMap '%s'", err, nm.GetName()) + return err + } + klog.Infof(" Entries deleted correctly from '%s'", nm.GetName()) + } + return nil +} + +// Removes right entries from more than one NamespaceMap (it depends on len(nms)). +func (r *NamespaceOffloadingReconciler) removeDesiredMappings(localName string, nms map[string]*mapsv1alpha1.NamespaceMap) error { + for _, nm := range nms { + if err := r.removeDesiredMapping(localName, nm); err != nil { + return err + } + } + return nil +} + +// Adds right entry on one NamespaceMap, if it isn't already there. +func (r *NamespaceOffloadingReconciler) addDesiredMapping(localName, remoteName string, + nm *mapsv1alpha1.NamespaceMap) error { + if nm.Spec.DesiredMapping == nil { + nm.Spec.DesiredMapping = map[string]string{} + } + + if _, ok := nm.Spec.DesiredMapping[localName]; !ok { + nm.Spec.DesiredMapping[localName] = remoteName + if err := r.Patch(context.TODO(), nm, client.Merge); err != nil { + klog.Errorf("%s --> Unable to add entry for namespace '%s' on NamespaceMap '%s'", + err, localName, nm.GetName()) + return err + } + klog.Infof("Entry for namespace '%s' successfully added on NamespaceMap '%s' ", + localName, nm.GetName()) + } + return nil +} + +// Adds right entries on more than one NamespaceMap (it depends on len(nms)), if entries aren't already there. +func (r *NamespaceOffloadingReconciler) addDesiredMappings(localName, remoteName string, + nms map[string]*mapsv1alpha1.NamespaceMap) error { + for _, nm := range nms { + if err := r.addDesiredMapping(localName, remoteName, nm); err != nil { + return err + } + } + return nil +} diff --git a/pkg/liqo-controller-manager/namespaceOffloading-controller/namespaceOffloading_controller.go b/pkg/liqo-controller-manager/namespaceOffloading-controller/namespaceOffloading_controller.go new file mode 100644 index 0000000000..57c8fd499b --- /dev/null +++ b/pkg/liqo-controller-manager/namespaceOffloading-controller/namespaceOffloading_controller.go @@ -0,0 +1,256 @@ +/* +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 namespaceoffloadingctrl + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + k8shelper "k8s.io/component-helpers/scheduling/corev1" + "k8s.io/klog" + "k8s.io/kubernetes/pkg/util/slice" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlutils "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + offv1alpha1 "github.com/liqotech/liqo/apis/offloading/v1alpha1" + mapsv1alpha1 "github.com/liqotech/liqo/apis/virtualKubelet/v1alpha1" + liqoconst "github.com/liqotech/liqo/pkg/consts" + liqoutils "github.com/liqotech/liqo/pkg/utils" +) + +// NamespaceOffloadingReconciler changes DesiredMapping field of NamespaceMaps if a remote Namespace must be +// created or deleted. +type NamespaceOffloadingReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +const ( + namespaceOffloadingControllerFinalizer = "namespaceoffloading-controller.liqo.io/finalizer" +) + +// Reconcile requires creation of remote namespaces according to ClusterSelector field. +func (r *NamespaceOffloadingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + namespaceOffloading := &offv1alpha1.NamespaceOffloading{} + if err := r.Get(context.TODO(), types.NamespacedName{ + Namespace: req.Namespace, + Name: liqoconst.DefaultNameNamespaceOffloading, + }, namespaceOffloading); err != nil { + klog.Errorf("%s --> Unable to get namespaceOffloading for the namespace '%s'", err, req.Namespace) + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // get all NamespaceMaps in the clsuter + clusterNamespaceMaps := &mapsv1alpha1.NamespaceMapList{} + if err := r.List(context.TODO(), clusterNamespaceMaps); err != nil { + klog.Error(err, " --> Unable to List NamespaceMaps") + return ctrl.Result{}, err + } + + if len(clusterNamespaceMaps.Items) == 0 { + err := fmt.Errorf("no NamespaceMaps at the moment in the cluster") + klog.Info(err) + return ctrl.Result{}, err + } + + clusterWithNoOffloading := make(map[string]*mapsv1alpha1.NamespaceMap) + for i := range clusterNamespaceMaps.Items { + clusterWithNoOffloading[clusterNamespaceMaps.Items[i].Labels[liqoconst.RemoteClusterID]] = &clusterNamespaceMaps.Items[i] + } + + // get Namespace associated with this NamespaceOffloading Resource + namespace := &corev1.Namespace{} + if err := r.Get(context.TODO(), types.NamespacedName{ + Name: namespaceOffloading.Namespace, + }, namespace); err != nil { + klog.Errorf("%s --> Unable to get the namespace '%s'", err, namespaceOffloading.Namespace) + return ctrl.Result{}, err + } + + // Remove DesiredMappings, namespaceOffloadingControllerFinalizer, and Liqo label from associated Namespace + if !namespaceOffloading.GetDeletionTimestamp().IsZero() { + // remove DesiredMappings, namespaceOffloadingControllerFinalizer + klog.Infof("The namespaceOffloading of namespace '%s' is requested to be deleted", + namespaceOffloading.Namespace) + if err := r.removeDesiredMappings(namespaceOffloading.Namespace, clusterWithNoOffloading); err != nil { + return ctrl.Result{}, err + } + + // remove Liqo label from the associated Namespace + delete(namespace.Labels, liqoconst.EnablingLiqoLabel) + if err := r.Update(context.TODO(), namespace); err != nil { + klog.Errorf(" %s --> Unable to remove Liqo label from the namespace '%s'", err, namespace.GetName()) + return ctrl.Result{}, err + } + + ctrlutils.RemoveFinalizer(namespaceOffloading, namespaceOffloadingControllerFinalizer) + if err := r.Update(context.TODO(), namespaceOffloading); err != nil { + klog.Errorf("%s --> Unable to remove finalizer from namespaceOffloading", err) + return ctrl.Result{}, err + } + klog.Info("Finalizer removed from namespaceOffloading") + return ctrl.Result{}, nil + } + + // If there is no namespaceOffloadingControllerFinalizer, we have to perform some initialization + if !ctrlutils.ContainsFinalizer(namespaceOffloading, namespaceOffloadingControllerFinalizer) { + // 1 - Add namespaceOffloadingControllerFinalizer + ctrlutils.AddFinalizer(namespaceOffloading, namespaceOffloadingControllerFinalizer) + // 2 - Add empty cluster selector if not specified by the user + if namespaceOffloading.Spec.ClusterSelector.Size() == 0 { + namespaceOffloading.Spec.ClusterSelector = corev1.NodeSelector{NodeSelectorTerms: []corev1.NodeSelectorTerm{}} + } + // 3 - Add remoteNamespaceName + switch { + case namespaceOffloading.Spec.NamespaceMappingStrategy == offv1alpha1.EnforceSameNameMappingStrategyType: + namespaceOffloading.Status.RemoteNamespaceName = namespaceOffloading.Namespace + default: + clusterID, err := liqoutils.GetClusterID(r.Client) + if err != nil { + return ctrl.Result{}, err + } + namespaceOffloading.Status.RemoteNamespaceName = fmt.Sprintf("%s-%s", namespaceOffloading.Namespace, clusterID) + } + // 4 - Set global OffloadingPhase=InProgress + namespaceOffloading.Status.OffloadingPhase = offv1alpha1.InProgressOffloadingPhaseType + + if err := r.Update(ctx, namespaceOffloading); err != nil { + klog.Errorf("%s --> Unable to update NamespaceOffloading in namespace '%s'", + err, namespaceOffloading.Namespace) + return ctrl.Result{}, err + } + } + + // Add Liqo label to associated Namespace, if it is not already there + if value, ok := namespace.Labels[liqoconst.EnablingLiqoLabel]; !ok || value != liqoconst.EnablingLiqoLabelValue { + if namespace.Labels == nil { + namespace.Labels = map[string]string{} + } + namespace.Labels[liqoconst.EnablingLiqoLabel] = liqoconst.EnablingLiqoLabelValue + if err := r.Patch(context.TODO(), namespace, client.Merge); err != nil { + klog.Errorf(" %s --> Unable to add liqo-label on namespace '%s'", err, namespace.GetName()) + return ctrl.Result{}, err + } + } + + // Add dediredMappings to NamespaceMaps + if namespaceOffloading.Spec.ClusterSelector.Size() == 0 { + klog.Infof(" Offload namespace '%s' on all remote clusters", namespaceOffloading.Namespace) + if err := r.addDesiredMappings(namespaceOffloading.Namespace, + namespaceOffloading.Status.RemoteNamespaceName, + clusterWithNoOffloading); err != nil { + return ctrl.Result{}, err + } + clusterWithNoOffloading = nil + } else { + virtualNodes := &corev1.NodeList{} + if err := r.List(context.TODO(), virtualNodes, + client.MatchingLabels{liqoconst.TypeLabel: liqoconst.TypeNode}); err != nil { + klog.Error(err, " --> Unable to List all virtual nodes") + return ctrl.Result{}, err + } + + if len(virtualNodes.Items) == 0 { + err := fmt.Errorf(" No VirtualNodes at the moment in the cluster") + klog.Info(err) + return ctrl.Result{}, err + } + + for i := range virtualNodes.Items { + match, err := k8shelper.MatchNodeSelectorTerms(&virtualNodes.Items[i], + &namespaceOffloading.Spec.ClusterSelector) + if err != nil { + klog.Infof("%s -> Unable to offload the namespace '%s'", err, namespaceOffloading.Namespace) + if namespaceOffloading.Annotations == nil { + namespaceOffloading.Annotations = map[string]string{} + } + namespaceOffloading.Annotations[liqoconst.EnablingLiqoLabel] = fmt.Sprintf("Invalid Cluster Selector : %s", err) + break + } + + if match { + if err = r.addDesiredMapping(namespaceOffloading.Namespace, + namespaceOffloading.Status.RemoteNamespaceName, + clusterWithNoOffloading[virtualNodes.Items[i].Annotations[liqoconst.RemoteClusterID]]); err != nil { + return ctrl.Result{}, err + } + delete(clusterWithNoOffloading, virtualNodes.Items[i].Annotations[liqoconst.RemoteClusterID]) + klog.Infof(" Offload namespace '%s' on remote cluster: %s", namespaceOffloading.Namespace, + virtualNodes.Items[i].Annotations[liqoconst.RemoteClusterID]) + } + } + } + + // Namespace not offloaded will have only the condition OffloadingRequired="false" + setOffloadingRequiredCondition(namespaceOffloading, clusterWithNoOffloading) + + if len(clusterWithNoOffloading) == len(clusterNamespaceMaps.Items) { + namespaceOffloading.Status.OffloadingPhase = offv1alpha1.NoClusterSelectedOffloadingPhaseType + } + + if err := r.Update(ctx, namespaceOffloading); err != nil { + klog.Errorf("%s --> Unable to update NamespaceOffloading in namespace '%s'", + err, namespaceOffloading.Namespace) + return ctrl.Result{}, err + } + return ctrl.Result{}, nil +} + +func setOffloadingRequiredCondition(nsof *offv1alpha1.NamespaceOffloading, + clusterWithNoOff map[string]*mapsv1alpha1.NamespaceMap) { + for clusterID := range clusterWithNoOff { + liqoutils.UpdateNamespaceOffloadingCondition(&nsof.Status, &offv1alpha1.RemoteNamespaceCondition{ + Type: offv1alpha1.NamespaceOffloadingRequired, + Status: corev1.ConditionFalse, + Reason: "ClusterNotSelected", + Message: "You have not selected this cluster through ClusterSelector fields", + }, clusterID) + } +} + +// Events not filtered: +// 1 -- creation of a NamespaceOffloading resource with the right default name. +// 2 -- deletion of a NamespaceOffloading resource with the right finalizer. +func namespaceOffloadingPredicate() predicate.Predicate { + return predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + if !(e.ObjectNew.GetDeletionTimestamp().IsZero()) && slice.ContainsString(e.ObjectNew.GetFinalizers(), + namespaceOffloadingControllerFinalizer, nil) { + return true + } + return false + }, + CreateFunc: func(e event.CreateEvent) bool { + return e.Object.GetName() == liqoconst.DefaultNameNamespaceOffloading + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return false + }, + } +} + +// SetupWithManager reconciles NamespaceOffloading Resources. +func (r *NamespaceOffloadingReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&offv1alpha1.NamespaceOffloading{}). + WithEventFilter(namespaceOffloadingPredicate()). + Complete(r) +}