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) +}