mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-02-20 04:49:52 +00:00
Compare commits
6 Commits
v0.5.0
...
maxgio92/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
857c338c53 | ||
|
|
5a9c25b125 | ||
|
|
3cd7bfe6d4 | ||
|
|
ff53cc2f38 | ||
|
|
852ab16323 | ||
|
|
9c18471879 |
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
119
controllers/tenant/clusterrolebindings.go
Normal file
119
controllers/tenant/clusterrolebindings.go
Normal file
@@ -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
controllers/tenant/clusterroles.go
Normal file
66
controllers/tenant/clusterroles.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user