diff --git a/apis/virtualKubelet/v1alpha1/namespacemap_types.go b/apis/virtualKubelet/v1alpha1/namespacemap_types.go index 18502e1b35..5afe923839 100644 --- a/apis/virtualKubelet/v1alpha1/namespacemap_types.go +++ b/apis/virtualKubelet/v1alpha1/namespacemap_types.go @@ -1,12 +1,8 @@ /* - - 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. diff --git a/pkg/liqo-controller-manager/offloadingStatus-controller/doc.go b/pkg/liqo-controller-manager/offloadingStatus-controller/doc.go new file mode 100644 index 0000000000..2a599f66b8 --- /dev/null +++ b/pkg/liqo-controller-manager/offloadingStatus-controller/doc.go @@ -0,0 +1,2 @@ +// Package offloadingstatuscontroller contains OffloadingStatus Controller logic. +package offloadingstatuscontroller diff --git a/pkg/liqo-controller-manager/offloadingStatus-controller/namespaceoffloading_status_management.go b/pkg/liqo-controller-manager/offloadingStatus-controller/namespaceoffloading_status_management.go new file mode 100644 index 0000000000..8a2f5e3c51 --- /dev/null +++ b/pkg/liqo-controller-manager/offloadingStatus-controller/namespaceoffloading_status_management.go @@ -0,0 +1,131 @@ +package offloadingstatuscontroller + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/klog" + + 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" +) + +// mapPhaseToRemoteNamespaceCondition selects the right remote condition according to the remote namespace +// phase obtained by means of NamespaceMap.Status.CurrentMapping. +func mapPhaseToRemoteNamespaceCondition(phase mapsv1alpha1.MappingPhase) offv1alpha1.RemoteNamespaceCondition { + var remoteCondition offv1alpha1.RemoteNamespaceCondition + switch { + case phase == mapsv1alpha1.MappingAccepted: + remoteCondition = offv1alpha1.RemoteNamespaceCondition{ + Type: offv1alpha1.NamespaceReady, + Status: corev1.ConditionTrue, + Reason: "RemoteNamespaceCreated", + Message: "Namespace correctly offloaded on this cluster", + } + case phase == mapsv1alpha1.MappingCreationLoopBackOff: + remoteCondition = offv1alpha1.RemoteNamespaceCondition{ + Type: offv1alpha1.NamespaceReady, + Status: corev1.ConditionFalse, + Reason: "CreationLoopBackOff", + Message: "Some problems occurred during remote Namespace creation", + } + case phase == mapsv1alpha1.MappingTerminating: + remoteCondition = offv1alpha1.RemoteNamespaceCondition{ + Type: offv1alpha1.NamespaceReady, + Status: corev1.ConditionFalse, + Reason: "TerminatingNamespace", + Message: "The remote Namespace is requested to be deleted", + } + // If phase is not specified. + default: + remoteCondition = offv1alpha1.RemoteNamespaceCondition{ + Type: offv1alpha1.NamespaceOffloadingRequired, + Status: corev1.ConditionFalse, + Reason: "ClusterNotSelected", + Message: "You have not selected this cluster through ClusterSelector fields", + } + } + return remoteCondition +} + +// assignClusterRemoteCondition sets the right remote namespace condition according to the remote namespace phase +// written in NamespaceMap.Status.CurrentMapping. +// If phase==nil the remote namespace condition OffloadingRequired=False is set. +func assignClusterRemoteCondition(noff *offv1alpha1.NamespaceOffloading, phase mapsv1alpha1.MappingPhase, clusterID string) { + if noff.Status.RemoteNamespacesConditions == nil { + noff.Status.RemoteNamespacesConditions = map[string]offv1alpha1.RemoteNamespaceConditions{} + } + + newCondition := mapPhaseToRemoteNamespaceCondition(phase) + // if the condition is already there, do nothing + if liqoutils.IsStatusConditionPresentAndEqual(noff.Status.RemoteNamespacesConditions[clusterID], newCondition.Type, newCondition.Status) { + return + } + var remoteConditions []offv1alpha1.RemoteNamespaceCondition + liqoutils.AddRemoteNamespaceCondition(&remoteConditions, &newCondition) + noff.Status.RemoteNamespacesConditions[clusterID] = remoteConditions + klog.Infof("Remote condition of type '%s' with Status '%s' for the remote namespace '%s' associated with the cluster '%s'", + remoteConditions[0].Type, remoteConditions[0].Status, noff.Namespace, clusterID) +} + +// todo: at the moment the global status InProgress is not implemented, at every reconcile the controller sets a global +// OffloadingStatus that reflects the current Status of NamespaceMaps +// If the NamespaceMap has a remote Status for that remote Namespace, the right remote condition is set according to the +// remote Namespace Phase. If there is no remote Status in the NamespaceMap, the OffloadingRequired=false condition is set +// this condition could be only transient until the NamespaceMap Status is updated or permanent if the local Namespace +// is not requested to be offloaded inside this cluster. +func setRemoteConditionsForEveryCluster(noff *offv1alpha1.NamespaceOffloading, nml *mapsv1alpha1.NamespaceMapList) { + for i := range nml.Items { + if remoteNamespaceStatus, ok := nml.Items[i].Status.CurrentMapping[noff.Namespace]; ok { + assignClusterRemoteCondition(noff, remoteNamespaceStatus.Phase, nml.Items[i].Labels[liqoconst.RemoteClusterID]) + continue + } + // Two cases in which there are no entry in NamespaceMap Status: + // - when the local namespace is not offloaded inside this cluster. + // - when the remote namespace previously created has been correctly removed from this cluster. + // In these cases the remote condition will be "OffloadingRequired=false" + assignClusterRemoteCondition(noff, "", nml.Items[i].Labels[liqoconst.RemoteClusterID]) + } +} + +// setNamespaceOffloadingStatus sets global offloading status according to the remote namespace conditions. +func setNamespaceOffloadingStatus(noff *offv1alpha1.NamespaceOffloading) { + ready := 0 + notReady := 0 + + for i := range noff.Status.RemoteNamespacesConditions { + condition := liqoutils.FindRemoteNamespaceCondition(noff.Status.RemoteNamespacesConditions[i], offv1alpha1.NamespaceReady) + if condition == nil { + continue + } + if condition.Status == corev1.ConditionTrue { + ready++ + } else { + notReady++ + } + } + + switch { + case !noff.DeletionTimestamp.IsZero(): + noff.Status.OffloadingPhase = offv1alpha1.TerminatingOffloadingPhaseType + if ready+notReady == 0 { + // The NamespaceOffloading is deleted only when there are no more remoteNamespaceCondition and + // the deletion timestamp is set. + for key := range noff.Status.RemoteNamespacesConditions { + delete(noff.Status.RemoteNamespacesConditions, key) + } + klog.Infof("NamespaceOffloading, in the namespace '%s', ready to be deleted", noff.Namespace) + } + case ready+notReady == 0: + noff.Status.OffloadingPhase = offv1alpha1.NoClusterSelectedOffloadingPhaseType + case ready == 0: + noff.Status.OffloadingPhase = offv1alpha1.AllFailedOffloadingPhaseType + case notReady == 0: + noff.Status.OffloadingPhase = offv1alpha1.ReadyOffloadingPhaseType + default: + noff.Status.OffloadingPhase = offv1alpha1.SomeFailedOffloadingPhaseType + } + + klog.Infof("The OffloadingStatus for NamespaceOffloading in the namespace '%s' is set to '%s'", + noff.Namespace, noff.Status.OffloadingPhase) +} diff --git a/pkg/liqo-controller-manager/offloadingStatus-controller/offloadingstatus_controller.go b/pkg/liqo-controller-manager/offloadingStatus-controller/offloadingstatus_controller.go new file mode 100644 index 0000000000..605aa28964 --- /dev/null +++ b/pkg/liqo-controller-manager/offloadingStatus-controller/offloadingstatus_controller.go @@ -0,0 +1,107 @@ +/* +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 offloadingstatuscontroller + +import ( + "context" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "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" +) + +// OffloadingStatusReconciler checks the status of all remote namespaces associated with this +// namespaceOffloading Resource, and sets the global offloading status in according to the feedbacks received +// from all remote clusters. +type OffloadingStatusReconciler struct { + client.Client + Scheme *runtime.Scheme + RequeueTime time.Duration +} + +// Controller Ownership: +// --> NamespaceOffloading.Status.RemoteConditions +// --> NamespaceOffloading.Status.OffloadingPhase + +// Reconcile sets the NamespaceOffloading Status checking the actual status of all remote Namespace. +// This reconcile is performed every RequeueTime. +func (r *OffloadingStatusReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + namespaceOffloading := &offv1alpha1.NamespaceOffloading{} + if err := r.Get(ctx, types.NamespacedName{ + Namespace: req.Namespace, + Name: liqoconst.DefaultNamespaceOffloadingName, + }, namespaceOffloading); err != nil { + if apierrors.IsNotFound(err) { + klog.Infof("There is no NamespaceOffloading resource in Namespace '%s'", req.Namespace) + return ctrl.Result{}, nil + } + klog.Errorf("%s --> Unable to get namespaceOffloading for the namespace '%s'", err, req.Namespace) + return ctrl.Result{}, err + } + + // Get all NamespaceMaps in the cluster + namespaceMapList := &mapsv1alpha1.NamespaceMapList{} + if err := r.List(ctx, namespaceMapList); err != nil { + klog.Errorf("%s -> unable to get NamespaceMaps", err) + return ctrl.Result{}, err + } + + if len(namespaceMapList.Items) == 0 { + klog.Info("No NamespaceMaps in the cluster at the moment") + return ctrl.Result{RequeueAfter: r.RequeueTime}, nil + } + + original := namespaceOffloading.DeepCopy() + + setRemoteConditionsForEveryCluster(namespaceOffloading, namespaceMapList) + + setNamespaceOffloadingStatus(namespaceOffloading) + + // Patch the status just one time at the end of the logic. + if err := r.Patch(ctx, namespaceOffloading, client.MergeFrom(original)); err != nil { + klog.Errorf("%s --> Unable to Patch NamespaceOffloading in the namespace '%s'", + err, namespaceOffloading.Namespace) + return ctrl.Result{}, err + } + + return ctrl.Result{RequeueAfter: r.RequeueTime}, nil +} + +// checkNamespaceOffloadingStatus calls Reconcile only when a new NamespaceOffloading is created. +func checkNamespaceOffloadingStatus() predicate.Predicate { + return predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + return false + }, + } +} + +// SetupWithManager reconciles when a new NamespaceOffloading is created. +func (r *OffloadingStatusReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&offv1alpha1.NamespaceOffloading{}). + WithEventFilter(checkNamespaceOffloadingStatus()). + Complete(r) +} diff --git a/pkg/liqo-controller-manager/offloadingStatus-controller/offloadingstatus_controller_test.go b/pkg/liqo-controller-manager/offloadingStatus-controller/offloadingstatus_controller_test.go new file mode 100644 index 0000000000..c69547f122 --- /dev/null +++ b/pkg/liqo-controller-manager/offloadingStatus-controller/offloadingstatus_controller_test.go @@ -0,0 +1,403 @@ +/* +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 offloadingstatuscontroller + +import ( + "context" + "fmt" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlutils "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + offv1alpha1 "github.com/liqotech/liqo/apis/offloading/v1alpha1" + mapsv1alpha1 "github.com/liqotech/liqo/apis/virtualKubelet/v1alpha1" + liqoconst "github.com/liqotech/liqo/pkg/consts" +) + +var _ = Describe("Namespace controller", func() { + + const ( + timeout = time.Second * 20 + interval = time.Millisecond * 500 + testFinalizer = "test-finalizer" + ) + + BeforeEach(func() { + + By(" 0 - BEFORE_EACH -> Clean NamespaceMap CurrentMapping") + + // 0.1 - Clean namespaceMaps CurrentMapping + Eventually(func() bool { + if err := homeClient.List(context.TODO(), nms); err != nil { + return false + } + Expect(len(nms.Items) == mapNumber).To(BeTrue()) + for i, _ := range nms.Items { + nms.Items[i].Status.CurrentMapping = map[string]mapsv1alpha1.RemoteNamespaceStatus{} + if err := homeClient.Update(context.TODO(), nms.Items[i].DeepCopy()); err != nil { + return false + } + } + return true + }, timeout, interval).Should(BeTrue()) + + // 0.2 - Check that they are cleaned + Eventually(func() bool { + if err := homeClient.List(context.TODO(), nms); err != nil { + return false + } + Expect(len(nms.Items) == mapNumber).To(BeTrue()) + for i, _ := range nms.Items { + if len(nms.Items[i].Status.CurrentMapping) != 0 { + return false + } + } + return true + }, timeout, interval).Should(BeTrue()) + + }) + + // Todo: this implementation is without InProgress Status + Context("Check RemoteNamespaceConditions and Status of NamespaceOffloading1", func() { + + It(" TEST 1: check NoClusterSelected status, when NamespaceMap Status is empty", func() { + + By(" 1 - Checking status of NamespaceOffloading ") + Eventually(func() bool { + if err := homeClient.Get(context.TODO(), types.NamespacedName{ + Name: liqoconst.DefaultNamespaceOffloadingName, + Namespace: namespace1Name}, namespaceOffloading1); err != nil { + return false + } + if namespaceOffloading1.Status.OffloadingPhase != offv1alpha1.NoClusterSelectedOffloadingPhaseType { + return false + } + if len(namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId1]) != 1 || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId1][0].Type != offv1alpha1.NamespaceOffloadingRequired || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId1][0].Status != corev1.ConditionFalse { + return false + } + if len(namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId2]) != 1 || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId2][0].Type != offv1alpha1.NamespaceOffloadingRequired || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId2][0].Status != corev1.ConditionFalse { + return false + } + if len(namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId3]) != 1 || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId3][0].Type != offv1alpha1.NamespaceOffloadingRequired || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId3][0].Status != corev1.ConditionFalse { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + }) + + It(" TEST 2: check Ready status", func() { + + By(" 1 - Get NamespaceMap associated to remote cluster 1 and change Status") + Eventually(func() bool { + if err := homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId1}); err != nil { + return false + } + Expect(len(nms.Items) == 1).To(BeTrue()) + nms.Items[0].Status.CurrentMapping = map[string]mapsv1alpha1.RemoteNamespaceStatus{} + nms.Items[0].Status.CurrentMapping[namespace1Name] = mapsv1alpha1.RemoteNamespaceStatus{ + RemoteNamespace: namespace1Name, + Phase: mapsv1alpha1.MappingAccepted, + } + err := homeClient.Update(context.TODO(), nms.Items[0].DeepCopy()) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By(" 2 - Checking Ready status of the NamespaceOffloading ") + Eventually(func() bool { + if err := homeClient.Get(context.TODO(), types.NamespacedName{ + Name: liqoconst.DefaultNamespaceOffloadingName, + Namespace: namespace1Name}, namespaceOffloading1); err != nil { + return false + } + if namespaceOffloading1.Status.OffloadingPhase != offv1alpha1.ReadyOffloadingPhaseType { + return false + } + if len(namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId1]) != 1 || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId1][0].Type != offv1alpha1.NamespaceReady || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId1][0].Status != corev1.ConditionTrue { + return false + } + if len(namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId2]) != 1 || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId2][0].Type != offv1alpha1.NamespaceOffloadingRequired || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId2][0].Status != corev1.ConditionFalse { + return false + } + if len(namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId3]) != 1 || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId3][0].Type != offv1alpha1.NamespaceOffloadingRequired || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId3][0].Status != corev1.ConditionFalse { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + }) + + It(" TEST 3: check AllFailed status", func() { + + By(" 1 - Get NamespaceMap associated to remote cluster 2 and change Status") + Eventually(func() bool { + if err := homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId2}); err != nil { + return false + } + Expect(len(nms.Items) == 1).To(BeTrue()) + nms.Items[0].Status.CurrentMapping = map[string]mapsv1alpha1.RemoteNamespaceStatus{} + nms.Items[0].Status.CurrentMapping[namespace1Name] = mapsv1alpha1.RemoteNamespaceStatus{ + RemoteNamespace: namespace1Name, + Phase: mapsv1alpha1.MappingCreationLoopBackOff, + } + err := homeClient.Update(context.TODO(), nms.Items[0].DeepCopy()) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By(" 2 - Checking AllFailed status of the NamespaceOffloading ") + Eventually(func() bool { + if err := homeClient.Get(context.TODO(), types.NamespacedName{ + Name: liqoconst.DefaultNamespaceOffloadingName, + Namespace: namespace1Name}, namespaceOffloading1); err != nil { + return false + } + if namespaceOffloading1.Status.OffloadingPhase != offv1alpha1.AllFailedOffloadingPhaseType { + return false + } + if len(namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId1]) != 1 || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId1][0].Type != offv1alpha1.NamespaceOffloadingRequired || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId1][0].Status != corev1.ConditionFalse { + return false + } + if len(namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId2]) != 1 || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId2][0].Type != offv1alpha1.NamespaceReady || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId2][0].Status != corev1.ConditionFalse { + return false + } + if len(namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId3]) != 1 || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId3][0].Type != offv1alpha1.NamespaceOffloadingRequired || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId3][0].Status != corev1.ConditionFalse { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + }) + + It(" TEST 4: check SomeFailed status", func() { + + By(" 1 - Get NamespaceMap associated to remote cluster 2 and change Status to MappingCreationBackoff") + Eventually(func() bool { + if err := homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId2}); err != nil { + return false + } + Expect(len(nms.Items) == 1).To(BeTrue()) + nms.Items[0].Status.CurrentMapping = map[string]mapsv1alpha1.RemoteNamespaceStatus{} + nms.Items[0].Status.CurrentMapping[namespace1Name] = mapsv1alpha1.RemoteNamespaceStatus{ + RemoteNamespace: namespace1Name, + Phase: mapsv1alpha1.MappingCreationLoopBackOff, + } + err := homeClient.Update(context.TODO(), nms.Items[0].DeepCopy()) + return err == nil + }, timeout, interval).Should(BeTrue()) + + time.Sleep(time.Second * 3) + + By(" 2 - Get NamespaceMap associated to remote cluster 1 and change Status to MappingAccepted") + Eventually(func() bool { + if err := homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId1}); err != nil { + return false + } + Expect(len(nms.Items) == 1).To(BeTrue()) + nms.Items[0].Status.CurrentMapping = map[string]mapsv1alpha1.RemoteNamespaceStatus{} + nms.Items[0].Status.CurrentMapping[namespace1Name] = mapsv1alpha1.RemoteNamespaceStatus{ + RemoteNamespace: namespace1Name, + Phase: mapsv1alpha1.MappingAccepted, + } + err := homeClient.Update(context.TODO(), nms.Items[0].DeepCopy()) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By(" 3 - Checking SomeFailed status of the NamespaceOffloading ") + + Eventually(func() bool { + if err := homeClient.Get(context.TODO(), types.NamespacedName{ + Name: liqoconst.DefaultNamespaceOffloadingName, + Namespace: namespace1Name}, namespaceOffloading1); err != nil { + return false + } + if namespaceOffloading1.Status.OffloadingPhase != offv1alpha1.SomeFailedOffloadingPhaseType { + return false + } + if len(namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId1]) != 1 || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId1][0].Type != offv1alpha1.NamespaceReady || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId1][0].Status != corev1.ConditionTrue { + return false + } + if len(namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId2]) != 1 || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId2][0].Type != offv1alpha1.NamespaceReady || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId2][0].Status != corev1.ConditionFalse { + return false + } + if len(namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId3]) != 1 || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId3][0].Type != offv1alpha1.NamespaceOffloadingRequired || + namespaceOffloading1.Status.RemoteNamespacesConditions[remoteClusterId3][0].Status != corev1.ConditionFalse { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + + Expect(homeClient.Delete(context.TODO(), namespaceOffloading1)).To(Succeed()) + }) + + }) + + Context("Check RemoteNamespaceConditions and Status of NamespaceOffloading when the deletion timestamp is set", func() { + + It(" TEST 5: set the Deletion timestamp on NamespaceOffloading and check the evolution of its status", func() { + + // The namespace name is associated with the test number + namespace5Name := "namespace5" + By(fmt.Sprintf(" 1 - Create the namespace '%s'", namespace5Name)) + namespace5 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace5Name, + }, + } + Expect(homeClient.Create(context.TODO(), namespace5)).To(Succeed()) + + By(" 2 - Create the associated NamespaceOffloading with finalizer") + namespaceOffloading5 := &offv1alpha1.NamespaceOffloading{ + ObjectMeta: metav1.ObjectMeta{ + Name: liqoconst.DefaultNamespaceOffloadingName, + Namespace: namespace5Name, + }, + Spec: offv1alpha1.NamespaceOffloadingSpec{ + NamespaceMappingStrategy: offv1alpha1.EnforceSameNameMappingStrategyType, + PodOffloadingStrategy: offv1alpha1.LocalAndRemotePodOffloadingStrategyType, + ClusterSelector: corev1.NodeSelector{NodeSelectorTerms: []corev1.NodeSelectorTerm{}}, + }, + } + ctrlutils.AddFinalizer(namespaceOffloading5, testFinalizer) + Expect(homeClient.Create(context.TODO(), namespaceOffloading5)).To(Succeed()) + + By(" 3 - Get NamespaceMap associated to remote cluster 2 and change Status to MappingCreationBackoff") + Eventually(func() bool { + if err := homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId2}); err != nil { + return false + } + Expect(len(nms.Items) == 1).To(BeTrue()) + nms.Items[0].Status.CurrentMapping = map[string]mapsv1alpha1.RemoteNamespaceStatus{} + nms.Items[0].Status.CurrentMapping[namespace5Name] = mapsv1alpha1.RemoteNamespaceStatus{ + RemoteNamespace: namespace5Name, + Phase: mapsv1alpha1.MappingCreationLoopBackOff, + } + err := homeClient.Update(context.TODO(), nms.Items[0].DeepCopy()) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By(" 4 - Get NamespaceMap associated to remote cluster 1 and change Status to MappingAccepted") + Eventually(func() bool { + if err := homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId1}); err != nil { + return false + } + Expect(len(nms.Items) == 1).To(BeTrue()) + nms.Items[0].Status.CurrentMapping = map[string]mapsv1alpha1.RemoteNamespaceStatus{} + nms.Items[0].Status.CurrentMapping[namespace5Name] = mapsv1alpha1.RemoteNamespaceStatus{ + RemoteNamespace: namespace5Name, + Phase: mapsv1alpha1.MappingAccepted, + } + err := homeClient.Update(context.TODO(), nms.Items[0].DeepCopy()) + return err == nil + }, timeout, interval).Should(BeTrue()) + + time.Sleep(time.Second * 3) + + By(" 5 - Set the deletion timestamp") + Eventually(func() bool { + if err := homeClient.Get(context.TODO(), types.NamespacedName{ + Name: liqoconst.DefaultNamespaceOffloadingName, + Namespace: namespace5Name}, namespaceOffloading5); err != nil { + return false + } + err := homeClient.Delete(context.TODO(), namespaceOffloading5) + return err == nil + }, timeout, interval).Should(BeTrue()) + + By(" 6 - Checking Terminating status of the NamespaceOffloading and the remote conditions") + Eventually(func() bool { + if err := homeClient.Get(context.TODO(), types.NamespacedName{ + Name: liqoconst.DefaultNamespaceOffloadingName, + Namespace: namespace5Name}, namespaceOffloading5); err != nil { + return false + } + if namespaceOffloading5.Status.OffloadingPhase != offv1alpha1.TerminatingOffloadingPhaseType { + return false + } + if len(namespaceOffloading5.Status.RemoteNamespacesConditions[remoteClusterId1]) != 1 || + namespaceOffloading5.Status.RemoteNamespacesConditions[remoteClusterId1][0].Type != offv1alpha1.NamespaceReady || + namespaceOffloading5.Status.RemoteNamespacesConditions[remoteClusterId1][0].Status != corev1.ConditionTrue { + return false + } + if len(namespaceOffloading5.Status.RemoteNamespacesConditions[remoteClusterId2]) != 1 || + namespaceOffloading5.Status.RemoteNamespacesConditions[remoteClusterId2][0].Type != offv1alpha1.NamespaceReady || + namespaceOffloading5.Status.RemoteNamespacesConditions[remoteClusterId2][0].Status != corev1.ConditionFalse { + return false + } + if len(namespaceOffloading5.Status.RemoteNamespacesConditions[remoteClusterId3]) != 1 || + namespaceOffloading5.Status.RemoteNamespacesConditions[remoteClusterId3][0].Type != offv1alpha1.NamespaceOffloadingRequired || + namespaceOffloading5.Status.RemoteNamespacesConditions[remoteClusterId3][0].Status != corev1.ConditionFalse { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + + By(" 7 - Clean NamespaceMap status") + Eventually(func() bool { + if err := homeClient.List(context.TODO(), nms); err != nil { + return false + } + Expect(len(nms.Items) == mapNumber).To(BeTrue()) + for i, _ := range nms.Items { + nms.Items[i].Status.CurrentMapping = map[string]mapsv1alpha1.RemoteNamespaceStatus{} + if err := homeClient.Update(context.TODO(), nms.Items[i].DeepCopy()); err != nil { + return false + } + } + return true + }, timeout, interval).Should(BeTrue()) + + By(" 8 - Checking Terminating status of the NamespaceOffloading and the remote conditions must be empty") + Eventually(func() bool { + if err := homeClient.Get(context.TODO(), types.NamespacedName{ + Name: liqoconst.DefaultNamespaceOffloadingName, + Namespace: namespace5Name}, namespaceOffloading5); err != nil { + return false + } + if namespaceOffloading5.Status.OffloadingPhase != offv1alpha1.TerminatingOffloadingPhaseType { + return false + } + return len(namespaceOffloading5.Status.RemoteNamespacesConditions[remoteClusterId1]) == 0 && + len(namespaceOffloading5.Status.RemoteNamespacesConditions[remoteClusterId2]) == 0 && + len(namespaceOffloading5.Status.RemoteNamespacesConditions[remoteClusterId3]) == 0 + }, timeout, interval).Should(BeTrue()) + + }) + + }) +}) diff --git a/pkg/liqo-controller-manager/offloadingStatus-controller/suite_test.go b/pkg/liqo-controller-manager/offloadingStatus-controller/suite_test.go new file mode 100644 index 0000000000..1ef316d9eb --- /dev/null +++ b/pkg/liqo-controller-manager/offloadingStatus-controller/suite_test.go @@ -0,0 +1,192 @@ +/* +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 offloadingstatuscontroller + +import ( + "context" + "flag" + "fmt" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/klog" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/envtest/printer" + + offv1alpha1 "github.com/liqotech/liqo/apis/offloading/v1alpha1" + mapsv1alpha1 "github.com/liqotech/liqo/apis/virtualKubelet/v1alpha1" + liqoconst "github.com/liqotech/liqo/pkg/consts" + // +kubebuilder:scaffold:imports +) + +const ( + + // namespace where the NamespaceMaps are created + mapNamespaceName = "default" + mapNumber = 3 + namespace1Name = "namespace1" + + remoteClusterId1 = "1-6a0e9f-b52-4ed0" + remoteClusterId2 = "2-899890-dsd-323s" + remoteClusterId3 = "3-refc453-ds43d-43rs" +) + +var ( + homeCfg *rest.Config + homeClient client.Client + homeClusterEnv *envtest.Environment + + // Resources + nms *mapsv1alpha1.NamespaceMapList + namespace1 *corev1.Namespace + namespaceOffloading1 *offv1alpha1.NamespaceOffloading + + nm1 *mapsv1alpha1.NamespaceMap + nm2 *mapsv1alpha1.NamespaceMap + nm3 *mapsv1alpha1.NamespaceMap + + flags *flag.FlagSet +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecsWithDefaultAndCustomReporters(t, + "Controller Suite", + []Reporter{printer.NewlineReporter{}}) +} + +var _ = BeforeSuite(func(done Done) { + + By("bootstrapping test environments") + + homeClusterEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "deployments", "liqo", "crds")}, + } + + flags = &flag.FlagSet{} + klog.InitFlags(flags) + _ = flags.Set("v", "2") + + var err error + + // Home cluster + homeCfg, err = homeClusterEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(homeCfg).ToNot(BeNil()) + + err = corev1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = mapsv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = offv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sManager, err := ctrl.NewManager(homeCfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + homeClient = k8sManager.GetClient() + Expect(homeClient).ToNot(BeNil()) + + err = (&OffloadingStatusReconciler{ + Client: homeClient, + Scheme: k8sManager.GetScheme(), + RequeueTime: time.Second * 3, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + err = k8sManager.Start(ctrl.SetupSignalHandler()) + Expect(err).ToNot(HaveOccurred()) + }() + + // Necessary resources in HomeCluster + nms = &mapsv1alpha1.NamespaceMapList{} + + nm1 = &mapsv1alpha1.NamespaceMap{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("%s-", remoteClusterId1), + Namespace: mapNamespaceName, + Labels: map[string]string{ + liqoconst.RemoteClusterID: remoteClusterId1, + }, + }, + } + + nm2 = &mapsv1alpha1.NamespaceMap{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("%s-", remoteClusterId2), + Namespace: mapNamespaceName, + Labels: map[string]string{ + liqoconst.RemoteClusterID: remoteClusterId2, + }, + }, + } + + nm3 = &mapsv1alpha1.NamespaceMap{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("%s-", remoteClusterId3), + Namespace: mapNamespaceName, + Labels: map[string]string{ + liqoconst.RemoteClusterID: remoteClusterId3, + }, + }, + } + + namespace1 = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace1Name, + }, + } + + namespaceOffloading1 = &offv1alpha1.NamespaceOffloading{ + ObjectMeta: metav1.ObjectMeta{ + Name: liqoconst.DefaultNamespaceOffloadingName, + Namespace: namespace1Name, + }, + Spec: offv1alpha1.NamespaceOffloadingSpec{ + NamespaceMappingStrategy: offv1alpha1.EnforceSameNameMappingStrategyType, + PodOffloadingStrategy: offv1alpha1.LocalAndRemotePodOffloadingStrategyType, + ClusterSelector: corev1.NodeSelector{NodeSelectorTerms: []corev1.NodeSelectorTerm{}}, + }, + } + + Expect(homeClient.Create(context.TODO(), namespace1)).Should(Succeed()) + Expect(homeClient.Create(context.TODO(), nm1)).Should(Succeed()) + Expect(homeClient.Create(context.TODO(), nm2)).Should(Succeed()) + Expect(homeClient.Create(context.TODO(), nm3)).Should(Succeed()) + Expect(homeClient.Create(context.TODO(), namespaceOffloading1)).Should(Succeed()) + + close(done) +}, 60) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := homeClusterEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) diff --git a/pkg/utils/remoteNamespaceConditions.go b/pkg/utils/remoteNamespaceConditions.go new file mode 100644 index 0000000000..350b49f91b --- /dev/null +++ b/pkg/utils/remoteNamespaceConditions.go @@ -0,0 +1,93 @@ +package utils + +import ( + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + offv1alpha1 "github.com/liqotech/liqo/apis/offloading/v1alpha1" +) + +// AddRemoteNamespaceCondition sets newCondition in the conditions slice. +// conditions must be non-nil. +// 1. if the condition of the specified type already exists (all fields of the existing condition are updated to +// newCondition, LastTransitionTime is set to now if the new status differs from the old status). +// 2. if a condition of the specified type does not exist (LastTransitionTime is set to now() if unset, and newCondition is appended). +func AddRemoteNamespaceCondition(conditions *[]offv1alpha1.RemoteNamespaceCondition, + newCondition *offv1alpha1.RemoteNamespaceCondition) { + if conditions == nil { + return + } + existingCondition := FindRemoteNamespaceCondition(*conditions, newCondition.Type) + if existingCondition == nil { + if newCondition.LastTransitionTime.IsZero() { + newCondition.LastTransitionTime = metav1.NewTime(time.Now()) + } + *conditions = append(*conditions, *newCondition) + return + } + + if existingCondition.Status != newCondition.Status { + existingCondition.Status = newCondition.Status + if !newCondition.LastTransitionTime.IsZero() { + existingCondition.LastTransitionTime = newCondition.LastTransitionTime + } else { + existingCondition.LastTransitionTime = metav1.NewTime(time.Now()) + } + } + + existingCondition.Reason = newCondition.Reason + existingCondition.Message = newCondition.Message +} + +// RemoveRemoteNamespaceCondition removes the corresponding conditionType from conditions. +// conditions must be non-nil. +func RemoveRemoteNamespaceCondition(conditions *[]offv1alpha1.RemoteNamespaceCondition, + conditionType offv1alpha1.RemoteNamespaceConditionType) { + if conditions == nil || len(*conditions) == 0 { + return + } + newConditions := make([]offv1alpha1.RemoteNamespaceCondition, 0, len(*conditions)-1) + for _, condition := range *conditions { + if condition.Type != conditionType { + newConditions = append(newConditions, condition) + } + } + *conditions = newConditions +} + +// FindRemoteNamespaceCondition finds the conditionType in conditions. +func FindRemoteNamespaceCondition(conditions []offv1alpha1.RemoteNamespaceCondition, + conditionType offv1alpha1.RemoteNamespaceConditionType) *offv1alpha1.RemoteNamespaceCondition { + for i := range conditions { + if conditions[i].Type == conditionType { + return &conditions[i] + } + } + + return nil +} + +// IsStatusConditionTrue returns true when the conditionType is present and set to `corev1.ConditionTrue`. +func IsStatusConditionTrue(conditions []offv1alpha1.RemoteNamespaceCondition, + conditionType offv1alpha1.RemoteNamespaceConditionType) bool { + return IsStatusConditionPresentAndEqual(conditions, conditionType, corev1.ConditionTrue) +} + +// IsStatusConditionFalse returns true when the conditionType is present and set to `corev1.ConditionFalse`. +func IsStatusConditionFalse(conditions []offv1alpha1.RemoteNamespaceCondition, + conditionType offv1alpha1.RemoteNamespaceConditionType) bool { + return IsStatusConditionPresentAndEqual(conditions, conditionType, corev1.ConditionFalse) +} + +// IsStatusConditionPresentAndEqual returns true when conditionType is present and equal to status. +func IsStatusConditionPresentAndEqual(conditions []offv1alpha1.RemoteNamespaceCondition, + conditionType offv1alpha1.RemoteNamespaceConditionType, status corev1.ConditionStatus) bool { + for _, condition := range conditions { + if condition.Type == conditionType { + return condition.Status == status + } + } + return false +}