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
+2 -2
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
+6 -4
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,
})
}
+61
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 {
+18 -12
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{
+3
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"`
+21
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
+53
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{})
}
+126 -1
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))