mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 04:26:45 +00:00
fix: allow administrator operations on tenant namespaces Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
388 lines
11 KiB
Go
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{})
|
|
}
|
|
})
|
|
})
|