mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 12:36:39 +00:00
feat: add globalresourcequota api (#2068)
* feat: add globalresourcequota api Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
This commit is contained in:
@@ -22,6 +22,8 @@ const (
|
||||
|
||||
ResourcePoolLabel = "projectcapsule.dev/pool"
|
||||
|
||||
GlobalResourceQuotaLabel = "projectcapsule.dev/global-resource-quota"
|
||||
|
||||
FreezeLabel = "projectcapsule.dev/freeze"
|
||||
|
||||
OwnerPromotionLabel = "owner.projectcapsule.dev/promote"
|
||||
@@ -41,6 +43,7 @@ const (
|
||||
LimitRangeLabel = "capsule.clastix.io/limit-range"
|
||||
NetworkPolicyLabel = "capsule.clastix.io/network-policy"
|
||||
ResourceQuotaLabel = "capsule.clastix.io/resource-quota"
|
||||
RuleQuotaLabel = "projectcapsule.dev/rule-quota"
|
||||
RolebindingLabel = "capsule.clastix.io/role-binding"
|
||||
)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ func NewManagedMetadata(
|
||||
TenantLabel,
|
||||
NewTenantLabel,
|
||||
ResourcePoolLabel,
|
||||
GlobalResourceQuotaLabel,
|
||||
FreezeLabel,
|
||||
OwnerPromotionLabel,
|
||||
ServiceAccountPromotionLabel,
|
||||
@@ -38,6 +39,7 @@ func NewManagedMetadata(
|
||||
LimitRangeLabel,
|
||||
NetworkPolicyLabel,
|
||||
ResourceQuotaLabel,
|
||||
RuleQuotaLabel,
|
||||
RolebindingLabel,
|
||||
),
|
||||
annotations: stringSet(
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package rules
|
||||
|
||||
import corev1 "k8s.io/api/core/v1"
|
||||
|
||||
// ResourceQuotaRule defines a named ResourceQuota specification generated by a
|
||||
// Tenant rule. Name is the durable identity of the generated
|
||||
// GlobalResourceQuota and must be unique across all rules of a Tenant.
|
||||
// +kubebuilder:object:generate=true
|
||||
type ResourceQuotaRule struct {
|
||||
corev1.ResourceQuotaSpec `json:",inline"`
|
||||
|
||||
// Name is the stable identity of this quota within the Tenant. Changing the
|
||||
// name replaces the generated GlobalResourceQuota; changing the quota or its
|
||||
// namespace selector updates the existing object.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:MaxLength=63
|
||||
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
|
||||
package rules
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// For future implementation where users might manage RuleStatus CRs themselves
|
||||
// +kubebuilder:object:generate=true
|
||||
@@ -15,6 +13,14 @@ type NamespaceRuleBodyNamespace struct {
|
||||
// +optional
|
||||
Audience []Audience `json:"audience,omitempty"`
|
||||
|
||||
// Quota contains native Kubernetes ResourceQuota specifications shared by
|
||||
// all namespaces selected by this rule. Unlike Enforce, quota accounting is
|
||||
// independent of the request audience.
|
||||
// +optional
|
||||
// +listType=map
|
||||
// +listMapKey=name
|
||||
Quota []ResourceQuotaRule `json:"quota,omitempty"`
|
||||
|
||||
// Enforcement for given rule
|
||||
//+optional
|
||||
Enforce *NamespaceRuleEnforceBody `json:"enforce,omitzero"`
|
||||
|
||||
@@ -99,6 +99,13 @@ func (in *NamespaceRuleBodyNamespace) DeepCopyInto(out *NamespaceRuleBodyNamespa
|
||||
*out = make([]Audience, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Quota != nil {
|
||||
in, out := &in.Quota, &out.Quota
|
||||
*out = make([]ResourceQuotaRule, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Enforce != nil {
|
||||
in, out := &in.Enforce, &out.Enforce
|
||||
*out = new(NamespaceRuleEnforceBody)
|
||||
@@ -347,6 +354,22 @@ func (in *OCIRegistry) DeepCopy() *OCIRegistry {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResourceQuotaRule) DeepCopyInto(out *ResourceQuotaRule) {
|
||||
*out = *in
|
||||
in.ResourceQuotaSpec.DeepCopyInto(&out.ResourceQuotaSpec)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceQuotaRule.
|
||||
func (in *ResourceQuotaRule) DeepCopy() *ResourceQuotaRule {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ResourceQuotaRule)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ServiceExternalNameRule) DeepCopyInto(out *ServiceExternalNameRule) {
|
||||
*out = *in
|
||||
|
||||
@@ -21,6 +21,8 @@ func ValidateRuleStatusBody(
|
||||
mapper k8smeta.RESTMapper,
|
||||
bodies []*rules.NamespaceRuleBodyNamespace,
|
||||
) error {
|
||||
quotaNames := make(map[string]string)
|
||||
|
||||
for i, rule := range bodies {
|
||||
if rule == nil {
|
||||
continue
|
||||
@@ -30,6 +32,10 @@ func ValidateRuleStatusBody(
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateQuotaRules(i, rule.Quota, quotaNames); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rule.Enforce == nil {
|
||||
continue
|
||||
}
|
||||
@@ -54,6 +60,43 @@ func ValidateRuleStatusBody(
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateQuotaRules(ruleIndex int, quotas []rules.ResourceQuotaRule, names map[string]string) error {
|
||||
for quotaIndex, quota := range quotas {
|
||||
path := fmt.Sprintf("rules[%d].quota[%d]", ruleIndex, quotaIndex)
|
||||
if errs := k8svalidation.IsDNS1123Label(quota.Name); len(errs) > 0 {
|
||||
return fmt.Errorf("%s.name %q is invalid: %s", path, quota.Name, strings.Join(errs, "; "))
|
||||
}
|
||||
|
||||
if previous, found := names[quota.Name]; found {
|
||||
return fmt.Errorf(
|
||||
"%s.name %q is invalid: quota name is already used by %s",
|
||||
path,
|
||||
quota.Name,
|
||||
previous,
|
||||
)
|
||||
}
|
||||
|
||||
names[quota.Name] = path
|
||||
|
||||
if len(quota.Hard) == 0 {
|
||||
return fmt.Errorf("%s.hard is invalid: at least one resource is required", path)
|
||||
}
|
||||
|
||||
for name, quantity := range quota.Hard {
|
||||
if quantity.Sign() < 0 {
|
||||
return fmt.Errorf(
|
||||
"rules[%d].quota[%d].hard[%q] is invalid: quantity must not be negative",
|
||||
ruleIndex,
|
||||
quotaIndex,
|
||||
name,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateIngressRules(
|
||||
ruleIndex int,
|
||||
ingress rules.NamespaceRuleEnforceIngressBody,
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apimeta "k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
@@ -37,6 +39,73 @@ func TestValidateRuleStatusBody(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid quota-only rule",
|
||||
mapper: mapper,
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{{
|
||||
Quota: []rules.ResourceQuotaRule{{
|
||||
Name: "shared-compute",
|
||||
ResourceQuotaSpec: corev1.ResourceQuotaSpec{Hard: corev1.ResourceList{
|
||||
corev1.ResourceRequestsCPU: resource.MustParse("8"),
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "quota name is required",
|
||||
mapper: mapper,
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{{
|
||||
Quota: []rules.ResourceQuotaRule{{
|
||||
ResourceQuotaSpec: corev1.ResourceQuotaSpec{Hard: corev1.ResourceList{
|
||||
corev1.ResourceRequestsCPU: resource.MustParse("8"),
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
wantErr: "rules[0].quota[0].name",
|
||||
},
|
||||
{
|
||||
name: "quota name must be a DNS label",
|
||||
mapper: mapper,
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{{
|
||||
Quota: []rules.ResourceQuotaRule{{
|
||||
Name: "Shared_Compute",
|
||||
ResourceQuotaSpec: corev1.ResourceQuotaSpec{Hard: corev1.ResourceList{
|
||||
corev1.ResourceRequestsCPU: resource.MustParse("8"),
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
wantErr: `rules[0].quota[0].name "Shared_Compute" is invalid`,
|
||||
},
|
||||
{
|
||||
name: "quota hard must not be empty",
|
||||
mapper: mapper,
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{{
|
||||
Quota: []rules.ResourceQuotaRule{{Name: "shared-compute"}},
|
||||
}},
|
||||
wantErr: "rules[0].quota[0].hard is invalid",
|
||||
},
|
||||
{
|
||||
name: "quota hard must not be negative",
|
||||
mapper: mapper,
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{{
|
||||
Quota: []rules.ResourceQuotaRule{{
|
||||
Name: "shared-compute",
|
||||
ResourceQuotaSpec: corev1.ResourceQuotaSpec{Hard: corev1.ResourceList{
|
||||
corev1.ResourceRequestsCPU: resource.MustParse("-1"),
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
wantErr: `rules[0].quota[0].hard["requests.cpu"] is invalid`,
|
||||
},
|
||||
{
|
||||
name: "quota names must be unique across rules",
|
||||
mapper: mapper,
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
{Quota: []rules.ResourceQuotaRule{{Name: "shared-compute", ResourceQuotaSpec: corev1.ResourceQuotaSpec{Hard: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1")}}}}},
|
||||
{Quota: []rules.ResourceQuotaRule{{Name: "shared-compute", ResourceQuotaSpec: corev1.ResourceQuotaSpec{Hard: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("1Gi")}}}}},
|
||||
},
|
||||
wantErr: `rules[1].quota[0].name "shared-compute" is invalid: quota name is already used by rules[0].quota[0]`,
|
||||
},
|
||||
{
|
||||
name: "valid workload service and metadata rules",
|
||||
mapper: mapper,
|
||||
|
||||
@@ -144,12 +144,6 @@ func positiveResource(resources corev1.ResourceList, name corev1.ResourceName) (
|
||||
return quantity, true
|
||||
}
|
||||
|
||||
//nolint:exhaustive
|
||||
func isSupportedQoSComputeResource(name corev1.ResourceName) bool {
|
||||
switch name {
|
||||
case corev1.ResourceCPU, corev1.ResourceMemory:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return name == corev1.ResourceCPU || name == corev1.ResourceMemory
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func BuildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]s
|
||||
annotations[meta.AvailableIngressClassesAnnotation] = strings.Join(ic.Exact, ",")
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
//nolint:staticcheck,nolintlint // Preserve annotations for the deprecated v1beta2 field until it is removed.
|
||||
if len(ic.Regex) > 0 {
|
||||
annotations[meta.AvailableIngressClassesRegexpAnnotation] = ic.Regex
|
||||
}
|
||||
@@ -188,7 +188,7 @@ func BuildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]s
|
||||
annotations[meta.AvailableStorageClassesAnnotation] = strings.Join(sc.Exact, ",")
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
//nolint:staticcheck,nolintlint // Preserve annotations for the deprecated v1beta2 field until it is removed.
|
||||
if len(sc.Regex) > 0 {
|
||||
annotations[meta.AvailableStorageClassesRegexpAnnotation] = sc.Regex
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
k8svalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
|
||||
)
|
||||
|
||||
func RuleGlobalResourceQuotaName(tnt *capsulev1beta2.Tenant, quotaName string) string {
|
||||
return fmt.Sprintf("%s-%s", tnt.Name, quotaName)
|
||||
}
|
||||
|
||||
func ValidateRuleGlobalResourceQuotaName(tnt *capsulev1beta2.Tenant, quotaName string) error {
|
||||
name := RuleGlobalResourceQuotaName(tnt, quotaName)
|
||||
if errs := k8svalidation.IsDNS1123Subdomain(name); len(errs) > 0 {
|
||||
return fmt.Errorf("generated GlobalResourceQuota name %q is invalid: %s", name, strings.Join(errs, "; "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RuleGlobalResourceQuota(
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
ruleIndex int,
|
||||
itemIndex int,
|
||||
) *capsulev1beta2.GlobalResourceQuota {
|
||||
rule := tnt.Spec.Rules[ruleIndex]
|
||||
quota := rule.Quota[itemIndex]
|
||||
|
||||
selector := &metav1.LabelSelector{}
|
||||
if rule.NamespaceSelector != nil {
|
||||
selector = rule.NamespaceSelector.DeepCopy()
|
||||
}
|
||||
|
||||
if selector.MatchLabels == nil {
|
||||
selector.MatchLabels = map[string]string{}
|
||||
}
|
||||
|
||||
// Tenant membership is part of the generated selector so a rule can never
|
||||
// consume quota from a namespace owned by another Tenant.
|
||||
selector.MatchLabels[meta.TenantLabel] = tnt.Name
|
||||
|
||||
return &capsulev1beta2.GlobalResourceQuota{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: RuleGlobalResourceQuotaName(tnt, quota.Name),
|
||||
},
|
||||
Spec: capsulev1beta2.GlobalResourceQuotaSpec{
|
||||
NamespaceSelectors: []selectors.NamespaceSelector{{LabelSelector: selector}},
|
||||
Quota: *quota.ResourceQuotaSpec.DeepCopy(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
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"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/api/rules"
|
||||
)
|
||||
|
||||
func TestRuleGlobalResourceQuota(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "tenant-a", UID: types.UID("tenant-uid")},
|
||||
Spec: capsulev1beta2.TenantSpec{Rules: []*rules.NamespaceRuleBodyTenant{{
|
||||
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
|
||||
Quota: []rules.ResourceQuotaRule{{
|
||||
Name: "shared-compute",
|
||||
ResourceQuotaSpec: corev1.ResourceQuotaSpec{Hard: corev1.ResourceList{
|
||||
corev1.ResourceRequestsMemory: resource.MustParse("16Gi"),
|
||||
}},
|
||||
}},
|
||||
},
|
||||
NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"tier": "paid"}},
|
||||
}}},
|
||||
}
|
||||
|
||||
quota := RuleGlobalResourceQuota(tnt, 0, 0)
|
||||
if quota.Name != RuleGlobalResourceQuotaName(tnt, "shared-compute") {
|
||||
t.Fatalf("generated name = %q, want deterministic helper name", quota.Name)
|
||||
}
|
||||
if len(quota.Spec.NamespaceSelectors) != 1 {
|
||||
t.Fatalf("namespace selectors = %d, want 1", len(quota.Spec.NamespaceSelectors))
|
||||
}
|
||||
selector := quota.Spec.NamespaceSelectors[0].LabelSelector
|
||||
if selector.MatchLabels["tier"] != "paid" || selector.MatchLabels[meta.TenantLabel] != tnt.Name {
|
||||
t.Fatalf("generated selector = %#v, want tenant and rule labels", selector)
|
||||
}
|
||||
if got := quota.Spec.Quota.Hard[corev1.ResourceRequestsMemory]; got.Cmp(resource.MustParse("16Gi")) != 0 {
|
||||
t.Fatalf("generated quota hard = %s, want 16Gi", got.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuleGlobalResourceQuotaNameIsStableAcrossRuleChanges(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "tenant-a", UID: types.UID("tenant-uid")},
|
||||
}
|
||||
|
||||
want := RuleGlobalResourceQuotaName(tnt, "shared-compute")
|
||||
if want != "tenant-a-shared-compute" {
|
||||
t.Fatalf("generated name = %q, want Tenant and quota name", want)
|
||||
}
|
||||
if got := RuleGlobalResourceQuotaName(tnt, "shared-compute"); got != want {
|
||||
t.Fatalf("same quota name generated %q, want %q", got, want)
|
||||
}
|
||||
if got := RuleGlobalResourceQuotaName(tnt, "service-count"); got == want {
|
||||
t.Fatalf("different quota names generated the same object name %q", got)
|
||||
}
|
||||
|
||||
recreated := tnt.DeepCopy()
|
||||
recreated.UID = types.UID("replacement-tenant-uid")
|
||||
if got := RuleGlobalResourceQuotaName(recreated, "shared-compute"); got != want {
|
||||
t.Fatalf("recreated Tenant generated %q, want stable name %q", got, want)
|
||||
}
|
||||
|
||||
longTenant := tnt.DeepCopy()
|
||||
longTenant.Name = strings.Repeat("a", 250)
|
||||
if err := ValidateRuleGlobalResourceQuotaName(longTenant, "shared-compute"); err == nil {
|
||||
t.Fatal("overlong generated name was accepted")
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -85,7 +85,12 @@ func BuildNamespaceRuleBodyStatus(
|
||||
continue
|
||||
}
|
||||
|
||||
selected = append(selected, body.DeepCopy())
|
||||
statusBody := body.DeepCopy()
|
||||
// Quotas are Tenant-level inputs used to generate cluster-scoped
|
||||
// GlobalResourceQuotas. They are not evaluated from per-namespace
|
||||
// RuleStatus objects and must not be projected into them.
|
||||
statusBody.Quota = nil
|
||||
selected = append(selected, statusBody)
|
||||
}
|
||||
|
||||
rendered, err := template.RenderNamespaceRuleBodies(
|
||||
@@ -104,6 +109,7 @@ func BuildNamespaceRuleBodyStatus(
|
||||
continue
|
||||
}
|
||||
|
||||
body.Quota = nil
|
||||
out = append(out, body)
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,8 @@ func TestBuildNamespaceRuleBodyStatus(t *testing.T) {
|
||||
"{{ .namespace.status.phase }}",
|
||||
}}},
|
||||
},
|
||||
}}
|
||||
}, Quota: []rules.ResourceQuotaRule{{Name: "shared-compute"}}}
|
||||
quotaOnly := &rules.NamespaceRuleBodyNamespace{Quota: []rules.ResourceQuotaRule{{Name: "object-counts"}}}
|
||||
unmatched := &rules.NamespaceRuleBodyNamespace{Enforce: &rules.NamespaceRuleEnforceBody{Action: rules.ActionTypeDeny}}
|
||||
tnt.Spec.Rules = []*rules.NamespaceRuleBodyTenant{
|
||||
{
|
||||
@@ -73,6 +74,10 @@ func TestBuildNamespaceRuleBodyStatus(t *testing.T) {
|
||||
NamespaceRuleBodyNamespace: unmatched,
|
||||
NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"env": "dev"}},
|
||||
},
|
||||
{
|
||||
NamespaceRuleBodyNamespace: quotaOnly,
|
||||
NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"env": "prod"}},
|
||||
},
|
||||
nil,
|
||||
{},
|
||||
}
|
||||
@@ -88,6 +93,9 @@ func TestBuildNamespaceRuleBodyStatus(t *testing.T) {
|
||||
if len(got) != 1 || got[0].Enforce.Action != rules.ActionTypeAudit {
|
||||
t.Fatalf("BuildNamespaceRuleBodyStatus() = %#v, want one audit rule", got)
|
||||
}
|
||||
if len(got[0].Quota) != 0 {
|
||||
t.Fatalf("BuildNamespaceRuleBodyStatus() quota = %#v, want none", got[0].Quota)
|
||||
}
|
||||
if !reflect.DeepEqual(got[0].Enforce.Workloads.Schedulers[0].Exact, []string{"Active", "Active"}) {
|
||||
t.Fatalf("BuildNamespaceRuleBodyStatus() scheduler = %#v", got[0].Enforce.Workloads.Schedulers)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user