From 857c338c53afa93c1a2e1241999a0393b6beda0d Mon Sep 17 00:00:00 2001 From: Massimiliano Giovagnoli Date: Sat, 13 Aug 2022 18:59:01 +0200 Subject: [PATCH] wip Signed-off-by: Massimiliano Giovagnoli --- api/v1beta1/owner_role.go | 13 +- api/v1beta1/tenant_labels.go | 2 + .../crd/bases/capsule.clastix.io_tenants.yaml | 3 + controllers/tenant/clusterrolebindings.go | 119 ++++++++++++++++++ controllers/tenant/clusterroles.go | 3 - controllers/tenant/manager.go | 8 ++ controllers/tenant/utils.go | 41 ++++++ 7 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 controllers/tenant/clusterrolebindings.go diff --git a/api/v1beta1/owner_role.go b/api/v1beta1/owner_role.go index da9e6cb0..ccd5df7f 100644 --- a/api/v1beta1/owner_role.go +++ b/api/v1beta1/owner_role.go @@ -40,21 +40,30 @@ func (in OwnerSpec) GetRoles(tenant Tenant, index int) []string { roles := []string{"admin", "capsule-namespace-deleter"} + if tenant.Spec.GitOpsReady { + roles = append(roles, in.getGitOpsRoles(tenant)...) + } + return roles } func (in OwnerSpec) GetClusterRoles(tenant Tenant) []string { if tenant.Spec.GitOpsReady { - return in.getGitOpsRoles(tenant) + return in.getGitOpsClusterRoles(tenant) } return []string{} } +func (in OwnerSpec) getGitOpsClusterRoles(tenant Tenant) []string { + return []string{ + "capsule-tenant-impersonator-" + tenant.Name + "-" + in.Name, + } +} + func (in OwnerSpec) getGitOpsRoles(tenant Tenant) []string { return []string{ "cluster-admin", - "capsule-tenant-impersonator-" + tenant.Name + "-" + in.Name, } } diff --git a/api/v1beta1/tenant_labels.go b/api/v1beta1/tenant_labels.go index 836e6810..356e08f7 100644 --- a/api/v1beta1/tenant_labels.go +++ b/api/v1beta1/tenant_labels.go @@ -24,6 +24,8 @@ func GetTypeLabel(t runtime.Object) (label string, err error) { return "capsule.clastix.io/resource-quota", nil case *rbacv1.RoleBinding: return "capsule.clastix.io/role-binding", nil + case *rbacv1.ClusterRoleBinding: + return "capsule.clastix.io/cluster-role-binding", nil default: err = fmt.Errorf("type %T is not mapped as Capsule label recognized", v) } diff --git a/config/crd/bases/capsule.clastix.io_tenants.yaml b/config/crd/bases/capsule.clastix.io_tenants.yaml index 0298fb28..eb9d4fc8 100644 --- a/config/crd/bases/capsule.clastix.io_tenants.yaml +++ b/config/crd/bases/capsule.clastix.io_tenants.yaml @@ -654,6 +654,9 @@ spec: allowedRegex: type: string type: object + gitOpsReady: + description: Configured RBAC for machine owners tailored for GitOps controllers. + type: boolean imagePullPolicies: description: Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional. items: diff --git a/controllers/tenant/clusterrolebindings.go b/controllers/tenant/clusterrolebindings.go new file mode 100644 index 00000000..1a4176bc --- /dev/null +++ b/controllers/tenant/clusterrolebindings.go @@ -0,0 +1,119 @@ +package tenant + +import ( + "context" + "fmt" + "hash/fnv" + + "golang.org/x/sync/errgroup" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + capsulev1beta1 "github.com/clastix/capsule/api/v1beta1" +) + +// Sync the dynamic Tenant Owner specific cluster-roles and additional ClusterRole Bindings, which can be used in many ways: +// applying Pod Security Policies or giving access to CRDs or specific API groups. +func (r *Manager) syncClusterRoleBindings(ctx context.Context, tenant *capsulev1beta1.Tenant) (err error) { + // hashing the ClusterRoleBinding name due to DNS RFC-1123 applied to Kubernetes labels + hashFn := func(binding capsulev1beta1.AdditionalRoleBindingsSpec) string { + h := fnv.New64a() + + _, _ = h.Write([]byte(binding.ClusterRoleName)) + + for _, sub := range binding.Subjects { + _, _ = h.Write([]byte(sub.Kind + sub.Name)) + } + + return fmt.Sprintf("%x", h.Sum64()) + } + // getting requested Role Binding keys + keys := make([]string, 0, len(tenant.Spec.Owners)) + // Generating for dynamic tenant owners cluster roles + for _, owner := range tenant.Spec.Owners { + for _, clusterRoleName := range owner.GetClusterRoles(*tenant) { + + cr := r.ownerClusterRoleBindings(owner, clusterRoleName) + + keys = append(keys, hashFn(cr)) + } + } + + group := new(errgroup.Group) + + group.Go(func() error { + return r.syncClusterRoleBinding(ctx, tenant, keys, hashFn) + }) + + return group.Wait() +} + +func (r *Manager) syncClusterRoleBinding(ctx context.Context, tenant *capsulev1beta1.Tenant, keys []string, hashFn func(binding capsulev1beta1.AdditionalRoleBindingsSpec) string) (err error) { + + var tenantLabel string + + var clusterRoleBindingLabel string + + if tenantLabel, err = capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{}); err != nil { + return + } + + if clusterRoleBindingLabel, err = capsulev1beta1.GetTypeLabel(&rbacv1.ClusterRoleBinding{}); err != nil { + return + } + + if err = r.pruningClusterResources(ctx, keys, &rbacv1.ClusterRoleBinding{}); err != nil { + return + } + + var clusterRoleBindings []capsulev1beta1.AdditionalRoleBindingsSpec + + for _, owner := range tenant.Spec.Owners { + for _, clusterRoleName := range owner.GetClusterRoles(*tenant) { + clusterRoleBindings = append(clusterRoleBindings, r.ownerClusterRoleBindings(owner, clusterRoleName)) + } + } + + for i, clusterRoleBinding := range clusterRoleBindings { + + clusterRoleBindingHashLabel := hashFn(clusterRoleBinding) + + target := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("capsule-%s-%d-%s", tenant.Name, i, clusterRoleBinding.ClusterRoleName), + }, + } + + var res controllerutil.OperationResult + res, err = controllerutil.CreateOrUpdate(ctx, r.Client, target, func() error { + target.ObjectMeta.Labels = map[string]string{ + tenantLabel: tenant.Name, + clusterRoleBindingLabel: clusterRoleBindingHashLabel, + } + target.RoleRef = rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: clusterRoleBinding.ClusterRoleName, + } + target.Subjects = clusterRoleBinding.Subjects + + return controllerutil.SetControllerReference(tenant, target, r.Client.Scheme()) + }) + + // TODO: find appropriate event Namespace. + r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring ClusterRoleBinding %s", target.GetName()), err) + + if err != nil { + r.Log.Error(err, "Cannot sync ClusterRoleBinding") + } + + r.Log.Info(fmt.Sprintf("ClusterRoleBinding sync result: %s", string(res)), "name", target.Name, "namespace", target.Namespace) + + if err != nil { + return + } + } + + return nil +} diff --git a/controllers/tenant/clusterroles.go b/controllers/tenant/clusterroles.go index 21cf4757..9d8559b6 100644 --- a/controllers/tenant/clusterroles.go +++ b/controllers/tenant/clusterroles.go @@ -47,9 +47,6 @@ func (r *Manager) ensureOwnerClusterRole(ctx context.Context, tenant *capsulev1b } resourceName := owner.Name - if owner.Kind == capsulev1beta1.ServiceAccountOwner { - resourceName = owner.Name - } _, err = controllerutil.CreateOrUpdate(ctx, r.Client, clusterRole, func() error { clusterRole.Rules = []rbacv1.PolicyRule{ diff --git a/controllers/tenant/manager.go b/controllers/tenant/manager.go index 7e7a53c0..92def68f 100644 --- a/controllers/tenant/manager.go +++ b/controllers/tenant/manager.go @@ -113,6 +113,14 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct return } + // Ensuring ClusterRoleBindings resources + r.Log.Info("Ensuring ClusterRoleBindings for Owners and Tenant") + + if err = r.syncClusterRoleBindings(ctx, instance); err != nil { + r.Log.Error(err, "Cannot sync ClusterRoleBindings items") + + return + } // Ensuring RoleBinding resources r.Log.Info("Ensuring RoleBindings for Owners and Tenant") diff --git a/controllers/tenant/utils.go b/controllers/tenant/utils.go index 00b47518..da92f760 100644 --- a/controllers/tenant/utils.go +++ b/controllers/tenant/utils.go @@ -14,6 +14,47 @@ import ( capsulev1beta1 "github.com/clastix/capsule/api/v1beta1" ) +// pruningClusterResources is taking care of removing the no more requested sub-resources as LimitRange, ResourceQuota or +// NetworkPolicy using the "exists" and "notin" LabelSelector to perform an outer-join removal. +func (r *Manager) pruningClusterResources(ctx context.Context, keys []string, obj client.Object) (err error) { + var capsuleLabel string + + if capsuleLabel, err = capsulev1beta1.GetTypeLabel(obj); err != nil { + return + } + + selector := labels.NewSelector() + + var exists *labels.Requirement + + if exists, err = labels.NewRequirement(capsuleLabel, selection.Exists, []string{}); err != nil { + return + } + + selector = selector.Add(*exists) + + if len(keys) > 0 { + var notIn *labels.Requirement + + if notIn, err = labels.NewRequirement(capsuleLabel, selection.NotIn, keys); err != nil { + return err + } + + selector = selector.Add(*notIn) + } + + r.Log.Info("Pruning objects with label selector " + selector.String()) + + return retry.RetryOnConflict(retry.DefaultBackoff, func() error { + return r.DeleteAllOf(ctx, obj, &client.DeleteAllOfOptions{ + ListOptions: client.ListOptions{ + LabelSelector: selector, + }, + DeleteOptions: client.DeleteOptions{}, + }) + }) +} + // pruningResources is taking care of removing the no more requested sub-resources as LimitRange, ResourceQuota or // NetworkPolicy using the "exists" and "notin" LabelSelector to perform an outer-join removal. func (r *Manager) pruningResources(ctx context.Context, ns string, keys []string, obj client.Object) (err error) {