Skip to content

Commit

Permalink
New NamespaceOffloading Controller
Browse files Browse the repository at this point in the history
This controller gets a just created NamespaceOffloading resource and adds entries to NamespaceMaps (Spec->DesiredMappings) in order to require the creation of remote namespaces, based on requirements specified by users.
When the NamespaceOffloading resource is deleted also the remote namespaces are requested to be deleted, so the entries in NamespaceMaps are removed.
  • Loading branch information
Andreagit97 committed May 24, 2021
1 parent 0c73e78 commit 20080ed
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
7 changes: 7 additions & 0 deletions pkg/consts/namespaceMapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package namespaceoffloadingctrl contains NamespaceOffloading Controller logic and some functions for adding
// DesiredMappings to NamespaceMaps Spec
package namespaceoffloadingctrl
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 20080ed

Please sign in to comment.