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{