Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(teamrolebindings): teamrbac supports single users #927

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
10 changes: 10 additions & 0 deletions charts/manager/crds/greenhouse.sap_teamrolebindings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ spec:
type: object
type: object
x-kubernetes-map-type: atomic
createNamespaces:
default: false
description: CreateNamespaces when enabled the controller will create
namespaces for RoleBindings if they do not exist.
type: boolean
namespaces:
description: |-
Namespaces is a list of namespaces in the Greenhouse Clusters to apply the RoleBinding to.
Expand All @@ -119,6 +124,11 @@ spec:
teamRoleRef:
description: TeamRoleRef references a Greenhouse TeamRole by name
type: string
usernames:
description: Usernames defines list of users to add to the (Cluster-)RoleBindings
items:
type: string
type: array
type: object
status:
description: TeamRoleBindingStatus defines the observed state of the TeamRoleBinding
Expand Down
44 changes: 44 additions & 0 deletions docs/reference/api/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3208,6 +3208,17 @@ <h3 id="greenhouse.sap/v1alpha1.TeamRoleBinding">TeamRoleBinding
</tr>
<tr>
<td>
<code>usernames</code><br>
<em>
[]string
</em>
</td>
<td>
<p>Usernames defines list of users to add to the (Cluster-)RoleBindings</p>
</td>
</tr>
<tr>
<td>
<code>clusterName</code><br>
<em>
string
Expand Down Expand Up @@ -3242,6 +3253,17 @@ <h3 id="greenhouse.sap/v1alpha1.TeamRoleBinding">TeamRoleBinding
If empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace.</p>
</td>
</tr>
<tr>
<td>
<code>createNamespaces</code><br>
<em>
bool
</em>
</td>
<td>
<p>CreateNamespaces when enabled the controller will create namespaces for RoleBindings if they do not exist.</p>
</td>
</tr>
</table>
</td>
</tr>
Expand Down Expand Up @@ -3302,6 +3324,17 @@ <h3 id="greenhouse.sap/v1alpha1.TeamRoleBindingSpec">TeamRoleBindingSpec
</tr>
<tr>
<td>
<code>usernames</code><br>
<em>
[]string
</em>
</td>
<td>
<p>Usernames defines list of users to add to the (Cluster-)RoleBindings</p>
</td>
</tr>
<tr>
<td>
<code>clusterName</code><br>
<em>
string
Expand Down Expand Up @@ -3336,6 +3369,17 @@ <h3 id="greenhouse.sap/v1alpha1.TeamRoleBindingSpec">TeamRoleBindingSpec
If empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace.</p>
</td>
</tr>
<tr>
<td>
<code>createNamespaces</code><br>
<em>
bool
</em>
</td>
<td>
<p>CreateNamespaces when enabled the controller will create namespaces for RoleBindings if they do not exist.</p>
</td>
</tr>
</tbody>
</table>
</div>
Expand Down
9 changes: 9 additions & 0 deletions docs/reference/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,10 @@ components:
type: object
type: object
x-kubernetes-map-type: atomic
createNamespaces:
default: false
description: CreateNamespaces when enabled the controller will create namespaces for RoleBindings if they do not exist.
type: boolean
namespaces:
description: Namespaces is a list of namespaces in the Greenhouse Clusters to apply the RoleBinding to.\nIf empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace.
items:
Expand All @@ -1067,6 +1071,11 @@ components:
teamRoleRef:
description: TeamRoleRef references a Greenhouse TeamRole by name
type: string
usernames:
description: Usernames defines list of users to add to the (Cluster-)RoleBindings
items:
type: string
type: array
type: object
status:
description: TeamRoleBindingStatus defines the observed state of the TeamRoleBinding
Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/greenhouse/v1alpha1/teamrolebinding_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ type TeamRoleBindingSpec struct {
TeamRoleRef string `json:"teamRoleRef,omitempty"`
// TeamRef references a Greenhouse Team by name
TeamRef string `json:"teamRef,omitempty"`
// Usernames defines list of users to add to the (Cluster-)RoleBindings
Usernames []string `json:"usernames,omitempty"`
// ClusterName is the name of the cluster the rbacv1 resources are created on.
ClusterName string `json:"clusterName,omitempty"`
// ClusterSelector is a label selector to select the Clusters the TeamRoleBinding should be deployed to.
ClusterSelector metav1.LabelSelector `json:"clusterSelector,omitempty"`
// Namespaces is a list of namespaces in the Greenhouse Clusters to apply the RoleBinding to.
// If empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace.
Namespaces []string `json:"namespaces,omitempty"`
// CreateNamespaces when enabled the controller will create namespaces for RoleBindings if they do not exist.
// +kubebuilder:default:=false
CreateNamespaces bool `json:"createNamespaces,omitempty"`
}

// TeamRoleBindingStatus defines the observed state of the TeamRoleBinding
Expand Down Expand Up @@ -139,4 +144,7 @@ const (

// RoleBindingFailed is the condition reason for the TeamRoleBinding when the RoleBinding could not be created
RoleBindingFailed ConditionReason = "RoleBindingFailed"

// CreateNamespacesFailed is the condition reason for the TeamRoleBinding when the namespaces could not be created
CreateNamespacesFailed ConditionReason = "CreateNamespacesFailed"
)
5 changes: 5 additions & 0 deletions pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 41 additions & 18 deletions pkg/controllers/teamrbac/teamrolebinding_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ func (r *TeamRoleBindingReconciler) EnsureDeleted(ctx context.Context, resource
func (r *TeamRoleBindingReconciler) doReconcile(ctx context.Context, teamRole *greenhousev1alpha1.TeamRole, clusters *greenhousev1alpha1.ClusterList, trb *greenhousev1alpha1.TeamRoleBinding, team *greenhousev1alpha1.Team) error {
failedClusters := []string{}
cr := initRBACClusterRole(teamRole)

for _, cluster := range clusters.Items {
remoteRestClient, err := clientutil.NewK8sClientFromCluster(ctx, r.Client, &cluster)
if err != nil {
Expand Down Expand Up @@ -235,7 +236,7 @@ func (r *TeamRoleBindingReconciler) doReconcile(ctx context.Context, teamRole *g
for _, namespace := range trb.Spec.Namespaces {
rbacRoleBinding := rbacRoleBinding(trb, cr, team, namespace)

if err := reconcileRoleBinding(ctx, remoteRestClient, &cluster, rbacRoleBinding); err != nil {
if err := reconcileRoleBinding(ctx, remoteRestClient, &cluster, rbacRoleBinding, trb.Spec.CreateNamespaces); err != nil {
r.recorder.Eventf(trb, corev1.EventTypeWarning, greenhousev1alpha1.FailedEvent, "Failed to reconcile RoleBinding %s in cluster/namespace %s/%s: ", rbacRoleBinding.GetName(), cluster.GetName(), namespace)
if !slices.Contains(failedClusters, cluster.GetName()) {
failedClusters = append(failedClusters, cluster.GetName())
Expand All @@ -248,6 +249,7 @@ func (r *TeamRoleBindingReconciler) doReconcile(ctx context.Context, teamRole *g
continue
}
}

trb.SetPropagationStatus(cluster.GetName(), metav1.ConditionTrue, greenhousev1alpha1.RBACReconciled, "")
}

Expand Down Expand Up @@ -438,13 +440,7 @@ func rbacRoleBinding(trb *greenhousev1alpha1.TeamRoleBinding, clusterRole *rbacv
Kind: clusterRole.Kind,
Name: clusterRole.GetName(),
},
Subjects: []rbacv1.Subject{
{
APIGroup: rbacv1.GroupName,
Kind: rbacv1.GroupKind,
Name: team.Spec.MappedIDPGroup,
},
},
Subjects: generateSubjects(trb.Spec.Usernames, team.Spec.MappedIDPGroup),
}
}

Expand All @@ -463,16 +459,28 @@ func rbacClusterRoleBinding(trb *greenhousev1alpha1.TeamRoleBinding, clusterRole
Kind: clusterRole.Kind,
Name: clusterRole.GetName(),
},
Subjects: []rbacv1.Subject{
{
APIGroup: rbacv1.GroupName,
Kind: rbacv1.GroupKind,
Name: team.Spec.MappedIDPGroup,
},
},
Subjects: generateSubjects(trb.Spec.Usernames, team.Spec.MappedIDPGroup),
}
}

// generateSubjects returns a list of subjects with mappedIDPGroup as a rbacv1.GroupKind, and any usernames as rbacv1.UserKind
func generateSubjects(usernames []string, mappedIDPGroup string) []rbacv1.Subject {
var subjects []rbacv1.Subject
for _, username := range usernames {
subjects = append(subjects, rbacv1.Subject{
Kind: rbacv1.UserKind,
APIGroup: rbacv1.GroupName,
Name: username,
})
}

return append(subjects, rbacv1.Subject{
APIGroup: rbacv1.GroupName,
Kind: rbacv1.GroupKind,
Name: mappedIDPGroup,
})
}

// getTeamRole retrieves the Role referenced by the given RoleBinding in the RoleBinding's Namespace
func getTeamRole(ctx context.Context, c client.Client, r record.EventRecorder, teamRoleBinding *greenhousev1alpha1.TeamRoleBinding) (*greenhousev1alpha1.TeamRole, error) {
if teamRoleBinding.Spec.TeamRoleRef == "" {
Expand Down Expand Up @@ -534,10 +542,10 @@ func reconcileClusterRoleBinding(ctx context.Context, cl client.Client, c *green
remoteCRB.Subjects = crb.Subjects
return nil
})

if err != nil {
return err
}

switch result {
case clientutil.OperationResultNone:
log.FromContext(ctx).Info("noop ClusterRoleBinding", "clusterRoleBinding", crb.GetName(), "cluster", c.GetName())
Expand All @@ -550,24 +558,39 @@ func reconcileClusterRoleBinding(ctx context.Context, cl client.Client, c *green
}

// reconcileRoleBinding creates or updates a RoleBinding in the Cluster the given client.Client is created for
func reconcileRoleBinding(ctx context.Context, cl client.Client, c *greenhousev1alpha1.Cluster, rb *rbacv1.RoleBinding) error {
func reconcileRoleBinding(ctx context.Context, cl client.Client, c *greenhousev1alpha1.Cluster, rb *rbacv1.RoleBinding, createNamespaces bool) error {
remoteRB := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: rb.Name,
Namespace: rb.Namespace,
},
}

if createNamespaces {
namespace := new(corev1.Namespace)
err := cl.Get(ctx, types.NamespacedName{Name: rb.Namespace}, namespace)
if err != nil {
if apierrors.IsNotFound(err) {
err := cl.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: rb.Namespace}})
if err != nil {
return err
}
} else {
return err
}
}
}

result, err := clientutil.CreateOrPatch(ctx, cl, remoteRB, func() error {
remoteRB.Labels = rb.Labels
remoteRB.RoleRef = rb.RoleRef
remoteRB.Subjects = rb.Subjects
return nil
})

if err != nil {
return err
}

switch result {
case clientutil.OperationResultNone:
log.FromContext(ctx).Info("noop RoleBinding", "roleBinding", rb.GetName(), "cluster", c.GetName(), "namespace", rb.GetNamespace())
Expand Down
41 changes: 34 additions & 7 deletions pkg/controllers/teamrbac/teamrolebinding_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@ import (

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"sigs.k8s.io/controller-runtime/pkg/client"

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/fields"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

greenhouseapis "github.com/cloudoperators/greenhouse/pkg/apis"
greenhousev1alpha1 "github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1"
Expand Down Expand Up @@ -90,13 +89,14 @@ var _ = Describe("Validate ClusterRole & RoleBinding on Remote Cluster", Ordered
}).Should(BeTrue(), "there should be no ClusterRoles left to list on the remote cluster")
})

Context("When editing clusterName or clusterSelctor on a TeamRoleBinding", func() {
Context("When editing clusterName or clusterSelector on a TeamRoleBinding", func() {
It("should remove the RoleBinding on the cluster that is no longer referenced by clusterName and reconcile the clusters referenced by clusterSelector", func() {
By("creating a TeamRoleBinding on the central cluster")
trb := setup.CreateTeamRoleBinding(test.Ctx, "test-rolebinding",
test.WithTeamRoleRef(teamRoleUT.Name),
test.WithTeamRef(teamUT.Name),
test.WithClusterName(clusterA.Name))
test.WithClusterName(clusterA.Name),
test.WithUsernames([]string{"test-user-1"}))
trbKey := types.NamespacedName{Name: trb.Name, Namespace: trb.Namespace}
By("validating the RoleBinding created on the remote clusterA")
remoteRoleBinding := &rbacv1.ClusterRoleBinding{}
Expand Down Expand Up @@ -146,6 +146,7 @@ var _ = Describe("Validate ClusterRole & RoleBinding on Remote Cluster", Ordered
}).Should(BeTrue(), "there should be no error getting the RoleBinding from ClusterB")
Expect(remoteRoleBinding.RoleRef.Name).To(HavePrefix(greenhouseapis.RBACPrefix))
Expect(remoteRoleBinding.RoleRef.Name).To(ContainSubstring(teamRoleUT.Name))
Expect(remoteRoleBinding.Subjects).To(HaveLen(2))

By("validating the TeamRoleBinding's status is updated")
Eventually(func(g Gomega) {
Expand Down Expand Up @@ -312,7 +313,8 @@ var _ = Describe("Validate ClusterRole & RoleBinding on Remote Cluster", Ordered
test.WithTeamRoleRef(teamRoleUT.Name),
test.WithTeamRef(teamUT.Name),
test.WithClusterName(clusterA.Name),
test.WithNamespaces(setup.Namespace()))
test.WithNamespaces(setup.Namespace()),
test.WithUsernames([]string{"test-user-1"}))

By("validating the RoleBinding created on the remote cluster")
remoteRoleBinding := &rbacv1.RoleBinding{}
Expand All @@ -321,11 +323,12 @@ var _ = Describe("Validate ClusterRole & RoleBinding on Remote Cluster", Ordered
Namespace: trb.Namespace,
}
Eventually(func(g Gomega) bool {
g.Expect(clusterAKubeClient.Get(context.TODO(), remoteRoleBindingName, remoteRoleBinding)).To(Succeed(), "there should be no error getting the RoleBinding from the Remote Cluster")
g.Expect(clusterAKubeClient.Get(test.Ctx, remoteRoleBindingName, remoteRoleBinding)).To(Succeed(), "there should be no error getting the RoleBinding from the Remote Cluster")
return !remoteRoleBinding.CreationTimestamp.IsZero()
}).Should(BeTrue(), "there should be no error getting the RoleBinding")
Expect(remoteRoleBinding.RoleRef.Name).To(HavePrefix(greenhouseapis.RBACPrefix))
Expect(remoteRoleBinding.RoleRef.Name).To(ContainSubstring(teamRoleUT.Name))
Expect(remoteRoleBinding.Subjects).To(HaveLen(2))

By("validating the ClusterRole created on the remote cluster")
remoteClusterRole := &rbacv1.ClusterRole{}
Expand Down Expand Up @@ -386,6 +389,30 @@ var _ = Describe("Validate ClusterRole & RoleBinding on Remote Cluster", Ordered
By("cleaning up the test")
test.EventuallyDeleted(test.Ctx, test.K8sClient, trb)
})

It("Should create namespaces when flag is set to true", func() {
By("creating a TeamRoleBinding on the central cluster")
trb := setup.CreateTeamRoleBinding(test.Ctx, "test-rolebinding",
test.WithTeamRoleRef(teamRoleUT.Name),
test.WithTeamRef(teamUT.Name),
test.WithClusterName(clusterB.Name),
test.WithNamespaces("non-existing-namespace-1", "non-existing-namespace-2"),
test.WithCreateNamespace(true))

By("checking that the Namespace is created")
namespace := &corev1.Namespace{}
Eventually(func(g Gomega) {
err := clusterBKubeClient.Get(test.Ctx, types.NamespacedName{Name: "non-existing-namespace-1"}, namespace)
g.Expect(err).ToNot(HaveOccurred(), "there should be no error getting the non-existing namespace")
}).Should(Succeed())
Eventually(func(g Gomega) {
err := clusterBKubeClient.Get(test.Ctx, types.NamespacedName{Name: "non-existing-namespace-2"}, namespace)
g.Expect(err).ToNot(HaveOccurred(), "there should be no error getting the non-existing namespace")
}).Should(Succeed())

By("cleaning up the test")
test.EventuallyDeleted(test.Ctx, test.K8sClient, trb)
})
})

Context("When creating a Greenhouse TeamRoleBinding with non-existing namespaces on the central cluster", func() {
Expand All @@ -404,7 +431,7 @@ var _ = Describe("Validate ClusterRole & RoleBinding on Remote Cluster", Ordered
Namespace: trb.Namespace,
}
Eventually(func(g Gomega) bool {
g.Expect(clusterAKubeClient.Get(context.TODO(), remoteRoleBindingName, remoteRoleBinding)).To(Succeed(), "there should be no error getting the RoleBinding from the Remote Cluster")
g.Expect(clusterAKubeClient.Get(test.Ctx, remoteRoleBindingName, remoteRoleBinding)).To(Succeed(), "there should be no error getting the RoleBinding from the Remote Cluster")
return !remoteRoleBinding.CreationTimestamp.IsZero()
}).Should(BeTrue(), "there should be no error getting the RoleBinding")
Expect(remoteRoleBinding.RoleRef.Name).To(HavePrefix(greenhouseapis.RBACPrefix))
Expand Down
Loading
Loading