Files
capsule/e2e/tenantowner_status_test.go
Oliver BählerandGitHub 15a848d844 fix: allow administrator operations on tenant namespaces (#2037)
fix: allow administrator operations on tenant namespaces 

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
2026-07-17 09:38:32 +02:00

388 lines
11 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
capmeta "github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rbac"
)
// tenantOwnerReady waits until the TenantOwner's Ready condition has the expected status.
func tenantOwnerReady(to *capsulev1beta2.TenantOwner, expected metav1.ConditionStatus) {
Eventually(func(g Gomega) {
current := &capsulev1beta2.TenantOwner{}
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: to.GetName()}, current)).To(Succeed())
cond := current.Status.Conditions.GetConditionByType(capmeta.ReadyCondition)
g.Expect(cond).NotTo(BeNil(), "TenantOwner %q should have a Ready condition", to.GetName())
g.Expect(cond.Status).To(
Equal(expected),
"TenantOwner %q Ready condition should be %s, got %s: %s",
to.GetName(), expected, cond.Status, cond.Message,
)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
// tenantOwnerMatchedTenants waits until the TenantOwner's status.matchedTenantNames
// equals the expected sorted list. The controller calls sort.Strings before persisting,
// so the assertion is order-sensitive to validate stable ordering for API consumers.
func tenantOwnerMatchedTenants(to *capsulev1beta2.TenantOwner, expected []string) {
Eventually(func(g Gomega) {
current := &capsulev1beta2.TenantOwner{}
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: to.GetName()}, current)).To(Succeed())
actual := current.Status.Tenants
if actual == nil {
actual = []string{}
}
g.Expect(actual).To(
Equal(expected),
"TenantOwner %q status.matchedTenantNames mismatch", to.GetName(),
)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
var _ = Describe("TenantOwner status tracks matched Tenants", Ordered, Label("tenantowner", "tenant", "permissions", "owners"), func() {
// to is a shared TenantOwner used across the sub-tests in this Ordered block.
// Two Tenants are created/deleted to verify count changes.
to := &capsulev1beta2.TenantOwner{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-to-status",
Labels: map[string]string{
"e2e-to-status": "true",
"e2e.suite.capsule/name": "owner_status_test",
},
},
Spec: capsulev1beta2.TenantOwnerSpec{
Aggregate: ptr.To(true),
CoreOwnerSpec: rbac.CoreOwnerSpec{
UserSpec: rbac.UserSpec{
Kind: rbac.UserOwner,
Name: "e2e-to-status-user",
},
ClusterRoles: []string{"view"},
},
},
}
tnt1 := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-to-status-tnt1",
},
Spec: capsulev1beta2.TenantSpec{
Permissions: capsulev1beta2.Permissions{
MatchOwners: []*metav1.LabelSelector{
{MatchLabels: map[string]string{"e2e.suite.capsule/name": "owner_status_test"}},
},
},
},
}
tnt2 := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-to-status-tnt2",
},
Spec: capsulev1beta2.TenantSpec{
Permissions: capsulev1beta2.Permissions{
MatchOwners: []*metav1.LabelSelector{
{MatchLabels: map[string]string{"e2e.suite.capsule/name": "owner_status_test"}},
},
},
},
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
to.ResourceVersion = ""
return k8sClient.Create(context.TODO(), to)
}).Should(Succeed())
})
JustAfterEach(func() {
EventuallyDeletion(to)
EventuallyDeletion(tnt1)
EventuallyDeletion(tnt2)
})
It("has Ready=True and empty tenant list when no Tenant matches", func() {
tenantOwnerReady(to, metav1.ConditionTrue)
tenantOwnerMatchedTenants(to, []string{})
})
It("reflects one matched Tenant in status", func() {
EventuallyCreation(func() error {
tnt1.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt1)
}).Should(Succeed())
TenantReadyTrue(tnt1)
tenantOwnerReady(to, metav1.ConditionTrue)
tenantOwnerMatchedTenants(to, []string{tnt1.Name})
})
It("reflects two matched Tenants in status", func() {
EventuallyCreation(func() error {
tnt1.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt1)
}).Should(Succeed())
EventuallyCreation(func() error {
tnt2.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt2)
}).Should(Succeed())
TenantReadyTrue(tnt1)
TenantReadyTrue(tnt2)
tenantOwnerReady(to, metav1.ConditionTrue)
tenantOwnerMatchedTenants(to, []string{tnt1.Name, tnt2.Name})
})
It("clears the matched Tenant from status when the Tenant is deleted", func() {
EventuallyCreation(func() error {
tnt1.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt1)
}).Should(Succeed())
TenantReadyTrue(tnt1)
tenantOwnerMatchedTenants(to, []string{tnt1.Name})
EventuallyDeletion(tnt1)
tenantOwnerReady(to, metav1.ConditionTrue)
tenantOwnerMatchedTenants(to, []string{})
})
It("updates status when a Tenant's matchOwners selector is removed", func() {
EventuallyCreation(func() error {
tnt1.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt1)
}).Should(Succeed())
TenantReadyTrue(tnt1)
tenantOwnerMatchedTenants(to, []string{tnt1.Name})
// Remove the matchOwners selector so the TenantOwner is no longer matched.
Eventually(func(g Gomega) {
current := &capsulev1beta2.Tenant{}
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.Name}, current)).To(Succeed())
current.Spec.Permissions.MatchOwners = nil
g.Expect(k8sClient.Update(context.TODO(), current)).To(Succeed())
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
tenantOwnerReady(to, metav1.ConditionTrue)
tenantOwnerMatchedTenants(to, []string{})
})
It("tracks observedGeneration in TenantOwner status", func() {
Eventually(func(g Gomega) {
current := &capsulev1beta2.TenantOwner{}
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: to.Name}, current)).To(Succeed())
g.Expect(current.Status.ObservedGeneration).To(
Equal(current.GetGeneration()),
"status.observedGeneration should equal metadata.generation",
)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
})
var _ = Describe("TenantOwner status with 10 matched Tenants", Ordered, Label("tenantowner", "status", "scale"), func() {
const tenantCount = 10
to := &capsulev1beta2.TenantOwner{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-to-status-10match",
Labels: map[string]string{
"e2e-to-status-10match": "true",
"e2e.suite.capsule/name": "owner_status_test",
},
},
Spec: capsulev1beta2.TenantOwnerSpec{
Aggregate: ptr.To(true),
CoreOwnerSpec: rbac.CoreOwnerSpec{
UserSpec: rbac.UserSpec{
Kind: rbac.UserOwner,
Name: "e2e-to-status-10match-user",
},
ClusterRoles: []string{"view"},
},
},
}
tenants := func() []*capsulev1beta2.Tenant {
tnts := make([]*capsulev1beta2.Tenant, tenantCount)
for i := range tnts {
tnts[i] = &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("e2e-to-status-10match-tnt-%d", i),
Labels: map[string]string{
"e2e.suite.capsule/name": "owner_status_test",
},
},
Spec: capsulev1beta2.TenantSpec{
Permissions: capsulev1beta2.Permissions{
MatchOwners: []*metav1.LabelSelector{
{MatchLabels: map[string]string{"e2e-to-status-10match": "true"}},
},
},
},
}
}
return tnts
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
to.ResourceVersion = ""
return k8sClient.Create(context.TODO(), to)
}).Should(Succeed())
for _, tnt := range tenants() {
tnt := tnt
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
}
})
JustAfterEach(func() {
EventuallyDeletion(to)
for _, tnt := range tenants() {
EventuallyDeletion(tnt)
}
})
It("reports all 10 matched Tenants and Ready=True", func() {
expectedNames := make([]string, tenantCount)
for i := range expectedNames {
expectedNames[i] = fmt.Sprintf("e2e-to-status-10match-tnt-%d", i)
}
tenantOwnerReady(to, metav1.ConditionTrue)
tenantOwnerMatchedTenants(to, expectedNames)
})
})
var _ = Describe("TenantOwner status fan-out: 10 TenantOwners matched by 1 Tenant", Ordered, Label("tenantowner", "status", "fanout"), func() {
const ownerCount = 10
owners := func() []*capsulev1beta2.TenantOwner {
tos := make([]*capsulev1beta2.TenantOwner, ownerCount)
for i := range tos {
tos[i] = &capsulev1beta2.TenantOwner{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("e2e-to-fanout-%d", i),
Labels: map[string]string{
"e2e-to-fanout": "true",
"e2e.suite.capsule/name": "owner_status_test",
},
},
Spec: capsulev1beta2.TenantOwnerSpec{
Aggregate: ptr.To(true),
CoreOwnerSpec: rbac.CoreOwnerSpec{
UserSpec: rbac.UserSpec{
Kind: rbac.UserOwner,
Name: fmt.Sprintf("e2e-to-fanout-user-%d", i),
},
ClusterRoles: []string{"view"},
},
},
}
}
return tos
}
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-tnt-fanout-single",
Labels: map[string]string{
"e2e.suite.capsule/name": "owner_status_test",
},
},
Spec: capsulev1beta2.TenantSpec{
Permissions: capsulev1beta2.Permissions{
MatchOwners: []*metav1.LabelSelector{
{MatchLabels: map[string]string{
"e2e-to-fanout": "true",
"e2e.suite.capsule/name": "owner_status_test",
}},
},
},
},
}
JustBeforeEach(func() {
for _, to := range owners() {
to := to
EventuallyCreation(func() error {
to.ResourceVersion = ""
return k8sClient.Create(context.TODO(), to)
}).Should(Succeed())
}
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
for _, to := range owners() {
EventuallyDeletion(to)
}
EventuallyDeletion(tnt)
})
It("updates all 10 TenantOwners when 1 Tenant is created", func() {
// All 10 TenantOwners should each report matchedTenants=1 and tenants=[tnt.Name].
for _, to := range owners() {
to := to
tenantOwnerReady(to, metav1.ConditionTrue)
tenantOwnerMatchedTenants(to, []string{tnt.Name})
}
})
It("clears all 10 TenantOwners when the Tenant is deleted", func() {
// Establish baseline: all matched.
for _, to := range owners() {
tenantOwnerMatchedTenants(to, []string{tnt.Name})
}
EventuallyDeletion(tnt)
// After deletion every TenantOwner should report zero matches.
for _, to := range owners() {
tenantOwnerMatchedTenants(to, []string{})
}
})
})