diff --git a/charts/manager/crds/greenhouse.sap_teamrolebindings.yaml b/charts/manager/crds/greenhouse.sap_teamrolebindings.yaml
index e0102fe96..57f2cc0ba 100644
--- a/charts/manager/crds/greenhouse.sap_teamrolebindings.yaml
+++ b/charts/manager/crds/greenhouse.sap_teamrolebindings.yaml
@@ -106,6 +106,11 @@ spec:
type: object
type: object
x-kubernetes-map-type: atomic
+ createNamespaces:
+ default: false
+ description: CreateNamespaces when the flag is set the controller
+ will create namespaces defined in the Namespaces field.
+ type: boolean
namespaces:
description: |-
Namespaces is a list of namespaces in the Greenhouse Clusters to apply the RoleBinding to.
@@ -113,6 +118,38 @@ spec:
items:
type: string
type: array
+ subjects:
+ description: Subjects define list of subject with this role
+ items:
+ description: |-
+ Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference,
+ or a value for non-objects such as user and group names.
+ properties:
+ apiGroup:
+ description: |-
+ APIGroup holds the API group of the referenced subject.
+ Defaults to "" for ServiceAccount subjects.
+ Defaults to "rbac.authorization.k8s.io" for User and Group subjects.
+ type: string
+ kind:
+ description: |-
+ Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount".
+ If the Authorizer does not recognized the kind value, the Authorizer should report an error.
+ type: string
+ name:
+ description: Name of the object being referenced.
+ type: string
+ namespace:
+ description: |-
+ Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty
+ the Authorizer should report an error.
+ type: string
+ required:
+ - kind
+ - name
+ type: object
+ x-kubernetes-map-type: atomic
+ type: array
teamRef:
description: TeamRef references a Greenhouse Team by name
type: string
diff --git a/docs/reference/api/index.html b/docs/reference/api/index.html
index b35aac9f7..9b0524c79 100644
--- a/docs/reference/api/index.html
+++ b/docs/reference/api/index.html
@@ -3196,6 +3196,19 @@
TeamRoleBinding
+subjects
+
+
+[]Kubernetes rbac/v1.Subject
+
+
+ |
+
+ Subjects define list of subject with this role
+ |
+
+
+
clusterName
string
@@ -3230,6 +3243,17 @@ TeamRoleBinding
If empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace.
|
+
+
+createNamespaces
+
+bool
+
+ |
+
+ CreateNamespaces when the flag is set the controller will create namespaces defined in the Namespaces field.
+ |
+
@@ -3290,6 +3314,19 @@ TeamRoleBindingSpec
+subjects
+
+
+[]Kubernetes rbac/v1.Subject
+
+
+ |
+
+ Subjects define list of subject with this role
+ |
+
+
+
clusterName
string
@@ -3324,6 +3361,17 @@ TeamRoleBindingSpec
If empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace.
|
+
+
+createNamespaces
+
+bool
+
+ |
+
+ CreateNamespaces when the flag is set the controller will create namespaces defined in the Namespaces field.
+ |
+
diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml
index d7884cace..661f9a32c 100755
--- a/docs/reference/api/openapi.yaml
+++ b/docs/reference/api/openapi.yaml
@@ -1159,11 +1159,38 @@ components:
type: object
type: object
x-kubernetes-map-type: atomic
+ createNamespaces:
+ default: false
+ description: CreateNamespaces when the flag is set the controller will create namespaces defined in the Namespaces field.
+ 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:
type: string
type: array
+ subjects:
+ description: Subjects define list of subject with this role
+ items:
+ description: Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference,\nor a value for non-objects such as user and group names.
+ properties:
+ apiGroup:
+ description: APIGroup holds the API group of the referenced subject.\nDefaults to "" for ServiceAccount subjects.\nDefaults to "rbac.authorization.k8s.io" for User and Group subjects.
+ type: string
+ kind:
+ description: Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount".\nIf the Authorizer does not recognized the kind value, the Authorizer should report an error.
+ type: string
+ name:
+ description: Name of the object being referenced.
+ type: string
+ namespace:
+ description: Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty\nthe Authorizer should report an error.
+ type: string
+ required:
+ - kind
+ - name
+ type: object
+ x-kubernetes-map-type: atomic
+ type: array
teamRef:
description: TeamRef references a Greenhouse Team by name
type: string
diff --git a/pkg/apis/greenhouse/v1alpha1/teamrolebinding_types.go b/pkg/apis/greenhouse/v1alpha1/teamrolebinding_types.go
index 69add14aa..50cc61d5d 100644
--- a/pkg/apis/greenhouse/v1alpha1/teamrolebinding_types.go
+++ b/pkg/apis/greenhouse/v1alpha1/teamrolebinding_types.go
@@ -6,6 +6,7 @@ package v1alpha1
import (
"slices"
+ rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
greenhouseapis "github.com/cloudoperators/greenhouse/pkg/apis"
@@ -17,6 +18,8 @@ type TeamRoleBindingSpec struct {
TeamRoleRef string `json:"teamRoleRef,omitempty"`
// TeamRef references a Greenhouse Team by name
TeamRef string `json:"teamRef,omitempty"`
+ // Subjects define list of subject with this role
+ Subjects []rbacv1.Subject `json:"subjects,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.
@@ -24,6 +27,9 @@ type TeamRoleBindingSpec struct {
// 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 the flag is set the controller will create namespaces defined in the Namespaces field.
+ // +kubebuilder:default:=false
+ CreateNamespaces bool `json:"createNamespaces,omitempty"`
}
// TeamRoleBindingStatus defines the observed state of the TeamRoleBinding
diff --git a/pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go
index 142d99cea..fd5d4c6b3 100644
--- a/pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go
@@ -1342,6 +1342,11 @@ func (in *TeamRoleBindingList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TeamRoleBindingSpec) DeepCopyInto(out *TeamRoleBindingSpec) {
*out = *in
+ if in.Subjects != nil {
+ in, out := &in.Subjects, &out.Subjects
+ *out = make([]rbacv1.Subject, len(*in))
+ copy(*out, *in)
+ }
in.ClusterSelector.DeepCopyInto(&out.ClusterSelector)
if in.Namespaces != nil {
in, out := &in.Namespaces, &out.Namespaces
diff --git a/pkg/controllers/teamrbac/teamrolebinding_controller.go b/pkg/controllers/teamrbac/teamrolebinding_controller.go
index 86f7dd257..0c8cce7da 100644
--- a/pkg/controllers/teamrbac/teamrolebinding_controller.go
+++ b/pkg/controllers/teamrbac/teamrolebinding_controller.go
@@ -48,6 +48,7 @@ type TeamRoleBindingReconciler struct {
//+kubebuilder:rbac:groups=greenhouse.sap,resources=teamrolebindings/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=greenhouse.sap,resources=teamrolebindings/finalizers,verbs=update
//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch
+//+kubebuilder:rbac:groups="",resources=namespaces,verbs=create;get
// SetupWithManager sets up the controller with the Manager.
func (r *TeamRoleBindingReconciler) SetupWithManager(name string, mgr ctrl.Manager) error {
@@ -198,6 +199,15 @@ 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)
+
+ if trb.Spec.CreateNamespaces {
+ err := r.createNamespaces(ctx, trb)
+ if err != nil {
+ trb.SetCondition(greenhousev1alpha1.FalseCondition(greenhousev1alpha1.RBACReady, greenhousev1alpha1.RBACReconcileFailed, err.Error()))
+ return err
+ }
+ }
+
for _, cluster := range clusters.Items {
remoteRestClient, err := clientutil.NewK8sClientFromCluster(ctx, r.Client, &cluster)
if err != nil {
@@ -438,13 +448,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: joinSubjectWithDefault(trb.Spec.Subjects, team.Spec.MappedIDPGroup),
}
}
@@ -463,16 +467,18 @@ 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: joinSubjectWithDefault(trb.Spec.Subjects, team.Spec.MappedIDPGroup),
}
}
+func joinSubjectWithDefault(subjects []rbacv1.Subject, mappedIDPGroup string) []rbacv1.Subject {
+ 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 == "" {
@@ -785,3 +791,23 @@ func computeReadyCondition(status greenhousev1alpha1.TeamRoleBindingStatus) gree
readyCondition.Message = "ready"
return readyCondition
}
+
+func (r *TeamRoleBindingReconciler) createNamespaces(ctx context.Context, teamRoleBinding *greenhousev1alpha1.TeamRoleBinding) error {
+ for _, nampespaceName := range teamRoleBinding.Spec.Namespaces {
+ namespace := new(corev1.Namespace)
+ err := r.Client.Get(ctx, types.NamespacedName{Name: nampespaceName}, namespace)
+ if err == nil {
+ continue
+ }
+ if apierrors.IsNotFound(err) {
+ err := r.Client.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nampespaceName}})
+ if err != nil {
+ return err
+ }
+ } else {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/controllers/teamrbac/teamrolebinding_controller_test.go b/pkg/controllers/teamrbac/teamrolebinding_controller_test.go
index 32b1b75fe..769867058 100644
--- a/pkg/controllers/teamrbac/teamrolebinding_controller_test.go
+++ b/pkg/controllers/teamrbac/teamrolebinding_controller_test.go
@@ -96,7 +96,12 @@ var _ = Describe("Validate ClusterRole & RoleBinding on Remote Cluster", Ordered
trb := setup.CreateTeamRoleBinding(test.Ctx, "test-rolebinding",
test.WithTeamRoleRef(teamRoleUT.Name),
test.WithTeamRef(teamUT.Name),
- test.WithClusterName(clusterA.Name))
+ test.WithClusterName(clusterA.Name),
+ test.WithSubject(rbacv1.Subject{
+ Kind: rbacv1.UserKind,
+ APIGroup: rbacv1.GroupName,
+ Name: "test-user-1",
+ }))
trbKey := types.NamespacedName{Name: trb.Name, Namespace: trb.Namespace}
By("validating the RoleBinding created on the remote clusterA")
remoteRoleBinding := &rbacv1.ClusterRoleBinding{}
@@ -146,6 +151,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) {
@@ -312,7 +318,12 @@ 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.WithSubject(rbacv1.Subject{
+ Kind: rbacv1.UserKind,
+ APIGroup: rbacv1.GroupName,
+ Name: "test-user-1",
+ }))
By("validating the RoleBinding created on the remote cluster")
remoteRoleBinding := &rbacv1.RoleBinding{}
@@ -326,6 +337,7 @@ var _ = Describe("Validate ClusterRole & RoleBinding on Remote Cluster", Ordered
}).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{}
@@ -386,6 +398,26 @@ 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(clusterA.Name),
+ test.WithNamespaces("non-existing-namespace"),
+ test.WithCreateNamespace(true))
+
+ By("checking that the Namespace is created")
+ namespace := &corev1.Namespace{}
+ Eventually(func(g Gomega) {
+ err := test.K8sClient.Get(test.Ctx, types.NamespacedName{Name: "non-existing-namespace"}, 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() {
diff --git a/pkg/test/resources.go b/pkg/test/resources.go
index f5bdd686a..97d54a57f 100644
--- a/pkg/test/resources.go
+++ b/pkg/test/resources.go
@@ -305,6 +305,18 @@ func WithNamespaces(namespaces ...string) func(*greenhousev1alpha1.TeamRoleBindi
}
}
+func WithCreateNamespace(createNamespaces bool) func(*greenhousev1alpha1.TeamRoleBinding) {
+ return func(trb *greenhousev1alpha1.TeamRoleBinding) {
+ trb.Spec.CreateNamespaces = createNamespaces
+ }
+}
+
+func WithSubject(subject rbacv1.Subject) func(*greenhousev1alpha1.TeamRoleBinding) {
+ return func(trb *greenhousev1alpha1.TeamRoleBinding) {
+ trb.Spec.Subjects = append(trb.Spec.Subjects, subject)
+ }
+}
+
// NewTeamRoleBinding returns a greenhousev1alpha1.TeamRoleBinding object. Opts can be used to set the desired state of the TeamRoleBinding.
func NewTeamRoleBinding(ctx context.Context, name, namespace string, opts ...func(*greenhousev1alpha1.TeamRoleBinding)) *greenhousev1alpha1.TeamRoleBinding {
trb := &greenhousev1alpha1.TeamRoleBinding{