mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 04:26:45 +00:00
feat: upstream enterprise preview (#1841)
feat: upstream enterprise preview --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net> Co-authored-by: CorentinPtrl <pitrel.corentin@gmail.com>
This commit is contained in:
co-authored by
CorentinPtrl
parent
7a65ab7afc
commit
cc4fb45d70
@@ -7,6 +7,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/rbac"
|
||||
)
|
||||
|
||||
// TenantSpec defines the desired state of Tenant.
|
||||
@@ -36,7 +37,7 @@ type TenantSpec struct {
|
||||
// +optional
|
||||
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitzero"`
|
||||
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
|
||||
AdditionalRoleBindings []api.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
|
||||
AdditionalRoleBindings []rbac.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
|
||||
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
|
||||
ImagePullPolicies []api.ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
|
||||
// Specifies the allowed priorityClasses assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed PriorityClasses. Optional.
|
||||
|
||||
@@ -9,6 +9,7 @@ package v1beta1
|
||||
|
||||
import (
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/rbac"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
@@ -323,7 +324,7 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
|
||||
in.ResourceQuota.DeepCopyInto(&out.ResourceQuota)
|
||||
if in.AdditionalRoleBindings != nil {
|
||||
in, out := &in.AdditionalRoleBindings, &out.AdditionalRoleBindings
|
||||
*out = make([]api.AdditionalRoleBindingsSpec, len(*in))
|
||||
*out = make([]rbac.AdditionalRoleBindingsSpec, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
|
||||
@@ -4,16 +4,11 @@
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/rbac"
|
||||
)
|
||||
|
||||
// CapsuleConfigurationStatus defines the Capsule configuration status.
|
||||
type CapsuleConfigurationStatus struct {
|
||||
// Last time all caches were invalided
|
||||
LastCacheInvalidation metav1.Time `json:"lastCacheInvalidation,omitempty"`
|
||||
|
||||
// Users which are considered Capsule Users and are bound to the Capsule Tenant construct.
|
||||
Users api.UserListSpec `json:"users,omitempty"`
|
||||
Users rbac.UserListSpec `json:"users,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4,26 +4,19 @@
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/api/rbac"
|
||||
"github.com/projectcapsule/capsule/pkg/runtime/admission"
|
||||
)
|
||||
|
||||
// CapsuleConfigurationSpec defines the Capsule configuration.
|
||||
type CapsuleConfigurationSpec struct {
|
||||
// Define entities which are considered part of the Capsule construct
|
||||
// Users not mentioned here will be ignored by Capsule
|
||||
Users api.UserListSpec `json:"users,omitempty"`
|
||||
// Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
|
||||
//
|
||||
// Names of the users considered as Capsule users.
|
||||
UserNames []string `json:"userNames,omitempty"`
|
||||
// Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
|
||||
//
|
||||
// Names of the groups considered as Capsule users.
|
||||
UserGroups []string `json:"userGroups,omitempty"`
|
||||
Users rbac.UserListSpec `json:"users,omitempty"`
|
||||
// Define groups which when found in the request of a user will be ignored by the Capsule
|
||||
// this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.
|
||||
IgnoreUserWithGroups []string `json:"ignoreUserWithGroups,omitempty"`
|
||||
@@ -54,7 +47,7 @@ type CapsuleConfigurationSpec struct {
|
||||
// These entities are automatically owners for all existing tenants. Meaning they can add namespaces to any tenant. However they must be specific by using the capsule label
|
||||
// for interacting with namespaces. Because if that label is not defined, it's assumed that namespace interaction was not targeted towards a tenant and will therefor
|
||||
// be ignored by capsule.
|
||||
Administrators api.UserListSpec `json:"administrators,omitempty"`
|
||||
Administrators rbac.UserListSpec `json:"administrators,omitempty"`
|
||||
// Configuration for dynamic Validating and Mutating Admission webhooks managed by Capsule.
|
||||
Admission DynamicAdmission `json:"admission,omitempty"`
|
||||
// Define Properties for managed ClusterRoles by Capsule
|
||||
@@ -63,6 +56,18 @@ type CapsuleConfigurationSpec struct {
|
||||
// Define the period of time upon a cache invalidation is executed for all caches.
|
||||
// +kubebuilder:default="24h"
|
||||
CacheInvalidation metav1.Duration `json:"cacheInvalidation"`
|
||||
// Service Account Client configuration for impersonation properties
|
||||
// +optional
|
||||
Impersonation ServiceAccountClient `json:"impersonation,omitzero"`
|
||||
|
||||
// Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
|
||||
//
|
||||
// Names of the users considered as Capsule users.
|
||||
UserNames []string `json:"userNames,omitempty"`
|
||||
// Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
|
||||
//
|
||||
// Names of the groups considered as Capsule users.
|
||||
UserGroups []string `json:"userGroups,omitempty"`
|
||||
}
|
||||
|
||||
type RBACConfiguration struct {
|
||||
@@ -81,24 +86,31 @@ type RBACConfiguration struct {
|
||||
}
|
||||
|
||||
type DynamicAdmission struct {
|
||||
// Service Name of the Admission Service
|
||||
// +kubebuilder:default=capsule-webhook-service
|
||||
ServiceName string `json:"serviceName,omitempty"`
|
||||
|
||||
// Configure dynamic Mutating Admission for Capsule
|
||||
Mutating DynamicAdmissionConfig `json:"mutating,omitempty"`
|
||||
Mutating *DynamicMutatingAdmissionConfig `json:"mutating,omitempty"`
|
||||
|
||||
// Configure dynamic Validating Admission for Capsule
|
||||
Validating DynamicAdmissionConfig `json:"validating,omitempty"`
|
||||
Validating *DynamicValidatingAdmissionConfig `json:"validating,omitempty"`
|
||||
}
|
||||
|
||||
type DynamicAdmissionConfig struct {
|
||||
// Name the Admission Webhook
|
||||
Name meta.RFC1123Name `json:"name,omitempty"`
|
||||
// Labels added to the Admission Webhook
|
||||
type DynamicValidatingAdmissionConfig struct {
|
||||
admission.DynamicAdmissionConfig `json:",inline"`
|
||||
|
||||
// Define Dynamic Admission Webhooks
|
||||
// +optional
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// Annotations added to the Admission Webhook
|
||||
Webhooks []*admission.ValidatingWebhook `json:"webhooks,omitempty"`
|
||||
}
|
||||
|
||||
type DynamicMutatingAdmissionConfig struct {
|
||||
admission.DynamicAdmissionConfig `json:",inline"`
|
||||
|
||||
// Define Dynamic Admission Webhooks
|
||||
// +optional
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
// From the upstram struct
|
||||
Client admissionregistrationv1.WebhookClientConfig `json:"client"`
|
||||
Webhooks []*admission.MutatingWebhook `json:"webhooks,omitempty"`
|
||||
}
|
||||
|
||||
type NodeMetadata struct {
|
||||
@@ -115,14 +127,47 @@ type CapsuleResources struct {
|
||||
// Must be in the same Namespace where the Capsule Deployment is deployed.
|
||||
// +kubebuilder:default=capsule-tls
|
||||
TLSSecretName string `json:"TLSSecretName"` //nolint:tagliatelle
|
||||
// Deprecated: use dynamic admission instead
|
||||
//
|
||||
// Name of the MutatingWebhookConfiguration which contains the dynamic admission controller paths and resources.
|
||||
// +kubebuilder:default=capsule-mutating-webhook-configuration
|
||||
MutatingWebhookConfigurationName string `json:"mutatingWebhookConfigurationName"`
|
||||
// Deprecated: use dynamic admission instead
|
||||
//
|
||||
// Name of the ValidatingWebhookConfiguration which contains the dynamic admission controller paths and resources.
|
||||
// +kubebuilder:default=capsule-validating-webhook-configuration
|
||||
ValidatingWebhookConfigurationName string `json:"validatingWebhookConfigurationName"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
type ServiceAccountClient struct {
|
||||
// Kubernetes API Endpoint to use for impersonation
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
// Namespace where the CA certificate secret is located
|
||||
CASecretNamespace meta.RFC1123SubdomainName `json:"caSecretNamespace,omitempty"`
|
||||
// Name of the secret containing the CA certificate
|
||||
CASecretName meta.RFC1123Name `json:"caSecretName,omitempty"`
|
||||
// Key in the secret that holds the CA certificate (e.g., "ca.crt")
|
||||
// +kubebuilder:default=ca.crt
|
||||
CASecretKey string `json:"caSecretKey,omitempty"`
|
||||
// If true, TLS certificate verification is skipped (not recommended for production)
|
||||
// +kubebuilder:default=false
|
||||
SkipTLSVerify bool `json:"skipTlsVerify,omitempty"`
|
||||
// Default ServiceAccount for global resources (GlobalTenantResource)
|
||||
// When defined, users are required to use this ServiceAccount anywhere in the cluster
|
||||
// unless they explicitly provide their own.
|
||||
GlobalDefaultServiceAccount meta.RFC1123Name `json:"globalDefaultServiceAccount,omitempty"`
|
||||
// Default ServiceAccount for global resources (GlobalTenantResource)
|
||||
// When defined, users are required to use this ServiceAccount anywhere in the cluster
|
||||
// unless they explicitly provide their own.
|
||||
// +optional
|
||||
GlobalDefaultServiceAccountNamespace meta.RFC1123SubdomainName `json:"globalDefaultServiceAccountNamespace,omitempty"`
|
||||
// Default ServiceAccount for namespaced resources (TenantResource)
|
||||
// When defined, users are required to use this ServiceAccount within the namespace
|
||||
// where they deploy the resource, unless they explicitly provide their own.
|
||||
TenantDefaultServiceAccount meta.RFC1123Name `json:"tenantDefaultServiceAccount,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Cluster
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1beta2
|
||||
|
||||
func (c *CustomQuotaSpec) CollectJSONPathExpressions() (expressions []string) {
|
||||
set := map[string]struct{}{}
|
||||
|
||||
for _, source := range c.Sources {
|
||||
if source.Path != "" {
|
||||
set[source.Path] = struct{}{}
|
||||
}
|
||||
|
||||
for _, sel := range source.Selectors {
|
||||
for _, fs := range sel.FieldSelectors {
|
||||
if fs != "" {
|
||||
set[fs] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for e := range set {
|
||||
expressions = append(expressions, e)
|
||||
}
|
||||
|
||||
return expressions
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
k8smeta "k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
)
|
||||
|
||||
// CustomQuotaStatus defines the observed state of GlobalResourceQuota.
|
||||
type CustomQuotaStatus struct {
|
||||
// Usage measurements
|
||||
// +optional
|
||||
Usage CustomQuotaStatusUsage `json:"usage,omitempty"`
|
||||
// Objects regarding this policy
|
||||
Claims []CustomQuotaClaimItem `json:"claims,omitempty"`
|
||||
// Targeting GVK
|
||||
Targets []CustomQuotaStatusTarget `json:"targets"`
|
||||
// Conditions
|
||||
Conditions meta.ConditionList `json:"conditions"`
|
||||
}
|
||||
|
||||
func (s *CustomQuotaStatus) HasClaimUID(uid types.UID) bool {
|
||||
for i := range s.Claims {
|
||||
if s.Claims[i].UID == uid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type CustomQuotaClaimItem struct {
|
||||
metav1.GroupVersionKind `json:",inline"`
|
||||
meta.NamespacedObjectWithUIDReference `json:",inline"`
|
||||
|
||||
// Resource Quantity for given item
|
||||
Usage resource.Quantity `json:"usage"`
|
||||
}
|
||||
|
||||
type CustomQuotaStatusTarget struct {
|
||||
metav1.GroupVersionKind `json:",inline"`
|
||||
CustomQuotaSpecSourceConfig `json:",inline"`
|
||||
|
||||
// Path on GVK where usage is evaluated
|
||||
Scope k8smeta.RESTScopeName `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// CustomQuotaStatus defines the observed state of GlobalResourceQuota.
|
||||
type CustomQuotaStatusUsage struct {
|
||||
// Used is the current observed total usage of the resource.
|
||||
// +optional
|
||||
Used resource.Quantity `json:"used"`
|
||||
// Used is the current observed total available of the resource (limit - used).
|
||||
// +optional
|
||||
Available resource.Quantity `json:"available"`
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/runtime/gvk"
|
||||
"github.com/projectcapsule/capsule/pkg/runtime/quota"
|
||||
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
|
||||
)
|
||||
|
||||
// CustomQuotaSpec.
|
||||
type CustomQuotaSpec struct {
|
||||
// Select items governed by this quota
|
||||
ScopeSelectors []metav1.LabelSelector `json:"scopeSelectors,omitempty"`
|
||||
// Resource Quantity as limit
|
||||
Limit resource.Quantity `json:"limit"`
|
||||
// Target resource
|
||||
Sources []CustomQuotaSpecSource `json:"sources,omitzero"`
|
||||
// Additional Options for the CustomQuotaSpecification
|
||||
// +kubebuilder:default:={emitMetricPerClaimUsage:false}
|
||||
Options *CustomQuotaOptionsSpec `json:"options,omitzero"`
|
||||
}
|
||||
|
||||
// CustomQuotaOptionsSpec.
|
||||
type CustomQuotaOptionsSpec struct {
|
||||
// Additionally expose usage metrics for each claim contributing to the quota.
|
||||
// This is disabled by default to avoid high cardinality in the metrics, but can be enabled for more granular monitoring and alerting.
|
||||
// +kubebuilder:default:=false
|
||||
EmitPerClaimMetrics bool `json:"emitMetricPerClaimUsage,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:validation:XValidation:rule="self.op == 'count' ? !has(self.path) || size(self.path) == 0 : has(self.path) && size(self.path) > 0",message="path must be empty when op is 'count'; otherwise path must be set and non-empty"
|
||||
type CustomQuotaSpecSource struct {
|
||||
gvk.VersionKind `json:",inline"`
|
||||
CustomQuotaSpecSourceConfig `json:",inline"`
|
||||
}
|
||||
|
||||
type CustomQuotaSpecSourceConfig struct {
|
||||
// Path on GVK where usage is evaluated.
|
||||
// Must be empty when op is "count".
|
||||
// Required and non-empty for all other operations.
|
||||
// +optional
|
||||
Path string `json:"path,omitempty"`
|
||||
|
||||
// Operation used to evaluate usage.
|
||||
// +kubebuilder:default:=add
|
||||
Operation quota.Operation `json:"op,omitempty"`
|
||||
|
||||
// Provide more granular selectors for these sources
|
||||
// The ScopeSelector and NamespaceSelector are always applied
|
||||
// Allowing these selectors to make further selecting on the resulting subset.
|
||||
Selectors []selectors.SelectorWithFields `json:"selectors,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:scope=Namespaced,shortName=cq
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="Limit",type="string",JSONPath=".spec.limit",description="The total limit available"
|
||||
// +kubebuilder:printcolumn:name="Used",type="string",JSONPath=".status.usage.used",description="The total used amount"
|
||||
// +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.usage.available",description="The total amount available"
|
||||
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Reconcile Status"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="Reconcile Message"
|
||||
|
||||
type CustomQuota struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// +optional
|
||||
metav1.ObjectMeta `json:"metadata,omitzero"`
|
||||
|
||||
Spec CustomQuotaSpec `json:"spec"`
|
||||
|
||||
// +optional
|
||||
Status CustomQuotaStatus `json:"status,omitzero"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// CustomQuotaList contains a list of CustomQuota.
|
||||
type CustomQuotaList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// +optional
|
||||
metav1.ListMeta `json:"metadata,omitzero"`
|
||||
|
||||
Items []CustomQuota `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&CustomQuota{}, &CustomQuotaList{})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1beta2
|
||||
|
||||
import "slices"
|
||||
|
||||
// CustomQuotaStatus defines the observed state of GlobalResourceQuota.
|
||||
type GlobalCustomQuotaStatus struct {
|
||||
CustomQuotaStatus `json:",inline"`
|
||||
|
||||
// Observed Namespaces
|
||||
Namespaces []string `json:"namespaces,omitempty"`
|
||||
}
|
||||
|
||||
func (g *GlobalCustomQuotaStatus) NamespacePresent(ns string) bool {
|
||||
return slices.Contains(g.Namespaces, ns)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2020-2026 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/runtime/selectors"
|
||||
)
|
||||
|
||||
// ClusterCustomQuotaSpec.
|
||||
type GlobalCustomQuotaSpec struct {
|
||||
CustomQuotaSpec `json:",inline"`
|
||||
|
||||
// Select specifc namespaces where this Quota selects items.
|
||||
NamespaceSelectors []selectors.NamespaceSelector `json:"namespaceSelectors,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:scope=Cluster,shortName=gcq
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="Limit",type="string",JSONPath=".spec.limit",description="The total limit available"
|
||||
// +kubebuilder:printcolumn:name="Used",type="string",JSONPath=".status.usage.used",description="The total used amount"
|
||||
// +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.usage.available",description="The total amount available"
|
||||
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Reconcile Status"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="Reconcile Message"
|
||||
|
||||
type GlobalCustomQuota struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// +optional
|
||||
metav1.ObjectMeta `json:"metadata,omitzero"`
|
||||
|
||||
Spec GlobalCustomQuotaSpec `json:"spec"`
|
||||
|
||||
// +optional
|
||||
Status GlobalCustomQuotaStatus `json:"status,omitzero"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// ClusterCustomQuotaList contains a list of ClusterCustomQuota.
|
||||
type GlobalCustomQuotaList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// +optional
|
||||
metav1.ListMeta `json:"metadata,omitzero"`
|
||||
|
||||
Items []GlobalCustomQuota `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&GlobalCustomQuota{}, &GlobalCustomQuotaList{})
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright 2020-2026 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"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
type NamespaceRule struct {
|
||||
// Enforce these properties via Rules
|
||||
NamespaceRuleBody `json:",inline"`
|
||||
|
||||
// Select namespaces which are going to usese
|
||||
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
type NamespaceRuleBody struct {
|
||||
// Enforcement Rules applied
|
||||
//+optional
|
||||
Enforce NamespaceRuleEnforceBody `json:"enforce,omitzero"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
type NamespaceRuleEnforceBody struct {
|
||||
// Define registries which are allowed to be used within this tenant
|
||||
// The rules are aggregated, since you can use Regular Expressions the match registry endpoints
|
||||
Registries []api.OCIRegistry `json:"registries,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
)
|
||||
|
||||
// QuantityLedgerReservation represents one active inflight reservation.
|
||||
// ID should be stable for retries of the same admission request.
|
||||
// In practice, admission.Request.UID is a good default.
|
||||
type QuantityLedgerReservation struct {
|
||||
// Unique reservation identifier.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
ID string `json:"id"`
|
||||
|
||||
// Amount reserved for this request.
|
||||
Usage resource.Quantity `json:"usage"`
|
||||
|
||||
// Object that this reservation is intended to create/update.
|
||||
ObjectRef QuantityLedgerObjectRef `json:"objectRef"`
|
||||
|
||||
// Time the reservation was first created.
|
||||
CreatedAt metav1.Time `json:"createdAt"`
|
||||
|
||||
// Time the reservation was last refreshed or updated.
|
||||
UpdatedAt metav1.Time `json:"updatedAt"`
|
||||
|
||||
// Time after which the reservation may be considered stale.
|
||||
// +optional
|
||||
ExpiresAt *metav1.Time `json:"expiresAt,omitempty"`
|
||||
}
|
||||
|
||||
// QuantityLedgerPendingDelete tracks objects that are expected to disappear from claims
|
||||
// soon, but may still temporarily appear during rebuild due to propagation delay.
|
||||
type QuantityLedgerPendingDelete struct {
|
||||
ObjectRef QuantityLedgerObjectRef `json:"objectRef"`
|
||||
CreatedAt metav1.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// QuantityLedgerStatus contains the mutable coordination state used by admission
|
||||
// and quota controllers.
|
||||
type QuantityLedgerStatus struct {
|
||||
// Reserved is the aggregate sum of all active reservations.
|
||||
// Controllers/webhooks should treat this as derived data from Reservations.
|
||||
// +optional
|
||||
Reserved resource.Quantity `json:"reserved,omitempty"`
|
||||
|
||||
// Active inflight reservations for this quota.
|
||||
// +optional
|
||||
Reservations []QuantityLedgerReservation `json:"reservations,omitempty"`
|
||||
|
||||
// Pending delete hints carried over from admission delete handling.
|
||||
// +optional
|
||||
PendingDeletes []QuantityLedgerPendingDelete `json:"pendingDeletes,omitempty"`
|
||||
|
||||
// Conditions for the resource claim
|
||||
// +optional
|
||||
Conditions meta.ConditionList `json:"conditions,omitzero"`
|
||||
|
||||
// Allocated is the admission-owned total that has been accepted by the webhook.
|
||||
// It must be updated only through optimistic concurrency on QuantityLedger.
|
||||
Allocated resource.Quantity `json:"allocated,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
// QuotaLedgerTargetRef identifies the quota object that owns this ledger.
|
||||
// Namespace is optional for cluster-scoped targets such as GlobalCustomQuota.
|
||||
type QuantityLedgerTargetRef struct {
|
||||
// APIGroup of the target quota resource, for example "capsule.clastix.io".
|
||||
// +optional
|
||||
APIGroup string `json:"apiGroup,omitempty"`
|
||||
|
||||
// Kind of the target quota resource, for example "CustomQuota" or "GlobalCustomQuota".
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Kind string `json:"kind"`
|
||||
|
||||
// Namespace of the target quota resource.
|
||||
// Must be empty for cluster-scoped targets.
|
||||
// +optional
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
|
||||
// Name of the target quota resource.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Name string `json:"name"`
|
||||
|
||||
// UID of the target quota resource.
|
||||
// Optional, but useful for stale reference detection.
|
||||
// +optional
|
||||
UID types.UID `json:"uid,omitempty"`
|
||||
}
|
||||
|
||||
// QuotaLedgerObjectRef identifies the object for which a reservation exists.
|
||||
// UID may be empty for CREATE admission before the object is persisted.
|
||||
type QuantityLedgerObjectRef struct {
|
||||
// APIGroup of the tracked object.
|
||||
// +optional
|
||||
APIGroup string `json:"apiGroup,omitempty"`
|
||||
|
||||
// APIVersion of the tracked object, for example "v1".
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
APIVersion string `json:"apiVersion"`
|
||||
|
||||
// Kind of the tracked object, for example "Pod".
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Kind string `json:"kind"`
|
||||
|
||||
// Namespace of the tracked object.
|
||||
// +optional
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
|
||||
// Name of the tracked object.
|
||||
// +optional
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// UID of the tracked object.
|
||||
// +optional
|
||||
UID types.UID `json:"uid,omitempty"`
|
||||
}
|
||||
|
||||
// QuotaLedgerSpec contains the immutable target reference.
|
||||
type QuantityLedgerSpec struct {
|
||||
// TargetRef points to the quota object that this ledger belongs to.
|
||||
TargetRef QuantityLedgerTargetRef `json:"targetRef"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:path=quantityledgers,scope=Namespaced,shortName=ql
|
||||
// +kubebuilder:printcolumn:name="TargetKind",type=string,JSONPath=`.spec.targetRef.kind`
|
||||
// +kubebuilder:printcolumn:name="TargetNamespace",type=string,JSONPath=`.spec.targetRef.namespace`
|
||||
// +kubebuilder:printcolumn:name="TargetName",type=string,JSONPath=`.spec.targetRef.name`
|
||||
// +kubebuilder:printcolumn:name="Reserved",type=string,JSONPath=`.status.reserved`
|
||||
// +kubebuilder:printcolumn:name="Reservations",type=integer,JSONPath=`.status.reservations.size()`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
type QuantityLedger struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec QuantityLedgerSpec `json:"spec,omitempty"`
|
||||
Status QuantityLedgerStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
type QuantityLedgerList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []QuantityLedger `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&QuantityLedger{}, &QuantityLedgerList{})
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package v1beta2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -15,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func (r *ResourcePool) GetQuotaName() string {
|
||||
return fmt.Sprintf("capsule-pool-%s", r.GetName())
|
||||
return meta.NameForManagedPoolResourceQuota(r.GetName())
|
||||
}
|
||||
|
||||
func (r *ResourcePool) AssignNamespaces(namespaces []corev1.Namespace) {
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1beta2
|
||||
package v1beta2_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetClaimFromStatus(t *testing.T) {
|
||||
@@ -20,7 +21,7 @@ func TestGetClaimFromStatus(t *testing.T) {
|
||||
testUID := types.UID("test-uid")
|
||||
otherUID := types.UID("wrong-uid")
|
||||
|
||||
claim := &ResourcePoolClaim{
|
||||
claim := &v1beta2.ResourcePoolClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "claim-a",
|
||||
Namespace: ns,
|
||||
@@ -28,11 +29,11 @@ func TestGetClaimFromStatus(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
pool := &ResourcePool{
|
||||
Status: ResourcePoolStatus{
|
||||
Claims: ResourcePoolNamespaceClaimsStatus{
|
||||
pool := &v1beta2.ResourcePool{
|
||||
Status: v1beta2.ResourcePoolStatus{
|
||||
Claims: v1beta2.ResourcePoolNamespaceClaimsStatus{
|
||||
ns: {
|
||||
&ResourcePoolClaimsItem{
|
||||
&v1beta2.ResourcePoolClaimsItem{
|
||||
NamespacedRFC1123ObjectReferenceWithNamespaceWithUID: meta.NamespacedRFC1123ObjectReferenceWithNamespaceWithUID{
|
||||
UID: testUID,
|
||||
},
|
||||
@@ -76,21 +77,21 @@ func makeResourceList(cpu, memory string) corev1.ResourceList {
|
||||
}
|
||||
}
|
||||
|
||||
func makeClaim(name, ns string, uid types.UID, res corev1.ResourceList) *ResourcePoolClaim {
|
||||
return &ResourcePoolClaim{
|
||||
func makeClaim(name, ns string, uid types.UID, res corev1.ResourceList) *v1beta2.ResourcePoolClaim {
|
||||
return &v1beta2.ResourcePoolClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: ns,
|
||||
UID: uid,
|
||||
},
|
||||
Spec: ResourcePoolClaimSpec{
|
||||
Spec: v1beta2.ResourcePoolClaimSpec{
|
||||
ResourceClaims: res,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignNamespaces(t *testing.T) {
|
||||
pool := &ResourcePool{}
|
||||
pool := &v1beta2.ResourcePool{}
|
||||
|
||||
namespaces := []corev1.Namespace{
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "active-ns"}, Status: corev1.NamespaceStatus{Phase: corev1.NamespaceActive}},
|
||||
@@ -104,12 +105,12 @@ func TestAssignNamespaces(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAssignClaims(t *testing.T) {
|
||||
pool := &ResourcePool{
|
||||
Status: ResourcePoolStatus{
|
||||
Claims: ResourcePoolNamespaceClaimsStatus{
|
||||
pool := &v1beta2.ResourcePool{
|
||||
Status: v1beta2.ResourcePoolStatus{
|
||||
Claims: v1beta2.ResourcePoolNamespaceClaimsStatus{
|
||||
"ns": {
|
||||
&ResourcePoolClaimsItem{},
|
||||
&ResourcePoolClaimsItem{},
|
||||
&v1beta2.ResourcePoolClaimsItem{},
|
||||
&v1beta2.ResourcePoolClaimsItem{},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -120,7 +121,7 @@ func TestAssignClaims(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAddRemoveClaimToStatus(t *testing.T) {
|
||||
pool := &ResourcePool{}
|
||||
pool := &v1beta2.ResourcePool{}
|
||||
|
||||
claim := makeClaim("claim-1", "ns", "uid-1", makeResourceList("1", "1Gi"))
|
||||
pool.AddClaimToStatus(claim)
|
||||
@@ -135,16 +136,16 @@ func TestAddRemoveClaimToStatus(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCalculateResources(t *testing.T) {
|
||||
pool := &ResourcePool{
|
||||
Status: ResourcePoolStatus{
|
||||
Allocation: ResourcePoolQuotaStatus{
|
||||
pool := &v1beta2.ResourcePool{
|
||||
Status: v1beta2.ResourcePoolStatus{
|
||||
Allocation: v1beta2.ResourcePoolQuotaStatus{
|
||||
Hard: corev1.ResourceList{
|
||||
corev1.ResourceLimitsCPU: resource.MustParse("2"),
|
||||
},
|
||||
},
|
||||
Claims: ResourcePoolNamespaceClaimsStatus{
|
||||
Claims: v1beta2.ResourcePoolNamespaceClaimsStatus{
|
||||
"ns": {
|
||||
&ResourcePoolClaimsItem{
|
||||
&v1beta2.ResourcePoolClaimsItem{
|
||||
Claims: corev1.ResourceList{
|
||||
corev1.ResourceLimitsCPU: resource.MustParse("1"),
|
||||
},
|
||||
@@ -164,9 +165,9 @@ func TestCalculateResources(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCanClaimFromPool(t *testing.T) {
|
||||
pool := &ResourcePool{
|
||||
Status: ResourcePoolStatus{
|
||||
Allocation: ResourcePoolQuotaStatus{
|
||||
pool := &v1beta2.ResourcePool{
|
||||
Status: v1beta2.ResourcePoolStatus{
|
||||
Allocation: v1beta2.ResourcePoolQuotaStatus{
|
||||
Hard: corev1.ResourceList{
|
||||
corev1.ResourceLimitsMemory: resource.MustParse("1Gi"),
|
||||
},
|
||||
@@ -189,16 +190,16 @@ func TestCanClaimFromPool(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetResourceQuotaHardResources(t *testing.T) {
|
||||
pool := &ResourcePool{
|
||||
Spec: ResourcePoolSpec{
|
||||
pool := &v1beta2.ResourcePool{
|
||||
Spec: v1beta2.ResourcePoolSpec{
|
||||
Defaults: corev1.ResourceList{
|
||||
corev1.ResourceLimitsCPU: resource.MustParse("1"),
|
||||
},
|
||||
},
|
||||
Status: ResourcePoolStatus{
|
||||
Claims: ResourcePoolNamespaceClaimsStatus{
|
||||
Status: v1beta2.ResourcePoolStatus{
|
||||
Claims: v1beta2.ResourcePoolNamespaceClaimsStatus{
|
||||
"ns": {
|
||||
&ResourcePoolClaimsItem{
|
||||
&v1beta2.ResourcePoolClaimsItem{
|
||||
Claims: corev1.ResourceList{
|
||||
corev1.ResourceLimitsCPU: resource.MustParse("1"),
|
||||
},
|
||||
@@ -214,11 +215,11 @@ func TestGetResourceQuotaHardResources(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetNamespaceClaims(t *testing.T) {
|
||||
pool := &ResourcePool{
|
||||
Status: ResourcePoolStatus{
|
||||
Claims: ResourcePoolNamespaceClaimsStatus{
|
||||
pool := &v1beta2.ResourcePool{
|
||||
Status: v1beta2.ResourcePoolStatus{
|
||||
Claims: v1beta2.ResourcePoolNamespaceClaimsStatus{
|
||||
"ns": {
|
||||
&ResourcePoolClaimsItem{
|
||||
&v1beta2.ResourcePoolClaimsItem{
|
||||
NamespacedRFC1123ObjectReferenceWithNamespaceWithUID: meta.NamespacedRFC1123ObjectReferenceWithNamespaceWithUID{UID: "uid1"},
|
||||
Claims: corev1.ResourceList{
|
||||
corev1.ResourceLimitsCPU: resource.MustParse("1"),
|
||||
@@ -236,11 +237,11 @@ func TestGetNamespaceClaims(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetClaimedByNamespaceClaims(t *testing.T) {
|
||||
pool := &ResourcePool{
|
||||
Status: ResourcePoolStatus{
|
||||
Claims: ResourcePoolNamespaceClaimsStatus{
|
||||
pool := &v1beta2.ResourcePool{
|
||||
Status: v1beta2.ResourcePoolStatus{
|
||||
Claims: v1beta2.ResourcePoolNamespaceClaimsStatus{
|
||||
"ns1": {
|
||||
&ResourcePoolClaimsItem{
|
||||
&v1beta2.ResourcePoolClaimsItem{
|
||||
Claims: makeResourceList("1", "1Gi"),
|
||||
},
|
||||
},
|
||||
@@ -258,8 +259,8 @@ func TestGetClaimedByNamespaceClaims(t *testing.T) {
|
||||
|
||||
func TestIsBoundToResourcePool_2(t *testing.T) {
|
||||
t.Run("bound to resource pool (Assigned=True)", func(t *testing.T) {
|
||||
claim := &ResourcePoolClaim{
|
||||
Status: ResourcePoolClaimStatus{
|
||||
claim := &v1beta2.ResourcePoolClaim{
|
||||
Status: v1beta2.ResourcePoolClaimStatus{
|
||||
Conditions: meta.ConditionList{},
|
||||
},
|
||||
}
|
||||
@@ -268,8 +269,8 @@ func TestIsBoundToResourcePool_2(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("not bound - wrong condition type", func(t *testing.T) {
|
||||
claim := &ResourcePoolClaim{
|
||||
Status: ResourcePoolClaimStatus{
|
||||
claim := &v1beta2.ResourcePoolClaim{
|
||||
Status: v1beta2.ResourcePoolClaimStatus{
|
||||
Conditions: meta.ConditionList{
|
||||
meta.Condition{},
|
||||
},
|
||||
@@ -284,8 +285,8 @@ func TestIsBoundToResourcePool_2(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("not bound - condition not true", func(t *testing.T) {
|
||||
claim := &ResourcePoolClaim{
|
||||
Status: ResourcePoolClaimStatus{
|
||||
claim := &v1beta2.ResourcePoolClaim{
|
||||
Status: v1beta2.ResourcePoolClaimStatus{
|
||||
Conditions: meta.ConditionList{
|
||||
meta.Condition{},
|
||||
},
|
||||
@@ -300,8 +301,8 @@ func TestIsBoundToResourcePool_2(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("not bound - condition not true", func(t *testing.T) {
|
||||
claim := &ResourcePoolClaim{
|
||||
Status: ResourcePoolClaimStatus{
|
||||
claim := &v1beta2.ResourcePoolClaim{
|
||||
Status: v1beta2.ResourcePoolClaimStatus{
|
||||
Conditions: meta.ConditionList{
|
||||
meta.Condition{},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1beta2
|
||||
package v1beta2_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -9,36 +9,34 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
)
|
||||
|
||||
func TestIsBoundToResourcePool(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
claim ResourcePoolClaim
|
||||
claim v1beta2.ResourcePoolClaim
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "bound to resource pool (Assigned=True)",
|
||||
claim: ResourcePoolClaim{
|
||||
Status: ResourcePoolClaimStatus{
|
||||
Conditions: meta.ConditionList{
|
||||
meta.Condition{
|
||||
Type: meta.BoundCondition,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: meta.SucceededReason,
|
||||
Message: "reconciled",
|
||||
LastTransitionTime: metav1.Now(),
|
||||
},
|
||||
claim: v1beta2.ResourcePoolClaim{
|
||||
Status: v1beta2.ResourcePoolClaimStatus{
|
||||
Condition: metav1.Condition{
|
||||
Type: meta.BoundCondition,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: meta.SucceededReason,
|
||||
Message: "reconciled",
|
||||
LastTransitionTime: metav1.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "not bound - wrong condition type",
|
||||
claim: ResourcePoolClaim{
|
||||
Status: ResourcePoolClaimStatus{
|
||||
claim: v1beta2.ResourcePoolClaim{
|
||||
Status: v1beta2.ResourcePoolClaimStatus{
|
||||
Conditions: meta.ConditionList{
|
||||
meta.Condition{
|
||||
Type: meta.AssignedCondition,
|
||||
@@ -54,8 +52,8 @@ func TestIsBoundToResourcePool(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "not bound - status not true",
|
||||
claim: ResourcePoolClaim{
|
||||
Status: ResourcePoolClaimStatus{
|
||||
claim: v1beta2.ResourcePoolClaim{
|
||||
Status: v1beta2.ResourcePoolClaimStatus{
|
||||
Conditions: meta.ConditionList{
|
||||
meta.Condition{
|
||||
Type: meta.AssignedCondition,
|
||||
@@ -71,8 +69,8 @@ func TestIsBoundToResourcePool(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "not bound - empty condition",
|
||||
claim: ResourcePoolClaim{
|
||||
Status: ResourcePoolClaimStatus{},
|
||||
claim: v1beta2.ResourcePoolClaim{
|
||||
Status: v1beta2.ResourcePoolClaimStatus{},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
@@ -89,16 +87,16 @@ func TestIsBoundToResourcePool(t *testing.T) {
|
||||
func TestGetPool(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
claim ResourcePoolClaim
|
||||
claim v1beta2.ResourcePoolClaim
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "returns status pool name when set",
|
||||
claim: ResourcePoolClaim{
|
||||
Spec: ResourcePoolClaimSpec{
|
||||
claim: v1beta2.ResourcePoolClaim{
|
||||
Spec: v1beta2.ResourcePoolClaimSpec{
|
||||
Pool: "spec-pool",
|
||||
},
|
||||
Status: ResourcePoolClaimStatus{
|
||||
Status: v1beta2.ResourcePoolClaimStatus{
|
||||
Pool: meta.LocalRFC1123ObjectReferenceWithUID{
|
||||
Name: meta.RFC1123Name("status-pool"),
|
||||
},
|
||||
@@ -108,11 +106,11 @@ func TestGetPool(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "falls back to spec pool when status pool name is empty",
|
||||
claim: ResourcePoolClaim{
|
||||
Spec: ResourcePoolClaimSpec{
|
||||
claim: v1beta2.ResourcePoolClaim{
|
||||
Spec: v1beta2.ResourcePoolClaimSpec{
|
||||
Pool: "spec-pool",
|
||||
},
|
||||
Status: ResourcePoolClaimStatus{
|
||||
Status: v1beta2.ResourcePoolClaimStatus{
|
||||
Pool: meta.LocalRFC1123ObjectReferenceWithUID{
|
||||
Name: meta.RFC1123Name(""),
|
||||
},
|
||||
@@ -122,11 +120,11 @@ func TestGetPool(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "falls back to spec pool when status pool struct is zero-value",
|
||||
claim: ResourcePoolClaim{
|
||||
Spec: ResourcePoolClaimSpec{
|
||||
claim: v1beta2.ResourcePoolClaim{
|
||||
Spec: v1beta2.ResourcePoolClaimSpec{
|
||||
Pool: "spec-pool",
|
||||
},
|
||||
Status: ResourcePoolClaimStatus{
|
||||
Status: v1beta2.ResourcePoolClaimStatus{
|
||||
Pool: meta.LocalRFC1123ObjectReferenceWithUID{},
|
||||
},
|
||||
},
|
||||
@@ -134,11 +132,11 @@ func TestGetPool(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "returns empty when both status and spec are empty",
|
||||
claim: ResourcePoolClaim{
|
||||
Spec: ResourcePoolClaimSpec{
|
||||
claim: v1beta2.ResourcePoolClaim{
|
||||
Spec: v1beta2.ResourcePoolClaimSpec{
|
||||
Pool: "",
|
||||
},
|
||||
Status: ResourcePoolClaimStatus{
|
||||
Status: v1beta2.ResourcePoolClaimStatus{
|
||||
Pool: meta.LocalRFC1123ObjectReferenceWithUID{
|
||||
Name: meta.RFC1123Name(""),
|
||||
},
|
||||
@@ -148,11 +146,11 @@ func TestGetPool(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "status wins even if spec differs",
|
||||
claim: ResourcePoolClaim{
|
||||
Spec: ResourcePoolClaimSpec{
|
||||
claim: v1beta2.ResourcePoolClaim{
|
||||
Spec: v1beta2.ResourcePoolClaimSpec{
|
||||
Pool: "spec-pool",
|
||||
},
|
||||
Status: ResourcePoolClaimStatus{
|
||||
Status: v1beta2.ResourcePoolClaimStatus{
|
||||
Pool: meta.LocalRFC1123ObjectReferenceWithUID{
|
||||
Name: meta.RFC1123Name("status-pool"),
|
||||
},
|
||||
|
||||
@@ -5,8 +5,21 @@ package v1beta2
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
)
|
||||
|
||||
// RuleStatus contains the accumulated rules applying to namespace it's deployed in.
|
||||
// +kubebuilder:object:generate=true
|
||||
type RuleStatusSpec struct {
|
||||
// Managed Enforcement properties per Namespace (aggregated from rules)
|
||||
//+optional
|
||||
Rule api.NamespaceRuleBodyNamespace `json:"rule,omitzero"`
|
||||
// Conditions
|
||||
Conditions meta.ConditionList `json:"conditions"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:subresource:status
|
||||
@@ -17,6 +30,9 @@ type RuleStatus struct {
|
||||
// +optional
|
||||
metav1.ObjectMeta `json:"metadata,omitzero"`
|
||||
|
||||
// +optional
|
||||
Spec []*api.NamespaceRuleBodyNamespace `json:"spec,omitzero"`
|
||||
|
||||
// +optional
|
||||
Status RuleStatusSpec `json:"status,omitzero"`
|
||||
}
|
||||
@@ -34,11 +50,3 @@ type RuleStatusList struct {
|
||||
func init() {
|
||||
SchemeBuilder.Register(&RuleStatus{}, &RuleStatusList{})
|
||||
}
|
||||
|
||||
// RuleStatus contains the accumulated rules applying to namespace it's deployed in.
|
||||
// +kubebuilder:object:generate=true
|
||||
type RuleStatusSpec struct {
|
||||
// Managed Enforcement properties per Namespace (aggregated from rules)
|
||||
//+optional
|
||||
Rule NamespaceRuleBody `json:"rule,omitzero"`
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
capsulev1beta1 "github.com/projectcapsule/capsule/api/v1beta1"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/api/rbac"
|
||||
)
|
||||
|
||||
func (in *Tenant) ConvertFrom(raw conversion.Hub) error {
|
||||
@@ -27,28 +28,28 @@ func (in *Tenant) ConvertFrom(raw conversion.Hub) error {
|
||||
}
|
||||
|
||||
in.ObjectMeta = src.ObjectMeta
|
||||
in.Spec.Owners = make(api.OwnerListSpec, 0, len(src.Spec.Owners))
|
||||
in.Spec.Owners = make(rbac.OwnerListSpec, 0, len(src.Spec.Owners))
|
||||
|
||||
for index, owner := range src.Spec.Owners {
|
||||
proxySettings := make([]api.ProxySettings, 0, len(owner.ProxyOperations))
|
||||
proxySettings := make([]rbac.ProxySettings, 0, len(owner.ProxyOperations))
|
||||
|
||||
for _, proxyOp := range owner.ProxyOperations {
|
||||
ops := make([]api.ProxyOperation, 0, len(proxyOp.Operations))
|
||||
ops := make([]rbac.ProxyOperation, 0, len(proxyOp.Operations))
|
||||
|
||||
for _, op := range proxyOp.Operations {
|
||||
ops = append(ops, api.ProxyOperation(op))
|
||||
ops = append(ops, rbac.ProxyOperation(op))
|
||||
}
|
||||
|
||||
proxySettings = append(proxySettings, api.ProxySettings{
|
||||
Kind: api.ProxyServiceKind(proxyOp.Kind),
|
||||
proxySettings = append(proxySettings, rbac.ProxySettings{
|
||||
Kind: rbac.ProxyServiceKind(proxyOp.Kind),
|
||||
Operations: ops,
|
||||
})
|
||||
}
|
||||
|
||||
in.Spec.Owners = append(in.Spec.Owners, api.OwnerSpec{
|
||||
CoreOwnerSpec: api.CoreOwnerSpec{
|
||||
UserSpec: api.UserSpec{
|
||||
Kind: api.OwnerKind(owner.Kind),
|
||||
in.Spec.Owners = append(in.Spec.Owners, rbac.OwnerSpec{
|
||||
CoreOwnerSpec: rbac.CoreOwnerSpec{
|
||||
UserSpec: rbac.UserSpec{
|
||||
Kind: rbac.OwnerKind(owner.Kind),
|
||||
Name: owner.Name,
|
||||
},
|
||||
ClusterRoles: owner.GetRoles(*src, index),
|
||||
@@ -281,6 +282,7 @@ func (in *Tenant) ConvertTo(raw conversion.Hub) error {
|
||||
dst.Status.Size = in.Status.Size
|
||||
dst.Status.Namespaces = in.Status.Namespaces
|
||||
|
||||
//nolint:exhaustive
|
||||
switch in.Status.State {
|
||||
case TenantStateActive:
|
||||
dst.Status.State = capsulev1beta1.TenantStateActive
|
||||
|
||||
+123
-67
@@ -4,17 +4,20 @@
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/rbac"
|
||||
)
|
||||
|
||||
func (in *Tenant) GetRoleBindings() []api.AdditionalRoleBindingsSpec {
|
||||
roleBindings := make([]api.AdditionalRoleBindingsSpec, 0, len(in.Spec.AdditionalRoleBindings))
|
||||
func (in *Tenant) GetRoleBindings() []rbac.AdditionalRoleBindingsSpec {
|
||||
roleBindings := make([]rbac.AdditionalRoleBindingsSpec, 0, len(in.Spec.AdditionalRoleBindings))
|
||||
|
||||
for _, owner := range in.Status.Owners {
|
||||
roleBindings = append(roleBindings, owner.ToAdditionalRolebindings()...)
|
||||
@@ -25,6 +28,16 @@ func (in *Tenant) GetRoleBindings() []api.AdditionalRoleBindingsSpec {
|
||||
return roleBindings
|
||||
}
|
||||
|
||||
func (in *Tenant) GetPromotionRoleBindings() []rbac.AdditionalRoleBindingsWithNamespaceSpec {
|
||||
roleBindings := make([]rbac.AdditionalRoleBindingsWithNamespaceSpec, 0, len(in.Status.Promotions))
|
||||
|
||||
for _, promotion := range in.Status.Promotions {
|
||||
roleBindings = append(roleBindings, promotion.ToAdditionalRolebindings()...)
|
||||
}
|
||||
|
||||
return roleBindings
|
||||
}
|
||||
|
||||
func (in *Tenant) IsFull() bool {
|
||||
// we don't have limits on assigned Namespaces
|
||||
if in.Spec.NamespaceOptions == nil || in.Spec.NamespaceOptions.Quota == nil {
|
||||
@@ -35,12 +48,10 @@ func (in *Tenant) IsFull() bool {
|
||||
}
|
||||
|
||||
func (in *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
|
||||
var l []string
|
||||
l := make([]string, 0, len(namespaces))
|
||||
|
||||
for _, ns := range namespaces {
|
||||
if ns.Status.Phase == corev1.NamespaceActive {
|
||||
l = append(l, ns.GetName())
|
||||
}
|
||||
l = append(l, ns.GetName())
|
||||
}
|
||||
|
||||
sort.Strings(l)
|
||||
@@ -49,14 +60,44 @@ func (in *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
|
||||
in.Status.Size = uint(len(l))
|
||||
}
|
||||
|
||||
func (in *Tenant) GetOwnerProxySettings(name string, kind api.OwnerKind) []api.ProxySettings {
|
||||
func (in *Tenant) GetNamespaces() (res []string) {
|
||||
return in.Status.Namespaces
|
||||
}
|
||||
|
||||
// Fetch all namespaces defined in the status.
|
||||
func (in *Tenant) GetNamespaceObjects(ctx context.Context, c client.Reader) (namespaces []corev1.Namespace, err error) {
|
||||
nsList := &corev1.NamespaceList{}
|
||||
|
||||
if len(in.Status.Namespaces) == 0 {
|
||||
return nsList.Items, nil
|
||||
}
|
||||
|
||||
req, err := labels.NewRequirement(
|
||||
corev1.LabelMetadataName,
|
||||
selection.In,
|
||||
in.Status.Namespaces,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
selector := labels.NewSelector().Add(*req)
|
||||
|
||||
if err := c.List(ctx, nsList, client.MatchingLabelsSelector{Selector: selector}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nsList.Items, nil
|
||||
}
|
||||
|
||||
func (in *Tenant) GetOwnerProxySettings(name string, kind rbac.OwnerKind) []rbac.ProxySettings {
|
||||
return in.Spec.Owners.FindOwner(name, kind).ProxyOperations
|
||||
}
|
||||
|
||||
// GetClusterRolePermissions returns a map where the clusterRole is the key
|
||||
// and the value is a list of permission subjects (kind and name) that reference that role.
|
||||
// These mappings are gathered from the owners and additionalRolebindings spec.
|
||||
func (in *Tenant) GetSubjectsByClusterRoles(ignoreOwnerKind []api.OwnerKind) (rolePerms map[string][]rbacv1.Subject) {
|
||||
func (in *Tenant) GetSubjectsByClusterRoles(ignoreOwnerKind []rbac.OwnerKind) (rolePerms map[string][]rbacv1.Subject) {
|
||||
rolePerms = make(map[string][]rbacv1.Subject)
|
||||
|
||||
// Helper to add permissions for a given clusterRole
|
||||
@@ -80,7 +121,7 @@ func (in *Tenant) GetSubjectsByClusterRoles(ignoreOwnerKind []api.OwnerKind) (ro
|
||||
}
|
||||
|
||||
// Process owners
|
||||
for _, owner := range in.Spec.Owners {
|
||||
for _, owner := range in.Status.Owners {
|
||||
if !isIgnoredKind(owner.Kind.String()) {
|
||||
for _, clusterRole := range owner.ClusterRoles {
|
||||
perm := rbacv1.Subject{
|
||||
@@ -108,72 +149,87 @@ func (in *Tenant) GetSubjectsByClusterRoles(ignoreOwnerKind []api.OwnerKind) (ro
|
||||
return rolePerms
|
||||
}
|
||||
|
||||
// Get the permissions for a tenant ordered by groups and users.
|
||||
func (in *Tenant) GetClusterRolesBySubject(ignoreOwnerKind []api.OwnerKind) (maps map[string]map[string]api.TenantSubjectRoles) {
|
||||
maps = make(map[string]map[string]api.TenantSubjectRoles)
|
||||
func (in *Tenant) GetClusterRolesBySubject(ignoreOwnerKind []rbac.OwnerKind) []rbac.SubjectRoles {
|
||||
ignore := make(map[string]struct{}, len(ignoreOwnerKind))
|
||||
for _, k := range ignoreOwnerKind {
|
||||
ignore[k.String()] = struct{}{}
|
||||
}
|
||||
|
||||
// Initialize a nested map for kind ("User", "Group") and name
|
||||
initNestedMap := func(kind string) {
|
||||
if _, exists := maps[kind]; !exists {
|
||||
maps[kind] = make(map[string]api.TenantSubjectRoles)
|
||||
roleSet := map[string]map[string]map[string]struct{}{}
|
||||
|
||||
ensure := func(kind, name string) map[string]struct{} {
|
||||
km, ok := roleSet[kind]
|
||||
if !ok {
|
||||
km = map[string]map[string]struct{}{}
|
||||
roleSet[kind] = km
|
||||
}
|
||||
|
||||
ns, ok := km[name]
|
||||
if !ok {
|
||||
ns = map[string]struct{}{}
|
||||
km[name] = ns
|
||||
}
|
||||
|
||||
return ns
|
||||
}
|
||||
|
||||
for _, owner := range in.Status.Owners {
|
||||
kind := owner.Kind.String()
|
||||
if _, skip := ignore[kind]; skip {
|
||||
continue
|
||||
}
|
||||
|
||||
s := ensure(kind, owner.Name)
|
||||
for _, r := range owner.ClusterRoles {
|
||||
s[r] = struct{}{}
|
||||
}
|
||||
}
|
||||
// Helper to check if a kind is in the ignoreOwnerKind list
|
||||
isIgnoredKind := func(kind string) bool {
|
||||
for _, ignored := range ignoreOwnerKind {
|
||||
if kind == ignored.String() {
|
||||
return true
|
||||
|
||||
for _, rb := range in.Spec.AdditionalRoleBindings {
|
||||
for _, subj := range rb.Subjects {
|
||||
if _, skip := ignore[subj.Kind]; skip {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
s := ensure(subj.Kind, subj.Name)
|
||||
s[rb.ClusterRoleName] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Process owners
|
||||
for _, owner := range in.Spec.Owners {
|
||||
if !isIgnoredKind(owner.Kind.String()) {
|
||||
initNestedMap(owner.Kind.String())
|
||||
// Flatten deterministically: sort kinds, names, roles
|
||||
kinds := make([]string, 0, len(roleSet))
|
||||
for k := range roleSet {
|
||||
kinds = append(kinds, k)
|
||||
}
|
||||
|
||||
if perm, exists := maps[owner.Kind.String()][owner.Name]; exists {
|
||||
// If the permission entry already exists, append cluster roles
|
||||
perm.ClusterRoles = append(perm.ClusterRoles, owner.ClusterRoles...)
|
||||
maps[owner.Kind.String()][owner.Name] = perm
|
||||
} else {
|
||||
// Create a new permission entry
|
||||
maps[owner.Kind.String()][owner.Name] = api.TenantSubjectRoles{
|
||||
ClusterRoles: owner.ClusterRoles,
|
||||
}
|
||||
sort.Strings(kinds)
|
||||
|
||||
totalSubjects := 0
|
||||
for _, byName := range roleSet {
|
||||
totalSubjects += len(byName)
|
||||
}
|
||||
|
||||
out := make([]rbac.SubjectRoles, 0, totalSubjects)
|
||||
|
||||
for _, kind := range kinds {
|
||||
names := make([]string, 0, len(roleSet[kind]))
|
||||
for n := range roleSet[kind] {
|
||||
names = append(names, n)
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
|
||||
for _, name := range names {
|
||||
roles := make([]string, 0, len(roleSet[kind][name]))
|
||||
for r := range roleSet[kind][name] {
|
||||
roles = append(roles, r)
|
||||
}
|
||||
|
||||
sort.Strings(roles)
|
||||
|
||||
out = append(out, rbac.SubjectRoles{Kind: kind, Name: name, Roles: roles})
|
||||
}
|
||||
}
|
||||
|
||||
// Process additional role bindings
|
||||
for _, role := range in.Spec.AdditionalRoleBindings {
|
||||
for _, subject := range role.Subjects {
|
||||
if !isIgnoredKind(subject.Kind) {
|
||||
initNestedMap(subject.Kind)
|
||||
|
||||
if perm, exists := maps[subject.Kind][subject.Name]; exists {
|
||||
// If the permission entry already exists, append cluster roles
|
||||
perm.ClusterRoles = append(perm.ClusterRoles, role.ClusterRoleName)
|
||||
maps[subject.Kind][subject.Name] = perm
|
||||
} else {
|
||||
// Create a new permission entry
|
||||
maps[subject.Kind][subject.Name] = api.TenantSubjectRoles{
|
||||
ClusterRoles: []string{role.ClusterRoleName},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates from cluster roles in both maps
|
||||
for kind, nameMap := range maps {
|
||||
for name, perm := range nameMap {
|
||||
perm.ClusterRoles = slices.Compact(perm.ClusterRoles)
|
||||
maps[kind][name] = perm
|
||||
}
|
||||
}
|
||||
|
||||
return maps
|
||||
return out
|
||||
}
|
||||
|
||||
+153
-119
@@ -1,83 +1,78 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1beta2
|
||||
package v1beta2_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
|
||||
"github.com/projectcapsule/capsule/api/v1beta2"
|
||||
capsulerbac "github.com/projectcapsule/capsule/pkg/api/rbac"
|
||||
)
|
||||
|
||||
var tenant = &Tenant{
|
||||
Spec: TenantSpec{
|
||||
Owners: []api.OwnerSpec{
|
||||
{
|
||||
CoreOwnerSpec: api.CoreOwnerSpec{
|
||||
UserSpec: api.UserSpec{
|
||||
Kind: "User",
|
||||
func testTenant() *v1beta2.Tenant {
|
||||
return &v1beta2.Tenant{
|
||||
Spec: v1beta2.TenantSpec{
|
||||
AdditionalRoleBindings: []capsulerbac.AdditionalRoleBindingsSpec{
|
||||
{
|
||||
ClusterRoleName: "developer",
|
||||
Subjects: []rbacv1.Subject{
|
||||
{Kind: "User", Name: "user2"},
|
||||
{Kind: "Group", Name: "group1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ClusterRoleName: "cluster-admin",
|
||||
Subjects: []rbacv1.Subject{
|
||||
{Kind: "User", Name: "user3"},
|
||||
{Kind: "Group", Name: "group1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ClusterRoleName: "deployer",
|
||||
Subjects: []rbacv1.Subject{
|
||||
{Kind: "ServiceAccount", Name: "system:serviceaccount:argocd:argo-operator"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: v1beta2.TenantStatus{
|
||||
Owners: capsulerbac.OwnerStatusListSpec{
|
||||
{
|
||||
UserSpec: capsulerbac.UserSpec{
|
||||
Kind: capsulerbac.UserOwner,
|
||||
Name: "user1",
|
||||
},
|
||||
ClusterRoles: []string{"cluster-admin", "read-only"},
|
||||
},
|
||||
},
|
||||
{
|
||||
CoreOwnerSpec: api.CoreOwnerSpec{
|
||||
UserSpec: api.UserSpec{
|
||||
Kind: "Group",
|
||||
{
|
||||
UserSpec: capsulerbac.UserSpec{
|
||||
Kind: capsulerbac.GroupOwner,
|
||||
Name: "group1",
|
||||
},
|
||||
ClusterRoles: []string{"edit"},
|
||||
},
|
||||
},
|
||||
{
|
||||
CoreOwnerSpec: api.CoreOwnerSpec{
|
||||
UserSpec: api.UserSpec{
|
||||
Kind: api.ServiceAccountOwner,
|
||||
{
|
||||
UserSpec: capsulerbac.UserSpec{
|
||||
Kind: capsulerbac.ServiceAccountOwner,
|
||||
Name: "service",
|
||||
},
|
||||
ClusterRoles: []string{"read-only"},
|
||||
},
|
||||
},
|
||||
},
|
||||
AdditionalRoleBindings: []api.AdditionalRoleBindingsSpec{
|
||||
{
|
||||
ClusterRoleName: "developer",
|
||||
Subjects: []rbacv1.Subject{
|
||||
{Kind: "User", Name: "user2"},
|
||||
{Kind: "Group", Name: "group1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ClusterRoleName: "cluster-admin",
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "User",
|
||||
Name: "user3",
|
||||
},
|
||||
{
|
||||
Kind: "Group",
|
||||
Name: "group1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ClusterRoleName: "deployer",
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: "system:serviceaccount:argocd:argo-operator",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestGetClusterRolePermissions tests the GetClusterRolePermissions function
|
||||
func TestGetSubjectsByClusterRoles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tenant := testTenant()
|
||||
|
||||
expected := map[string][]rbacv1.Subject{
|
||||
"cluster-admin": {
|
||||
{Kind: "User", Name: "user1"},
|
||||
@@ -100,15 +95,11 @@ func TestGetSubjectsByClusterRoles(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
// Call the function to test
|
||||
permissions := tenant.GetSubjectsByClusterRoles(nil)
|
||||
|
||||
if !reflect.DeepEqual(permissions, expected) {
|
||||
t.Errorf("Expected %v, but got %v", expected, permissions)
|
||||
got := tenant.GetSubjectsByClusterRoles(nil)
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Fatalf("expected %#v\n got %#v", expected, got)
|
||||
}
|
||||
|
||||
// Ignore SubjectTypes (Ignores ServiceAccounts)
|
||||
ignored := tenant.GetSubjectsByClusterRoles([]api.OwnerKind{"ServiceAccount"})
|
||||
expectedIgnored := map[string][]rbacv1.Subject{
|
||||
"cluster-admin": {
|
||||
{Kind: "User", Name: "user1"},
|
||||
@@ -127,78 +118,121 @@ func TestGetSubjectsByClusterRoles(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(ignored, expectedIgnored) {
|
||||
t.Errorf("Expected %v, but got %v", expectedIgnored, ignored)
|
||||
gotIgnored := tenant.GetSubjectsByClusterRoles([]capsulerbac.OwnerKind{capsulerbac.ServiceAccountOwner})
|
||||
if !reflect.DeepEqual(gotIgnored, expectedIgnored) {
|
||||
t.Fatalf("expected %#v\n got %#v", expectedIgnored, gotIgnored)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetClusterRolesBySubject(t *testing.T) {
|
||||
func TestGetClusterRolesBySubjectSorted(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expected := map[string]map[string]api.TenantSubjectRoles{
|
||||
"User": {
|
||||
"user1": {
|
||||
ClusterRoles: []string{"cluster-admin", "read-only"},
|
||||
},
|
||||
"user2": {
|
||||
ClusterRoles: []string{"developer"},
|
||||
},
|
||||
"user3": {
|
||||
ClusterRoles: []string{"cluster-admin"},
|
||||
tenant := &v1beta2.Tenant{
|
||||
Spec: v1beta2.TenantSpec{
|
||||
AdditionalRoleBindings: []capsulerbac.AdditionalRoleBindingsSpec{
|
||||
{
|
||||
ClusterRoleName: "deployer",
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: "system:serviceaccount:argocd:argo-operator",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ClusterRoleName: "developer",
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "Group",
|
||||
Name: "group1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"Group": {
|
||||
"group1": {
|
||||
ClusterRoles: []string{"edit", "developer", "cluster-admin"},
|
||||
},
|
||||
},
|
||||
"ServiceAccount": {
|
||||
"service": {
|
||||
ClusterRoles: []string{"read-only"},
|
||||
},
|
||||
"system:serviceaccount:argocd:argo-operator": {
|
||||
ClusterRoles: []string{"deployer"},
|
||||
Status: v1beta2.TenantStatus{
|
||||
Owners: capsulerbac.OwnerStatusListSpec{
|
||||
{
|
||||
UserSpec: capsulerbac.UserSpec{
|
||||
Kind: capsulerbac.UserOwner,
|
||||
Name: "user1",
|
||||
},
|
||||
ClusterRoles: []string{"cluster-admin", "read-only"},
|
||||
},
|
||||
{
|
||||
UserSpec: capsulerbac.UserSpec{
|
||||
Kind: capsulerbac.UserOwner,
|
||||
Name: "user2",
|
||||
},
|
||||
ClusterRoles: []string{"developer"},
|
||||
},
|
||||
{
|
||||
UserSpec: capsulerbac.UserSpec{
|
||||
Kind: capsulerbac.UserOwner,
|
||||
Name: "user3",
|
||||
},
|
||||
ClusterRoles: []string{"cluster-admin"},
|
||||
},
|
||||
{
|
||||
UserSpec: capsulerbac.UserSpec{
|
||||
Kind: capsulerbac.GroupOwner,
|
||||
Name: "group1",
|
||||
},
|
||||
ClusterRoles: []string{"edit", "developer", "cluster-admin"},
|
||||
},
|
||||
{
|
||||
UserSpec: capsulerbac.UserSpec{
|
||||
Kind: capsulerbac.ServiceAccountOwner,
|
||||
Name: "service",
|
||||
},
|
||||
ClusterRoles: []string{"read-only"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
permissions := tenant.GetClusterRolesBySubject(nil)
|
||||
if !reflect.DeepEqual(permissions, expected) {
|
||||
t.Errorf("Expected %v, but got %v", expected, permissions)
|
||||
expected := []capsulerbac.SubjectRoles{
|
||||
{Kind: "Group", Name: "group1", Roles: []string{"cluster-admin", "developer", "edit"}},
|
||||
{Kind: "ServiceAccount", Name: "service", Roles: []string{"read-only"}},
|
||||
{Kind: "ServiceAccount", Name: "system:serviceaccount:argocd:argo-operator", Roles: []string{"deployer"}},
|
||||
{Kind: "User", Name: "user1", Roles: []string{"cluster-admin", "read-only"}},
|
||||
{Kind: "User", Name: "user2", Roles: []string{"developer"}},
|
||||
{Kind: "User", Name: "user3", Roles: []string{"cluster-admin"}},
|
||||
}
|
||||
|
||||
delete(expected, "ServiceAccount")
|
||||
ignored := tenant.GetClusterRolesBySubject([]api.OwnerKind{"ServiceAccount"})
|
||||
t.Run("includes all kinds and is deterministic, deduped and sorted", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if !reflect.DeepEqual(ignored, expected) {
|
||||
t.Errorf("Expected %v, but got %v", expected, ignored)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to run tests
|
||||
func TestMain(t *testing.M) {
|
||||
t.Run()
|
||||
}
|
||||
|
||||
// permissionsEqual checks the equality of two TenantPermission structs.
|
||||
func permissionsEqual(a, b api.TenantSubjectRoles) bool {
|
||||
if a.Kind != b.Kind {
|
||||
return false
|
||||
}
|
||||
if len(a.ClusterRoles) != len(b.ClusterRoles) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Create a map to count occurrences of cluster roles
|
||||
counts := make(map[string]int)
|
||||
for _, role := range a.ClusterRoles {
|
||||
counts[role]++
|
||||
}
|
||||
for _, role := range b.ClusterRoles {
|
||||
counts[role]--
|
||||
if counts[role] < 0 {
|
||||
return false // More occurrences in b than in a
|
||||
got := tenant.GetClusterRolesBySubject(nil)
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Fatalf("expected %#v\n got %#v", expected, got)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
t.Run("ignores ServiceAccount kind", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := tenant.GetClusterRolesBySubject([]capsulerbac.OwnerKind{capsulerbac.ServiceAccountOwner})
|
||||
|
||||
expectedIgnored := []capsulerbac.SubjectRoles{
|
||||
{Kind: "Group", Name: "group1", Roles: []string{"cluster-admin", "developer", "edit"}},
|
||||
{Kind: "User", Name: "user1", Roles: []string{"cluster-admin", "read-only"}},
|
||||
{Kind: "User", Name: "user2", Roles: []string{"developer"}},
|
||||
{Kind: "User", Name: "user3", Roles: []string{"cluster-admin"}},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, expectedIgnored) {
|
||||
t.Fatalf("expected %#v\n got %#v", expectedIgnored, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty tenant yields empty slice", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
empty := &v1beta2.Tenant{}
|
||||
got := empty.GetClusterRolesBySubject(nil)
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("expected empty, got %#v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,14 +8,16 @@ import (
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/api/rbac"
|
||||
)
|
||||
|
||||
// +kubebuilder:validation:Enum=Cordoned;Active
|
||||
// +kubebuilder:validation:Enum=Cordoned;Active;Terminating
|
||||
type tenantState string
|
||||
|
||||
const (
|
||||
TenantStateActive tenantState = "Active"
|
||||
TenantStateCordoned tenantState = "Cordoned"
|
||||
TenantStateActive tenantState = "Active"
|
||||
TenantStateCordoned tenantState = "Cordoned"
|
||||
TenantStateTerminating tenantState = "Terminating"
|
||||
)
|
||||
|
||||
// Returns the observed state of the Tenant.
|
||||
@@ -24,9 +26,11 @@ type TenantStatus struct {
|
||||
TenantAvailableStatus `json:",inline"`
|
||||
|
||||
// Collected owners for this tenant
|
||||
Owners api.OwnerStatusListSpec `json:"owners,omitempty"`
|
||||
Owners rbac.OwnerStatusListSpec `json:"owners,omitempty"`
|
||||
// Promoted ServiceAccounts across the Tenant
|
||||
Promotions rbac.PromotionStatusListSpec `json:"promotions,omitempty"`
|
||||
// +kubebuilder:default=Active
|
||||
// The operational state of the Tenant. Possible values are "Active", "Cordoned".
|
||||
// The operational state of the Tenant. Possible values are "Active", "Cordoned" or "Terminating".
|
||||
State tenantState `json:"state"`
|
||||
// How many namespaces are assigned to the Tenant.
|
||||
Size uint `json:"size"`
|
||||
@@ -52,6 +56,16 @@ type TenantStatusNamespaceItem struct {
|
||||
Enforce TenantStatusNamespaceEnforcement `json:"enforce,omitzero"`
|
||||
}
|
||||
|
||||
// RuleStatus contains the accumulated rules applying to namespace it's deployed in.
|
||||
// +kubebuilder:object:generate=true
|
||||
type TenantStatusRuleStatusItem struct {
|
||||
// Promotions originating from this namespace
|
||||
Promotions rbac.OwnerStatusListSpec `json:"promotions,omitempty"`
|
||||
|
||||
// Target Namespaces for this rule
|
||||
TargetNamespaces []meta.RFC1123SubdomainName `json:"namespaces,omitempty"`
|
||||
}
|
||||
|
||||
type TenantStatusNamespaceEnforcement struct {
|
||||
// Registries which are allowed within this namespace
|
||||
Registries []api.OCIRegistry `json:"registry,omitempty"`
|
||||
|
||||
@@ -6,16 +6,23 @@ package v1beta2
|
||||
import (
|
||||
"context"
|
||||
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
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/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/api/rbac"
|
||||
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
|
||||
)
|
||||
|
||||
// TenantSpec defines the desired state of Tenant.
|
||||
type TenantSpec struct {
|
||||
// Specify additional data relating to the tenant.
|
||||
// Mainly useable in templating and more accessible than labels/annotations.
|
||||
// +optional
|
||||
Data apiextensionsv1.JSON `json:"data"`
|
||||
|
||||
// Specify Permissions for the Tenant.
|
||||
// +optional
|
||||
Permissions Permissions `json:"permissions,omitzero"`
|
||||
@@ -25,11 +32,11 @@ type TenantSpec struct {
|
||||
//
|
||||
// Read More: https://projectcapsule.dev/docs/tenants/rules/
|
||||
//+optional
|
||||
Rules []*NamespaceRule `json:"rules,omitzero"`
|
||||
Rules []*api.NamespaceRuleBodyTenant `json:"rules,omitzero"`
|
||||
|
||||
// Specifies the owners of the Tenant.
|
||||
// Optional
|
||||
Owners api.OwnerListSpec `json:"owners,omitempty"`
|
||||
Owners rbac.OwnerListSpec `json:"owners,omitempty"`
|
||||
// Specifies options for the Namespaces, such as additional metadata or maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
|
||||
NamespaceOptions *NamespaceOptions `json:"namespaceOptions,omitempty"`
|
||||
// Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
|
||||
@@ -50,7 +57,7 @@ type TenantSpec struct {
|
||||
// +optional
|
||||
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitzero"`
|
||||
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
|
||||
AdditionalRoleBindings []api.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
|
||||
AdditionalRoleBindings []rbac.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
|
||||
// Specifies the allowed RuntimeClasses assigned to the Tenant.
|
||||
// Capsule assures that all Pods resources created in the Tenant can use only one of the allowed RuntimeClasses.
|
||||
// Optional.
|
||||
@@ -108,11 +115,15 @@ type Permissions struct {
|
||||
// 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"`
|
||||
|
||||
// ClusterRoles granted to the promoted ServiceAccounts across the Tenant
|
||||
//+kubebuilder:default:=true
|
||||
AllowOwnerPromotion bool `json:"allowOwnerPromotion,omitempty"`
|
||||
}
|
||||
|
||||
func (p *Permissions) ListMatchingOwners(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
c client.Reader,
|
||||
tnt string,
|
||||
opts ...client.ListOption,
|
||||
) ([]*TenantOwner, error) {
|
||||
@@ -150,10 +161,6 @@ type Tenant struct {
|
||||
Status TenantStatus `json:"status,omitzero"`
|
||||
}
|
||||
|
||||
func (in *Tenant) GetNamespaces() (res []string) {
|
||||
return in.Status.Namespaces
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// TenantList contains a list of Tenant.
|
||||
|
||||
@@ -6,13 +6,13 @@ package v1beta2
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/rbac"
|
||||
)
|
||||
|
||||
// TenantOwnerSpec defines the desired state of TenantOwner.
|
||||
type TenantOwnerSpec struct {
|
||||
// Subject
|
||||
api.CoreOwnerSpec `json:",inline"`
|
||||
rbac.CoreOwnerSpec `json:",inline"`
|
||||
|
||||
// Adds the given subject as capsule user. When enabled this subject does not have to be
|
||||
// mentioned in the CapsuleConfiguration as Capsule User. In almost all scenarios Tenant Owners
|
||||
@@ -26,7 +26,7 @@ type TenantOwnerStatus struct{}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Cluster
|
||||
// +kubebuilder:resource:scope=Cluster,shortName=to
|
||||
|
||||
// TenantOwner is the Schema for the tenantowners API.
|
||||
type TenantOwner struct {
|
||||
|
||||
@@ -5,13 +5,25 @@ package v1beta2
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
)
|
||||
|
||||
// GlobalTenantResourceSpec defines the desired state of GlobalTenantResource.
|
||||
type GlobalTenantResourceSpec struct {
|
||||
TenantResourceSpec `json:",inline"`
|
||||
TenantResourceCommonSpec `json:",inline"`
|
||||
|
||||
// Local ServiceAccount which will perform all the actions defined in the TenantResource
|
||||
// You must provide permissions accordingly to that ServiceAccount
|
||||
//+optional
|
||||
ServiceAccount *meta.NamespacedRFC1123ObjectReferenceWithNamespace `json:"serviceAccount,omitzero"`
|
||||
// Resource Scope, Can either be
|
||||
// - Tenant: Create Resources for each tenant in selected Tenants
|
||||
// - Namespace: Create Resources for each namespace in selected Tenants
|
||||
// +kubebuilder:default:=Namespace
|
||||
// +optional
|
||||
Scope api.ResourceScope `json:"scope"`
|
||||
// Defines the Tenant selector used target the tenants on which resources must be propagated.
|
||||
// +optional
|
||||
TenantSelector metav1.LabelSelector `json:"tenantSelector,omitzero"`
|
||||
@@ -19,27 +31,19 @@ type GlobalTenantResourceSpec struct {
|
||||
|
||||
// GlobalTenantResourceStatus defines the observed state of GlobalTenantResource.
|
||||
type GlobalTenantResourceStatus struct {
|
||||
TenantResourceCommonStatus `json:",inline"`
|
||||
|
||||
// List of Tenants addressed by the GlobalTenantResource.
|
||||
SelectedTenants []string `json:"selectedTenants"`
|
||||
// List of the replicated resources for the given TenantResource.
|
||||
ProcessedItems ProcessedItems `json:"processedItems,omitzero"`
|
||||
}
|
||||
|
||||
type ProcessedItems []ObjectReferenceStatus
|
||||
|
||||
func (p *ProcessedItems) AsSet() sets.Set[string] {
|
||||
set := sets.New[string]()
|
||||
|
||||
for _, i := range *p {
|
||||
set.Insert(i.String())
|
||||
}
|
||||
|
||||
return set
|
||||
SelectedTenants []string `json:"selectedTenants,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Cluster
|
||||
// +kubebuilder:resource:scope=Cluster,shortName=gtr
|
||||
// +kubebuilder:printcolumn:name="Items",type="integer",JSONPath=".status.size",description="The total amount of items being replicated"
|
||||
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Reconcile Status for the tenant"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="Reconcile Message for the tenant"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
|
||||
|
||||
// GlobalTenantResource allows to propagate resource replications to a specific subset of Tenant resources.
|
||||
type GlobalTenantResource struct {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
func (in *GlobalTenantResource) AssignTenants(tnts []Tenant) {
|
||||
l := make([]string, 0, len(tnts))
|
||||
|
||||
for _, tnt := range tnts {
|
||||
l = append(l, tnt.GetName())
|
||||
}
|
||||
|
||||
sort.Strings(l)
|
||||
|
||||
in.Status.SelectedTenants = l
|
||||
}
|
||||
@@ -5,52 +5,31 @@ package v1beta2
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
)
|
||||
|
||||
// TenantResourceSpec defines the desired state of TenantResource.
|
||||
type TenantResourceSpec struct {
|
||||
// Define the period of time upon a second reconciliation must be invoked.
|
||||
// Keep in mind that any change to the manifests will trigger a new reconciliation.
|
||||
// +kubebuilder:default="60s"
|
||||
ResyncPeriod metav1.Duration `json:"resyncPeriod"`
|
||||
// When the replicated resource manifest is deleted, all the objects replicated so far will be automatically deleted.
|
||||
// Disable this to keep replicated resources although the deletion of the replication manifest.
|
||||
// +kubebuilder:default=true
|
||||
PruningOnDelete *bool `json:"pruningOnDelete,omitempty"`
|
||||
// Defines the rules to select targeting Namespace, along with the objects that must be replicated.
|
||||
Resources []ResourceSpec `json:"resources"`
|
||||
}
|
||||
TenantResourceCommonSpec `json:",inline"`
|
||||
|
||||
type ResourceSpec struct {
|
||||
// Defines the Namespace selector to select the Tenant Namespaces on which the resources must be propagated.
|
||||
// In case of nil value, all the Tenant Namespaces are targeted.
|
||||
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
|
||||
// List of the resources already existing in other Namespaces that must be replicated.
|
||||
NamespacedItems []ObjectReference `json:"namespacedItems,omitempty"`
|
||||
// List of raw resources that must be replicated.
|
||||
RawItems []RawExtension `json:"rawItems,omitempty"`
|
||||
// Besides the Capsule metadata required by TenantResource controller, defines additional metadata that must be
|
||||
// added to the replicated resources.
|
||||
AdditionalMetadata *api.AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:validation:XEmbeddedResource
|
||||
// +kubebuilder:validation:XPreserveUnknownFields
|
||||
type RawExtension struct {
|
||||
runtime.RawExtension `json:",inline"`
|
||||
// Local ServiceAccount which will perform all the actions defined in the TenantResource
|
||||
// You must provide permissions accordingly to that ServiceAccount
|
||||
//+optional
|
||||
ServiceAccount *meta.LocalRFC1123ObjectReference `json:"serviceAccount,omitzero"`
|
||||
}
|
||||
|
||||
// TenantResourceStatus defines the observed state of TenantResource.
|
||||
type TenantResourceStatus struct {
|
||||
// List of the replicated resources for the given TenantResource.
|
||||
ProcessedItems ProcessedItems `json:"processedItems"`
|
||||
TenantResourceCommonStatus `json:",inline"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="Items",type="integer",JSONPath=".status.size",description="The total amount of items being replicated"
|
||||
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Reconcile Status for the tenant"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="Reconcile Message for the tenant"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
|
||||
|
||||
// TenantResource allows a Tenant Owner, if enabled with proper RBAC, to propagate resources in its Namespace.
|
||||
// The object must be deployed in a Tenant Namespace, and cannot reference object living in non-Tenant namespaces.
|
||||
|
||||
@@ -4,71 +4,98 @@
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
tpl "github.com/projectcapsule/capsule/pkg/template"
|
||||
)
|
||||
|
||||
type ObjectReferenceAbstract struct {
|
||||
// Kind of the referent.
|
||||
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
Kind string `json:"kind"`
|
||||
// Namespace of the referent.
|
||||
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/
|
||||
Namespace string `json:"namespace"`
|
||||
// API version of the referent.
|
||||
APIVersion string `json:"apiVersion,omitempty"`
|
||||
type TenantResourceCommonStatus struct {
|
||||
// Condition of the GlobalTenantResource.
|
||||
Conditions meta.ConditionList `json:"conditions,omitempty"`
|
||||
|
||||
// List of the replicated resources for the given TenantResource.
|
||||
//+optional
|
||||
ProcessedItems meta.ProcessedItems `json:"processedItems,omitzero"`
|
||||
|
||||
// How many items are being replicated by the TenantResource.
|
||||
Size uint `json:"size"`
|
||||
|
||||
// Serviceaccount used for impersonation
|
||||
//+optional
|
||||
ServiceAccount *meta.NamespacedRFC1123ObjectReferenceWithNamespace `json:"serviceAccount,omitzero"`
|
||||
}
|
||||
|
||||
type ObjectReferenceStatus struct {
|
||||
ObjectReferenceAbstract `json:",inline"`
|
||||
|
||||
// Name of the referent.
|
||||
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
Name string `json:"name"`
|
||||
func (s *TenantResourceCommonStatus) UpdateStats() {
|
||||
s.Size = uint(len(s.ProcessedItems))
|
||||
}
|
||||
|
||||
type ObjectReference struct {
|
||||
ObjectReferenceAbstract `json:",inline"`
|
||||
|
||||
// Label selector used to select the given resources in the given Namespace.
|
||||
Selector metav1.LabelSelector `json:"selector"`
|
||||
type TenantResourceCommonSpec struct {
|
||||
// Provide additional settings
|
||||
// +kubebuilder:default={}
|
||||
Settings TenantResourceCommonSpecSettings `json:"settings,omitzero"`
|
||||
// DependsOn may contain a meta.NamespacedObjectReference slice
|
||||
// with references to TenantResource resources that must be ready before this
|
||||
// TenantResource can be reconciled.
|
||||
// +optional
|
||||
DependsOn []meta.LocalRFC1123ObjectReference `json:"dependsOn,omitempty"`
|
||||
// Define the period of time upon a second reconciliation must be invoked.
|
||||
// Keep in mind that any change to the manifests will trigger a new reconciliation.
|
||||
// +kubebuilder:default="60s"
|
||||
ResyncPeriod metav1.Duration `json:"resyncPeriod"`
|
||||
// When the replicated resource manifest is deleted, all the objects replicated so far will be automatically deleted.
|
||||
// Disable this to keep replicated resources although the deletion of the replication manifest.
|
||||
// +kubebuilder:default=true
|
||||
PruningOnDelete *bool `json:"pruningOnDelete,omitempty"`
|
||||
// When cordoning a replication it will no longer execute any applies or deletions (paused).
|
||||
// This is useful for maintenances
|
||||
// +kubebuilder:default=false
|
||||
Cordoned *bool `json:"cordoned,omitempty"`
|
||||
// Defines the rules to select targeting Namespace, along with the objects that must be replicated.
|
||||
Resources []ResourceSpec `json:"resources"`
|
||||
}
|
||||
|
||||
func (in *ObjectReferenceStatus) String() string {
|
||||
return fmt.Sprintf("Kind=%s,APIVersion=%s,Namespace=%s,Name=%s", in.Kind, in.APIVersion, in.Namespace, in.Name)
|
||||
type TenantResourceCommonSpecSettings struct {
|
||||
// Enabling this allows TenanResources to interact with objects which were not created by a TenantResource. In this case on prune no deletion of the entire object is made.
|
||||
// +kubebuilder:default=false
|
||||
Adopt *bool `json:"adopt,omitempty"`
|
||||
// Force indicates that in case of conflicts with server-side apply, the client should acquire ownership of the conflicting field.
|
||||
// You may create collisions with this.
|
||||
// +kubebuilder:default=false
|
||||
Force *bool `json:"force,omitempty"`
|
||||
}
|
||||
|
||||
func (in *ObjectReferenceStatus) ParseFromString(value string) error {
|
||||
rawParts := strings.Split(value, ",")
|
||||
type ResourceSpec struct {
|
||||
// Defines the Namespace selector to select the Tenant Namespaces on which the resources must be propagated.
|
||||
// In case of nil value, all the Tenant Namespaces are targeted.
|
||||
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
|
||||
// List of the resources already existing in other Namespaces that must be replicated.
|
||||
NamespacedItems []tpl.ResourceReference `json:"namespacedItems,omitempty"`
|
||||
// List of raw resources that must be replicated.
|
||||
RawItems []RawExtension `json:"rawItems,omitempty"`
|
||||
// Besides the Capsule metadata required by TenantResource controller, defines additional metadata that must be
|
||||
// added to the replicated resources.
|
||||
AdditionalMetadata *api.AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
|
||||
// Templates for advanced use cases
|
||||
Generators []TemplateItemSpec `json:"generators,omitempty"`
|
||||
// Provide additional template context, which can be used throughout all
|
||||
// the declared items for the replication
|
||||
// +optional
|
||||
Context *tpl.TemplateContext `json:"context,omitempty"`
|
||||
}
|
||||
|
||||
if len(rawParts) != 4 {
|
||||
return fmt.Errorf("unexpected raw parts")
|
||||
}
|
||||
// +kubebuilder:validation:XPreserveUnknownFields
|
||||
type RawExtension struct {
|
||||
runtime.RawExtension `json:",inline"`
|
||||
}
|
||||
|
||||
for _, i := range rawParts {
|
||||
parts := strings.Split(i, "=")
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("unrecognized separator")
|
||||
}
|
||||
|
||||
k, v := parts[0], parts[1]
|
||||
|
||||
switch k {
|
||||
case "Kind":
|
||||
in.Kind = v
|
||||
case "APIVersion":
|
||||
in.APIVersion = v
|
||||
case "Namespace":
|
||||
in.Namespace = v
|
||||
case "Name":
|
||||
in.Name = v
|
||||
default:
|
||||
return fmt.Errorf("unrecognized marker: %s", k)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
type TemplateItemSpec struct {
|
||||
// Template contains any amount of yaml which is applied to Kubernetes.
|
||||
// This can be a single resource or multiple resources
|
||||
Template string `json:"template,omitempty"`
|
||||
// Missing Key Option for templating
|
||||
// +kubebuilder:default=zero
|
||||
MissingKey tpl.MissingKeyOption `json:"missingKey,omitempty"`
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user