From 6f1295ae9d28618873df4d9da1ac0bc48d08a5f8 Mon Sep 17 00:00:00 2001 From: Sam Dowell Date: Mon, 23 Jun 2025 10:12:50 -0700 Subject: [PATCH] fix: prevent SSA from creating CR while CRD terminating This change adds consistent validation for server side apply to prevent new CustomResources from being creating while its CustomResourceDefinition is in the terminating state. --- .../pkg/apiserver/customresource_handler.go | 50 ++++++++++++++++--- .../test/integration/finalization_test.go | 33 ++++++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index 9afeb2f80ce..0fbfcd53e51 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -17,6 +17,8 @@ limitations under the License. package apiserver import ( + "context" + "errors" "fmt" "net/http" "sort" @@ -384,17 +386,20 @@ func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, req if justCreated { time.Sleep(2 * time.Second) } + + a := r.admission if terminating { - err := apierrors.NewMethodNotSupported(schema.GroupResource{Group: requestInfo.APIGroup, Resource: requestInfo.Resource}, requestInfo.Verb) - err.ErrStatus.Message = fmt.Sprintf("%v not allowed while custom resource definition is terminating", requestInfo.Verb) - responsewriters.ErrorNegotiated(err, Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req) - return nil + a = &forbidCreateAdmission{delegate: a} } - return handlers.CreateResource(storage, requestScope, r.admission) + return handlers.CreateResource(storage, requestScope, a) case "update": return handlers.UpdateResource(storage, requestScope, r.admission) case "patch": - return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes) + a := r.admission + if terminating { + a = &forbidCreateAdmission{delegate: a} + } + return handlers.PatchResource(storage, requestScope, a, supportedTypes) case "delete": allowsOptions := true return handlers.DeleteResource(storage, allowsOptions, requestScope, r.admission) @@ -1452,3 +1457,36 @@ func buildOpenAPIModelsForApply(staticOpenAPISpec map[string]*spec.Schema, crd * } return mergedOpenAPI.Components.Schemas, nil } + +// forbidCreateAdmission is an admission.Interface wrapper that prevents a +// CustomResource from being created while its CRD is terminating. +type forbidCreateAdmission struct { + delegate admission.Interface +} + +func (f *forbidCreateAdmission) Handles(operation admission.Operation) bool { + if operation == admission.Create { + return true + } + return f.delegate.Handles(operation) +} + +func (f *forbidCreateAdmission) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + if a.GetOperation() == admission.Create { + return apierrors.NewForbidden(a.GetResource().GroupResource(), a.GetName(), errors.New("create not allowed while custom resource definition is terminating")) + } + if delegate, ok := f.delegate.(admission.MutationInterface); ok { + return delegate.Admit(ctx, a, o) + } + return nil +} + +func (f *forbidCreateAdmission) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + if a.GetOperation() == admission.Create { + return apierrors.NewForbidden(a.GetResource().GroupResource(), a.GetName(), errors.New("create not allowed while custom resource definition is terminating")) + } + if delegate, ok := f.delegate.(admission.ValidationInterface); ok { + return delegate.Validate(ctx, a, o) + } + return nil +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/finalization_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/finalization_test.go index 045d7bb06b6..11435f45c22 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/finalization_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/finalization_test.go @@ -163,3 +163,36 @@ func TestFinalizationAndDeletion(t *testing.T) { t.Fatalf("unable to delete crd: %v", err) } } + +func TestApplyCRDuringCRDFinalization(t *testing.T) { + tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) + require.NoError(t, err) + defer tearDown() + + // Create a CRD with a finalizer which will stall deletion + noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.ClusterScoped) + noxuDefinition.SetFinalizers([]string{"noxu.example.com/finalizer"}) + noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + require.NoError(t, err) + + // Delete the CRD. Since it has a finalizer it will be stuck in terminating state + err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Delete(t.Context(), noxuDefinition.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + + // Try to create a CR using SSA. This should fail due to the CRD validation + ns := "not-the-default" + name := "foo123" + noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) + + err = wait.PollUntilContextTimeout(t.Context(), 100*time.Millisecond, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) { + instance := fixtures.NewNoxuInstance(ns, name) + _, err := noxuResourceClient.Apply(ctx, name, instance, metav1.ApplyOptions{DryRun: []string{"All"}, FieldManager: "manager"}) + if err == nil { + t.Log("apply was not blocked, retrying...") + return false, nil + } + return true, err + }) + wantErr := `create not allowed while custom resource definition is terminating` + require.ErrorContains(t, err, wantErr) +}