feat(tenant): add dedicated tenantowner crd (#1764)

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
This commit is contained in:
Oliver Bähler
2025-12-02 15:21:46 +01:00
committed by GitHub
parent beb1cd3de4
commit d812a0c722
105 changed files with 2703 additions and 543 deletions

View File

@@ -64,4 +64,11 @@ resources:
kind: ResourcePoolClaim
path: github.com/projectcapsule/capsule/api/v1beta2
version: v1beta2
- api:
crdVersion: v1
domain: clastix.io
group: capsule
kind: TenantOwner
path: github.com/projectcapsule/capsule/api/v1beta2
version: v1beta2
version: "3"

View File

@@ -7,13 +7,13 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/misc"
)
// ResourcePoolSpec.
type ResourcePoolSpec struct {
// Selector to match the namespaces that should be managed by the GlobalResourceQuota
Selectors []api.NamespaceSelector `json:"selectors,omitempty"`
Selectors []misc.NamespaceSelector `json:"selectors,omitempty"`
// Define the resourcequota served by this resourcepool.
Quota corev1.ResourceQuotaSpec `json:"quota"`
// The Defaults given for each namespace, the default is not counted towards the total allocation

View File

@@ -46,11 +46,13 @@ func (in *Tenant) ConvertFrom(raw conversion.Hub) error {
}
in.Spec.Owners = append(in.Spec.Owners, api.OwnerSpec{
UserSpec: api.UserSpec{
Kind: api.OwnerKind(owner.Kind),
Name: owner.Name,
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.OwnerKind(owner.Kind),
Name: owner.Name,
},
ClusterRoles: owner.GetRoles(*src, index),
},
ClusterRoles: owner.GetRoles(*src, index),
ProxyOperations: proxySettings,
})
}

View File

@@ -4,15 +4,76 @@
package v1beta2
import (
"context"
"slices"
"sort"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
func (in *Tenant) CollectOwners(ctx context.Context, c client.Client, allowPromotion bool, admins api.UserListSpec) (api.OwnerStatusListSpec, error) {
owners := in.Spec.Owners.ToStatusOwners()
// Promoted ServiceAccounts
if allowPromotion && len(in.Status.Namespaces) > 0 {
saList := &corev1.ServiceAccountList{}
if err := c.List(ctx, saList,
client.MatchingLabels{
meta.OwnerPromotionLabel: meta.OwnerPromotionLabelTrigger,
},
); err != nil {
return nil, err
}
for _, sa := range saList.Items {
for _, ns := range in.Status.Namespaces {
if sa.GetNamespace() != ns {
continue
}
owners.Upsert(api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: serviceaccount.ServiceAccountUsernamePrefix + sa.Namespace + ":" + sa.Name,
},
ClusterRoles: []string{
api.ProvisionerRoleName,
api.DeleterRoleName,
},
})
}
}
}
// Administrators
for _, a := range admins {
owners.Upsert(api.CoreOwnerSpec{
UserSpec: a,
ClusterRoles: []string{
api.DeleterRoleName,
},
})
}
// Dedicated Owner Objects
listed, err := in.Spec.Permissions.ListMatchingOwners(ctx, c)
if err != nil {
return nil, err
}
for _, o := range listed {
owners.Upsert(o.Spec.CoreOwnerSpec)
}
return owners, nil
}
func (in *Tenant) IsFull() bool {
// we don't have limits on assigned Namespaces
if in.Spec.NamespaceOptions == nil || in.Spec.NamespaceOptions.Quota == nil {

View File

@@ -15,25 +15,31 @@ var tenant = &Tenant{
Spec: TenantSpec{
Owners: []api.OwnerSpec{
{
UserSpec: api.UserSpec{
Kind: "User",
Name: "user1",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: "User",
Name: "user1",
},
ClusterRoles: []string{"cluster-admin", "read-only"},
},
ClusterRoles: []string{"cluster-admin", "read-only"},
},
{
UserSpec: api.UserSpec{
Kind: "Group",
Name: "group1",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: "Group",
Name: "group1",
},
ClusterRoles: []string{"edit"},
},
ClusterRoles: []string{"edit"},
},
{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: "service",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: "service",
},
ClusterRoles: []string{"read-only"},
},
ClusterRoles: []string{"read-only"},
},
},
AdditionalRoleBindings: []api.AdditionalRoleBindingsSpec{

View File

@@ -6,6 +6,7 @@ package v1beta2
import (
k8stypes "k8s.io/apimachinery/pkg/types"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
@@ -22,6 +23,8 @@ type TenantStatus struct {
// Allowed Cluster Objects within Tenant
TenantAvailableStatus `json:",inline"`
// Collected owners for this tenant
Owners api.OwnerStatusListSpec `json:"owners,omitempty"`
// +kubebuilder:default=Active
// The operational state of the Tenant. Possible values are "Active", "Cordoned".
State tenantState `json:"state"`

View File

@@ -4,13 +4,19 @@
package v1beta2
import (
"context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/misc"
)
// TenantSpec defines the desired state of Tenant.
type TenantSpec struct {
// Specify Permissions for the Tenant.
Permissions Permissions `json:"permissions,omitempty"`
// Specifies the owners of the Tenant.
// Optional
Owners api.OwnerListSpec `json:"owners,omitempty"`
@@ -72,6 +78,21 @@ type TenantSpec struct {
ForceTenantPrefix *bool `json:"forceTenantPrefix,omitempty"`
}
type Permissions struct {
// Matches TenantOwner objects which are promoted to owners of this tenant
// The elements are OR operations and independent. You can see the resulting Tenant Owners
// in the Status.Owners specification of the Tenant.
MatchOwners []*metav1.LabelSelector `json:"matchOwners,omitempty"`
}
func (p *Permissions) ListMatchingOwners(
ctx context.Context,
c client.Client,
opts ...client.ListOption,
) ([]*TenantOwner, error) {
return misc.ListBySelectors[*TenantOwner](ctx, c, &TenantOwnerList{}, p.MatchOwners)
}
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status

View File

@@ -0,0 +1,53 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/projectcapsule/capsule/pkg/api"
)
// TenantOwnerSpec defines the desired state of TenantOwner.
type TenantOwnerSpec struct {
api.CoreOwnerSpec `json:",inline"`
}
// TenantOwnerStatus defines the observed state of TenantOwner.
type TenantOwnerStatus struct{}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// TenantOwner is the Schema for the tenantowners API.
type TenantOwner struct {
metav1.TypeMeta `json:",inline"`
// metadata is a standard object metadata.
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
// spec defines the desired state of TenantOwner.
// +required
Spec TenantOwnerSpec `json:"spec"`
// status defines the observed state of TenantOwner.
// +optional
Status TenantOwnerStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
// TenantOwnerList contains a list of TenantOwner.
type TenantOwnerList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []TenantOwner `json:"items"`
}
func init() {
SchemeBuilder.Register(&TenantOwner{}, &TenantOwnerList{})
}

View File

@@ -10,6 +10,7 @@ package v1beta2
import (
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/misc"
corev1 "k8s.io/api/core/v1"
"k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -410,6 +411,32 @@ func (in *ObjectReferenceStatus) DeepCopy() *ObjectReferenceStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Permissions) DeepCopyInto(out *Permissions) {
*out = *in
if in.MatchOwners != nil {
in, out := &in.MatchOwners, &out.MatchOwners
*out = make([]*metav1.LabelSelector, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(metav1.LabelSelector)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Permissions.
func (in *Permissions) DeepCopy() *Permissions {
if in == nil {
return nil
}
out := new(Permissions)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in ProcessedItems) DeepCopyInto(out *ProcessedItems) {
{
@@ -727,7 +754,7 @@ func (in *ResourcePoolSpec) DeepCopyInto(out *ResourcePoolSpec) {
*out = *in
if in.Selectors != nil {
in, out := &in.Selectors, &out.Selectors
*out = make([]api.NamespaceSelector, len(*in))
*out = make([]misc.NamespaceSelector, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@@ -982,6 +1009,96 @@ func (in *TenantList) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantOwner) DeepCopyInto(out *TenantOwner) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantOwner.
func (in *TenantOwner) DeepCopy() *TenantOwner {
if in == nil {
return nil
}
out := new(TenantOwner)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantOwner) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantOwnerList) DeepCopyInto(out *TenantOwnerList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]TenantOwner, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantOwnerList.
func (in *TenantOwnerList) DeepCopy() *TenantOwnerList {
if in == nil {
return nil
}
out := new(TenantOwnerList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantOwnerList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantOwnerSpec) DeepCopyInto(out *TenantOwnerSpec) {
*out = *in
in.CoreOwnerSpec.DeepCopyInto(&out.CoreOwnerSpec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantOwnerSpec.
func (in *TenantOwnerSpec) DeepCopy() *TenantOwnerSpec {
if in == nil {
return nil
}
out := new(TenantOwnerSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantOwnerStatus) DeepCopyInto(out *TenantOwnerStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantOwnerStatus.
func (in *TenantOwnerStatus) DeepCopy() *TenantOwnerStatus {
if in == nil {
return nil
}
out := new(TenantOwnerStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantResource) DeepCopyInto(out *TenantResource) {
*out = *in
@@ -1092,6 +1209,7 @@ func (in *TenantResourceStatus) DeepCopy() *TenantResourceStatus {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = *in
in.Permissions.DeepCopyInto(&out.Permissions)
if in.Owners != nil {
in, out := &in.Owners, &out.Owners
*out = make(api.OwnerListSpec, len(*in))
@@ -1179,6 +1297,13 @@ func (in *TenantSpec) DeepCopy() *TenantSpec {
func (in *TenantStatus) DeepCopyInto(out *TenantStatus) {
*out = *in
in.TenantAvailableStatus.DeepCopyInto(&out.TenantAvailableStatus)
if in.Owners != nil {
in, out := &in.Owners, &out.Owners
*out = make(api.OwnerStatusListSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Namespaces != nil {
in, out := &in.Namespaces, &out.Namespaces
*out = make([]string, len(*in))

View File

@@ -0,0 +1,74 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
name: tenantowners.capsule.clastix.io
spec:
group: capsule.clastix.io
names:
kind: TenantOwner
listKind: TenantOwnerList
plural: tenantowners
singular: tenantowner
scope: Cluster
versions:
- name: v1beta2
schema:
openAPIV3Schema:
description: TenantOwner is the Schema for the tenantowners API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: spec defines the desired state of TenantOwner.
properties:
clusterRoles:
default:
- admin
- capsule-namespace-deleter
description: Defines additional cluster-roles for the specific Owner.
items:
type: string
type: array
kind:
description: Kind of entity. Possible values are "User", "Group",
and "ServiceAccount"
enum:
- User
- Group
- ServiceAccount
type: string
name:
description: Name of the entity.
type: string
required:
- kind
- name
type: object
status:
description: status defines the observed state of TenantOwner.
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -2140,6 +2140,65 @@ spec:
- name
type: object
type: array
permissions:
description: Specify Permissions for the Tenant.
properties:
matchOwners:
description: |-
Matches TenantOwner objects which are promoted to owners of this tenant
The elements are OR operations and independent. You can see the resulting Tenant Owners
in the Status.Owners specification of the Tenant.
items:
description: |-
A label selector is a label query over a set of resources. The result of matchLabels and
matchExpressions are ANDed. An empty label selector matches all objects. A null
label selector matches no objects.
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: array
type: object
podOptions:
description: Specifies options for the Pods deployed in the Tenant
namespaces, such as additional metadata.
@@ -2596,6 +2655,35 @@ spec:
items:
type: string
type: array
owners:
description: Collected owners for this tenant
items:
properties:
clusterRoles:
default:
- admin
- capsule-namespace-deleter
description: Defines additional cluster-roles for the specific
Owner.
items:
type: string
type: array
kind:
description: Kind of entity. Possible values are "User", "Group",
and "ServiceAccount"
enum:
- User
- Group
- ServiceAccount
type: string
name:
description: Name of the entity.
type: string
required:
- kind
- name
type: object
type: array
size:
description: How many namespaces are assigned to the Tenant.
type: integer

View File

@@ -31,6 +31,7 @@ rules:
- tenantresources.capsule.clastix.io
- globaltenantresources.capsule.clastix.io
- tenants.capsule.clastix.io
- tenantowners.capsule.clastix.io
verbs:
- create
- delete

View File

@@ -282,7 +282,7 @@ func main() {
tenantvalidation.ServiceAccountNameHandler(),
tenantvalidation.ForbiddenAnnotationsRegexHandler(),
tenantvalidation.ProtectedHandler(),
tenantvalidation.WarningHandler(),
tenantvalidation.WarningHandler(cfg),
),
route.NamespaceValidation(
namespacevalidation.NamespaceHandler(

View File

@@ -24,9 +24,11 @@ var _ = Describe("creating a Namespace with an additional Role Binding", Label("
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "dale",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "dale",
Kind: "User",
},
},
},
},

View File

@@ -27,9 +27,11 @@ var _ = Describe("Administrators", Label("namespace", "permissions"), func() {
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "paul",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "paul",
Kind: "User",
},
},
},
},
@@ -43,9 +45,11 @@ var _ = Describe("Administrators", Label("namespace", "permissions"), func() {
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "george",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "george",
Kind: "User",
},
},
},
},

View File

@@ -24,9 +24,11 @@ var _ = Describe("enforcing an allowed set of Service external IPs", Label("tena
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "google",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "google",
Kind: "User",
},
},
},
},

View File

@@ -35,9 +35,11 @@ var _ = Describe("enforcing a Container Registry", Label("tenant", "images", "re
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "matt",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "matt",
Kind: "User",
},
},
},
},

View File

@@ -25,15 +25,19 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "alice",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "alice",
Kind: "User",
},
},
},
{
UserSpec: api.UserSpec{
Name: "bob",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "bob",
Kind: "User",
},
},
},
},

View File

@@ -33,9 +33,11 @@ var _ = Describe("when Tenant limits custom Resource Quota", Label("resourcequot
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "resource",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "resource",
Kind: "User",
},
},
},
},

View File

@@ -25,9 +25,11 @@ var _ = Describe("creating an ExternalName service when it is disabled for Tenan
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "google",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "google",
Kind: "User",
},
},
},
},

View File

@@ -31,9 +31,11 @@ var _ = Describe("creating an Ingress with a wildcard when it is denied for the
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "scott",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "scott",
Kind: "User",
},
},
},
},

View File

@@ -25,9 +25,11 @@ var _ = Describe("creating a LoadBalancer service when it is disabled for Tenant
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "amazon",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "amazon",
Kind: "User",
},
},
},
},

View File

@@ -25,9 +25,11 @@ var _ = Describe("creating a nodePort service when it is disabled for Tenant", L
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "google",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "google",
Kind: "User",
},
},
},
},

View File

@@ -22,18 +22,22 @@ var _ = Describe("defining dynamic Tenant Owner Cluster Roles", Label("tenant"),
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Kind: "User",
Name: "michonne",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: "User",
Name: "michonne",
},
ClusterRoles: []string{"editor", "manager"},
},
ClusterRoles: []string{"editor", "manager"},
},
{
UserSpec: api.UserSpec{
Name: "kingdom",
Kind: "Group",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "kingdom",
Kind: "Group",
},
ClusterRoles: []string{"readonly"},
},
ClusterRoles: []string{"readonly"},
},
},
},

View File

@@ -25,9 +25,11 @@ var _ = Describe("creating a LoadBalancer service when it is enabled for Tenant"
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "netflix",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "netflix",
Kind: "User",
},
},
},
},

View File

@@ -24,9 +24,11 @@ var _ = Describe("creating a nodePort service when it is enabled for Tenant", La
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "google",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "google",
Kind: "User",
},
},
},
},

View File

@@ -68,9 +68,11 @@ var _ = Describe("creating a tenant with various forbidden regexes", Label("tena
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "alice",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "alice",
Kind: "User",
},
},
},
},

View File

@@ -23,9 +23,11 @@ var _ = Describe("creating a Namespace with Tenant name prefix enforcement at Te
ForceTenantPrefix: &[]bool{true}[0],
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
},
},
},
},
@@ -39,9 +41,11 @@ var _ = Describe("creating a Namespace with Tenant name prefix enforcement at Te
ForceTenantPrefix: &[]bool{false}[0],
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
},
},
},
},

View File

@@ -22,9 +22,11 @@ var _ = Describe("creating a Namespace with Tenant name prefix enforcement", Lab
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
},
},
},
},
@@ -37,9 +39,11 @@ var _ = Describe("creating a Namespace with Tenant name prefix enforcement", Lab
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
},
},
},
},

View File

@@ -79,9 +79,11 @@ var _ = Describe("when Tenant handles Gateway classes", Label("tenant", "classes
Spec: capsulev1beta2.TenantSpec{
Owners: []api.OwnerSpec{
{
UserSpec: api.UserSpec{
Name: "gateway-default-and-label-selector",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gateway-default-and-label-selector",
Kind: "User",
},
},
},
},
@@ -110,9 +112,11 @@ var _ = Describe("when Tenant handles Gateway classes", Label("tenant", "classes
Spec: capsulev1beta2.TenantSpec{
Owners: []api.OwnerSpec{
{
UserSpec: api.UserSpec{
Name: "gateway-with-label-selector-only",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gateway-with-label-selector-only",
Kind: "User",
},
},
},
},
@@ -140,9 +144,11 @@ var _ = Describe("when Tenant handles Gateway classes", Label("tenant", "classes
Spec: capsulev1beta2.TenantSpec{
Owners: []api.OwnerSpec{
{
UserSpec: api.UserSpec{
Name: "e2e-gateway-no-restrictions",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "e2e-gateway-no-restrictions",
Kind: "User",
},
},
},
},

View File

@@ -34,9 +34,11 @@ var _ = Describe("Creating a GlobalTenantResource object", func() {
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "solar-user",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "solar-user",
Kind: "User",
},
},
},
},
@@ -53,9 +55,11 @@ var _ = Describe("Creating a GlobalTenantResource object", func() {
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "wind-user",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "wind-user",
Kind: "User",
},
},
},
},

View File

@@ -24,9 +24,11 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("tenant", "imag
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "alex",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "alex",
Kind: "User",
},
},
},
},

View File

@@ -24,9 +24,11 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("tenant", "images"
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "axel",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "axel",
Kind: "User",
},
},
},
},

View File

@@ -27,9 +27,11 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1",
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "ingress",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "ingress",
Kind: "User",
},
},
},
},

View File

@@ -32,9 +32,11 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1"
Spec: capsulev1beta2.TenantSpec{
Owners: []api.OwnerSpec{
{
UserSpec: api.UserSpec{
Name: "ingress-selector",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "ingress-selector",
Kind: "User",
},
},
},
},
@@ -63,9 +65,11 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1"
Spec: capsulev1beta2.TenantSpec{
Owners: []api.OwnerSpec{
{
UserSpec: api.UserSpec{
Name: "ingress-default",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "ingress-default",
Kind: "User",
},
},
},
},

View File

@@ -27,9 +27,11 @@ var _ = Describe("when handling Cluster scoped Ingress hostnames collision", Lab
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "ingress-tenant-one",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "ingress-tenant-one",
Kind: "User",
},
},
},
},
@@ -45,9 +47,11 @@ var _ = Describe("when handling Cluster scoped Ingress hostnames collision", Lab
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "ingress-tenant-two",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "ingress-tenant-two",
Kind: "User",
},
},
},
},

View File

@@ -27,9 +27,11 @@ var _ = Describe("when disabling Ingress hostnames collision", Label("ingress"),
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "ingress-disabled",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "ingress-disabled",
Kind: "User",
},
},
},
},

View File

@@ -27,9 +27,11 @@ var _ = Describe("when handling Namespace scoped Ingress hostnames collision", L
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "ingress-namespace",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "ingress-namespace",
Kind: "User",
},
},
},
},

View File

@@ -27,9 +27,11 @@ var _ = Describe("when handling Tenant scoped Ingress hostnames collision", Labe
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "ingress-tenant",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "ingress-tenant",
Kind: "User",
},
},
},
},

View File

@@ -27,9 +27,11 @@ var _ = Describe("when Tenant handles Ingress hostnames", Label("ingress"), func
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "hostname",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "hostname",
Kind: "User",
},
},
},
},

View File

@@ -20,9 +20,11 @@ var _ = Describe("creating a Namespace creation with no Tenant assigned", Label(
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "missing",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "missing",
Kind: "User",
},
},
},
},

View File

@@ -34,9 +34,11 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
},
},
},
},

View File

@@ -25,9 +25,11 @@ var _ = Describe("creating several Namespaces for a Tenant", Label("namespace"),
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "charlie",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "charlie",
Kind: "User",
},
},
},
},

View File

@@ -26,15 +26,19 @@ var _ = Describe("creating several Namespaces for a Tenant", Label("namespace",
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
},
},
},
{
UserSpec: api.UserSpec{
Kind: "ServiceAccount",
Name: "system:serviceaccount:attacker-system:attacker",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: "ServiceAccount",
Name: "system:serviceaccount:attacker-system:attacker",
},
},
},
},

View File

@@ -31,9 +31,11 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
},
},
},
},

View File

@@ -31,9 +31,11 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
},
},
},
},

View File

@@ -24,9 +24,11 @@ var _ = Describe("creating namespace with status lifecycle", Label("namespace",
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
},
},
},
},

View File

@@ -35,9 +35,11 @@ var _ = Describe("creating a Namespace with user-specified labels and annotation
},
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
},
},
},
},

View File

@@ -22,21 +22,27 @@ var _ = Describe("creating a Namespaces as different type of Tenant owners", Lab
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "alice",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "alice",
Kind: "User",
},
},
},
{
UserSpec: api.UserSpec{
Name: "bob",
Kind: "Group",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "bob",
Kind: "Group",
},
},
},
{
UserSpec: api.UserSpec{
Name: "system:serviceaccount:new-namespace-sa:default",
Kind: "ServiceAccount",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "system:serviceaccount:new-namespace-sa:default",
Kind: "ServiceAccount",
},
},
},
},

View File

@@ -29,9 +29,11 @@ var _ = Describe("modifying node labels and annotations", Label("config", "nodes
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
},
},
},
},

View File

@@ -23,9 +23,11 @@ var _ = Describe("creating a Namespace in over-quota of three", Label("namespace
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "bob",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "bob",
Kind: "User",
},
},
},
},

View File

@@ -27,9 +27,11 @@ var _ = Describe("when Tenant owner interacts with the webhooks", Label("tenant"
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "ruby",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "ruby",
Kind: "User",
},
},
},
},

531
e2e/owners_test.go Normal file
View File

@@ -0,0 +1,531 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
)
var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
originConfig := &capsulev1beta2.CapsuleConfiguration{}
tnt1 := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-owners-1",
},
Spec: capsulev1beta2.TenantSpec{
Permissions: capsulev1beta2.Permissions{
MatchOwners: []*metav1.LabelSelector{
{
MatchLabels: map[string]string{
"customer": "x",
},
},
{
MatchLabels: map[string]string{
"team": "devops",
},
},
},
},
Owners: api.OwnerListSpec{
{
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "e2e-owners-1",
Kind: "User",
},
},
},
{
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "e2e-owners-1-group",
Kind: "Group",
},
},
},
{
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "system:serviceaccount:capsule-system:capsule",
Kind: "ServiceAccount",
},
},
},
},
},
}
tnt2 := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-owners-2",
},
Spec: capsulev1beta2.TenantSpec{
Permissions: capsulev1beta2.Permissions{
MatchOwners: []*metav1.LabelSelector{
{
MatchLabels: map[string]string{
"customer": "x",
},
},
{
MatchLabels: map[string]string{
"team": "infrastructure",
},
},
},
},
Owners: api.OwnerListSpec{
{
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "e2e-owners-2",
Kind: "User",
},
},
},
{
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "e2e-owners-2-group",
Kind: "Group",
},
},
},
{
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "system:serviceaccount:capsule-system:capsule",
Kind: "ServiceAccount",
},
},
},
},
},
}
ownersInfra := &capsulev1beta2.TenantOwner{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-owners-infra",
Labels: map[string]string{
"team": "infrastructure",
},
},
Spec: capsulev1beta2.TenantOwnerSpec{
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "oidc:comp:administrators",
},
ClusterRoles: []string{
"mega-admin",
},
},
},
}
ownersDevops := &capsulev1beta2.TenantOwner{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-owners-devops",
Labels: map[string]string{
"team": "devops",
},
},
Spec: capsulev1beta2.TenantOwnerSpec{
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "oidc:comp:devops",
},
ClusterRoles: []string{
"namespaced-admin",
},
},
},
}
ownersCommon := &capsulev1beta2.TenantOwner{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-owners-common",
Labels: map[string]string{
"team": "infrastructure",
"customer": "x",
},
},
Spec: capsulev1beta2.TenantOwnerSpec{
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: "system:serviceaccount:capsule-system:capsule",
},
ClusterRoles: []string{
"service-admin",
},
},
},
}
JustBeforeEach(func() {
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed())
for _, tnt := range []*capsulev1beta2.Tenant{tnt1, tnt2} {
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
}
for _, tnt := range []*capsulev1beta2.TenantOwner{ownersInfra, ownersDevops, ownersCommon} {
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
}
})
JustAfterEach(func() {
for _, tnt := range []*capsulev1beta2.Tenant{tnt1, tnt2} {
err := k8sClient.Delete(context.TODO(), tnt)
Expect(client.IgnoreNotFound(err)).To(Succeed())
}
for _, owners := range []*capsulev1beta2.TenantOwner{ownersInfra, ownersDevops, ownersCommon} {
err := k8sClient.Delete(context.TODO(), owners)
Expect(client.IgnoreNotFound(err)).To(Succeed())
}
})
It("Verify owners for", func() {
By("checking owners (e2e-owners-1)", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
expectedOwners := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "e2e-owners-1-group",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "oidc:comp:devops",
},
ClusterRoles: []string{"namespaced-admin"},
},
{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: "system:serviceaccount:capsule-system:capsule",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter", "service-admin"},
},
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "e2e-owners-1",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
To(Equal(normalizeOwners(expectedOwners)))
VerifyTenantRoleBindings(t)
})
By("checking owners (e2e-owners-2)", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt2.GetName()}, t)).Should(Succeed())
expectedOwners := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "e2e-owners-2-group",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "oidc:comp:administrators",
},
ClusterRoles: []string{"mega-admin"},
},
{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: "system:serviceaccount:capsule-system:capsule",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter", "service-admin"},
},
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "e2e-owners-2",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
To(Equal(normalizeOwners(expectedOwners)))
VerifyTenantRoleBindings(t)
})
By("remove common tenant-owners", func() {
Expect(k8sClient.Delete(context.TODO(), ownersCommon)).Should(Succeed())
})
By("checking owners (e2e-owners-1)", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
expectedOwners := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "e2e-owners-1-group",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "oidc:comp:devops",
},
ClusterRoles: []string{"namespaced-admin"},
},
{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: "system:serviceaccount:capsule-system:capsule",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "e2e-owners-1",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
To(Equal(normalizeOwners(expectedOwners)))
VerifyTenantRoleBindings(t)
})
By("checking owners (e2e-owners-2)", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt2.GetName()}, t)).Should(Succeed())
expectedOwners := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "e2e-owners-2-group",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "oidc:comp:administrators",
},
ClusterRoles: []string{"mega-admin"},
},
{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: "system:serviceaccount:capsule-system:capsule",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "e2e-owners-2",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
To(Equal(normalizeOwners(expectedOwners)))
VerifyTenantRoleBindings(t)
})
By("remove admin tenant-owners", func() {
Expect(k8sClient.Delete(context.TODO(), ownersInfra)).Should(Succeed())
})
By("checking owners (e2e-owners-1)", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
expectedOwners := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "e2e-owners-1-group",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "oidc:comp:devops",
},
ClusterRoles: []string{"namespaced-admin"},
},
{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: "system:serviceaccount:capsule-system:capsule",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "e2e-owners-1",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
To(Equal(normalizeOwners(expectedOwners)))
VerifyTenantRoleBindings(t)
})
By("checking owners (e2e-owners-2)", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt2.GetName()}, t)).Should(Succeed())
expectedOwners := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "e2e-owners-2-group",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: "system:serviceaccount:capsule-system:capsule",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "e2e-owners-2",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
To(Equal(normalizeOwners(expectedOwners)))
VerifyTenantRoleBindings(t)
})
By("remove admin tenant-owners", func() {
Expect(k8sClient.Delete(context.TODO(), ownersDevops)).Should(Succeed())
})
By("checking owners (e2e-owners-1)", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
expectedOwners := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "e2e-owners-1-group",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: "system:serviceaccount:capsule-system:capsule",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "e2e-owners-1",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
To(Equal(normalizeOwners(expectedOwners)))
VerifyTenantRoleBindings(t)
})
By("checking owners (e2e-owners-2)", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt2.GetName()}, t)).Should(Succeed())
expectedOwners := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
Name: "e2e-owners-2-group",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: "system:serviceaccount:capsule-system:capsule",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "e2e-owners-2",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
To(Equal(normalizeOwners(expectedOwners)))
VerifyTenantRoleBindings(t)
})
})
})

View File

@@ -24,9 +24,11 @@ var _ = Describe("adding metadata to Pod objects", Label("pod"), func() {
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
},
},
},
},

View File

@@ -30,9 +30,11 @@ var _ = Describe("enforcing a Priority Class", Label("pod", "classes"), func() {
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "paul",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "paul",
Kind: "User",
},
},
},
},
@@ -56,9 +58,11 @@ var _ = Describe("enforcing a Priority Class", Label("pod", "classes"), func() {
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "george",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "george",
Kind: "User",
},
},
},
},
@@ -85,9 +89,11 @@ var _ = Describe("enforcing a Priority Class", Label("pod", "classes"), func() {
Spec: capsulev1beta2.TenantSpec{
Owners: []api.OwnerSpec{
{
UserSpec: api.UserSpec{
Name: "e2e-priority-class-no-restrictions",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "e2e-priority-class-no-restrictions",
Kind: "User",
},
},
},
},

View File

@@ -31,9 +31,11 @@ var _ = Describe("enforcing a Runtime Class", Label("pod", "classes", "current")
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "george",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "george",
Kind: "User",
},
},
},
},
@@ -61,9 +63,11 @@ var _ = Describe("enforcing a Runtime Class", Label("pod", "classes", "current")
Spec: capsulev1beta2.TenantSpec{
Owners: []api.OwnerSpec{
{
UserSpec: api.UserSpec{
Name: "e2e-gateway-no-restrictions",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "e2e-gateway-no-restrictions",
Kind: "User",
},
},
},
},

View File

@@ -27,9 +27,11 @@ var _ = Describe("preventing PersistentVolume cross-tenant mount", Label("tenant
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "jessica",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "jessica",
Kind: "User",
},
},
},
},
@@ -43,9 +45,11 @@ var _ = Describe("preventing PersistentVolume cross-tenant mount", Label("tenant
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "leto",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "leto",
Kind: "User",
},
},
},
},

View File

@@ -25,9 +25,11 @@ var _ = Describe("creating a Namespace with a protected Namespace regex enabled"
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "alice",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "alice",
Kind: "User",
},
},
},
},

View File

@@ -29,9 +29,11 @@ var _ = Describe("exceeding a Tenant resource quota", Label("resourcequota"), fu
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "bobby",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "bobby",
Kind: "User",
},
},
},
},

View File

@@ -17,8 +17,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/misc"
"github.com/projectcapsule/capsule/pkg/utils"
)
@@ -83,7 +83,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []api.NamespaceSelector{
Selectors: []misc.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -522,7 +522,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []api.NamespaceSelector{
Selectors: []misc.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -694,7 +694,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []api.NamespaceSelector{
Selectors: []misc.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -950,7 +950,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []api.NamespaceSelector{
Selectors: []misc.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -1292,7 +1292,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []api.NamespaceSelector{
Selectors: []misc.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -1450,7 +1450,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []api.NamespaceSelector{
Selectors: []misc.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -1558,7 +1558,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []api.NamespaceSelector{
Selectors: []misc.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -1666,7 +1666,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []api.NamespaceSelector{
Selectors: []misc.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{

View File

@@ -17,6 +17,7 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/misc"
)
var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
@@ -30,9 +31,11 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "wind-user",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "wind-user",
Kind: "User",
},
},
},
},
@@ -99,7 +102,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []api.NamespaceSelector{
Selectors: []misc.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -302,7 +305,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
Config: capsulev1beta2.ResourcePoolSpecConfiguration{
DeleteBoundResources: ptr.To(false),
},
Selectors: []api.NamespaceSelector{
Selectors: []misc.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -492,7 +495,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
Config: capsulev1beta2.ResourcePoolSpecConfiguration{
DeleteBoundResources: ptr.To(false),
},
Selectors: []api.NamespaceSelector{
Selectors: []misc.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -521,7 +524,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
Config: capsulev1beta2.ResourcePoolSpecConfiguration{
DeleteBoundResources: ptr.To(false),
},
Selectors: []api.NamespaceSelector{
Selectors: []misc.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{

View File

@@ -18,7 +18,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
ctrlrbac "github.com/projectcapsule/capsule/internal/controllers/rbac"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
@@ -33,9 +32,11 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "alice",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "alice",
Kind: "User",
},
},
},
},
@@ -280,7 +281,7 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("
Eventually(func(g Gomega) []rbacv1.Subject {
crb := &rbacv1.ClusterRoleBinding{}
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ctrlrbac.ProvisionerRoleName}, crb)
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: api.ProvisionerRoleName}, crb)
g.Expect(err).NotTo(HaveOccurred())
return crb.Subjects
@@ -329,7 +330,7 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("
Eventually(func(g Gomega) []rbacv1.Subject {
crb := &rbacv1.ClusterRoleBinding{}
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ctrlrbac.ProvisionerRoleName}, crb)
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: api.ProvisionerRoleName}, crb)
g.Expect(err).NotTo(HaveOccurred())
return crb.Subjects
@@ -351,5 +352,4 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("
Expect(saClient.Delete(context.TODO(), secondNs)).To(Not(Succeed()))
})
})

View File

@@ -29,9 +29,11 @@ var _ = Describe("trying to escalate from a Tenant Namespace ServiceAccount", La
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "mario",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "mario",
Kind: "User",
},
},
},
},

View File

@@ -26,9 +26,11 @@ var _ = Describe("verify scalability", Label("scalability"), func() {
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
},
},
},
},

View File

@@ -25,9 +25,11 @@ var _ = Describe("creating a Namespace trying to select a third Tenant", Label("
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "undefined",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "undefined",
Kind: "User",
},
},
},
},

View File

@@ -22,9 +22,11 @@ var _ = Describe("creating a Namespace without a Tenant selector when user owns
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
},
},
},
},
@@ -37,9 +39,11 @@ var _ = Describe("creating a Namespace without a Tenant selector when user owns
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
},
},
},
},
@@ -52,9 +56,11 @@ var _ = Describe("creating a Namespace without a Tenant selector when user owns
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "john",
Kind: "Group",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "john",
Kind: "Group",
},
},
},
},
@@ -67,9 +73,11 @@ var _ = Describe("creating a Namespace without a Tenant selector when user owns
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "john",
Kind: "Group",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "john",
Kind: "Group",
},
},
},
},

View File

@@ -26,9 +26,11 @@ var _ = Describe("creating a Namespace with Tenant selector when user owns multi
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
},
},
},
},
@@ -41,9 +43,11 @@ var _ = Describe("creating a Namespace with Tenant selector when user owns multi
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
},
},
},
},

View File

@@ -34,9 +34,11 @@ var _ = Describe("creating a Service with user-specified labels and annotations"
},
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
},
},
},
},

View File

@@ -31,9 +31,11 @@ var _ = Describe("adding metadata to Service objects", Label("tenant", "service"
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
},
},
},
},

View File

@@ -35,9 +35,11 @@ var _ = Describe("when Tenant handles Storage classes", Label("tenant", "classes
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "selector",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "selector",
Kind: "User",
},
},
},
},
@@ -64,9 +66,11 @@ var _ = Describe("when Tenant handles Storage classes", Label("tenant", "classes
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "default",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "default",
Kind: "User",
},
},
},
},
@@ -90,9 +94,11 @@ var _ = Describe("when Tenant handles Storage classes", Label("tenant", "classes
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "no-restrictions",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "no-restrictions",
Kind: "User",
},
},
},
},

View File

@@ -27,9 +27,11 @@ var _ = Describe("cordoning a Tenant", Label("tenant"), func() {
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "jim",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "jim",
Kind: "User",
},
},
},
},

View File

@@ -35,9 +35,11 @@ var _ = Describe("adding metadata to a Tenant", Label("tenant"), func() {
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "jim",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "jim",
Kind: "User",
},
},
},
},

View File

@@ -22,9 +22,11 @@ var _ = Describe("creating a Tenant with wrong name", Label("tenant"), func() {
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
},
},
},
},

View File

@@ -24,9 +24,11 @@ var _ = Describe("Deleting a tenant with protected annotation", Label("tenant"),
PreventDeletion: true,
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
},
},
},
},

View File

@@ -29,9 +29,11 @@ var _ = Describe("changing Tenant managed Kubernetes resources", Label("tenant")
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "laura",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "laura",
Kind: "User",
},
},
},
},

View File

@@ -29,9 +29,11 @@ var _ = Describe("creating namespaces within a Tenant with resources", Label("te
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "john",
Kind: "User",
},
},
},
},

View File

@@ -32,9 +32,11 @@ var _ = Describe("Creating a TenantResource object", Label("tenantresource"), fu
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
UserSpec: api.UserSpec{
Name: "solar-user",
Kind: "User",
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "solar-user",
Kind: "User",
},
},
},
},

View File

@@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strings"
"time"
@@ -327,6 +328,66 @@ func CheckForOwnerRoleBindings(ns *corev1.Namespace, owner api.OwnerSpec, roles
}
}
func VerifyTenantRoleBindings(
tnt *capsulev1beta2.Tenant,
) {
Eventually(func(g Gomega) {
// List all RoleBindings once per namespace to avoid repeated API calls.
for _, ns := range tnt.Status.Namespaces {
for i, owner := range tnt.Status.Owners {
for _, role := range owner.ClusterRoles {
rbName := fmt.Sprintf("capsule-%s-%d-%s", tnt.Name, i, role)
rb := &rbacv1.RoleBinding{}
err := k8sClient.Get(context.Background(), client.ObjectKey{
Namespace: ns,
Name: rbName,
}, rb)
g.Expect(err).ToNot(HaveOccurred(),
"expected RoleBinding %s/%s to exist", ns, rbName)
g.Expect(rb.RoleRef.Name).To(Equal(role),
"expected RoleBinding %s/%s to have RoleRef.Name=%q",
ns, rbName, role)
g.Expect(rb.Subjects).ToNot(BeEmpty(),
"expected RoleBinding %s/%s to have at least one subject", ns, rbName)
foundSubject := false
for _, s := range rb.Subjects {
if s.Kind == string(owner.Kind) && s.Name == owner.Name {
foundSubject = true
break
}
}
g.Expect(foundSubject).To(BeTrue(),
"expected RoleBinding %s/%s to contain subject %s/%s",
ns, rb.Name, owner.Kind, owner.Name)
}
}
}
}).WithTimeout(30 * time.Second).WithPolling(500 * time.Millisecond).Should(Succeed())
}
func normalizeOwners(in api.OwnerStatusListSpec) api.OwnerStatusListSpec {
// copy to avoid mutating the original
out := make(api.OwnerStatusListSpec, len(in))
copy(out, in)
// sort outer slice by kind+name
sort.Sort(api.GetByKindAndName(out))
// sort roles inside each owner so role order doesn't matter
for i := range out {
sort.Strings(out[i].ClusterRoles)
}
return out
}
func GetKubernetesVersion() *versionUtil.Version {
var serverVersion *version.Info
var err error

2
go.sum
View File

@@ -232,8 +232,6 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=

View File

@@ -38,7 +38,7 @@ type Manager struct {
//nolint:revive
func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) (err error) {
namesPredicate := utils.NamesMatchingPredicate(ProvisionerRoleName, DeleterRoleName)
namesPredicate := utils.NamesMatchingPredicate(api.ProvisionerRoleName, api.DeleterRoleName)
crErr := ctrl.NewControllerManagedBy(mgr).
For(&rbacv1.ClusterRole{}, namesPredicate).
@@ -63,7 +63,7 @@ func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, ctrlCo
r.handleSAChange(ctx, e.Object)
},
UpdateFunc: func(ctx context.Context, e event.TypedUpdateEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
if promotionLabelsChanged(e.ObjectOld.GetLabels(), e.ObjectNew.GetLabels()) {
if utils.LabelsChanged([]string{meta.OwnerPromotionLabel}, e.ObjectOld.GetLabels(), e.ObjectNew.GetLabels()) {
r.handleSAChange(ctx, e.ObjectNew)
}
},
@@ -83,9 +83,9 @@ func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, ctrlCo
// Resource kinds and we're just interested to the ones with the said name since they're bounded together.
func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res reconcile.Result, err error) {
switch request.Name {
case ProvisionerRoleName:
if err = r.EnsureClusterRole(ctx, ProvisionerRoleName); err != nil {
r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", ProvisionerRoleName)
case api.ProvisionerRoleName:
if err = r.EnsureClusterRole(ctx, api.ProvisionerRoleName); err != nil {
r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", api.ProvisionerRoleName)
break
}
@@ -95,9 +95,9 @@ func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
break
}
case DeleterRoleName:
if err = r.EnsureClusterRole(ctx, DeleterRoleName); err != nil {
r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", DeleterRoleName)
case api.DeleterRoleName:
if err = r.EnsureClusterRole(ctx, api.DeleterRoleName); err != nil {
r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", api.DeleterRoleName)
}
}
@@ -106,12 +106,12 @@ func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
func (r *Manager) EnsureClusterRoleBindingsProvisioner(ctx context.Context) error {
crb := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{Name: ProvisionerRoleName},
ObjectMeta: metav1.ObjectMeta{Name: api.ProvisionerRoleName},
}
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, crb, func() error {
crb.RoleRef = provisionerClusterRoleBinding.RoleRef
crb.RoleRef = api.ProvisionerClusterRoleBinding.RoleRef
crb.Subjects = nil
for _, entity := range r.Configuration.Administrators() {
@@ -179,7 +179,7 @@ func (r *Manager) EnsureClusterRoleBindingsProvisioner(ctx context.Context) erro
}
func (r *Manager) EnsureClusterRole(ctx context.Context, roleName string) (err error) {
role, ok := clusterRoles[roleName]
role, ok := api.ClusterRoles[roleName]
if !ok {
return fmt.Errorf("clusterRole %s is not mapped", roleName)
}
@@ -203,7 +203,7 @@ func (r *Manager) EnsureClusterRole(ctx context.Context, roleName string) (err e
// since we're not creating empty CR and CRB upon Capsule installation: it's a run-once task, since the reconciliation
// is handled by the Reconciler implemented interface.
func (r *Manager) Start(ctx context.Context) error {
for roleName := range clusterRoles {
for roleName := range api.ClusterRoles {
r.Log.V(4).Info("setting up ClusterRoles", "ClusterRole", roleName)
if err := r.EnsureClusterRole(ctx, roleName); err != nil {
@@ -237,20 +237,3 @@ func (r *Manager) handleSAChange(ctx context.Context, obj client.Object) {
r.Log.Error(err, "cannot update ClusterRoleBinding upon ServiceAccount event")
}
}
func promotionLabelsChanged(oldLabels, newLabels map[string]string) bool {
keys := []string{
meta.OwnerPromotionLabel,
}
for _, key := range keys {
oldVal, oldOK := oldLabels[key]
newVal, newOK := newLabels[key]
if oldOK != newOK || oldVal != newVal {
return true
}
}
return false
}

View File

@@ -17,20 +17,25 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
"k8s.io/client-go/util/workqueue"
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"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/internal/controllers/utils"
"github.com/projectcapsule/capsule/internal/metrics"
"github.com/projectcapsule/capsule/pkg/api"
meta "github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/configuration"
)
@@ -47,7 +52,12 @@ type Manager struct {
func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error {
return ctrl.NewControllerManagedBy(mgr).
For(&capsulev1beta2.Tenant{}).
For(
&capsulev1beta2.Tenant{},
builder.WithPredicates(
predicate.GenerationChangedPredicate{},
),
).
Owns(&networkingv1.NetworkPolicy{}).
Owns(&corev1.LimitRange{}).
Owns(&corev1.ResourceQuota{}).
@@ -64,29 +74,129 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.Controller
).
Watches(
&storagev1.StorageClass{},
handler.EnqueueRequestsFromMapFunc(r.enqueueAllTenants),
r.statusOnlyHandlerClasses(
r.reconcileClassStatus,
r.collectAvailableStorageClasses,
"cannot collect storage classes",
),
builder.WithPredicates(utils.UpdatedMetadataPredicate),
).
Watches(
&gatewayv1.GatewayClass{},
handler.EnqueueRequestsFromMapFunc(r.enqueueAllTenants),
builder.WithPredicates(utils.UpdatedMetadataPredicate),
).
Watches(
&networkingv1.IngressClass{},
handler.EnqueueRequestsFromMapFunc(r.enqueueAllTenants),
r.statusOnlyHandlerClasses(
r.reconcileClassStatus,
r.collectAvailableGatewayClasses,
"cannot collect gateway classes",
),
builder.WithPredicates(utils.UpdatedMetadataPredicate),
).
Watches(
&schedulingv1.PriorityClass{},
handler.EnqueueRequestsFromMapFunc(r.enqueueAllTenants),
r.statusOnlyHandlerClasses(
r.reconcileClassStatus,
r.collectAvailablePriorityClasses,
"cannot collect priority classes",
),
builder.WithPredicates(utils.UpdatedMetadataPredicate),
).
Watches(
&nodev1.RuntimeClass{},
handler.EnqueueRequestsFromMapFunc(r.enqueueAllTenants),
r.statusOnlyHandlerClasses(
r.reconcileClassStatus,
r.collectAvailableRuntimeClasses,
"cannot collect runtime classes",
),
builder.WithPredicates(utils.UpdatedMetadataPredicate),
).
Watches(
&capsulev1beta2.TenantOwner{},
handler.TypedFuncs[client.Object, ctrl.Request]{
CreateFunc: func(
ctx context.Context,
e event.TypedCreateEvent[client.Object],
q workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
r.enqueueForTenantsWithCondition(
ctx,
e.Object,
q,
func(tnt *capsulev1beta2.Tenant, c client.Object) bool {
return len(tnt.Spec.Permissions.MatchOwners) > 0
})
},
UpdateFunc: func(
ctx context.Context,
e event.TypedUpdateEvent[client.Object],
q workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
r.enqueueForTenantsWithCondition(
ctx,
e.ObjectNew,
q,
func(tnt *capsulev1beta2.Tenant, c client.Object) bool {
return len(tnt.Spec.Permissions.MatchOwners) > 0
})
},
DeleteFunc: func(
ctx context.Context,
e event.TypedDeleteEvent[client.Object],
q workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
r.enqueueTenantsForTenantOwner(ctx, e.Object, q)
},
},
).
Watches(
&corev1.ServiceAccount{},
handler.TypedFuncs[client.Object, ctrl.Request]{
CreateFunc: func(
ctx context.Context,
e event.TypedCreateEvent[client.Object],
q workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
r.enqueueForTenantsWithCondition(ctx, e.Object, q, func(tnt *capsulev1beta2.Tenant, c client.Object) bool {
for _, n := range tnt.Status.Namespaces {
if n == c.GetNamespace() {
return true
}
}
return false
})
},
UpdateFunc: func(
ctx context.Context,
e event.TypedUpdateEvent[client.Object],
q workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
r.enqueueForTenantsWithCondition(ctx, e.ObjectNew, q, func(tnt *capsulev1beta2.Tenant, c client.Object) bool {
for _, n := range tnt.Status.Namespaces {
if n == c.GetNamespace() {
return true
}
}
return false
})
},
DeleteFunc: func(
ctx context.Context,
e event.TypedDeleteEvent[client.Object],
q workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
r.enqueueForTenantsWithCondition(ctx, e.Object, q, func(tnt *capsulev1beta2.Tenant, c client.Object) bool {
_, found := tnt.Status.Owners.FindOwner(
serviceaccount.ServiceAccountUsernamePrefix+c.GetNamespace()+":"+c.GetName(),
api.ServiceAccountOwner,
)
return found
})
},
},
builder.WithPredicates(utils.PromotedServiceaccountPredicate),
).
WithOptions(controller.Options{MaxConcurrentReconciles: ctrlConfig.MaxConcurrentReconciles}).
Complete(r)
}
@@ -120,6 +230,13 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
}
}()
// Collect Ownership for Status
if err = r.collectOwners(ctx, instance); err != nil {
err = fmt.Errorf("cannot collect available owners: %w", err)
return result, err
}
// Ensuring Metadata.
err, updated := r.ensureMetadata(ctx, instance)
if err != nil {

View File

@@ -15,28 +15,11 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/internal/controllers/rbac"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
// ownerClusterRoleBindings generates a Capsule AdditionalRoleBinding object for the Owner dynamic clusterrole in order
// to take advantage of the additional role binding feature.
func (r *Manager) ownerClusterRoleBindings(owner api.OwnerSpec, clusterRole string) api.AdditionalRoleBindingsSpec {
rb := r.userClusterRoleBindings(owner.UserSpec, clusterRole)
if owner.Labels != nil {
rb.Labels = owner.Labels
}
if owner.Annotations != nil {
rb.Labels = owner.Annotations
}
return rb
}
func (r *Manager) userClusterRoleBindings(owner api.UserSpec, clusterRole string) api.AdditionalRoleBindingsSpec {
func (r *Manager) userClusterRoleBindings(owner api.CoreOwnerSpec, clusterRole string) api.AdditionalRoleBindingsSpec {
var subject rbacv1.Subject
if owner.Kind == "ServiceAccount" {
@@ -78,12 +61,13 @@ func (r *Manager) syncRoleBindings(ctx context.Context, tenant *capsulev1beta2.T
return fmt.Sprintf("%x", h.Sum64())
}
// getting requested Role Binding keys
keys := make([]string, 0, len(tenant.Spec.Owners))
keys := make([]string, 0, len(tenant.Status.Owners))
// Generating for dynamic tenant owners cluster roles
for _, owner := range tenant.Spec.Owners {
for _, owner := range tenant.Status.Owners {
for _, clusterRoleName := range owner.ClusterRoles {
cr := r.ownerClusterRoleBindings(owner, clusterRoleName)
cr := r.userClusterRoleBindings(owner, clusterRoleName)
keys = append(keys, hashFn(cr))
}
@@ -93,12 +77,6 @@ func (r *Manager) syncRoleBindings(ctx context.Context, tenant *capsulev1beta2.T
keys = append(keys, hashFn(i))
}
for _, i := range r.Configuration.Administrators() {
cr := r.userClusterRoleBindings(i, rbac.DeleterRoleName)
keys = append(keys, hashFn(cr))
}
group := new(errgroup.Group)
for _, ns := range tenant.Status.Namespaces {
@@ -119,16 +97,12 @@ func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsule
roleBindings := make([]api.AdditionalRoleBindingsSpec, 0)
for _, owner := range tenant.Spec.Owners {
for _, owner := range tenant.Status.Owners {
for _, clusterRoleName := range owner.ClusterRoles {
roleBindings = append(roleBindings, r.ownerClusterRoleBindings(owner, clusterRoleName))
roleBindings = append(roleBindings, r.userClusterRoleBindings(owner, clusterRoleName))
}
}
for _, a := range r.Configuration.Administrators() {
roleBindings = append(roleBindings, r.userClusterRoleBindings(a, rbac.DeleterRoleName))
}
roleBindings = append(roleBindings, tenant.Spec.AdditionalRoleBindings...)
for i, roleBinding := range roleBindings {

View File

@@ -5,6 +5,7 @@ package tenant
import (
"context"
"fmt"
"sort"
nodev1 "k8s.io/api/node/v1"
@@ -21,11 +22,86 @@ import (
"github.com/projectcapsule/capsule/pkg/api"
)
// Sets a label on the Tenant object with it's name.
func (r *Manager) collectOwners(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) {
owners, err := tnt.CollectOwners(
ctx,
r.Client,
r.Configuration.AllowServiceAccountPromotion(),
r.Configuration.Administrators(),
)
if err != nil {
return err
}
// No Direct Update needed as status is always posted
tnt.Status.Owners = owners
return nil
}
func (r Manager) reconcileClassStatus(
ctx context.Context,
fn func(context.Context, *capsulev1beta2.Tenant) error,
) (err error) {
tntList := &capsulev1beta2.TenantList{}
if err = r.List(ctx, tntList); err != nil {
return err
}
for i := range tntList.Items {
t := &tntList.Items[i]
// Collect Ownership for Status
if err = fn(ctx, t); err != nil {
err = fmt.Errorf("cannot collect available classes: %w", err)
return err
}
if err = r.updateTenantStatus(ctx, t, err); err != nil {
err = fmt.Errorf("cannot update tenant status: %w", err)
return err
}
}
return err
}
func (r *Manager) collectAvailableResources(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) {
log := log.FromContext(ctx)
log.V(5).Info("collecting available storageclasses")
if err = r.collectAvailableStorageClasses(ctx, tnt); err != nil {
return err
}
log.V(5).Info("collected available storageclasses", "size", len(tnt.Status.Classes.StorageClasses))
if err = r.collectAvailablePriorityClasses(ctx, tnt); err != nil {
return err
}
log.V(5).Info("collected available priorityclasses", "size", len(tnt.Status.Classes.PriorityClasses))
if err = r.collectAvailableGatewayClasses(ctx, tnt); err != nil {
return err
}
log.V(5).Info("collected available gatewayclasses", "size", len(tnt.Status.Classes.GatewayClasses))
if err = r.collectAvailableRuntimeClasses(ctx, tnt); err != nil {
return err
}
log.V(5).Info("collected available runtimeclasses", "size", len(tnt.Status.Classes.RuntimeClasses))
return nil
}
func (r *Manager) collectAvailableStorageClasses(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) {
if tnt.Status.Classes.StorageClasses, err = listObjectNamesBySelector(
ctx,
r.Client,
@@ -35,8 +111,10 @@ func (r *Manager) collectAvailableResources(ctx context.Context, tnt *capsulev1b
return err
}
log.V(5).Info("collected available storageclasses", "size", len(tnt.Status.Classes.StorageClasses))
return nil
}
func (r *Manager) collectAvailablePriorityClasses(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) {
if tnt.Status.Classes.PriorityClasses, err = listObjectNamesBySelector(
ctx,
r.Client,
@@ -46,8 +124,10 @@ func (r *Manager) collectAvailableResources(ctx context.Context, tnt *capsulev1b
return err
}
log.V(5).Info("collected available priorityclasses", "size", len(tnt.Status.Classes.PriorityClasses))
return nil
}
func (r *Manager) collectAvailableGatewayClasses(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) {
if tnt.Status.Classes.GatewayClasses, err = listObjectNamesBySelector(
ctx,
r.Client,
@@ -57,8 +137,10 @@ func (r *Manager) collectAvailableResources(ctx context.Context, tnt *capsulev1b
return err
}
log.V(5).Info("collected available gatewayclasses", "size", len(tnt.Status.Classes.GatewayClasses))
return nil
}
func (r *Manager) collectAvailableRuntimeClasses(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) {
if tnt.Status.Classes.RuntimeClasses, err = listObjectNamesBySelector(
ctx,
r.Client,
@@ -68,8 +150,6 @@ func (r *Manager) collectAvailableResources(ctx context.Context, tnt *capsulev1b
return err
}
log.V(5).Info("collected available runtimeclasses", "size", len(tnt.Status.Classes.RuntimeClasses))
return nil
}
@@ -81,13 +161,7 @@ func listObjectNamesBySelector(
allowed *api.DefaultAllowedListSpec,
list client.ObjectList,
opts ...client.ListOption,
) (objects []string, err error) {
defer func() {
if err == nil {
sort.Strings(objects)
}
}()
) ([]string, error) {
if err := c.List(ctx, list, opts...); err != nil {
return nil, err
}
@@ -97,8 +171,9 @@ func listObjectNamesBySelector(
return nil, err
}
allNames := make(map[string]struct{})
objects := make([]string, 0)
allNames := make(map[string]struct{})
selected := make(map[string]struct{})
hasSelector := false
@@ -117,9 +192,12 @@ func listObjectNamesBySelector(
objects = append(objects, accessor.GetName())
}
sort.Strings(objects)
return objects, nil
}
// Prepare selector
var sel labels.Selector
if hasSelector {
sel, err = metav1.LabelSelectorAsSelector(&allowed.LabelSelector)
@@ -128,6 +206,7 @@ func listObjectNamesBySelector(
}
}
// Evaluate objects
for _, obj := range objs {
accessor, err := meta.Accessor(obj)
if err != nil {
@@ -140,7 +219,6 @@ func listObjectNamesBySelector(
if hasSelector {
lbls := labels.Set(accessor.GetLabels())
if sel.Matches(lbls) {
selected[name] = struct{}{}
}
@@ -157,10 +235,6 @@ func listObjectNamesBySelector(
continue
}
if _, already := selected[name]; already {
continue
}
selected[name] = struct{}{}
}
@@ -168,5 +242,7 @@ func listObjectNamesBySelector(
objects = append(objects, name)
}
sort.Strings(objects)
return objects, nil
}

View File

@@ -12,14 +12,116 @@ import (
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/utils"
)
func (r *Manager) statusOnlyHandlerClasses(
fn func(ctx context.Context, perTenant func(context.Context, *capsulev1beta2.Tenant) error) error,
perTenant func(context.Context, *capsulev1beta2.Tenant) error,
errMsg string,
) *handler.TypedFuncs[client.Object, reconcile.Request] {
return &handler.TypedFuncs[client.Object, reconcile.Request]{
CreateFunc: func(
ctx context.Context,
_ event.TypedCreateEvent[client.Object],
_ workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
if err := fn(ctx, perTenant); err != nil {
r.Log.Error(err, errMsg)
}
},
UpdateFunc: func(
ctx context.Context,
_ event.TypedUpdateEvent[client.Object],
_ workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
if err := fn(ctx, perTenant); err != nil {
r.Log.Error(err, errMsg)
}
},
DeleteFunc: func(
ctx context.Context,
_ event.TypedDeleteEvent[client.Object],
_ workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
if err := fn(ctx, perTenant); err != nil {
r.Log.Error(err, errMsg)
}
},
}
}
func (r *Manager) enqueueTenantsForTenantOwner(
ctx context.Context,
tenantOwner client.Object,
q workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
var tenants capsulev1beta2.TenantList
if err := r.List(ctx, &tenants); err != nil {
r.Log.Error(err, "failed to list Tenants for Tenant Owner event")
return
}
owner, ok := tenantOwner.(*capsulev1beta2.TenantOwner)
if !ok {
return
}
for i := range tenants.Items {
tnt := &tenants.Items[i]
if _, found := tnt.Status.Owners.FindOwner(
owner.Spec.Name,
owner.Spec.Kind,
); !found {
continue
}
q.Add(reconcile.Request{
NamespacedName: types.NamespacedName{
Name: tnt.Name,
},
})
}
}
func (r *Manager) enqueueForTenantsWithCondition(
ctx context.Context,
obj client.Object,
q workqueue.TypedRateLimitingInterface[reconcile.Request],
fn func(*capsulev1beta2.Tenant, client.Object) bool,
) {
var tenants capsulev1beta2.TenantList
if err := r.List(ctx, &tenants); err != nil {
r.Log.Error(err, "failed to list Tenants for class event")
return
}
for i := range tenants.Items {
tnt := &tenants.Items[i]
if !fn(tnt, obj) {
continue
}
q.Add(reconcile.Request{
NamespacedName: types.NamespacedName{
Name: tnt.Name,
},
})
}
}
func (r *Manager) enqueueAllTenants(ctx context.Context, _ client.Object) []reconcile.Request {
var tenants capsulev1beta2.TenantList
if err := r.List(ctx, &tenants); err != nil {

View File

@@ -10,6 +10,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
var CapsuleConfigSpecChangedPredicate = predicate.Funcs{
@@ -32,6 +33,31 @@ var CapsuleConfigSpecChangedPredicate = predicate.Funcs{
GenericFunc: func(e event.GenericEvent) bool { return false },
}
var PromotedServiceaccountPredicate = predicate.TypedFuncs[client.Object]{
CreateFunc: func(e event.TypedCreateEvent[client.Object]) bool {
v, ok := e.Object.GetLabels()[meta.OwnerPromotionLabel]
return ok && v == meta.OwnerPromotionLabelTrigger
},
DeleteFunc: func(e event.TypedDeleteEvent[client.Object]) bool {
v, ok := e.Object.GetLabels()[meta.OwnerPromotionLabel]
return ok && v == meta.OwnerPromotionLabelTrigger
},
UpdateFunc: func(e event.TypedUpdateEvent[client.Object]) bool {
oldVal, oldOK := e.ObjectOld.GetLabels()[meta.OwnerPromotionLabel]
newVal, newOK := e.ObjectNew.GetLabels()[meta.OwnerPromotionLabel]
return oldOK != newOK || oldVal != newVal
},
GenericFunc: func(event.TypedGenericEvent[client.Object]) bool {
return false
},
}
var UpdatedMetadataPredicate = predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool { return true },
DeleteFunc: func(e event.DeleteEvent) bool { return true },
@@ -57,6 +83,19 @@ func labelsEqual(a, b map[string]string) bool {
return true
}
func LabelsChanged(keys []string, oldLabels, newLabels map[string]string) bool {
for _, key := range keys {
oldVal, oldOK := oldLabels[key]
newVal, newOK := newLabels[key]
if oldOK != newOK || oldVal != newVal {
return true
}
}
return false
}
func NamesMatchingPredicate(names ...string) builder.Predicates {
return builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
for _, name := range names {

View File

@@ -132,12 +132,7 @@ func (h *handler) OnUpdate(c client.Client, decoder admission.Decoder, recorder
}
}
} else {
owned, err := tenant.NamespaceIsOwned(ctx, c, h.cfg, oldNs, tnt, req.UserInfo)
if err != nil {
return utils.ErroredResponse(err)
}
if !owned {
if owned := tenant.NamespaceIsOwned(ctx, c, h.cfg, oldNs, tnt, req.UserInfo); !owned {
recorder.Eventf(oldNs, corev1.EventTypeWarning, "NamespacePatch", "Namespace %s can not be patched", oldNs.GetName())
response := admission.Denied("Denied patch request for this namespace")

View File

@@ -6,7 +6,6 @@ package validation
import (
"context"
"fmt"
"net/http"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"
@@ -62,14 +61,7 @@ func (h *patchHandler) OnUpdate(
return func(ctx context.Context, req admission.Request) *admission.Response {
e := fmt.Sprintf("namespace/%s can not be patched", ns.Name)
ok, err := users.IsTenantOwner(ctx, c, h.cfg, tnt, req.UserInfo)
if err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
if ok {
if ok := users.IsTenantOwnerByStatus(ctx, c, h.cfg, tnt, req.UserInfo); ok {
return nil
}

View File

@@ -13,7 +13,6 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
capsulewebhook "github.com/projectcapsule/capsule/internal/webhook"
"github.com/projectcapsule/capsule/internal/webhook/utils"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/configuration"
"github.com/projectcapsule/capsule/pkg/utils/users"
@@ -85,12 +84,7 @@ func (h *validating) handle(
}
// We don't want to allow promoted serviceaccounts to promote other serviceaccounts
allowed, err := users.IsTenantOwner(ctx, c, h.cfg, tnt, req.UserInfo)
if err != nil {
return utils.ErroredResponse(err)
}
if allowed {
if ok := users.IsTenantOwnerByStatus(ctx, c, h.cfg, tnt, req.UserInfo); ok {
return nil
}

View File

@@ -14,17 +14,27 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
capsulewebhook "github.com/projectcapsule/capsule/internal/webhook"
"github.com/projectcapsule/capsule/internal/webhook/utils"
"github.com/projectcapsule/capsule/pkg/configuration"
)
type warningHandler struct{}
func WarningHandler() capsulewebhook.Handler {
return &warningHandler{}
type warningHandler struct {
cfg configuration.Configuration
}
func (h *warningHandler) OnCreate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
return func(_ context.Context, req admission.Request) *admission.Response {
return h.handle(decoder, req)
func WarningHandler(cfg configuration.Configuration) capsulewebhook.Handler {
return &warningHandler{
cfg: cfg,
}
}
func (h *warningHandler) OnCreate(c client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
tnt := &capsulev1beta2.Tenant{}
if err := decoder.Decode(req, tnt); err != nil {
return utils.ErroredResponse(err)
}
return h.handle(tnt, decoder, req)
}
}
@@ -36,16 +46,16 @@ func (h *warningHandler) OnDelete(client.Client, admission.Decoder, record.Event
func (h *warningHandler) OnUpdate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
return func(_ context.Context, req admission.Request) *admission.Response {
return h.handle(decoder, req)
tnt := &capsulev1beta2.Tenant{}
if err := decoder.Decode(req, tnt); err != nil {
return utils.ErroredResponse(err)
}
return h.handle(tnt, decoder, req)
}
}
func (h *warningHandler) handle(decoder admission.Decoder, req admission.Request) *admission.Response {
tenant := &capsulev1beta2.Tenant{}
if err := decoder.Decode(req, tenant); err != nil {
return utils.ErroredResponse(err)
}
func (h *warningHandler) handle(tnt *capsulev1beta2.Tenant, decoder admission.Decoder, req admission.Request) *admission.Response {
response := &admission.Response{
AdmissionResponse: admissionv1.AdmissionResponse{
UID: req.UID,
@@ -53,31 +63,31 @@ func (h *warningHandler) handle(decoder admission.Decoder, req admission.Request
},
}
if len(tenant.Spec.LimitRanges.Items) > 0 {
if len(tnt.Spec.LimitRanges.Items) > 0 {
response.Warnings = append(response.Warnings, "Limitranges are deprecated and will be removed int the future. You need to consider to migrate to TenantReplications: https://projectcapsule.dev/docs/tenants/enforcement/#limitrange-distribution-with-tenantreplications.")
}
if len(tenant.Spec.NetworkPolicies.Items) > 0 {
if len(tnt.Spec.NetworkPolicies.Items) > 0 {
response.Warnings = append(response.Warnings, "NetworkPolicies are deprecated and will be removed int the future. You need to consider to migrate to TenantReplications: https://projectcapsule.dev/docs/tenants/enforcement/#networkpolicy-distribution-with-tenantreplications.")
}
if tenant.Spec.NamespaceOptions != nil && tenant.Spec.NamespaceOptions.AdditionalMetadata != nil {
if tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.AdditionalMetadata != nil {
response.Warnings = append(response.Warnings, "additionalMetadata is deprecated and will be removed int the future. You need to consider to migrate to AdditionalMetadataList: https://projectcapsule.dev/docs/tenants/enforcement/#additionalmetadatalist.")
}
if tenant.Spec.StorageClasses != nil && tenant.Spec.StorageClasses.Regex != "" {
if tnt.Spec.StorageClasses != nil && tnt.Spec.StorageClasses.Regex != "" {
response.Warnings = append(response.Warnings, "Using the regex property to select StorageClasses is deprecated and will be removed int the future.")
}
if tenant.Spec.GatewayOptions.AllowedClasses != nil && tenant.Spec.GatewayOptions.AllowedClasses.Regex != "" {
if tnt.Spec.GatewayOptions.AllowedClasses != nil && tnt.Spec.GatewayOptions.AllowedClasses.Regex != "" {
response.Warnings = append(response.Warnings, "Using the regex property to select GatewayClasses is deprecated and will be removed int the future.")
}
if tenant.Spec.PriorityClasses != nil && tenant.Spec.PriorityClasses.Regex != "" {
if tnt.Spec.PriorityClasses != nil && tnt.Spec.PriorityClasses.Regex != "" {
response.Warnings = append(response.Warnings, "Using the regex property to select PriorityClasses is deprecated and will be removed int the future.")
}
if tenant.Spec.RuntimeClasses != nil && tenant.Spec.RuntimeClasses.Regex != "" {
if tnt.Spec.RuntimeClasses != nil && tnt.Spec.RuntimeClasses.Regex != "" {
response.Warnings = append(response.Warnings, "Using the regex property to select RuntimeClasses is deprecated and will be removed int the future.")
}

143
pkg/api/misc/selectors.go Normal file
View File

@@ -0,0 +1,143 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package misc
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// Selector for resources and their labels or selecting origin namespaces
// +kubebuilder:object:generate=true
type NamespaceSelector struct {
// Select Items based on their labels. If the namespaceSelector is also set, the selector is applied
// to items within the selected namespaces. Otherwise for all the items.
*metav1.LabelSelector `json:",inline"`
}
// GetMatchingNamespaces retrieves the list of namespaces that match the NamespaceSelector.
func (s *NamespaceSelector) GetMatchingNamespaces(ctx context.Context, client client.Client) ([]corev1.Namespace, error) {
if s.LabelSelector == nil {
return nil, nil // No namespace selector means all namespaces
}
nsSelector, err := metav1.LabelSelectorAsSelector(s.LabelSelector)
if err != nil {
return nil, fmt.Errorf("invalid namespace selector: %w", err)
}
namespaceList := &corev1.NamespaceList{}
if err := client.List(ctx, namespaceList); err != nil {
return nil, fmt.Errorf("failed to list namespaces: %w", err)
}
var matchingNamespaces []corev1.Namespace
for _, ns := range namespaceList.Items {
if nsSelector.Matches(labels.Set(ns.Labels)) {
matchingNamespaces = append(matchingNamespaces, ns)
}
}
return matchingNamespaces, nil
}
// ListBySelectors lists objects of type T (using list L), then returns all items that
// match ANY of the provided LabelSelectors. The result is unique by namespace/name.
func ListBySelectors[T client.Object](
ctx context.Context,
c client.Client,
list client.ObjectList,
selectors []*metav1.LabelSelector,
) ([]T, error) {
if len(selectors) == 0 {
return nil, nil
}
if list == nil {
return nil, fmt.Errorf("list must not be nil")
}
// Preallocate with upper bound (len(selectors)); nil selectors will just not be used.
selList := make([]labels.Selector, 0, len(selectors))
for _, ls := range selectors {
if ls == nil {
continue
}
sel, err := metav1.LabelSelectorAsSelector(ls)
if err != nil {
return nil, fmt.Errorf("invalid label selector %v: %w", ls, err)
}
selList = append(selList, sel)
}
if len(selList) == 0 {
return nil, nil
}
// List all objects once
if err := c.List(ctx, list); err != nil {
return nil, fmt.Errorf("listing objects: %w", err)
}
rawItems, err := meta.ExtractList(list)
if err != nil {
return nil, fmt.Errorf("extracting list items: %w", err)
}
// Deduplicate by namespace/name
seen := make(map[client.ObjectKey]struct{}, len(rawItems))
// Upper bound: at most len(rawItems) will match; good enough for prealloc.
result := make([]T, 0, len(rawItems))
for _, obj := range rawItems {
typed, ok := obj.(T)
if !ok {
continue
}
lbls := typed.GetLabels()
if len(lbls) == 0 {
continue
}
set := labels.Set(lbls)
// Match against ANY selector
matched := false
for _, sel := range selList {
if sel.Matches(set) {
matched = true
break
}
}
if !matched {
continue
}
key := client.ObjectKeyFromObject(typed)
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
result = append(result, typed)
}
return result, nil
}

View File

@@ -0,0 +1,32 @@
//go:build !ignore_autogenerated
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
// Code generated by controller-gen. DO NOT EDIT.
package misc
import (
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceSelector) DeepCopyInto(out *NamespaceSelector) {
*out = *in
if in.LabelSelector != nil {
in, out := &in.LabelSelector, &out.LabelSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceSelector.
func (in *NamespaceSelector) DeepCopy() *NamespaceSelector {
if in == nil {
return nil
}
out := new(NamespaceSelector)
in.DeepCopyInto(out)
return out
}

View File

@@ -6,11 +6,8 @@ package api
// +kubebuilder:object:generate=true
type OwnerSpec struct {
UserSpec `json:",inline"`
CoreOwnerSpec `json:",inline"`
// Defines additional cluster-roles for the specific Owner.
// +kubebuilder:default={admin,capsule-namespace-deleter}
ClusterRoles []string `json:"clusterRoles,omitempty"`
// Proxy settings for tenant owner.
ProxyOperations []ProxySettings `json:"proxySettings,omitempty"`
// Additional Labels for the synchronized rolebindings
@@ -19,6 +16,16 @@ type OwnerSpec struct {
Annotations map[string]string `json:"annotations,omitempty"`
}
// +kubebuilder:object:generate=true
type CoreOwnerSpec struct {
UserSpec `json:",inline"`
// Defines additional cluster-roles for the specific Owner.
// +kubebuilder:default={admin,capsule-namespace-deleter}
ClusterRoles []string `json:"clusterRoles,omitempty"`
}
// +kubebuilder:validation:Enum=User;Group;ServiceAccount
type OwnerKind string

View File

@@ -1,7 +1,6 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
//nolint:dupl
package api
import (
@@ -31,6 +30,15 @@ func (o OwnerListSpec) IsOwner(name string, groups []string) bool {
return false
}
func (o OwnerListSpec) ToStatusOwners() OwnerStatusListSpec {
list := OwnerStatusListSpec{}
for _, owner := range o {
list = append(list, owner.CoreOwnerSpec)
}
return list
}
func (o OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec) {
sort.Sort(ByKindAndName(o))
i := sort.Search(len(o), func(i int) bool {

View File

@@ -11,9 +11,11 @@ import (
func TestOwnerListSpec_FindOwner(t *testing.T) {
bla := OwnerSpec{
UserSpec: UserSpec{
Kind: UserOwner,
Name: "bla",
CoreOwnerSpec: CoreOwnerSpec{
UserSpec: UserSpec{
Kind: UserOwner,
Name: "bla",
},
},
ProxyOperations: []ProxySettings{
{
@@ -23,9 +25,11 @@ func TestOwnerListSpec_FindOwner(t *testing.T) {
},
}
bar := OwnerSpec{
UserSpec: UserSpec{
Kind: GroupOwner,
Name: "bar",
CoreOwnerSpec: CoreOwnerSpec{
UserSpec: UserSpec{
Kind: GroupOwner,
Name: "bar",
},
},
ProxyOperations: []ProxySettings{
{
@@ -35,9 +39,11 @@ func TestOwnerListSpec_FindOwner(t *testing.T) {
},
}
baz := OwnerSpec{
UserSpec: UserSpec{
Kind: UserOwner,
Name: "baz",
CoreOwnerSpec: CoreOwnerSpec{
UserSpec: UserSpec{
Kind: UserOwner,
Name: "baz",
},
},
ProxyOperations: []ProxySettings{
{
@@ -47,9 +53,11 @@ func TestOwnerListSpec_FindOwner(t *testing.T) {
},
}
fim := OwnerSpec{
UserSpec: UserSpec{
Kind: ServiceAccountOwner,
Name: "fim",
CoreOwnerSpec: CoreOwnerSpec{
UserSpec: UserSpec{
Kind: ServiceAccountOwner,
Name: "fim",
},
},
ProxyOperations: []ProxySettings{
{
@@ -59,9 +67,11 @@ func TestOwnerListSpec_FindOwner(t *testing.T) {
},
}
bom := OwnerSpec{
UserSpec: UserSpec{
Kind: GroupOwner,
Name: "bom",
CoreOwnerSpec: CoreOwnerSpec{
UserSpec: UserSpec{
Kind: GroupOwner,
Name: "bom",
},
},
ProxyOperations: []ProxySettings{
{
@@ -75,9 +85,11 @@ func TestOwnerListSpec_FindOwner(t *testing.T) {
},
}
qip := OwnerSpec{
UserSpec: UserSpec{
Kind: ServiceAccountOwner,
Name: "qip",
CoreOwnerSpec: CoreOwnerSpec{
UserSpec: UserSpec{
Kind: ServiceAccountOwner,
Name: "qip",
},
},
ProxyOperations: []ProxySettings{
{

View File

@@ -0,0 +1,136 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package api
import (
"sort"
)
// +kubebuilder:object:generate=true
type OwnerStatusListSpec []CoreOwnerSpec
func (o *OwnerStatusListSpec) Upsert(
newOwner CoreOwnerSpec,
) {
owners := *o
// Ensure slice is sorted before binary search
sort.Sort(GetByKindAndName(owners))
less := func(a, b CoreOwnerSpec) bool {
if a.Kind.String() != b.Kind.String() {
return a.Kind.String() < b.Kind.String()
}
return a.Name < b.Name
}
// Find the first index where owners[i] >= newOwner
idx := sort.Search(len(owners), func(i int) bool {
return !less(owners[i], newOwner)
})
// If we found an exact match (same Kind + Name), merge ClusterRoles
if idx < len(owners) && !less(owners[idx], newOwner) && !less(newOwner, owners[idx]) {
existing := &owners[idx]
roleSet := make(map[string]struct{}, len(existing.ClusterRoles))
for _, r := range existing.ClusterRoles {
roleSet[r] = struct{}{}
}
for _, r := range newOwner.ClusterRoles {
if _, ok := roleSet[r]; !ok {
existing.ClusterRoles = append(existing.ClusterRoles, r)
roleSet[r] = struct{}{}
}
}
*o = owners
return
}
// Not found: append and keep sorted
owners = append(owners, newOwner)
sort.Sort(GetByKindAndName(owners))
*o = owners
}
func (o OwnerStatusListSpec) IsOwner(name string, groups []string) bool {
var groupSet map[string]struct{}
if len(groups) > 0 {
groupSet = make(map[string]struct{}, len(groups))
for _, g := range groups {
groupSet[g] = struct{}{}
}
}
for _, owner := range o {
switch owner.Kind {
case UserOwner, ServiceAccountOwner:
if name == owner.Name {
return true
}
case GroupOwner:
if groupSet == nil {
continue
}
if _, ok := groupSet[owner.Name]; ok {
return true
}
}
}
return false
}
func (o OwnerStatusListSpec) FindOwner(name string, kind OwnerKind) (CoreOwnerSpec, bool) {
// Sort in-place by (Kind.String(), Name).
sort.Sort(GetByKindAndName(o))
targetKind := kind.String()
n := len(o)
idx := sort.Search(n, func(i int) bool {
ki := o[i].Kind.String()
switch {
case ki > targetKind:
return true
case ki < targetKind:
return false
default:
return o[i].Name >= name
}
})
if idx < n &&
o[idx].Kind.String() == targetKind &&
o[idx].Name == name {
return o[idx], true
}
return CoreOwnerSpec{}, false
}
type GetByKindAndName OwnerStatusListSpec
func (b GetByKindAndName) Len() int {
return len(b)
}
func (b GetByKindAndName) Less(i, j int) bool {
if b[i].Kind.String() != b[j].Kind.String() {
return b[i].Kind.String() < b[j].Kind.String()
}
return b[i].Name < b[j].Name
}
func (b GetByKindAndName) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}

View File

@@ -0,0 +1,355 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package api_test
import (
"math/rand"
"reflect"
"sort"
"testing"
"time"
"github.com/projectcapsule/capsule/pkg/api"
)
func slowIsOwner(o api.OwnerStatusListSpec, name string, groups []string) bool {
for _, owner := range o {
switch owner.Kind {
case api.UserOwner, api.ServiceAccountOwner:
if name == owner.Name {
return true
}
case api.GroupOwner:
for _, group := range groups {
if group == owner.Name {
return true
}
}
}
}
return false
}
// linearFind is the obvious, slow, but correct reference implementation.
func linearFind(o api.OwnerStatusListSpec, name string, kind api.OwnerKind) (api.CoreOwnerSpec, bool) {
for _, x := range o {
if x.Kind == kind && x.Name == name {
return x, true
}
}
return api.CoreOwnerSpec{}, false
}
// randomName generates a simple lowercase name of length n.
func randomName(rnd *rand.Rand, n int) string {
const letters = "abcdefghijklmnopqrstuvwxyz"
b := make([]byte, n)
for i := range b {
b[i] = letters[rnd.Intn(len(letters))]
}
return string(b)
}
func TestUpsert_AddsNewOwnerToEmptyList(t *testing.T) {
var list api.OwnerStatusListSpec
list.Upsert(api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"admin"},
})
if len(list) != 1 {
t.Fatalf("expected 1 owner, got %d", len(list))
}
got := list[0]
if got.Kind != api.UserOwner || got.Name != "alice" {
t.Fatalf("unexpected owner: %+v", got)
}
if !reflect.DeepEqual(got.ClusterRoles, []string{"admin"}) {
t.Fatalf("unexpected roles: %#v", got.ClusterRoles)
}
}
func TestUpsert_MergesClusterRolesForExistingOwner(t *testing.T) {
list := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
}
list.Upsert(api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"extra-sad"},
})
if len(list) != 1 {
t.Fatalf("expected 1 owner, got %d", len(list))
}
got := list[0]
if got.Kind != api.UserOwner || got.Name != "alice" {
t.Fatalf("unexpected owner: %+v", got)
}
// Roles should be union of both sets, order: existing roles first, then new ones
expected := []string{"admin", "capsule-namespace-deleter", "extra-sad"}
if !reflect.DeepEqual(got.ClusterRoles, expected) {
t.Fatalf("expected roles %v, got %v", expected, got.ClusterRoles)
}
}
func TestUpsert_DeduplicatesClusterRoles(t *testing.T) {
list := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"admin", "viewer"},
},
}
list.Upsert(api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"viewer", "editor"},
})
if len(list) != 1 {
t.Fatalf("expected 1 owner, got %d", len(list))
}
got := list[0]
expected := []string{"admin", "viewer", "editor"}
if !reflect.DeepEqual(got.ClusterRoles, expected) {
t.Fatalf("expected roles %v, got %v", expected, got.ClusterRoles)
}
}
func TestUpsert_KeepsListSortedAndMergesIntoExistingInUnsortedInitialSlice(t *testing.T) {
// Start with an unsorted slice, as could come from API/server
list := api.OwnerStatusListSpec{
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "bob",
},
ClusterRoles: []string{"bob-role"},
},
{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"admin"},
},
}
// Upsert another alice
list.Upsert(api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "alice",
},
ClusterRoles: []string{"extra"},
})
if len(list) != 2 {
t.Fatalf("expected 2 owners (alice, bob), got %d", len(list))
}
// Ensure sorted by Kind.Name: alice before bob
// (relies on ByKindAndName order)
sorted := make(api.OwnerStatusListSpec, len(list))
copy(sorted, list)
sort.Sort(api.GetByKindAndName(sorted))
if !reflect.DeepEqual(list, sorted) {
t.Fatalf("expected list to be sorted by kind+name, got %#v", list)
}
// Find alice and check roles
var alice *api.CoreOwnerSpec
for i := range list {
if list[i].Name == "alice" {
alice = &list[i]
break
}
}
if alice == nil {
t.Fatalf("alice not found in list")
}
expectedRoles := []string{"admin", "extra"}
if !reflect.DeepEqual(alice.ClusterRoles, expectedRoles) {
t.Fatalf("expected alice roles %v, got %v", expectedRoles, alice.ClusterRoles)
}
}
func TestGetByKindAndNameOrdering(t *testing.T) {
o := api.OwnerStatusListSpec{
api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "b", Kind: api.ServiceAccountOwner}},
api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "z", Kind: api.UserOwner}},
api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "a", Kind: api.GroupOwner}},
api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "a", Kind: api.UserOwner}},
}
// Sort using production ordering
got := append(api.OwnerStatusListSpec(nil), o...)
sort.Sort(api.GetByKindAndName(got))
// Manually sorted expectation using the same logic.
want := append(api.OwnerStatusListSpec(nil), o...)
sort.Slice(want, func(i, j int) bool {
if want[i].Kind.String() != want[j].Kind.String() {
return want[i].Kind.String() < want[j].Kind.String()
}
return want[i].Name < want[j].Name
})
if len(got) != len(want) {
t.Fatalf("length mismatch: got %d, want %d", len(got), len(want))
}
for i := range got {
if !reflect.DeepEqual(got[i], want[i]) {
t.Fatalf("ordering mismatch at %d: got %+v, want %+v", i, got[i], want[i])
}
}
}
func TestFindOwner_Randomized(t *testing.T) {
rnd := rand.New(rand.NewSource(42)) // fixed seed for deterministic test runs
ownerKinds := []api.OwnerKind{
api.GroupOwner,
api.UserOwner,
api.ServiceAccountOwner,
}
const (
numLists = 200
maxLength = 40
numLookupsPerList = 80
)
for listIdx := 0; listIdx < numLists; listIdx++ {
var list api.OwnerStatusListSpec
n := rnd.Intn(maxLength)
for i := 0; i < n; i++ {
k := ownerKinds[rnd.Intn(len(ownerKinds))]
list = append(list, api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: randomName(rnd, 3+rnd.Intn(4)), // length 36
Kind: k,
},
})
}
for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ {
var qName string
var qKind api.OwnerKind
if len(list) > 0 && rnd.Float64() < 0.6 {
// 60% of lookups: pick a real element, must be found
pick := list[rnd.Intn(len(list))]
qName = pick.Name
qKind = pick.Kind
} else {
// 40%: random query, may or may not exist
qName = randomName(rnd, 3+rnd.Intn(4))
qKind = ownerKinds[rnd.Intn(len(ownerKinds))]
}
listCopy := append(api.OwnerStatusListSpec(nil), list...)
gotOwner, gotFound := listCopy.FindOwner(qName, qKind)
wantOwner, wantFound := linearFind(list, qName, qKind)
if gotFound != wantFound {
t.Fatalf("list=%d lookup=%d: found mismatch for (%q,%v): got=%v, want=%v",
listIdx, lookupIdx, qName, qKind, gotFound, wantFound)
}
if gotFound && !reflect.DeepEqual(gotOwner, wantOwner) {
t.Fatalf("list=%d lookup=%d: owner mismatch for (%q,%v):\n got= %+v\nwant= %+v",
listIdx, lookupIdx, qName, qKind, gotOwner, wantOwner)
}
}
}
}
func TestIsOwner_RandomizedMatchesSlowImplementation(t *testing.T) {
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
ownerKinds := []api.OwnerKind{
api.UserOwner,
api.GroupOwner,
api.ServiceAccountOwner,
}
const (
numLists = 200
maxOwnersPerList = 30
numLookupsPerList = 80
maxGroupsPerUser = 10
)
for listIdx := 0; listIdx < numLists; listIdx++ {
// Generate a random owner list (possibly with duplicates).
var owners api.OwnerStatusListSpec
nOwners := rnd.Intn(maxOwnersPerList)
for i := 0; i < nOwners; i++ {
kind := ownerKinds[rnd.Intn(len(ownerKinds))]
owners = append(owners, api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: randomName(rnd, 3+rnd.Intn(4)), // length 36
Kind: kind,
},
})
}
for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ {
// Generate a random userName and groups,
// sometimes biased to hit existing owners/groups.
var userName string
var groups []string
// 50% of the time: pick an existing owner name as userName
if len(owners) > 0 && rnd.Float64() < 0.5 {
pick := owners[rnd.Intn(len(owners))]
userName = pick.Name
} else {
userName = randomName(rnd, 3+rnd.Intn(4))
}
// Random groups, sometimes including owner names
nGroups := rnd.Intn(maxGroupsPerUser)
for i := 0; i < nGroups; i++ {
if len(owners) > 0 && rnd.Float64() < 0.5 {
pick := owners[rnd.Intn(len(owners))]
groups = append(groups, pick.Name)
} else {
groups = append(groups, randomName(rnd, 3+rnd.Intn(4)))
}
}
got := owners.IsOwner(userName, groups)
want := slowIsOwner(owners, userName, groups)
if got != want {
t.Fatalf("list=%d lookup=%d: mismatch\n owners=%v\n user=%q\n groups=%v\n optimized=%v\n slow=%v",
listIdx, lookupIdx, owners, userName, groups, got, want)
}
}
}
}

View File

@@ -1,7 +1,7 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package rbac
package api
import (
rbacv1 "k8s.io/api/rbac/v1"
@@ -14,7 +14,7 @@ const (
)
var (
clusterRoles = map[string]*rbacv1.ClusterRole{
ClusterRoles = map[string]*rbacv1.ClusterRole{
ProvisionerRoleName: {
ObjectMeta: metav1.ObjectMeta{
Name: ProvisionerRoleName,
@@ -41,7 +41,7 @@ var (
},
}
provisionerClusterRoleBinding = &rbacv1.ClusterRoleBinding{
ProvisionerClusterRoleBinding = &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: ProvisionerRoleName,
},

View File

@@ -1,49 +0,0 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// Selector for resources and their labels or selecting origin namespaces
// +kubebuilder:object:generate=true
type NamespaceSelector struct {
// Select Items based on their labels. If the namespaceSelector is also set, the selector is applied
// to items within the selected namespaces. Otherwise for all the items.
*metav1.LabelSelector `json:",inline"`
}
// GetMatchingNamespaces retrieves the list of namespaces that match the NamespaceSelector.
func (s *NamespaceSelector) GetMatchingNamespaces(ctx context.Context, client client.Client) ([]corev1.Namespace, error) {
if s.LabelSelector == nil {
return nil, nil // No namespace selector means all namespaces
}
nsSelector, err := metav1.LabelSelectorAsSelector(s.LabelSelector)
if err != nil {
return nil, fmt.Errorf("invalid namespace selector: %w", err)
}
namespaceList := &corev1.NamespaceList{}
if err := client.List(ctx, namespaceList); err != nil {
return nil, fmt.Errorf("failed to list namespaces: %w", err)
}
var matchingNamespaces []corev1.Namespace
for _, ns := range namespaceList.Items {
if nsSelector.Matches(labels.Set(ns.Labels)) {
matchingNamespaces = append(matchingNamespaces, ns)
}
}
return matchingNamespaces, nil
}

View File

@@ -1,7 +1,6 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
//nolint:dupl
package api
import (

View File

@@ -161,6 +161,27 @@ func (in *AllowedServices) DeepCopy() *AllowedServices {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CoreOwnerSpec) DeepCopyInto(out *CoreOwnerSpec) {
*out = *in
out.UserSpec = in.UserSpec
if in.ClusterRoles != nil {
in, out := &in.ClusterRoles, &out.ClusterRoles
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoreOwnerSpec.
func (in *CoreOwnerSpec) DeepCopy() *CoreOwnerSpec {
if in == nil {
return nil
}
out := new(CoreOwnerSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DefaultAllowedListSpec) DeepCopyInto(out *DefaultAllowedListSpec) {
*out = *in
@@ -239,26 +260,6 @@ func (in *LimitRangesSpec) DeepCopy() *LimitRangesSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceSelector) DeepCopyInto(out *NamespaceSelector) {
*out = *in
if in.LabelSelector != nil {
in, out := &in.LabelSelector, &out.LabelSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceSelector.
func (in *NamespaceSelector) DeepCopy() *NamespaceSelector {
if in == nil {
return nil
}
out := new(NamespaceSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkPolicySpec) DeepCopyInto(out *NetworkPolicySpec) {
*out = *in
@@ -305,12 +306,7 @@ func (in OwnerListSpec) DeepCopy() OwnerListSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OwnerSpec) DeepCopyInto(out *OwnerSpec) {
*out = *in
out.UserSpec = in.UserSpec
if in.ClusterRoles != nil {
in, out := &in.ClusterRoles, &out.ClusterRoles
*out = make([]string, len(*in))
copy(*out, *in)
}
in.CoreOwnerSpec.DeepCopyInto(&out.CoreOwnerSpec)
if in.ProxyOperations != nil {
in, out := &in.ProxyOperations, &out.ProxyOperations
*out = make([]ProxySettings, len(*in))
@@ -344,6 +340,27 @@ func (in *OwnerSpec) DeepCopy() *OwnerSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in OwnerStatusListSpec) DeepCopyInto(out *OwnerStatusListSpec) {
{
in := &in
*out = make(OwnerStatusListSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OwnerStatusListSpec.
func (in OwnerStatusListSpec) DeepCopy() OwnerStatusListSpec {
if in == nil {
return nil
}
out := new(OwnerStatusListSpec)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PodOptions) DeepCopyInto(out *PodOptions) {
*out = *in

Some files were not shown because too many files have changed in this diff Show More