mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-23 06:26:43 +00:00
* feat: implement performance optimizations Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add administrator capabilities Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add administrator capabilities Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * chore * feat: add administrator capabilities Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
265 lines
7.3 KiB
Go
265 lines
7.3 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package mutation
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/go-logr/logr"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
|
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
"github.com/projectcapsule/capsule/pkg/api/meta"
|
|
"github.com/projectcapsule/capsule/pkg/api/rbac"
|
|
capevents "github.com/projectcapsule/capsule/pkg/runtime/events"
|
|
"github.com/projectcapsule/capsule/pkg/tenant"
|
|
"github.com/projectcapsule/capsule/pkg/users"
|
|
)
|
|
|
|
func TestOwnerReferenceHandlerAllowsAdminTenantMigration(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
green := testTenant("green", "green-uid")
|
|
blue := testTenant("blue", "blue-uid")
|
|
oldNs := testTenantNamespace("workloads", green)
|
|
newNs := testTenantNamespace("workloads", blue)
|
|
c := testClient(t, green, blue)
|
|
|
|
response := (&ownerReferenceHandler{}).OnUpdate(
|
|
c,
|
|
c,
|
|
users.AdmissionUser{Type: users.AdmissionUserAdmin},
|
|
newNs,
|
|
oldNs,
|
|
nil,
|
|
nil,
|
|
)(context.Background(), admission.Request{})
|
|
|
|
if response != nil {
|
|
t.Fatalf("expected migration to proceed, got response %#v", response)
|
|
}
|
|
assertTenantAssignment(t, newNs, blue)
|
|
}
|
|
|
|
func TestOwnerReferenceHandlerAllowsAdminTenantDetachment(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
green := testTenant("green", "green-uid")
|
|
oldNs := testTenantNamespace("workloads", green)
|
|
newNs := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: oldNs.GetName()}}
|
|
c := testClient(t, green)
|
|
|
|
response := (&ownerReferenceHandler{}).OnUpdate(
|
|
c,
|
|
c,
|
|
users.AdmissionUser{Type: users.AdmissionUserAdmin},
|
|
newNs,
|
|
oldNs,
|
|
nil,
|
|
nil,
|
|
)(context.Background(), admission.Request{})
|
|
|
|
if response != nil {
|
|
t.Fatalf("expected detachment to proceed, got response %#v", response)
|
|
}
|
|
if tenant.HasTenantReference(newNs) {
|
|
t.Fatalf("detached namespace still has tenant ownership: %#v", newNs.ObjectMeta)
|
|
}
|
|
}
|
|
|
|
func TestOwnerReferenceHandlerRejectsTenantOwnerAssignmentChanges(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
owner := rbac.CoreOwnerSpec{UserSpec: rbac.UserSpec{Name: "alice", Kind: rbac.UserOwner}}
|
|
green := testTenant("green", "green-uid")
|
|
green.Status.Owners = rbac.OwnerStatusListSpec{owner}
|
|
blue := testTenant("blue", "blue-uid")
|
|
oldNs := testTenantNamespace("workloads", green)
|
|
recorder := capevents.NewEventRecorder(nil, logr.Discard(), nil, nil)
|
|
|
|
tests := []struct {
|
|
name string
|
|
new *corev1.Namespace
|
|
}{
|
|
{name: "migration", new: testTenantNamespace("workloads", blue)},
|
|
{
|
|
name: "label migration with empty ownerReferences",
|
|
new: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
|
|
Name: "workloads",
|
|
Labels: map[string]string{meta.TenantLabel: blue.Name},
|
|
}},
|
|
},
|
|
{name: "detachment", new: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "workloads"}}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := testClient(t, green, blue)
|
|
newNs := tt.new.DeepCopy()
|
|
|
|
response := (&ownerReferenceHandler{}).OnUpdate(
|
|
c,
|
|
c,
|
|
users.AdmissionUser{Type: users.AdmissionUserCapsule, Username: owner.Name},
|
|
newNs,
|
|
oldNs.DeepCopy(),
|
|
nil,
|
|
recorder,
|
|
)(context.Background(), admission.Request{})
|
|
|
|
if response == nil || response.Allowed {
|
|
t.Fatalf("expected assignment change to be denied, got %#v", response)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOwnerReferenceHandlerRepairsTenantOwnerReferences(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
owner := rbac.CoreOwnerSpec{UserSpec: rbac.UserSpec{Name: "alice", Kind: rbac.UserOwner}}
|
|
green := testTenant("green", "green-uid")
|
|
green.Status.Owners = rbac.OwnerStatusListSpec{owner}
|
|
oldNs := testTenantNamespace("workloads", green)
|
|
recorder := capevents.NewEventRecorder(nil, logr.Discard(), nil, nil)
|
|
|
|
tests := []struct {
|
|
name string
|
|
new *corev1.Namespace
|
|
}{
|
|
{
|
|
name: "missing reference",
|
|
new: func() *corev1.Namespace {
|
|
ns := oldNs.DeepCopy()
|
|
ns.OwnerReferences = nil
|
|
|
|
return ns
|
|
}(),
|
|
},
|
|
{
|
|
name: "additional reference",
|
|
new: func() *corev1.Namespace {
|
|
ns := oldNs.DeepCopy()
|
|
ns.OwnerReferences = append(ns.OwnerReferences, metav1.OwnerReference{
|
|
APIVersion: capsulev1beta2.GroupVersion.String(),
|
|
Kind: tenant.ObjectReferenceTenantKind,
|
|
Name: "blue",
|
|
UID: "blue-uid",
|
|
})
|
|
|
|
return ns
|
|
}(),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := testClient(t, green)
|
|
newNs := tt.new.DeepCopy()
|
|
|
|
response := (&ownerReferenceHandler{}).OnUpdate(
|
|
c,
|
|
c,
|
|
users.AdmissionUser{Type: users.AdmissionUserCapsule, Username: owner.Name},
|
|
newNs,
|
|
oldNs.DeepCopy(),
|
|
nil,
|
|
recorder,
|
|
)(context.Background(), admission.Request{})
|
|
|
|
if response != nil {
|
|
t.Fatalf("expected ownerReferences to be normalized, got %#v", response)
|
|
}
|
|
assertTenantAssignment(t, newNs, green)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOwnerReferenceHandlerRejectsNonOwnerJoin(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
green := testTenant("green", "green-uid")
|
|
c := testClient(t, green)
|
|
recorder := capevents.NewEventRecorder(nil, logr.Discard(), nil, nil)
|
|
|
|
response := (&ownerReferenceHandler{}).OnUpdate(
|
|
c,
|
|
c,
|
|
users.AdmissionUser{Type: users.AdmissionUserCapsule, Username: "alice"},
|
|
testTenantNamespace("workloads", green),
|
|
&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "workloads"}},
|
|
nil,
|
|
recorder,
|
|
)(context.Background(), admission.Request{})
|
|
|
|
if response == nil || response.Allowed {
|
|
t.Fatalf("expected unmanaged namespace join to be denied, got %#v", response)
|
|
}
|
|
}
|
|
|
|
func testTenant(name string, uid types.UID) *capsulev1beta2.Tenant {
|
|
return &capsulev1beta2.Tenant{ObjectMeta: metav1.ObjectMeta{Name: name, UID: uid}}
|
|
}
|
|
|
|
func testTenantNamespace(name string, tnt *capsulev1beta2.Tenant) *corev1.Namespace {
|
|
return &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Labels: map[string]string{meta.TenantLabel: tnt.GetName()},
|
|
OwnerReferences: []metav1.OwnerReference{{
|
|
APIVersion: capsulev1beta2.GroupVersion.String(),
|
|
Kind: tenant.ObjectReferenceTenantKind,
|
|
Name: tnt.GetName(),
|
|
UID: tnt.GetUID(),
|
|
}},
|
|
}}
|
|
}
|
|
|
|
func assertTenantAssignment(t *testing.T, ns *corev1.Namespace, tnt *capsulev1beta2.Tenant) {
|
|
t.Helper()
|
|
|
|
if got := tenant.TenanLabelValue(ns); got != tnt.GetName() {
|
|
t.Fatalf("tenant label = %q, want %q", got, tnt.GetName())
|
|
}
|
|
|
|
refs := tenant.TenantOwnerReferences(ns)
|
|
if len(refs) != 1 || !tenant.IsTenantOwnerReferenceForTenant(refs[0], tnt) {
|
|
t.Fatalf("tenant ownerReferences = %#v, want only tenant %q", refs, tnt.GetName())
|
|
}
|
|
}
|
|
|
|
func testClient(t *testing.T, tenants ...*capsulev1beta2.Tenant) client.Client {
|
|
t.Helper()
|
|
|
|
objects := make([]client.Object, 0, len(tenants))
|
|
for _, tnt := range tenants {
|
|
objects = append(objects, tnt)
|
|
}
|
|
|
|
return fake.NewClientBuilder().
|
|
WithScheme(testScheme(t)).
|
|
WithObjects(objects...).
|
|
Build()
|
|
}
|
|
|
|
func testScheme(t *testing.T) *runtime.Scheme {
|
|
t.Helper()
|
|
|
|
scheme := runtime.NewScheme()
|
|
if err := corev1.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add core API to scheme: %v", err)
|
|
}
|
|
if err := capsulev1beta2.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add Capsule API to scheme: %v", err)
|
|
}
|
|
|
|
return scheme
|
|
}
|