diff --git a/cmd/greenhouse/controllers.go b/cmd/greenhouse/controllers.go index bc9d4d3d1..9803b9ee7 100644 --- a/cmd/greenhouse/controllers.go +++ b/cmd/greenhouse/controllers.go @@ -20,11 +20,7 @@ import ( // knownControllers contains all controllers to be registered when starting the operator. var knownControllers = map[string]func(controllerName string, mgr ctrl.Manager) error{ // Organization controllers. - "organizationController": (&organizationcontrollers.OrganizationReconciler{}).SetupWithManager, - "organizationRBAC": (&organizationcontrollers.RBACReconciler{}).SetupWithManager, - "organizationDEX": startOrganizationDexReconciler, - "organizationServiceProxy": (&organizationcontrollers.ServiceProxyReconciler{}).SetupWithManager, - "organizationTeamRoleSeeder": (&organizationcontrollers.TeamRoleSeederReconciler{}).SetupWithManager, + "organizationController": startOrganizationReconciler, // Team controllers. "teamPropagation": (&teamcontrollers.TeamPropagationReconciler{}).SetupWithManager, @@ -75,12 +71,12 @@ func isControllerEnabled(controllerName string) bool { return false } -func startOrganizationDexReconciler(name string, mgr ctrl.Manager) error { +func startOrganizationReconciler(name string, mgr ctrl.Manager) error { namespace := "greenhouse" if v, ok := os.LookupEnv("POD_NAMESPACE"); ok { namespace = v } - return (&organizationcontrollers.DexReconciler{ + return (&organizationcontrollers.OrganizationReconciler{ Namespace: namespace, }).SetupWithManager(name, mgr) } diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index 0d8756e5e..bce8de460 100755 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: Greenhouse - version: ca963d1 + version: 388f129 description: PlusOne operations platform paths: /TeamMembership: diff --git a/pkg/apis/greenhouse/v1alpha1/organization_types.go b/pkg/apis/greenhouse/v1alpha1/organization_types.go index da14e1862..0fb58cc71 100644 --- a/pkg/apis/greenhouse/v1alpha1/organization_types.go +++ b/pkg/apis/greenhouse/v1alpha1/organization_types.go @@ -16,6 +16,23 @@ const ( SCIMRequestFailedReason ConditionReason = "SCIMRequestFailed" // SCIMConfigNotProvidedReason is set when scim config is not present in spec as it is optional SCIMConfigNotProvidedReason ConditionReason = "SCIMConfigNotProvided" + + // NamespaceCreated is set when the namespace for organization is created. + NamespaceCreated ConditionType = "NamespaceCreated" + // OrganizationRBACConfigured is set when the RBAC for organization is configured + OrganizationRBACConfigured ConditionType = "OrganizationRBACConfigured" + // OrganizationDefaultTeamRolesConfigured is set when default team roles are configured + OrganizationDefaultTeamRolesConfigured ConditionType = "OrganizationDefaultTeamRolesConfigured" + // ServiceProxyProvisioned is set when the service proxy is provisioned + ServiceProxyProvisioned ConditionType = "ServiceProxyProvisioned" + // OrganizationOICDConfigured is set when the OICD is configured + OrganizationOICDConfigured ConditionType = "OrganizationOICDConfigured" + // DexReconcileFailed is set when dex reconcile step has failed + DexReconcileFailed ConditionReason = "DexReconcileFailed" + // OAuthOICDFailed is set when OAuth reconciler has failed + OAuthOICDFailed ConditionReason = "OAuthOICDFailed" + // OrganizationAdminTeamConfigured is set when the admin team is configured for organization + OrganizationAdminTeamConfigured ConditionType = "OrganizationAdminTeamConfigured" ) // OrganizationSpec defines the desired state of Organization diff --git a/pkg/controllers/organization/dex_controller.go b/pkg/controllers/organization/dex.go similarity index 72% rename from pkg/controllers/organization/dex_controller.go rename to pkg/controllers/organization/dex.go index 936fb4d78..b73c7c13d 100644 --- a/pkg/controllers/organization/dex_controller.go +++ b/pkg/controllers/organization/dex.go @@ -18,31 +18,20 @@ import ( corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" greenhousesapv1alpha1 "github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1" "github.com/cloudoperators/greenhouse/pkg/clientutil" "github.com/cloudoperators/greenhouse/pkg/common" dexapi "github.com/cloudoperators/greenhouse/pkg/dex/api" - "github.com/cloudoperators/greenhouse/pkg/lifecycle" "github.com/cloudoperators/greenhouse/pkg/util" ) const dexConnectorTypeGreenhouse = "greenhouse-oidc" -// DexReconciler reconciles a Organization object -type DexReconciler struct { - client.Client - recorder record.EventRecorder - Namespace string -} - //+kubebuilder:rbac:groups=greenhouse.sap,resources=organizations,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=greenhouse.sap,resources=organizations/status,verbs=get;update;patch //+kubebuilder:rbac:groups=greenhouse.sap,resources=organizations/finalizers,verbs=update @@ -51,52 +40,7 @@ type DexReconciler struct { //+kubebuilder:rbac:groups=dex.coreos.com,resources=connectors;oauth2clients,verbs=get;list;watch;create;update;patch //+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch -// SetupWithManager sets up the controller with the Manager. -func (r *DexReconciler) SetupWithManager(name string, mgr ctrl.Manager) error { - r.Client = mgr.GetClient() - r.recorder = mgr.GetEventRecorderFor(name) - if r.Namespace == "" { - return errors.New("namespace required but missing") - } - return ctrl.NewControllerManagedBy(mgr). - Named(name). - For(&greenhousesapv1alpha1.Organization{}, - builder.WithPredicates(clientutil.PredicateHasOICDConfigured())). - Owns(&dexapi.Connector{}). - Owns(&dexapi.OAuth2Client{}). - // Watch secrets referenced by organizations for confidential values. - Watches(&corev1.Secret{}, - handler.EnqueueRequestsFromMapFunc(r.enqueueOrganizationForReferencedSecret), - builder.WithPredicates(clientutil.PredicateHasOICDConfigured())). - Complete(r) -} - -func (r *DexReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - return lifecycle.Reconcile(ctx, r.Client, req.NamespacedName, &greenhousesapv1alpha1.Organization{}, r, noStatus()) -} - -func (r *DexReconciler) EnsureDeleted(_ context.Context, _ lifecycle.RuntimeObject) (ctrl.Result, lifecycle.ReconcileResult, error) { - return ctrl.Result{}, lifecycle.Success, nil // nothing to do in that case -} - -func (r *DexReconciler) EnsureCreated(ctx context.Context, object lifecycle.RuntimeObject) (ctrl.Result, lifecycle.ReconcileResult, error) { - org, ok := object.(*greenhousesapv1alpha1.Organization) - if !ok { - return ctrl.Result{}, lifecycle.Failed, errors.Errorf("RuntimeObject has incompatible type.") - } - - if err := r.reconcileDexConnector(ctx, org); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - - if err := r.reconcileOAuth2Client(ctx, org); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - - return ctrl.Result{}, lifecycle.Success, nil -} - -func (r *DexReconciler) reconcileDexConnector(ctx context.Context, org *greenhousesapv1alpha1.Organization) error { +func (r *OrganizationReconciler) reconcileDexConnector(ctx context.Context, org *greenhousesapv1alpha1.Organization) error { clientID, err := clientutil.GetSecretKeyFromSecretKeyReference(ctx, r.Client, org.Name, org.Spec.Authentication.OIDCConfig.ClientIDReference) if err != nil { return err @@ -147,7 +91,7 @@ func (r *DexReconciler) reconcileDexConnector(ctx context.Context, org *greenhou return nil } -func (r *DexReconciler) enqueueOrganizationForReferencedSecret(_ context.Context, o client.Object) []ctrl.Request { +func (r *OrganizationReconciler) enqueueOrganizationForReferencedSecret(_ context.Context, o client.Object) []ctrl.Request { var org = new(greenhousesapv1alpha1.Organization) if err := r.Get(context.Background(), types.NamespacedName{Namespace: "", Name: o.GetNamespace()}, org); err != nil { return nil @@ -155,7 +99,7 @@ func (r *DexReconciler) enqueueOrganizationForReferencedSecret(_ context.Context return []ctrl.Request{{NamespacedName: client.ObjectKeyFromObject(org)}} } -func (r *DexReconciler) discoverOIDCRedirectURL(ctx context.Context, org *greenhousesapv1alpha1.Organization) (string, error) { +func (r *OrganizationReconciler) discoverOIDCRedirectURL(ctx context.Context, org *greenhousesapv1alpha1.Organization) (string, error) { if r := org.Spec.Authentication.OIDCConfig.RedirectURI; r != "" { return r, nil } @@ -173,7 +117,7 @@ func (r *DexReconciler) discoverOIDCRedirectURL(ctx context.Context, org *greenh return "", errors.New("oidc redirect URL not provided and cannot be discovered") } -func (r *DexReconciler) reconcileOAuth2Client(ctx context.Context, org *greenhousesapv1alpha1.Organization) error { +func (r *OrganizationReconciler) reconcileOAuth2Client(ctx context.Context, org *greenhousesapv1alpha1.Organization) error { var oAuth2Client = new(dexapi.OAuth2Client) oAuth2Client.ObjectMeta.Name = encodedOAuth2ClientName(org.Name) oAuth2Client.ObjectMeta.Namespace = r.Namespace diff --git a/pkg/controllers/organization/organization_controller.go b/pkg/controllers/organization/organization_controller.go index cd123f8c0..77c968fc2 100644 --- a/pkg/controllers/organization/organization_controller.go +++ b/pkg/controllers/organization/organization_controller.go @@ -8,14 +8,19 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" greenhousesapv1alpha1 "github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1" "github.com/cloudoperators/greenhouse/pkg/clientutil" + dexapi "github.com/cloudoperators/greenhouse/pkg/dex/api" "github.com/cloudoperators/greenhouse/pkg/lifecycle" "github.com/cloudoperators/greenhouse/pkg/scim" ) @@ -25,21 +30,35 @@ var ( exposedConditions = []greenhousesapv1alpha1.ConditionType{ greenhousesapv1alpha1.ReadyCondition, greenhousesapv1alpha1.SCIMAPIAvailableCondition, + greenhousesapv1alpha1.ServiceProxyProvisioned, + greenhousesapv1alpha1.OrganizationOICDConfigured, + greenhousesapv1alpha1.OrganizationAdminTeamConfigured, + greenhousesapv1alpha1.ServiceProxyProvisioned, + greenhousesapv1alpha1.OrganizationDefaultTeamRolesConfigured, + greenhousesapv1alpha1.NamespaceCreated, + greenhousesapv1alpha1.OrganizationRBACConfigured, } ) // OrganizationReconciler reconciles an Organization object type OrganizationReconciler struct { client.Client - recorder record.EventRecorder + recorder record.EventRecorder + Namespace string } //+kubebuilder:rbac:groups=greenhouse.sap,resources=organizations,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=greenhouse.sap,resources=organizations/status,verbs=get;update;patch //+kubebuilder:rbac:groups=greenhouse.sap,resources=organizations/finalizers,verbs=update //+kubebuilder:rbac:groups=greenhouse.sap,resources=teams,verbs=get;watch;create;update;patch +//+kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=clusterroles;clusterrolebindings;roles;rolebindings,verbs=get;list;watch;create;update;patch +//+kubebuilder:rbac:groups=greenhouse.sap,resources=plugindefinitions,verbs=get;list;watch +//+kubebuilder:rbac:groups=greenhouse.sap,resources=plugins,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=greenhouse.sap,resources=teamroles,verbs=get;list;watch;create;update;patch //+kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch //+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch;create;update;patch +//+kubebuilder:rbac:groups=dex.coreos.com,resources=connectors;oauth2clients,verbs=get;list;watch;create;update;patch +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch // SetupWithManager sets up the controller with the Manager. func (r *OrganizationReconciler) SetupWithManager(name string, mgr ctrl.Manager) error { @@ -50,6 +69,23 @@ func (r *OrganizationReconciler) SetupWithManager(name string, mgr ctrl.Manager) For(&greenhousesapv1alpha1.Organization{}). Owns(&corev1.Namespace{}). Owns(&greenhousesapv1alpha1.Team{}). + Owns(&greenhousesapv1alpha1.TeamRole{}). + Owns(&greenhousesapv1alpha1.Plugin{}). + Owns(&rbacv1.Role{}). + Owns(&rbacv1.RoleBinding{}). + Owns(&rbacv1.ClusterRole{}). + Owns(&rbacv1.ClusterRoleBinding{}). + Owns(&dexapi.Connector{}). + Owns(&dexapi.OAuth2Client{}). + Watches(&corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.enqueueOrganizationForReferencedSecret), + builder.WithPredicates(clientutil.PredicateHasOICDConfigured())). + Watches(&greenhousesapv1alpha1.PluginDefinition{}, + handler.EnqueueRequestsFromMapFunc(r.enqueueAllOrganizationsForServiceProxyPluginDefinition), + builder.WithPredicates(predicate.And( + clientutil.PredicateByName(serviceProxyName), + predicate.GenerationChangedPredicate{}, + ))). Complete(r) } @@ -70,12 +106,47 @@ func (r *OrganizationReconciler) EnsureCreated(ctx context.Context, object lifec initOrganizationStatus(org) if err := r.reconcileNamespace(ctx, org); err != nil { + org.SetCondition(greenhousesapv1alpha1.FalseCondition(greenhousesapv1alpha1.NamespaceCreated, "", err.Error())) return ctrl.Result{}, lifecycle.Failed, err } + org.SetCondition(greenhousesapv1alpha1.TrueCondition(greenhousesapv1alpha1.NamespaceCreated, "", "")) + + if err := r.reconcileRBAC(ctx, org); err != nil { + org.SetCondition(greenhousesapv1alpha1.FalseCondition(greenhousesapv1alpha1.OrganizationRBACConfigured, "", err.Error())) + return ctrl.Result{}, lifecycle.Failed, err + } + org.SetCondition(greenhousesapv1alpha1.TrueCondition(greenhousesapv1alpha1.OrganizationRBACConfigured, "", "")) + + if err := r.reconcileDefaultTeamRoles(ctx, org); err != nil { + org.SetCondition(greenhousesapv1alpha1.FalseCondition(greenhousesapv1alpha1.OrganizationDefaultTeamRolesConfigured, "", err.Error())) + return ctrl.Result{}, lifecycle.Failed, err + } + org.SetCondition(greenhousesapv1alpha1.TrueCondition(greenhousesapv1alpha1.OrganizationDefaultTeamRolesConfigured, "", "")) + + if err := r.reconcileServiceProxy(ctx, org); err != nil { + org.SetCondition(greenhousesapv1alpha1.FalseCondition(greenhousesapv1alpha1.ServiceProxyProvisioned, "", err.Error())) + return ctrl.Result{}, lifecycle.Failed, err + } + org.SetCondition(greenhousesapv1alpha1.TrueCondition(greenhousesapv1alpha1.ServiceProxyProvisioned, "", "")) + + if org.Spec.Authentication != nil && org.Spec.Authentication.OIDCConfig != nil { + if err := r.reconcileDexConnector(ctx, org); err != nil { + org.SetCondition(greenhousesapv1alpha1.FalseCondition(greenhousesapv1alpha1.OrganizationOICDConfigured, greenhousesapv1alpha1.DexReconcileFailed, "")) + return ctrl.Result{}, lifecycle.Failed, err + } + + if err := r.reconcileOAuth2Client(ctx, org); err != nil { + org.SetCondition(greenhousesapv1alpha1.FalseCondition(greenhousesapv1alpha1.OrganizationOICDConfigured, greenhousesapv1alpha1.OAuthOICDFailed, err.Error())) + return ctrl.Result{}, lifecycle.Failed, err + } + org.SetCondition(greenhousesapv1alpha1.TrueCondition(greenhousesapv1alpha1.OrganizationOICDConfigured, "", "")) + } if err := r.reconcileAdminTeam(ctx, org); err != nil { + org.SetCondition(greenhousesapv1alpha1.FalseCondition(greenhousesapv1alpha1.OrganizationAdminTeamConfigured, "", err.Error())) return ctrl.Result{}, lifecycle.Failed, err } + org.SetCondition(greenhousesapv1alpha1.TrueCondition(greenhousesapv1alpha1.OrganizationAdminTeamConfigured, "", "")) return ctrl.Result{}, lifecycle.Success, nil } @@ -127,6 +198,52 @@ func (r *OrganizationReconciler) reconcileAdminTeam(ctx context.Context, org *gr return nil } +func (r *OrganizationReconciler) reconcileRBAC(ctx context.Context, org *greenhousesapv1alpha1.Organization) error { + // NOTE: The below code is intentionally rather explicit for transparency reasons as several Kubernetes resources + // are involved granting permissions on both cluster and namespace level based on organization, team membership and roles. + // The PolicyRules can be found in the pkg/rbac/role. + + // RBAC for organization admins for cluster- and namespace-scoped resources. + if err := r.reconcileClusterRole(ctx, org, admin); err != nil { + return err + } + if err := r.reconcileClusterRoleBinding(ctx, org, admin); err != nil { + return err + } + if err := r.reconcileRole(ctx, org, admin); err != nil { + return err + } + if err := r.reconcileRoleBinding(ctx, org, admin); err != nil { + return err + } + + // RBAC for organization members for cluster- and namespace-scoped resources. + if err := r.reconcileClusterRole(ctx, org, member); err != nil { + return err + } + if err := r.reconcileClusterRoleBinding(ctx, org, member); err != nil { + return err + } + if err := r.reconcileRole(ctx, org, member); err != nil { + return err + } + if err := r.reconcileRoleBinding(ctx, org, member); err != nil { + return err + } + + // RBAC roles for organization cluster admins to access namespace-scoped resources. + if err := r.reconcileRole(ctx, org, clusterAdmin); err != nil { + return err + } + + // RBAC roles for organization plugin admins to access namespace-scoped resources. + if err := r.reconcileRole(ctx, org, pluginAdmin); err != nil { + return err + } + + return nil +} + func (r *OrganizationReconciler) checkSCIMAPIAvailability(ctx context.Context, org *greenhousesapv1alpha1.Organization) greenhousesapv1alpha1.Condition { if org.Spec.Authentication == nil || org.Spec.Authentication.SCIMConfig == nil { // SCIM Config is optional. diff --git a/pkg/controllers/organization/organization_controller_test.go b/pkg/controllers/organization/organization_controller_test.go index ec5d1078e..73b8378f1 100644 --- a/pkg/controllers/organization/organization_controller_test.go +++ b/pkg/controllers/organization/organization_controller_test.go @@ -194,27 +194,29 @@ var _ = Describe("Test Organization reconciliation", Ordered, func() { }).Should(Succeed(), "Organization should have set correct status condition") By("updating Organization with SCIM Config without the secret") - err := setup.Get(test.Ctx, types.NamespacedName{Name: testOrgName}, testOrg) - Expect(err).ToNot(HaveOccurred(), "there should be no error getting the Organization") - testOrg.Spec.Authentication = &greenhousev1alpha1.Authentication{ - SCIMConfig: &greenhousev1alpha1.SCIMConfig{ - BaseURL: groupsServer.URL, - BasicAuthUser: greenhousev1alpha1.ValueFromSource{ - Secret: &greenhousev1alpha1.SecretKeyReference{ - Name: "test-secret", - Key: "basicAuthUser", + Eventually(func(g Gomega) { // In 'Eventually' block to avoid flaky tests. + err := setup.Get(test.Ctx, types.NamespacedName{Name: testOrgName}, testOrg) + g.Expect(err).ToNot(HaveOccurred(), "there should be no error getting the Organization") + testOrg.Spec.Authentication = &greenhousev1alpha1.Authentication{ + SCIMConfig: &greenhousev1alpha1.SCIMConfig{ + BaseURL: groupsServer.URL, + BasicAuthUser: greenhousev1alpha1.ValueFromSource{ + Secret: &greenhousev1alpha1.SecretKeyReference{ + Name: "test-secret", + Key: "basicAuthUser", + }, }, - }, - BasicAuthPw: greenhousev1alpha1.ValueFromSource{ - Secret: &greenhousev1alpha1.SecretKeyReference{ - Name: "test-secret", - Key: "basicAuthPw", + BasicAuthPw: greenhousev1alpha1.ValueFromSource{ + Secret: &greenhousev1alpha1.SecretKeyReference{ + Name: "test-secret", + Key: "basicAuthPw", + }, }, }, - }, - } - err = setup.Update(test.Ctx, testOrg) - Expect(err).ToNot(HaveOccurred(), "there should be no error updating the Organization") + } + err = setup.Update(test.Ctx, testOrg) + g.Expect(err).ToNot(HaveOccurred(), "there should be no error updating the Organization") + }).Should(Succeed(), "Organization should have set correct status condition") By("checking Organization status") Eventually(func(g Gomega) { @@ -232,7 +234,7 @@ var _ = Describe("Test Organization reconciliation", Ordered, func() { createSecretForSCIMConfig(testOrgName) By("setting labels on Organization to trigger reconciliation") - err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { err := setup.Get(test.Ctx, types.NamespacedName{Name: testOrgName}, testOrg) Expect(err).ToNot(HaveOccurred(), "there should be no error getting the Organization") testOrg.Labels = map[string]string{"test": "label"} diff --git a/pkg/controllers/organization/rbac_controller.go b/pkg/controllers/organization/rbac.go similarity index 58% rename from pkg/controllers/organization/rbac_controller.go rename to pkg/controllers/organization/rbac.go index 5b527edcf..dac1a957d 100644 --- a/pkg/controllers/organization/rbac_controller.go +++ b/pkg/controllers/organization/rbac.go @@ -7,18 +7,13 @@ import ( "context" "fmt" - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/client-go/tools/record" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" greenhouseapisv1alpha1 "github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1" "github.com/cloudoperators/greenhouse/pkg/clientutil" - "github.com/cloudoperators/greenhouse/pkg/lifecycle" "github.com/cloudoperators/greenhouse/pkg/rbac" ) @@ -31,92 +26,7 @@ const ( pluginAdmin ) -// RBACReconciler reconciles an Organization object and manages RBAC permissions based on organization and team membership. -type RBACReconciler struct { - client.Client - recorder record.EventRecorder -} - -//+kubebuilder:rbac:groups=greenhouse.sap,resources=organizations,verbs=get;list;watch;create;update;patch -//+kubebuilder:rbac:groups=greenhouse.sap,resources=organizations/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=greenhouse.sap,resources=organizations/finalizers,verbs=update -//+kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=clusterroles;clusterrolebindings;roles;rolebindings,verbs=get;list;watch;create;update;patch -//+kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch;delete - -// SetupWithManager sets up the controller with the Manager. -func (r *RBACReconciler) SetupWithManager(name string, mgr ctrl.Manager) error { - r.Client = mgr.GetClient() - r.recorder = mgr.GetEventRecorderFor(name) - return ctrl.NewControllerManagedBy(mgr). - Named(name). - For(&greenhouseapisv1alpha1.Organization{}). - Owns(&rbacv1.Role{}). - Owns(&rbacv1.RoleBinding{}). - Owns(&rbacv1.ClusterRole{}). - Owns(&rbacv1.ClusterRoleBinding{}). - Complete(r) -} - -func (r *RBACReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - return lifecycle.Reconcile(ctx, r.Client, req.NamespacedName, &greenhouseapisv1alpha1.Organization{}, r, noStatus()) -} - -func (r *RBACReconciler) EnsureDeleted(_ context.Context, _ lifecycle.RuntimeObject) (ctrl.Result, lifecycle.ReconcileResult, error) { - return ctrl.Result{}, lifecycle.Success, nil // nothing to do in that case -} - -func (r *RBACReconciler) EnsureCreated(ctx context.Context, object lifecycle.RuntimeObject) (ctrl.Result, lifecycle.ReconcileResult, error) { - org, ok := object.(*greenhouseapisv1alpha1.Organization) - if !ok { - return ctrl.Result{}, lifecycle.Failed, errors.Errorf("RuntimeObject has incompatible type.") - } - - // NOTE: The below code is intentionally rather explicit for transparency reasons as several Kubernetes resources - // are involved granting permissions on both cluster and namespace level based on organization, team membership and roles. - // The PolicyRules can be found in the pkg/rbac/role. - - // RBAC for organization admins for cluster- and namespace-scoped resources. - if err := r.reconcileClusterRole(ctx, org, admin); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - if err := r.reconcileClusterRoleBinding(ctx, org, admin); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - if err := r.reconcileRole(ctx, org, admin); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - if err := r.reconcileRoleBinding(ctx, org, admin); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - - // RBAC for organization members for cluster- and namespace-scoped resources. - if err := r.reconcileClusterRole(ctx, org, member); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - if err := r.reconcileClusterRoleBinding(ctx, org, member); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - if err := r.reconcileRole(ctx, org, member); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - if err := r.reconcileRoleBinding(ctx, org, member); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - - // RBAC roles for organization cluster admins to access namespace-scoped resources. - if err := r.reconcileRole(ctx, org, clusterAdmin); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - - // RBAC roles for organization plugin admins to access namespace-scoped resources. - if err := r.reconcileRole(ctx, org, pluginAdmin); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - - return ctrl.Result{}, lifecycle.Success, nil -} - -func (r *RBACReconciler) reconcileClusterRole(ctx context.Context, org *greenhouseapisv1alpha1.Organization, group userGroup) error { +func (r *OrganizationReconciler) reconcileClusterRole(ctx context.Context, org *greenhouseapisv1alpha1.Organization, group userGroup) error { var clusterRoleName string var clusterRoleRules []rbacv1.PolicyRule @@ -154,7 +64,7 @@ func (r *RBACReconciler) reconcileClusterRole(ctx context.Context, org *greenhou return nil } -func (r *RBACReconciler) reconcileClusterRoleBinding(ctx context.Context, org *greenhouseapisv1alpha1.Organization, group userGroup) error { +func (r *OrganizationReconciler) reconcileClusterRoleBinding(ctx context.Context, org *greenhouseapisv1alpha1.Organization, group userGroup) error { var clusterRoleBindingName = "" switch group { @@ -199,7 +109,7 @@ func (r *RBACReconciler) reconcileClusterRoleBinding(ctx context.Context, org *g return nil } -func (r *RBACReconciler) reconcileRole(ctx context.Context, org *greenhouseapisv1alpha1.Organization, group userGroup) error { +func (r *OrganizationReconciler) reconcileRole(ctx context.Context, org *greenhouseapisv1alpha1.Organization, group userGroup) error { var roleName string var roleRules []rbacv1.PolicyRule @@ -242,7 +152,7 @@ func (r *RBACReconciler) reconcileRole(ctx context.Context, org *greenhouseapisv return nil } -func (r *RBACReconciler) reconcileRoleBinding(ctx context.Context, org *greenhouseapisv1alpha1.Organization, group userGroup) error { +func (r *OrganizationReconciler) reconcileRoleBinding(ctx context.Context, org *greenhouseapisv1alpha1.Organization, group userGroup) error { var roleBindingName = "" switch group { diff --git a/pkg/controllers/organization/rbac_controller_test.go b/pkg/controllers/organization/rbac_test.go similarity index 100% rename from pkg/controllers/organization/rbac_controller_test.go rename to pkg/controllers/organization/rbac_test.go diff --git a/pkg/controllers/organization/service_proxy_controller.go b/pkg/controllers/organization/service_proxy.go similarity index 58% rename from pkg/controllers/organization/service_proxy_controller.go rename to pkg/controllers/organization/service_proxy.go index a6ae4c33d..520bc3352 100644 --- a/pkg/controllers/organization/service_proxy_controller.go +++ b/pkg/controllers/organization/service_proxy.go @@ -8,80 +8,25 @@ import ( "encoding/json" "fmt" - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" greenhousesapv1alpha1 "github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1" "github.com/cloudoperators/greenhouse/pkg/clientutil" "github.com/cloudoperators/greenhouse/pkg/common" - "github.com/cloudoperators/greenhouse/pkg/lifecycle" "github.com/cloudoperators/greenhouse/pkg/version" ) const serviceProxyName = "service-proxy" -// ServiceProxyReconciler reconciles a ServiceProxy Plugin for a Organization object -type ServiceProxyReconciler struct { - client.Client - recorder record.EventRecorder -} - -//+kubebuilder:rbac:groups=greenhouse.sap,resources=organizations,verbs=get;list;watch -//+kubebuilder:rbac:groups=greenhouse.sap,resources=plugins,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=greenhouse.sap,resources=plugindefinitions,verbs=get;list;watch - -// SetupWithManager sets up the controller with the Manager. -func (r *ServiceProxyReconciler) SetupWithManager(name string, mgr ctrl.Manager) error { - r.Client = mgr.GetClient() - r.recorder = mgr.GetEventRecorderFor(name) - return ctrl.NewControllerManagedBy(mgr). - Named(name). - For(&greenhousesapv1alpha1.Organization{}). - Owns(&greenhousesapv1alpha1.Plugin{}). - // If the service-proxy PluginDefinition was changed, reconcile all Organizations. - Watches(&greenhousesapv1alpha1.PluginDefinition{}, - handler.EnqueueRequestsFromMapFunc(r.enqueueAllOrganizationsForServiceProxyPluginDefinition), - builder.WithPredicates(predicate.And( - clientutil.PredicateByName(serviceProxyName), - predicate.GenerationChangedPredicate{}, - ))). - Complete(r) -} - -func (r *ServiceProxyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - return lifecycle.Reconcile(ctx, r.Client, req.NamespacedName, &greenhousesapv1alpha1.Organization{}, r, noStatus()) -} - -func (r *ServiceProxyReconciler) EnsureDeleted(_ context.Context, _ lifecycle.RuntimeObject) (ctrl.Result, lifecycle.ReconcileResult, error) { - return ctrl.Result{}, lifecycle.Success, nil // nothing to do in that case -} - -func (r *ServiceProxyReconciler) EnsureCreated(ctx context.Context, object lifecycle.RuntimeObject) (ctrl.Result, lifecycle.ReconcileResult, error) { - org, ok := object.(*greenhousesapv1alpha1.Organization) - if !ok { - return ctrl.Result{}, lifecycle.Failed, errors.Errorf("RuntimeObject has incompatible type.") - } - - if err := r.reconcileServiceProxy(ctx, org); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - - return ctrl.Result{}, lifecycle.Success, nil -} - -func (r *ServiceProxyReconciler) reconcileServiceProxy(ctx context.Context, org *greenhousesapv1alpha1.Organization) error { +func (r *OrganizationReconciler) reconcileServiceProxy(ctx context.Context, org *greenhousesapv1alpha1.Organization) error { domain := fmt.Sprintf("%s.%s", org.Name, common.DNSDomain) domainJSON, err := json.Marshal(domain) if err != nil { @@ -140,7 +85,7 @@ func (r *ServiceProxyReconciler) reconcileServiceProxy(ctx context.Context, org return nil } -func (r *ServiceProxyReconciler) enqueueAllOrganizationsForServiceProxyPluginDefinition(ctx context.Context, o client.Object) []ctrl.Request { +func (r *OrganizationReconciler) enqueueAllOrganizationsForServiceProxyPluginDefinition(ctx context.Context, o client.Object) []ctrl.Request { return listOrganizationsAsReconcileRequests(ctx, r.Client) } diff --git a/pkg/controllers/organization/service_proxy_controller_test.go b/pkg/controllers/organization/service_proxy_test.go similarity index 100% rename from pkg/controllers/organization/service_proxy_controller_test.go rename to pkg/controllers/organization/service_proxy_test.go diff --git a/pkg/controllers/organization/suite_test.go b/pkg/controllers/organization/suite_test.go index aaeb64059..861f224cb 100644 --- a/pkg/controllers/organization/suite_test.go +++ b/pkg/controllers/organization/suite_test.go @@ -30,9 +30,6 @@ var _ = BeforeSuite(func() { groupsServer = scim.ReturnDefaultGroupResponseMockServer() test.RegisterController("organizationController", (&organizationpkg.OrganizationReconciler{}).SetupWithManager) - test.RegisterController("organizationRBAC", (&organizationpkg.RBACReconciler{}).SetupWithManager) - test.RegisterController("organizationServiceProxy", (&organizationpkg.ServiceProxyReconciler{}).SetupWithManager) - test.RegisterController("teamRoleSeeder", (&organizationpkg.TeamRoleSeederReconciler{}).SetupWithManager) test.RegisterWebhook("orgWebhook", admission.SetupOrganizationWebhookWithManager) test.RegisterWebhook("teamWebhook", admission.SetupTeamWebhookWithManager) test.RegisterWebhook("pluginDefinitionWebhook", admission.SetupPluginDefinitionWebhookWithManager) diff --git a/pkg/controllers/organization/teamrole_seeder_controller.go b/pkg/controllers/organization/teamrole_seeder.go similarity index 59% rename from pkg/controllers/organization/teamrole_seeder_controller.go rename to pkg/controllers/organization/teamrole_seeder.go index bb52d3e9b..51c4a3190 100644 --- a/pkg/controllers/organization/teamrole_seeder_controller.go +++ b/pkg/controllers/organization/teamrole_seeder.go @@ -6,19 +6,14 @@ package organization import ( "context" - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/record" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" greenhousesapv1alpha1 "github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1" "github.com/cloudoperators/greenhouse/pkg/clientutil" - "github.com/cloudoperators/greenhouse/pkg/lifecycle" ) var defaultTeamRoles = map[string]greenhousesapv1alpha1.TeamRoleSpec{ @@ -80,49 +75,7 @@ var defaultTeamRoles = map[string]greenhousesapv1alpha1.TeamRoleSpec{ }, } -// TeamRoleSeederReconciler reconciles a Organization object -type TeamRoleSeederReconciler struct { - client.Client - recorder record.EventRecorder -} - -//+kubebuilder:rbac:groups=greenhouse.sap,resources=organizations,verbs=get;list;watch -//+kubebuilder:rbac:groups=greenhouse.sap,resources=teamroles,verbs=get;list;watch;create;update;patch -//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch - -// SetupWithManager sets up the controller with the Manager. -func (r *TeamRoleSeederReconciler) SetupWithManager(name string, mgr ctrl.Manager) error { - r.Client = mgr.GetClient() - r.recorder = mgr.GetEventRecorderFor(name) - return ctrl.NewControllerManagedBy(mgr). - Named(name). - For(&greenhousesapv1alpha1.Organization{}). - Owns(&greenhousesapv1alpha1.TeamRole{}). - Complete(r) -} - -func (r *TeamRoleSeederReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - return lifecycle.Reconcile(ctx, r.Client, req.NamespacedName, &greenhousesapv1alpha1.Organization{}, r, noStatus()) -} - -func (r *TeamRoleSeederReconciler) EnsureDeleted(_ context.Context, _ lifecycle.RuntimeObject) (ctrl.Result, lifecycle.ReconcileResult, error) { - return ctrl.Result{}, lifecycle.Success, nil // nothing to do in that case -} - -func (r *TeamRoleSeederReconciler) EnsureCreated(ctx context.Context, object lifecycle.RuntimeObject) (ctrl.Result, lifecycle.ReconcileResult, error) { - org, ok := object.(*greenhousesapv1alpha1.Organization) - if !ok { - return ctrl.Result{}, lifecycle.Failed, errors.Errorf("RuntimeObject has incompatible type.") - } - - if err := r.reconcileDefaultTeamRoles(ctx, org); err != nil { - return ctrl.Result{}, lifecycle.Failed, err - } - - return ctrl.Result{}, lifecycle.Success, nil -} - -func (r *TeamRoleSeederReconciler) reconcileDefaultTeamRoles(ctx context.Context, org *greenhousesapv1alpha1.Organization) error { +func (r *OrganizationReconciler) reconcileDefaultTeamRoles(ctx context.Context, org *greenhousesapv1alpha1.Organization) error { for name, teamRoleSpec := range defaultTeamRoles { var tr = new(greenhousesapv1alpha1.TeamRole) tr.Name = name diff --git a/pkg/controllers/organization/teamrole_seeder_controller_test.go b/pkg/controllers/organization/teamrole_seeder_test.go similarity index 100% rename from pkg/controllers/organization/teamrole_seeder_controller_test.go rename to pkg/controllers/organization/teamrole_seeder_test.go diff --git a/pkg/controllers/organization/utils.go b/pkg/controllers/organization/utils.go deleted file mode 100644 index 2f3d8f321..000000000 --- a/pkg/controllers/organization/utils.go +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - -package organization - -import ( - "context" - - "github.com/cloudoperators/greenhouse/pkg/lifecycle" -) - -// noStatus - empty noStatus func to avoid continuous reconciliations for org controllers that don't implement conditions -// TODO: remove this once organization controllers are merged -func noStatus() lifecycle.Conditioner { - return func(_ context.Context, _ lifecycle.RuntimeObject) {} -} diff --git a/pkg/test/env.go b/pkg/test/env.go index 9eca61eef..50c1bc73c 100644 --- a/pkg/test/env.go +++ b/pkg/test/env.go @@ -30,6 +30,7 @@ import ( greenhousesapv1alpha1 "github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1" "github.com/cloudoperators/greenhouse/pkg/clientutil" + dexapi "github.com/cloudoperators/greenhouse/pkg/dex/api" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" ) @@ -253,6 +254,8 @@ func StartControlPlane(port string, installCRDs, installWebhooks bool) (*rest.Co To(Succeed(), "there must no error adding the clientgo api to the scheme") Expect(apiextensionsv1.AddToScheme(scheme.Scheme)). To(Succeed(), "there must be no error adding the apiextensions api to the scheme") + Expect(dexapi.AddToScheme(scheme.Scheme)). + To(Succeed(), "there must be no error adding the dex api to the scheme") // Create k8s client k8sClient, err := clientutil.NewK8sClient(cfg)