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

WIP: feat/gitops ready rbac #631

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion api/v1beta1/owner_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,33 @@ func (in OwnerSpec) GetRoles(tenant Tenant, index int) []string {
}
}

return []string{"admin", "capsule-namespace-deleter"}
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.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",
}
}

func (in OwnerSpec) convertMap() map[string]string {
Expand Down
2 changes: 2 additions & 0 deletions api/v1beta1/tenant_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 2 additions & 0 deletions api/v1beta1/tenant_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type TenantSpec struct {
ImagePullPolicies []ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
// Specifies the allowed priorityClasses assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed PriorityClasses. Optional.
PriorityClasses *AllowedListSpec `json:"priorityClasses,omitempty"`
// Configured RBAC for machine owners tailored for GitOps controllers.
GitOpsReady bool `json:"gitOpsReady,omitempty"`
}

//+kubebuilder:object:root=true
Expand Down
3 changes: 3 additions & 0 deletions config/crd/bases/capsule.clastix.io_tenants.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
119 changes: 119 additions & 0 deletions controllers/tenant/clusterrolebindings.go
Original file line number Diff line number Diff line change
@@ -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
}
66 changes: 66 additions & 0 deletions controllers/tenant/clusterroles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package tenant

import (
"context"

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"
)

const (
ImpersonatorRoleName = "capsule-tenant-impersonator"
)

// Sync the Tenant Owner specific cluster-roles.
// When the Tenant is configured GitOpsReady additional (Cluster)Roles are created, then bound.
func (r *Manager) syncClusterRoles(ctx context.Context, tenant *capsulev1beta1.Tenant) (err error) {

// If the Tenant will be reconciled the GitOps-way,
// Tenant Owners might be machine GitOps reconciler identities.
if tenant.Spec.GitOpsReady {
for _, owner := range tenant.Spec.Owners {
if err = r.ensureOwnerClusterRole(ctx, tenant, &owner, ImpersonatorRoleName); err != nil {
r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", ImpersonatorRoleName)
return err
}
}
}

return
}

func (r *Manager) ensureOwnerClusterRole(ctx context.Context, tenant *capsulev1beta1.Tenant, owner *capsulev1beta1.OwnerSpec, roleName string) (err error) {
switch roleName {
case ImpersonatorRoleName:
clusterRole := &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: roleName + "-" + tenant.Name + "-" + owner.Name,
},
}

resource := "users"
if owner.Kind == capsulev1beta1.GroupOwner {
resource = "groups"
}

resourceName := owner.Name

_, err = controllerutil.CreateOrUpdate(ctx, r.Client, clusterRole, func() error {
clusterRole.Rules = []rbacv1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{resource},
Verbs: []string{"impersonate"},
ResourceNames: []string{resourceName},
},
}

return nil
})
}

return
}
16 changes: 16 additions & 0 deletions controllers/tenant/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct

return
}
// Ensuring ClusterRoles resources
r.Log.Info("Ensuring ClusterRoles for Owners and Tenant")

if err = r.syncClusterRoles(ctx, instance); err != nil {
r.Log.Error(err, "Cannot sync ClusterRoles items")

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")

Expand Down
41 changes: 41 additions & 0 deletions controllers/tenant/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down