mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-20 04:56:44 +00:00
* fix: do not mutate on update and bound pvcs Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: do not mutate on update and bound pvcs Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
328 lines
8.1 KiB
Go
328 lines
8.1 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package pvc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
admissionv1 "k8s.io/api/admission/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"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/runtime/handlers"
|
|
)
|
|
|
|
func TestMutatingHandlerSkipsDynamicClaimsBeforeTenantLookup(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
scheme := runtime.NewScheme()
|
|
if err := corev1.AddToScheme(scheme); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
base := fake.NewClientBuilder().WithScheme(scheme).Build()
|
|
reader := &pvcCountingReader{Reader: base}
|
|
handler := MutatingHandler()
|
|
request := pvcAdmissionRequest(t, &corev1.PersistentVolumeClaim{})
|
|
|
|
if response := handler.OnCreate(nil, reader, admission.NewDecoder(scheme), nil)(
|
|
context.Background(),
|
|
request,
|
|
); response != nil {
|
|
t.Fatalf("response = %#v, want nil", response)
|
|
}
|
|
|
|
if reader.gets != 0 {
|
|
t.Fatalf("tenant lookup gets = %d, want 0 for a dynamic claim", reader.gets)
|
|
}
|
|
}
|
|
|
|
func TestValidatingHandlerSkipsTerminatingClaimWithUnchangedSpec(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
scheme := runtime.NewScheme()
|
|
if err := corev1.AddToScheme(scheme); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
now := metav1.Now()
|
|
oldPVC := &corev1.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
DeletionTimestamp: &now,
|
|
Finalizers: []string{"kubernetes.io/pvc-protection"},
|
|
},
|
|
Spec: corev1.PersistentVolumeClaimSpec{VolumeName: "caladan"},
|
|
}
|
|
newPVC := oldPVC.DeepCopy()
|
|
newPVC.Finalizers = nil
|
|
|
|
base := fake.NewClientBuilder().WithScheme(scheme).Build()
|
|
reader := &pvcCountingReader{Reader: base}
|
|
handler := Handler(PersistentVolumeValidatingVolume())
|
|
request := pvcUpdateAdmissionRequest(t, oldPVC, newPVC)
|
|
|
|
if response := handler.OnUpdate(nil, reader, admission.NewDecoder(scheme), nil)(
|
|
context.Background(),
|
|
request,
|
|
); response != nil {
|
|
t.Fatalf("response = %#v, want nil", response)
|
|
}
|
|
|
|
if reader.gets != 0 {
|
|
t.Fatalf("admission gets = %d, want 0 for terminating claim cleanup", reader.gets)
|
|
}
|
|
}
|
|
|
|
func TestHandlersSkipBoundClaimsBeforeTenantLookup(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
scheme := runtime.NewScheme()
|
|
if err := corev1.AddToScheme(scheme); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldPVC := &corev1.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "nginx-logs"},
|
|
Spec: corev1.PersistentVolumeClaimSpec{
|
|
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{
|
|
"velero.io/dynamic-pv-restore": "test.nginx-logs.sg75p",
|
|
}},
|
|
VolumeName: "pvc-f9bc7e8d",
|
|
},
|
|
Status: corev1.PersistentVolumeClaimStatus{Phase: corev1.ClaimBound},
|
|
}
|
|
newPVC := oldPVC.DeepCopy()
|
|
newPVC.Labels = map[string]string{"test": "test"}
|
|
|
|
tests := []struct {
|
|
name string
|
|
handler handlers.Handler
|
|
}{
|
|
{
|
|
name: "mutating",
|
|
handler: MutatingHandler(PersistentVolumeMutatingVolume()),
|
|
},
|
|
{
|
|
name: "validating",
|
|
handler: Handler(PersistentVolumeValidatingVolume()),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
base := fake.NewClientBuilder().WithScheme(scheme).Build()
|
|
reader := &pvcCountingReader{Reader: base}
|
|
request := pvcUpdateAdmissionRequest(t, oldPVC, newPVC)
|
|
|
|
if response := tt.handler.OnUpdate(nil, reader, admission.NewDecoder(scheme), nil)(
|
|
context.Background(),
|
|
request,
|
|
); response != nil {
|
|
t.Fatalf("response = %#v, want nil", response)
|
|
}
|
|
|
|
if reader.gets != 0 {
|
|
t.Fatalf("tenant lookup gets = %d, want 0 for a bound claim", reader.gets)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVolumeHooksSkipBoundClaimUpdates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
scheme := runtime.NewScheme()
|
|
if err := corev1.AddToScheme(scheme); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldPVC := &corev1.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "nginx-logs"},
|
|
Spec: corev1.PersistentVolumeClaimSpec{
|
|
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{
|
|
"velero.io/dynamic-pv-restore": "test.nginx-logs.sg75p",
|
|
}},
|
|
VolumeName: "pvc-f9bc7e8d",
|
|
},
|
|
Status: corev1.PersistentVolumeClaimStatus{Phase: corev1.ClaimBound},
|
|
}
|
|
newPVC := oldPVC.DeepCopy()
|
|
newPVC.Labels = map[string]string{"test": "test"}
|
|
tnt := &capsulev1beta2.Tenant{ObjectMeta: metav1.ObjectMeta{Name: "test"}}
|
|
request := pvcUpdateAdmissionRequest(t, oldPVC, newPVC)
|
|
decoder := admission.NewDecoder(scheme)
|
|
|
|
if response := PersistentVolumeMutatingVolume().OnUpdate(
|
|
nil,
|
|
nil,
|
|
oldPVC,
|
|
newPVC,
|
|
decoder,
|
|
nil,
|
|
tnt,
|
|
)(context.Background(), request); response != nil {
|
|
t.Fatalf("mutating response = %#v, want nil", response)
|
|
}
|
|
|
|
if len(newPVC.Spec.Selector.MatchExpressions) != 0 {
|
|
t.Fatalf("selector expressions = %#v, want unchanged", newPVC.Spec.Selector.MatchExpressions)
|
|
}
|
|
|
|
if response := PersistentVolumeValidatingVolume().OnUpdate(
|
|
nil,
|
|
nil,
|
|
oldPVC,
|
|
newPVC,
|
|
decoder,
|
|
nil,
|
|
tnt,
|
|
)(context.Background(), request); response != nil {
|
|
t.Fatalf("validating response = %#v, want nil", response)
|
|
}
|
|
}
|
|
|
|
func TestRequiresPVCSpecValidation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := metav1.Now()
|
|
terminating := &corev1.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now},
|
|
Spec: corev1.PersistentVolumeClaimSpec{VolumeName: "caladan"},
|
|
}
|
|
changed := terminating.DeepCopy()
|
|
changed.Spec.VolumeName = "salusa"
|
|
active := terminating.DeepCopy()
|
|
active.DeletionTimestamp = nil
|
|
bound := active.DeepCopy()
|
|
bound.Status.Phase = corev1.ClaimBound
|
|
resizedBound := bound.DeepCopy()
|
|
resizedBound.Spec.Resources.Requests = corev1.ResourceList{
|
|
corev1.ResourceStorage: resource.MustParse("2Gi"),
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
operation admissionv1.Operation
|
|
oldPVC *corev1.PersistentVolumeClaim
|
|
pvc *corev1.PersistentVolumeClaim
|
|
want bool
|
|
}{
|
|
{
|
|
name: "create",
|
|
operation: admissionv1.Create,
|
|
pvc: active,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "active update",
|
|
operation: admissionv1.Update,
|
|
oldPVC: active.DeepCopy(),
|
|
pvc: active,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "bound metadata update",
|
|
operation: admissionv1.Update,
|
|
oldPVC: bound.DeepCopy(),
|
|
pvc: bound,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "bound resize",
|
|
operation: admissionv1.Update,
|
|
oldPVC: bound.DeepCopy(),
|
|
pvc: resizedBound,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "terminating finalizer update",
|
|
operation: admissionv1.Update,
|
|
oldPVC: terminating.DeepCopy(),
|
|
pvc: terminating,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "terminating spec update",
|
|
operation: admissionv1.Update,
|
|
oldPVC: terminating.DeepCopy(),
|
|
pvc: changed,
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{
|
|
Operation: tt.operation,
|
|
}}
|
|
|
|
if got := requiresPVCSpecValidation(req, tt.pvc, tt.oldPVC); got != tt.want {
|
|
t.Fatalf("requiresPVCSpecValidation() = %t, want %t", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type pvcCountingReader struct {
|
|
client.Reader
|
|
|
|
gets int
|
|
}
|
|
|
|
func (r *pvcCountingReader) Get(
|
|
ctx context.Context,
|
|
key client.ObjectKey,
|
|
obj client.Object,
|
|
opts ...client.GetOption,
|
|
) error {
|
|
r.gets++
|
|
|
|
return r.Reader.Get(ctx, key, obj, opts...)
|
|
}
|
|
|
|
func pvcAdmissionRequest(t *testing.T, pvc *corev1.PersistentVolumeClaim) admission.Request {
|
|
t.Helper()
|
|
|
|
raw, err := json.Marshal(pvc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{
|
|
Operation: admissionv1.Create,
|
|
Namespace: "solar",
|
|
Object: runtime.RawExtension{
|
|
Raw: raw,
|
|
},
|
|
}}
|
|
}
|
|
|
|
func pvcUpdateAdmissionRequest(
|
|
t *testing.T,
|
|
oldPVC *corev1.PersistentVolumeClaim,
|
|
pvc *corev1.PersistentVolumeClaim,
|
|
) admission.Request {
|
|
t.Helper()
|
|
|
|
request := pvcAdmissionRequest(t, pvc)
|
|
request.Operation = admissionv1.Update
|
|
|
|
raw, err := json.Marshal(oldPVC)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
request.OldObject = runtime.RawExtension{Raw: raw}
|
|
|
|
return request
|
|
}
|