Standardize on exemption format

This commit is contained in:
Joe Betz 2026-04-08 15:39:17 -04:00
parent f8a4ffef40
commit cdcbdc3c43
No known key found for this signature in database
GPG key ID: 83FEBBE24213FEF6
3 changed files with 63 additions and 45 deletions

View file

@ -22,6 +22,7 @@ import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
)
@ -40,26 +41,26 @@ func TestFieldsWipingConsistency(t *testing.T) {
// All new APIs should use ResetObjectMetaForStatus.
statusDoesNotWipeMetadata := sets.New(
// https://github.com/kubernetes/kubernetes/issues/137681
"apiextensions.k8s.io/customresourcedefinitions",
"customresourcedefinitions.apiextensions.k8s.io",
// APIs that do not use ResetObjectMetaForStatus:
"apps/daemonsets",
"apps/replicasets",
"apps/statefulsets",
"batch/cronjobs",
"batch/jobs",
"autoscaling/horizontalpodautoscalers",
"networking.k8s.io/ingresses",
"certificatesigningrequests.certificates.k8s.io",
"cronjobs.batch",
"daemonsets.apps",
"horizontalpodautoscalers.autoscaling",
"ingresses.networking.k8s.io",
"jobs.batch",
"namespaces",
"nodes",
"persistentvolumes",
"persistentvolumeclaims",
"persistentvolumes",
"poddisruptionbudgets.policy",
"pods",
"replicasets.apps",
"replicationcontrollers",
"resourcequotas",
"services",
"policy/poddisruptionbudgets",
"namespaces",
"certificates.k8s.io/certificatesigningrequests",
"statefulsets.apps",
)
TestAllDefinitions(t, "reset-fields-test", func(t *testing.T, api Definition) {
@ -171,16 +172,11 @@ func TestFieldsWipingConsistency(t *testing.T) {
t.Errorf("Status endpoint: SSA ResetMetadata (%v) and PrepareForUpdate wipeMetadata (%v) behaviors do not match.", ssaStatusResetsMetadata, statusWipesMetadata)
}
gr := api.Mapping.Resource.Resource
if api.Mapping.Resource.Group != "" {
gr = api.Mapping.Resource.Group + "/" + gr
}
// Enforce field wiping behaviors, with allowlists for pre-existing exceptions.
assertWiped(t, gr, "create must wipe status", createWipesStatus, createDoesNotWipeStatus)
assertWiped(t, gr, "update must wipe status", mainWipesStatus, sets.New[string]()) // No exemptions exist for this, please do not add any in the future.
assertWiped(t, gr, "status update must wipe spec", statusWipesSpec, sets.New[string]()) // No exemptions exist for this, please do not add any in the future.
assertWiped(t, gr, "status update must wipe metadata", statusWipesMetadata, statusDoesNotWipeMetadata)
assertWiped(t, api.Mapping.Resource, "create must wipe status", createWipesStatus, createDoesNotWipeStatus)
assertWiped(t, api.Mapping.Resource, "update must wipe status", mainWipesStatus, sets.New[string]()) // No exemptions exist for this, please do not add any in the future.
assertWiped(t, api.Mapping.Resource, "status update must wipe spec", statusWipesSpec, sets.New[string]()) // No exemptions exist for this, please do not add any in the future.
assertWiped(t, api.Mapping.Resource, "status update must wipe metadata", statusWipesMetadata, statusDoesNotWipeMetadata)
if err := rsc.Delete(context.TODO(), name, *metav1.NewDeleteOptions(0)); err != nil {
t.Logf("Failed to delete %v: %v", name, err)
@ -223,15 +219,16 @@ func managedFieldsOwnTopLevelField(t *testing.T, fieldsV1 *metav1.FieldsV1, fiel
}
// assertWiped checks that a field wiping behavior holds, with an allowlist for known exceptions.
func assertWiped(t *testing.T, gr string, msg string, wiped bool, allowed sets.Set[string]) {
func assertWiped(t *testing.T, gvr schema.GroupVersionResource, msg string, wiped bool, allowed sets.Set[string]) {
t.Helper()
if allowed.Has(gr) {
name := ResourceString(gvr)
if matchesException(gvr, allowed) {
if wiped {
t.Errorf("%s: %s unexpectedly wiped. Remove it from the exception list.", gr, msg)
t.Errorf("%s: %s unexpectedly wiped. Remove it from the exception list.", name, msg)
}
} else {
if !wiped {
t.Errorf("%s: %s", gr, msg)
t.Errorf("%s: %s", name, msg)
}
}
}

View file

@ -21,7 +21,6 @@ import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
)
@ -36,19 +35,19 @@ func TestGenerationManagement(t *testing.T) {
// DO NOT ADD NEW ENTRIES HERE.
// This tracks resources that have status but do not manage generation.
generationExempt := sets.New[schema.GroupResource](
schema.GroupResource{Group: "apiregistration.k8s.io", Resource: "apiservices"},
schema.GroupResource{Group: "autoscaling", Resource: "horizontalpodautoscalers"},
schema.GroupResource{Group: "certificates.k8s.io", Resource: "certificatesigningrequests"},
schema.GroupResource{Group: "networking.k8s.io", Resource: "servicecidrs"},
schema.GroupResource{Group: "resource.k8s.io", Resource: "resourceclaims"},
schema.GroupResource{Group: "storage.k8s.io", Resource: "volumeattachments"},
schema.GroupResource{Group: "", Resource: "persistentvolumeclaims"},
schema.GroupResource{Group: "", Resource: "namespaces"},
schema.GroupResource{Group: "", Resource: "nodes"},
schema.GroupResource{Group: "", Resource: "persistentvolumes"},
schema.GroupResource{Group: "", Resource: "resourcequotas"},
schema.GroupResource{Group: "", Resource: "services"},
generationExempt := sets.New(
"apiservices.apiregistration.k8s.io",
"certificatesigningrequests.certificates.k8s.io",
"horizontalpodautoscalers.autoscaling",
"namespaces",
"nodes",
"persistentvolumeclaims",
"persistentvolumes",
"resourceclaims.resource.k8s.io",
"resourcequotas",
"servicecidrs.networking.k8s.io",
"services",
"volumeattachments.storage.k8s.io",
)
TestAllDefinitions(t, "generation-namespace", func(t *testing.T, api Definition) {
@ -80,7 +79,7 @@ func TestGenerationManagement(t *testing.T) {
}
// Verify that generation initializes to 1.
if generationExempt.Has(api.Mapping.Resource.GroupResource()) {
if matchesException(api.Mapping.Resource, generationExempt) {
if baseline.GetGeneration() != 0 {
t.Errorf("Expected generation exempt resource always have generation 0, but got %v", baseline.GetGeneration())
}
@ -112,7 +111,7 @@ func TestGenerationManagement(t *testing.T) {
if err != nil {
t.Logf("Patch to main endpoint failed: %v", err)
} else if result.GetGeneration() <= afterStatus.GetGeneration() {
if generationExempt.Has(api.Mapping.Resource.GroupResource()) {
if matchesException(api.Mapping.Resource, generationExempt) {
if result.GetGeneration() != 0 {
t.Errorf("Expected generation exempt resource always have generation 0, but got %v", result.GetGeneration())
}
@ -120,9 +119,5 @@ func TestGenerationManagement(t *testing.T) {
t.Errorf("Expected generation to monotonically increase after spec update (was %v, got %v)", afterStatus.GetGeneration(), result.GetGeneration())
}
}
if err := rsc.Delete(context.TODO(), name, *metav1.NewDeleteOptions(0)); err != nil {
t.Logf("Failed to delete %v: %v", name, err)
}
})
}

View file

@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
@ -72,6 +73,9 @@ func (d *Definition) ResourceClient() dynamic.ResourceInterface {
return d.DynamicClient.Resource(d.Mapping.Resource).Namespace(namespace)
}
// TestAllDefinitions starts an apiserver and runs testFunc against every
// discoverable resource. It registers a fixed set of CRDs so that a sample
// set of custom resources discoverable and tested.
func TestAllDefinitions(t *testing.T, testNamespace string, testFunc DefinitionTestFunc) {
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), []string{"--disable-admission-plugins", "ServiceAccount,TaintNodesByCondition"}, framework.SharedEtcd())
if err != nil {
@ -169,6 +173,28 @@ func CreateMapping(groupVersion string, resource metav1.APIResource) (*meta.REST
}, nil
}
// ResourceString returns the kubectl-style "resource.version.group" representation
// of a GroupVersionResource (e.g. "deployments.v1.apps", "pods.v1").
func ResourceString(gvr schema.GroupVersionResource) string {
if gvr.Group == "" {
return gvr.Resource + "." + gvr.Version
}
return gvr.Resource + "." + gvr.Version + "." + gvr.Group
}
// matchesException returns true if gvr matches any entry in exceptions.
// Each entry must be a kubectl-style resource string in either
// "resource.group" form (e.g. "pods", "deployments.apps",
// "customresourcedefinitions.apiextensions.k8s.io"), which matches all
// versions of that resource, or "resource.version.group" form (e.g.
// "pods.v1", "deployments.v1.apps"), which matches a single version.
func matchesException(gvr schema.GroupVersionResource, exceptions sets.Set[string]) bool {
if exceptions.Has(gvr.GroupResource().String()) {
return true
}
return exceptions.Has(ResourceString(gvr))
}
// TestObj is a generic test helper that creates an Unstructured object from a creation stub
// and explicitly sets the status from a separate JSON payload.
func TestObj(t *testing.T, stub, status string, gvk schema.GroupVersionKind) *unstructured.Unstructured {