Skip to content

Commit

Permalink
Check NamespaceMap controller privileges on remote cluster.
Browse files Browse the repository at this point in the history
When the NamespaceMap controller creates new remote namespace, it must check that right privileges are set to operate in that namespace.
  • Loading branch information
Andreagit97 committed Jul 1, 2021
1 parent 1c00407 commit 737b138
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 35 deletions.
6 changes: 6 additions & 0 deletions pkg/consts/namespace_mapping.go
Expand Up @@ -23,4 +23,10 @@ const (
SchedulingLiqoLabel = "liqo.io/scheduling-enabled"
// SchedulingLiqoLabelValue unique value allowed for SchedulingLiqoLabel.
SchedulingLiqoLabelValue = "true"
// RoleBindingLabelKey label that some RoleBindings in the remote namespace must have. In every remote namespace
// there are some RoleBindings that provide the local virtualKubelet with some privileges. These RoleBindings just
// described must have that RoleBindingLabel.
RoleBindingLabelKey = "capsule.clastix.io/tenant"
// RoleBindingLabelValuePrefix prefix of the value that the RoleBindingLabel must have.
RoleBindingLabelValuePrefix = "tenant"
)
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"

corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -24,7 +25,7 @@ const (

// This function creates a remote Namespace inside the remote cluster, if it doesn't exist yet.
// The right client to use is chosen by means of NamespaceMap's cluster-id.
func (r *NamespaceMapReconciler) createRemoteNamespace(ctx context.Context, remoteClusterID, remoteName string) error {
func (r *NamespaceMapReconciler) createRemoteNamespace(ctx context.Context, remoteClusterID, remoteNamespaceName string) error {
if err := r.checkRemoteClientPresence(ctx, remoteClusterID); err != nil {
return err
}
Expand All @@ -33,34 +34,44 @@ func (r *NamespaceMapReconciler) createRemoteNamespace(ctx context.Context, remo
annotationKey := fmt.Sprintf("%s-%s/%s", remoteNamespaceAnnotationPrefix, r.LocalClusterID, liqoSuffix)
remoteNamespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: remoteName,
Name: remoteNamespaceName,
Annotations: map[string]string{
annotationKey: remoteNamespaceAnnotationValue,
},
},
}

if err := r.RemoteClients[remoteClusterID].Create(ctx, remoteNamespace); err != nil {
if apierrors.IsAlreadyExists(err) {
if errGet := r.RemoteClients[remoteClusterID].Get(ctx, types.NamespacedName{Name: remoteName}, remoteNamespace); errGet == nil {
if value, ok := remoteNamespace.Annotations[annotationKey]; ok && value == remoteNamespaceAnnotationValue {
klog.Infof("the namespace '%s' has already been created inside the remote cluster '%s'",
remoteNamespace.Name, remoteClusterID)
return nil
}
}
}
klog.Errorf("%s -> Namespace '%s' cannot be created inside cluster '%s'", err,
remoteName, remoteClusterID)
// Trying to create the remote namespace.
if err := r.RemoteClients[remoteClusterID].Create(ctx, remoteNamespace); err != nil && !apierrors.IsAlreadyExists(err) {
klog.Errorf("%s -> unable to create the remote namespace '%s' inside the remote cluster '%s'",
err, remoteNamespaceName, remoteClusterID)
return err
}

// Whether the remote namespace is created successfully or already exists, is necessary to:
// 1 - Try to get it
if err := r.RemoteClients[remoteClusterID].Get(ctx, types.NamespacedName{Name: remoteNamespaceName}, remoteNamespace); err != nil {
klog.Errorf("%s -> unable to get the remote namespace '%s'", err, remoteNamespaceName)
return err
}
// 2 - Check if the remote namespace was created by the NamespaceMap controller.
if value, ok := remoteNamespace.Annotations[annotationKey]; !ok || value != remoteNamespaceAnnotationValue {
err := fmt.Errorf("a namespace with the name '%s' already exists inside the remote cluster '%s'",
remoteNamespaceName, remoteClusterID)
klog.Error(err)
return err
}
// 3 - Check if the virtual kubelet will have the right privileges on the remote namespace.
if err := checkRemoteNamespacePrivileges(ctx, r.RemoteClients[remoteClusterID], remoteNamespaceName, r.LocalClusterID); err != nil {
return err
}

klog.Infof("The namespace '%s' is created inside the remote cluster: '%s'", remoteNamespace.Name, remoteClusterID)
klog.Infof("The namespace '%s' is correctly inside the remote cluster: '%s'", remoteNamespaceName, remoteClusterID)
return nil
}

// For every entry of DesiredMapping create remote Namespace if it has not already being created.
// This function tries to create all the remote namespaces requested in DesiredMapping (NamespaceMap->Spec->DesiredMapping).
// ensureRemoteNamespacesExistence tries to create all the remote namespaces requested in DesiredMapping (NamespaceMap->Spec->DesiredMapping).
func (r *NamespaceMapReconciler) ensureRemoteNamespacesExistence(ctx context.Context, nm *mapsv1alpha1.NamespaceMap) bool {
errorCondition := false
for localName, remoteName := range nm.Spec.DesiredMapping {
Expand All @@ -80,7 +91,7 @@ func (r *NamespaceMapReconciler) ensureRemoteNamespacesExistence(ctx context.Con
return errorCondition
}

// This function deletes a remote Namespace from the remote cluster, the right client to use is chosen
// deleteRemoteNamespace deletes a remote Namespace from the remote cluster, the right client to use is chosen
// by NamespaceMap's cluster-id. This function return nil (success) only when the remote Namespace is really deleted,
// so the get Api returns a "NotFound".
func (r *NamespaceMapReconciler) deleteRemoteNamespace(ctx context.Context, remoteClusterID, remoteName string) error {
Expand Down Expand Up @@ -177,3 +188,22 @@ func (r *NamespaceMapReconciler) namespaceMapDeletionProcess(ctx context.Context
}
return fmt.Errorf("remote namespaces deletion phase in progress")
}

// checkRemoteNamespacePrivileges checks that the right roleBindings are inside the remote namespace to understand if the
// virtual kubelet will have the right privileges on that namespace.
func checkRemoteNamespacePrivileges(ctx context.Context, cl client.Client, remoteNamespaceName, localClusterID string) error {
roleBindingLabelValue := fmt.Sprintf("%s-%s", liqoconst.RoleBindingLabelValuePrefix, localClusterID)
roleBindingList := &rbacv1.RoleBindingList{}
if err := cl.List(ctx, roleBindingList, client.InNamespace(remoteNamespaceName),
client.MatchingLabels{liqoconst.RoleBindingLabelKey: roleBindingLabelValue}); err != nil {
klog.Errorf("%s -> unable to list roleBindings in the remote namespace '%s'", err, remoteNamespaceName)
return err
}
if len(roleBindingList.Items) < 2 {
err := fmt.Errorf("virtual kubelet will not have the necessary privileges on the remote namespace '%s'",
remoteNamespaceName)
klog.Error(err)
return err
}
return nil
}
Expand Up @@ -31,6 +31,7 @@ import (

mapsv1alpha1 "github.com/liqotech/liqo/apis/virtualKubelet/v1alpha1"
liqoconst "github.com/liqotech/liqo/pkg/consts"
testutils "github.com/liqotech/liqo/pkg/liqo-controller-manager/namespaceMap-controller/testUtils"
)

var _ = Describe("NamespaceMap controller", func() {
Expand Down Expand Up @@ -80,13 +81,31 @@ var _ = Describe("NamespaceMap controller", func() {
By(" 2 - Checking remote namespace existence")
Eventually(func() bool {
remoteNamespace := &corev1.Namespace{}
if err := remoteClient2.Get(context.TODO(), types.NamespacedName{Name: namespace1Name}, remoteNamespace); err != nil {
if err := remoteClient1.Get(context.TODO(), types.NamespacedName{Name: namespace1Name}, remoteNamespace); err != nil {
return false
}
return remoteNamespace.Annotations[liqoAnnotationKey] == remoteNamespaceAnnotationValue
}, timeout, interval).Should(BeTrue())

By(" 3 - Checking status of CurrentMapping entry: must be 'Accepted'")
By(" 3 - Checking status of CurrentMapping entry: must be 'CreationLoopBackOff'")
Eventually(func() bool {
if err := homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId1}); err != nil {
return false
}
if len(nms.Items) == 0 {
return false
}
return nms.Items[0].Status.CurrentMapping[namespace1Name].RemoteNamespace == namespace1Name &&
nms.Items[0].Status.CurrentMapping[namespace1Name].Phase == mapsv1alpha1.MappingCreationLoopBackOff
}, timeout, interval).Should(BeTrue())

By(" 4 - Insert fake RoleBindings inside the remote namespace")
roleBinding1 := testutils.GetRoleBindingForASpecificNamespace(namespace1Name, localClusterID, 1)
roleBinding2 := testutils.GetRoleBindingForASpecificNamespace(namespace1Name, localClusterID, 2)
Expect(remoteClient1.Create(context.TODO(), &roleBinding1)).To(Succeed())
Expect(remoteClient1.Create(context.TODO(), &roleBinding2)).To(Succeed())

By(" 5 - Checking status of CurrentMapping entry: must be 'Accepted'")
Eventually(func() bool {
if err := homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId1}); err != nil {
return false
Expand Down Expand Up @@ -121,7 +140,7 @@ var _ = Describe("NamespaceMap controller", func() {
By(fmt.Sprintf(" 6 - Check deletion timestamp of remote namespace '%s'", namespace1Name))
Eventually(func() bool {
remoteNamespace := &corev1.Namespace{}
Expect(remoteClient2.Get(context.TODO(), types.NamespacedName{Name: namespace1Name}, remoteNamespace)).To(Succeed())
Expect(remoteClient1.Get(context.TODO(), types.NamespacedName{Name: namespace1Name}, remoteNamespace)).To(Succeed())
return !remoteNamespace.DeletionTimestamp.IsZero()
}, timeout, interval).Should(BeTrue())

Expand Down Expand Up @@ -170,14 +189,14 @@ var _ = Describe("NamespaceMap controller", func() {
It(fmt.Sprintf("Check NamespaceMap status when a remote namespace with the same name '%s' "+
"already exists on remote cluster '%s'", namespace1Name, remoteClusterId1), func() {
remoteName := "pippo"
By(fmt.Sprintf(" 1 - Create remote namespace '%s' if it isn't already there", remoteName))
By(fmt.Sprintf(" 1 - Create remote namespace '%s'", remoteName))
Eventually(func() bool {
remoteNamespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: remoteName,
},
}
err := remoteClient2.Create(context.TODO(), remoteNamespace)
err := remoteClient1.Create(context.TODO(), remoteNamespace)
return err == nil
}, timeout, interval).Should(BeTrue())

Expand Down Expand Up @@ -236,9 +255,6 @@ var _ = Describe("NamespaceMap controller", func() {

})

// manca il test che testa la cancellazione della risorsa
// aggiungere un po' di namespace remoti prima con le desired entry
// guardare se parte la logica di cancellazione
Context("Check the deletion phase of the NamespaceMap", func() {

It("Create some remote Namespace and then verify the deletion logic when the NamespaceMap is "+
Expand Down Expand Up @@ -286,28 +302,46 @@ var _ = Describe("NamespaceMap controller", func() {
return err == nil
}, timeout, interval).Should(BeTrue())

By(" 4 - Check len of DesiredMapping (len==4) and MappingPhase must be 'Accepted' ")
By(fmt.Sprintf(" 4 - Create 2 rolebindings for the namespace %s", namespace2Name))
roleBinding1 := testutils.GetRoleBindingForASpecificNamespace(namespace2Name, localClusterID, 1)
roleBinding2 := testutils.GetRoleBindingForASpecificNamespace(namespace2Name, localClusterID, 2)
Eventually(func() bool {
return remoteClient1.Create(context.TODO(), &roleBinding1) == nil
}, timeout, interval).Should(BeTrue())
Eventually(func() bool {
return remoteClient1.Create(context.TODO(), &roleBinding2) == nil
}, timeout, interval).Should(BeTrue())

By(fmt.Sprintf(" 5 - Create just 1 rolebindings for the namespace %s", namespace3Name))
roleBinding1 = testutils.GetRoleBindingForASpecificNamespace(namespace3Name, localClusterID, 1)
Eventually(func() bool {
return remoteClient1.Create(context.TODO(), &roleBinding1) == nil
}, timeout, interval).Should(BeTrue())

// No roleBindings are created for the remote namespaces 'namespace4Name' and 'namespace5Name'.

By(" 6 - Check len of DesiredMapping (len==4) and MappingPhase must be 'Accepted' ")
Eventually(func() bool {
Expect(homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId1})).To(Succeed())
Expect(len(nms.Items) == 1).To(BeTrue())
if len(nms.Items[0].Spec.DesiredMapping) != 4 {
return false
}
return nms.Items[0].Status.CurrentMapping[namespace2Name].Phase == mapsv1alpha1.MappingAccepted &&
nms.Items[0].Status.CurrentMapping[namespace3Name].Phase == mapsv1alpha1.MappingAccepted &&
nms.Items[0].Status.CurrentMapping[namespace4Name].Phase == mapsv1alpha1.MappingAccepted &&
nms.Items[0].Status.CurrentMapping[namespace5Name].Phase == mapsv1alpha1.MappingAccepted
nms.Items[0].Status.CurrentMapping[namespace3Name].Phase == mapsv1alpha1.MappingCreationLoopBackOff &&
nms.Items[0].Status.CurrentMapping[namespace4Name].Phase == mapsv1alpha1.MappingCreationLoopBackOff &&
nms.Items[0].Status.CurrentMapping[namespace5Name].Phase == mapsv1alpha1.MappingCreationLoopBackOff
}, timeout, interval).Should(BeTrue())

By(" 5 - Delete NamespaceMap, so the deletion timestamp is set")
By(" 7 - Delete NamespaceMap, so the deletion timestamp is set")
Eventually(func() bool {
Expect(homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId1})).To(Succeed())
Expect(len(nms.Items) == 1).To(BeTrue())
err := homeClient.Delete(context.TODO(), nms.Items[0].DeepCopy())
return err == nil
}, timeout, interval).Should(BeTrue())

By(" 6 - Check if all remote Namespaces are in terminating phase ")
By(" 8 - Check if all remote Namespaces are in terminating phase ")
Eventually(func() bool {
Expect(homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId1})).To(Succeed())
Expect(len(nms.Items) == 1).To(BeTrue())
Expand All @@ -317,7 +351,7 @@ var _ = Describe("NamespaceMap controller", func() {
nms.Items[0].Status.CurrentMapping[namespace5Name].Phase == mapsv1alpha1.MappingTerminating
}, timeout, interval).Should(BeTrue())

By(" 7 - Check if NamespaceMap Controller finalizer is still there")
By(" 9 - Check if NamespaceMap Controller finalizer is still there")
Eventually(func() bool {
if err := homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId1}); err != nil {
return false
Expand All @@ -326,7 +360,7 @@ var _ = Describe("NamespaceMap controller", func() {
return ctrlutils.ContainsFinalizer(nms.Items[0].DeepCopy(), namespaceMapControllerFinalizer)
}, timeout*2, interval).Should(BeTrue())

By(" 8 - Clean NamespaceMap status")
By(" 10 - Clean NamespaceMap status")
Eventually(func() bool {
Expect(homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId1})).To(Succeed())
Expect(len(nms.Items) == 1).To(BeTrue())
Expand All @@ -335,7 +369,7 @@ var _ = Describe("NamespaceMap controller", func() {
return err == nil
}, timeout, interval).Should(BeTrue())

By(" 9 - Check if NamespaceMap is removed")
By(" 10 - Check if the NamespaceMap is removed")
Eventually(func() bool {
if err := homeClient.List(context.TODO(), nms, client.MatchingLabels{liqoconst.RemoteClusterID: remoteClusterId1}); err != nil {
return false
Expand Down
Expand Up @@ -134,7 +134,7 @@ var _ = BeforeSuite(func(done Done) {
Expect(homeClient).ToNot(BeNil())

controllerClients := map[string]client.Client{
remoteClusterId1: remoteClient2,
remoteClusterId1: remoteClient1,
}

// Necessary resources in HomeCluster
Expand Down
@@ -0,0 +1,2 @@
// Package namespacemapctrltestutils provides utility function for namespaceMap controller testing.
package namespacemapctrltestutils
@@ -0,0 +1,37 @@
package namespacemapctrltestutils

import (
"fmt"

rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

liqoconst "github.com/liqotech/liqo/pkg/consts"
)

const (
roleBindingName = "role-binding"
roleType = "Role"
roleName = "fake"
)

// The remote namespace must have at least 2 roleBinding with the clastix label.

// GetRoleBindingForASpecificNamespace provides a roleBinding in the namespace passed as parameter.
// The name of the RoleBinding is associated to the index passed as second parameter.
func GetRoleBindingForASpecificNamespace(namespaceName, localClusterID string, index int) rbacv1.RoleBinding {
return rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%d", roleBindingName, index),
Namespace: namespaceName,
Labels: map[string]string{
liqoconst.RoleBindingLabelKey: fmt.Sprintf("%s-%s", liqoconst.RoleBindingLabelValuePrefix, localClusterID),
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: roleType,
Name: roleName,
},
}
}
43 changes: 43 additions & 0 deletions pkg/utils/foreignCluster/getTenantNamespaceName.go
@@ -0,0 +1,43 @@
package foreigncluster

import (
"context"
"fmt"

"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// GetLocalTenantNamespaceName gets the name of the local tenant namespace associated with a specific peering (remoteClusterID).
func GetLocalTenantNamespaceName(ctx context.Context, cl client.Client, remoteClusterID string) (string, error) {
fc, err := GetForeignClusterByID(ctx, cl, remoteClusterID)
if err != nil {
klog.Errorf("%s -> unable to get foreignCluster associated with the clusterID '%s'", err, remoteClusterID)
return "", err
}

if fc.Status.TenantControlNamespace.Local == "" {
err = fmt.Errorf("there is no tenant namespace associated with the peering with the remote cluster '%s'",
remoteClusterID)
klog.Error(err)
return "", err
}
return fc.Status.TenantControlNamespace.Local, nil
}

// GetRemoteTenantNamespaceName gets the name of the remote tenant namespace associated with a specific peering (remoteClusterID).
func GetRemoteTenantNamespaceName(ctx context.Context, cl client.Client, remoteClusterID string) (string, error) {
fc, err := GetForeignClusterByID(ctx, cl, remoteClusterID)
if err != nil {
klog.Errorf("%s -> unable to get foreignCluster associated with the clusterID '%s'", err, remoteClusterID)
return "", err
}

if fc.Status.TenantControlNamespace.Remote == "" {
err = fmt.Errorf("there is no tenant namespace associated with the peering with the remote cluster '%s'",
remoteClusterID)
klog.Error(err)
return "", err
}
return fc.Status.TenantControlNamespace.Remote, nil
}

0 comments on commit 737b138

Please sign in to comment.