Introduce apidefinition test helper and organize tests around it

This commit is contained in:
Joe Betz 2026-04-09 16:37:46 -04:00
parent f5c7b42274
commit 643ec7035b
No known key found for this signature in database
GPG key ID: 83FEBBE24213FEF6
7 changed files with 698 additions and 872 deletions

View file

@ -0,0 +1,128 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apidefinitions
import (
"context"
"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"
)
// TestGenerationManagement tests that metadata.generation is managed when a resource is updated.
//
// The test ensures:
// 1. Generation initializes to 1.
// 2. Generation monotonically increases for each spec update.
// 3. Generation does not increase for status updates.
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"},
)
TestAllDefinitions(t, "generation-namespace", func(t *testing.T, api Definition) {
if !api.HasStatus() {
t.Skip()
}
if !api.HasVerb("patch") || !api.HasVerb("get") || !api.HasVerb("update") {
t.Skip("Resource does not support patch, get, and update")
}
differentSpec := api.StorageData.MutatedStub
if differentSpec == "" {
t.Skipf("No conflicting spec data defined for %v to test generation bump", api.Mapping.Resource)
}
status := api.StorageData.GetStatusStub()
obj := TestObj(t, api.StorageData.Stub, status, api.Mapping.GroupVersionKind)
name := obj.GetName()
rsc := api.ResourceClient()
_, err := rsc.Create(context.TODO(), obj, metav1.CreateOptions{FieldManager: "spec-manager"})
if err != nil {
t.Fatalf("Failed to create via SSA: %v", err)
}
baseline, err := rsc.Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
t.Fatalf("Failed to get baseline: %v", err)
}
// Verify that generation initializes to 1.
if generationExempt.Has(api.Mapping.Resource.GroupResource()) {
if baseline.GetGeneration() != 0 {
t.Errorf("Expected generation exempt resource always have generation 0, but got %v", baseline.GetGeneration())
}
} else if baseline.GetGeneration() != int64(1) {
t.Errorf("Expected generation to be %v on create, got %v", 1, baseline.GetGeneration())
}
// Verify that updating status does NOT bump generation.
update := api.StorageData.GetMutatedStatusStub()
statusObj := TestObj(t, api.StorageData.Stub, update, api.Mapping.GroupVersionKind)
statusObj.SetName(name)
_, err = rsc.ApplyStatus(context.TODO(), name, statusObj, metav1.ApplyOptions{FieldManager: "status-manager", Force: true})
if err != nil {
t.Fatalf("Failed to apply status via SSA: %v", err)
}
afterStatus, err := rsc.Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
t.Fatalf("Failed to get after status update: %v", err)
}
if afterStatus.GetGeneration() != baseline.GetGeneration() {
t.Errorf("Expected generation to remain %v after status update, but got %v", baseline.GetGeneration(), afterStatus.GetGeneration())
}
// Verify that updating spec does bump generation.
result, err := rsc.Patch(context.TODO(), name, types.MergePatchType, []byte(differentSpec), metav1.PatchOptions{})
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 result.GetGeneration() != 0 {
t.Errorf("Expected generation exempt resource always have generation 0, but got %v", result.GetGeneration())
}
} else {
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

@ -0,0 +1,190 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apidefinitions
import (
"context"
"encoding/json"
"slices"
"strings"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
v1 "k8s.io/api/core/v1"
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration/etcd"
"k8s.io/kubernetes/test/integration/framework"
)
type DefinitionTestFunc func(t *testing.T, setup Definition)
type Definition struct {
Config *rest.Config
Client kubernetes.Interface
DynamicClient dynamic.Interface
Mapping *meta.RESTMapping
Resource metav1.APIResource
Namespace string
StorageData etcd.StorageData
Subresources []string
}
func (d *Definition) HasVerb(verb string) bool {
return slices.Contains(d.Resource.Verbs, verb)
}
func (d *Definition) HasSubresource(subresource string) bool {
return slices.Contains(d.Subresources, subresource)
}
func (d *Definition) HasStatus() bool {
return d.HasSubresource("status")
}
// ResourceClient returns a dynamic resource client scoped to the appropriate namespace.
func (d *Definition) ResourceClient() dynamic.ResourceInterface {
namespace := d.Namespace
if d.Mapping.Scope == meta.RESTScopeRoot {
namespace = ""
}
return d.DynamicClient.Resource(d.Mapping.Resource).Namespace(namespace)
}
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 {
t.Fatal(err)
}
defer server.TearDownFn()
client, err := kubernetes.NewForConfig(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
storageData := etcd.GetEtcdStorageDataForNamespace(testNamespace)
_, resourceLists, err := client.Discovery().ServerGroupsAndResources()
if err != nil {
t.Fatalf("Failed to get ServerGroupsAndResources: %v", err)
}
for _, resourceList := range resourceLists {
for _, resource := range resourceList.APIResources {
// Iterate over root resources
if strings.Contains(resource.Name, "/") {
continue
}
mapping, err := CreateMapping(resourceList.GroupVersion, resource)
if err != nil {
t.Fatal(err)
}
t.Run(mapping.Resource.String(), func(t *testing.T) {
storageData, ok := storageData[mapping.Resource]
if !ok {
t.Skipf("no test data for %s in etcd.GetEtcdStorageData, skipping", mapping.Resource)
}
var subresources []string
for _, r := range resourceList.APIResources {
if suffix, ok := strings.CutPrefix(r.Name, resource.Name+"/"); ok {
subresources = append(subresources, suffix)
}
}
setup := Definition{
Config: server.ClientConfig,
Client: client,
DynamicClient: dynamicClient,
Mapping: mapping,
Resource: resource,
Namespace: testNamespace,
StorageData: storageData,
Subresources: subresources,
}
testFunc(t, setup)
})
}
}
}
func CreateMapping(groupVersion string, resource metav1.APIResource) (*meta.RESTMapping, error) {
gv, err := schema.ParseGroupVersion(groupVersion)
if err != nil {
return nil, err
}
if len(resource.Group) > 0 || len(resource.Version) > 0 {
gv = schema.GroupVersion{
Group: resource.Group,
Version: resource.Version,
}
}
gvk := gv.WithKind(resource.Kind)
gvr := gv.WithResource(resource.Name)
scope := meta.RESTScopeRoot
if resource.Namespaced {
scope = meta.RESTScopeNamespace
}
return &meta.RESTMapping{
Resource: gvr,
GroupVersionKind: gvk,
Scope: scope,
}, nil
}
// 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 {
t.Helper()
obj := &unstructured.Unstructured{}
if err := json.Unmarshal([]byte(stub), &obj.Object); err != nil {
t.Fatal(err)
}
var statusObj map[string]interface{}
if err := json.Unmarshal([]byte(status), &statusObj); err != nil {
t.Fatal(err)
}
if s, ok := statusObj["status"]; ok {
obj.Object["status"] = s
}
obj.SetAPIVersion(gvk.GroupVersion().String())
obj.SetKind(gvk.Kind)
return obj
}

View file

@ -0,0 +1,27 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apidefinitions
import (
"testing"
"k8s.io/kubernetes/test/integration/framework"
)
func TestMain(m *testing.M) {
framework.EtcdMain(m.Run)
}

View file

@ -22,10 +22,13 @@ import (
"reflect"
"testing"
v1 "k8s.io/api/core/v1"
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/client-go/kubernetes"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
@ -44,7 +47,7 @@ func TestApplyCRDNoSchema(t *testing.T) {
defer server.TearDownFn()
config := server.ClientConfig
apiExtensionClient, err := clientset.NewForConfig(config)
apiExtensionClient, err := apiextensionsclientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
@ -65,13 +68,13 @@ func TestApplyCRDNoSchema(t *testing.T) {
name := "mytest"
rest := apiExtensionClient.Discovery().RESTClient()
yamlBody := []byte(fmt.Sprintf(`
yamlBody := fmt.Appendf(nil, `
apiVersion: %s
kind: %s
metadata:
name: %s
spec:
replicas: 1`, apiVersion, kind, name))
replicas: 1`, apiVersion, kind, name)
result, err := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, noxuDefinition.Spec.Names.Plural).
Name(name).
@ -306,3 +309,111 @@ func nearlyRemovedBetaMultipleVersionNoxuCRDWithStatus(scope apiextensionsv1beta
},
}
}
func TestUpdateStatusWithOldVersion(t *testing.T) {
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), []string{"--disable-admission-plugins", "ServiceAccount,TaintNodesByCondition"}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
client, err := kubernetes.NewForConfig(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
apiExtensionClient, err := apiextensionsclientset.NewForConfig(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
noxuBetaDefinition := nearlyRemovedBetaMultipleVersionNoxuCRDWithStatus(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err := fixtures.CreateCRDUsingRemovedAPI(server.EtcdClient, server.EtcdStoragePrefix, noxuBetaDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
kind := noxuDefinition.Spec.Names.Kind
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[1].Name
name := "mytest"
rest := apiExtensionClient.Discovery().RESTClient()
// create namespace ns test
if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "reset-fields-namespace"}}, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
// Create the resource using the v1 CRD API.
yamlBody := fmt.Appendf(nil, `
apiVersion: %s
kind: %s
metadata:
name: %s
namespace: %s
spec:
a: value-for-a
b: value-for-b`, apiVersion, kind, name, "reset-fields-namespace")
result, err := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[1].Name, "/namespaces", "reset-fields-namespace", noxuDefinition.Spec.Names.Plural).
Name(name).
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw(context.TODO())
if err != nil {
t.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(result))
}
t.Logf("result: %s", string(result))
oldManagedFields, err := getManagedFields(result)
if err != nil {
t.Fatalf("failed to get managed fields: %v", err)
}
// When updating the status subresource via the v1beta1 CRD API,
// we assign a value to the spec field for testing purposes.
// However, in this case, the operation should NOT trigger any field manager updates
// related to server-side apply tracking.
updateStatusBytes := []byte(`{
"spec": { "a": "value-for-a-update" },
"status": {
"a": "status-for-a"
}
}`)
result, err = rest.Patch(types.MergePatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, "/namespaces", "reset-fields-namespace", noxuDefinition.Spec.Names.Plural).
Name(name).
SubResource("status").
Param("fieldManager", "subresource_test").
Body(updateStatusBytes).
DoRaw(context.TODO())
if err != nil {
t.Fatalf("Error updating subresource: %v ", err)
}
t.Logf("result: %s", string(result))
newManagedFields, err := getManagedFields(result)
if err != nil {
t.Fatalf("failed to get managed fields: %v", err)
}
// newManagedFields should include oldManagedFields
var applyManagerFound, subresourceManagerFound bool
for i, field := range newManagedFields {
if field.Manager == "apply_test" {
if !reflect.DeepEqual(newManagedFields[i], oldManagedFields[0]) {
t.Fatalf("Expected managed fields to not have changed when trying manually setting them via subresoures.\n\nExpected: %#v\n\nGot: %#v", oldManagedFields[0], newManagedFields[i])
}
applyManagerFound = true
}
if field.Manager == "subresource_test" {
subresourceManagerFound = true
}
}
if !applyManagerFound {
t.Errorf("expected field manager 'apply_test' to be present in newManagedFields")
}
if !subresourceManagerFound {
t.Errorf("expected field manager 'subresource_test' to be present in newManagedFields")
}
}

View file

@ -24,63 +24,12 @@ import (
"strings"
"testing"
v1 "k8s.io/api/core/v1"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration/etcd"
"k8s.io/kubernetes/test/integration/framework"
"k8s.io/kubernetes/test/utils/image"
"k8s.io/kubernetes/test/integration/apiserver/apidefinitions"
)
// namespace used for all tests, do not change this
const resetFieldsNamespace = "reset-fields-namespace"
// resetFieldsStatusData contains statuses for all the resources in the
// statusData list with slightly different data to create a field manager
// conflict.
var resetFieldsStatusData = map[schema.GroupVersionResource]string{
gvr("", "v1", "persistentvolumes"): `{"status": {"message": "hello2"}}`,
gvr("", "v1", "resourcequotas"): `{"status": {"used": {"cpu": "25M"}}}`,
gvr("", "v1", "services"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.2", "ipMode": "VIP"}]}}}`,
gvr("extensions", "v1beta1", "ingresses"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.2"}]}}}`,
gvr("networking.k8s.io", "v1beta1", "ingresses"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.2"}]}}}`,
gvr("networking.k8s.io", "v1", "ingresses"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.2"}]}}}`,
gvr("autoscaling", "v1", "horizontalpodautoscalers"): `{"status": {"currentReplicas": 25}}`,
gvr("autoscaling", "v2", "horizontalpodautoscalers"): `{"status": {"currentReplicas": 25}}`,
gvr("batch", "v1", "cronjobs"): `{"status": {"lastScheduleTime": "2020-01-01T00:00:00Z"}}`,
gvr("batch", "v1beta1", "cronjobs"): `{"status": {"lastScheduleTime": "2020-01-01T00:00:00Z"}}`,
gvr("storage.k8s.io", "v1", "volumeattachments"): `{"status": {"attached": false}}`,
gvr("policy", "v1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 25}}`,
gvr("policy", "v1beta1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 25}}`,
gvr("resource.k8s.io", "v1beta1", "resourceclaims"): `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-other-value"]}] }]}}}}`,
gvr("resource.k8s.io", "v1beta2", "resourceclaims"): `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-other-value"]}] }]}}}}`,
gvr("resource.k8s.io", "v1", "resourceclaims"): `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-other-value"]}] }]}}}}`,
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"False","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`,
// standard for []metav1.Condition
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies"): `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies"): `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
gvr("networking.k8s.io", "v1alpha1", "servicecidrs"): `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
gvr("networking.k8s.io", "v1beta1", "servicecidrs"): `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
gvr("networking.k8s.io", "v1", "servicecidrs"): `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
}
// resetFieldsStatusDefault conflicts with statusDefault
const resetFieldsStatusDefault = `{"status": {"conditions": [{"type": "MyStatus", "status":"False"}]}}`
var resetFieldsSkippedResources = map[string]struct{}{}
// noConflicts is the set of resources for which
// a conflict cannot occur.
var noConflicts = map[string]struct{}{
@ -98,656 +47,71 @@ var noConflicts = map[string]struct{}{
"namespaces": {},
}
var image2 = image.GetE2EImage(image.Etcd)
// resetFieldsSpecData contains conflicting data with the objects in
// etcd.GetEtcdStorageDataForNamespace()
// It contains the minimal changes needed to conflict with all the fields
// added to resetFields by the strategy of each resource.
// In most cases, just one field on the spec is changed, but
// some also wipe metadata or other fields.
var resetFieldsSpecData = map[schema.GroupVersionResource]string{
gvr("", "v1", "resourcequotas"): `{"spec": {"hard": {"cpu": "25M"}}}`,
gvr("", "v1", "namespaces"): `{"spec": {"finalizers": ["kubernetes2"]}}`,
gvr("", "v1", "nodes"): `{"spec": {"unschedulable": false}}`,
gvr("", "v1", "persistentvolumes"): `{"spec": {"capacity": {"storage": "23M"}}}`,
gvr("", "v1", "persistentvolumeclaims"): `{"spec": {"resources": {"limits": {"storage": "21M"}}}}`,
gvr("", "v1", "pods"): `{"metadata": {"deletionTimestamp": "2020-01-01T00:00:00Z", "ownerReferences":[]}, "spec": {"containers": [{"image": "` + image2 + `", "name": "container7"}]}}`,
gvr("", "v1", "replicationcontrollers"): `{"spec": {"selector": {"new": "stuff2"}}}`,
gvr("", "v1", "resourcequotas"): `{"spec": {"hard": {"cpu": "25M"}}}`,
gvr("", "v1", "services"): `{"spec": {"type": "ClusterIP"}}`,
gvr("apps", "v1", "daemonsets"): `{"spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container6"}]}}}}`,
gvr("apps", "v1", "deployments"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container6"}]}}}}`,
gvr("apps", "v1", "replicasets"): `{"spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container4"}]}}}}`,
gvr("apps", "v1", "statefulsets"): `{"spec": {"selector": {"matchLabels": {"a2": "b2"}}}}`,
gvr("autoscaling", "v1", "horizontalpodautoscalers"): `{"spec": {"maxReplicas": 23}}`,
gvr("autoscaling", "v2", "horizontalpodautoscalers"): `{"spec": {"maxReplicas": 23}}`,
gvr("autoscaling", "v2beta1", "horizontalpodautoscalers"): `{"spec": {"maxReplicas": 23}}`,
gvr("autoscaling", "v2beta2", "horizontalpodautoscalers"): `{"spec": {"maxReplicas": 23}}`,
gvr("batch", "v1", "jobs"): `{"spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container1"}]}}}}`,
gvr("batch", "v1", "cronjobs"): `{"spec": {"jobTemplate": {"spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container0"}]}}}}}}`,
gvr("batch", "v1beta1", "cronjobs"): `{"spec": {"jobTemplate": {"spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container0"}]}}}}}}`,
gvr("certificates.k8s.io", "v1", "certificatesigningrequests"): `{}`,
gvr("certificates.k8s.io", "v1beta1", "certificatesigningrequests"): `{}`,
gvr("flowcontrol.apiserver.k8s.io", "v1alpha1", "flowschemas"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"priorityLevelConfiguration": {"name": "name2"}}}`,
gvr("flowcontrol.apiserver.k8s.io", "v1beta1", "flowschemas"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"priorityLevelConfiguration": {"name": "name2"}}}`,
gvr("flowcontrol.apiserver.k8s.io", "v1beta2", "flowschemas"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"priorityLevelConfiguration": {"name": "name2"}}}`,
gvr("flowcontrol.apiserver.k8s.io", "v1beta3", "flowschemas"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"priorityLevelConfiguration": {"name": "name2"}}}`,
gvr("flowcontrol.apiserver.k8s.io", "v1", "flowschemas"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"priorityLevelConfiguration": {"name": "name2"}}}`,
gvr("flowcontrol.apiserver.k8s.io", "v1alpha1", "prioritylevelconfigurations"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"limited": {"assuredConcurrencyShares": 23}}}`,
gvr("flowcontrol.apiserver.k8s.io", "v1beta1", "prioritylevelconfigurations"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"limited": {"assuredConcurrencyShares": 23}}}`,
gvr("flowcontrol.apiserver.k8s.io", "v1beta2", "prioritylevelconfigurations"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"limited": {"assuredConcurrencyShares": 23}}}`,
gvr("flowcontrol.apiserver.k8s.io", "v1beta3", "prioritylevelconfigurations"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"limited": {"nominalConcurrencyShares": 23}}}`,
gvr("flowcontrol.apiserver.k8s.io", "v1", "prioritylevelconfigurations"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"limited": {"nominalConcurrencyShares": 23}}}`,
gvr("extensions", "v1beta1", "ingresses"): `{"spec": {"backend": {"serviceName": "service2"}}}`,
gvr("networking.k8s.io", "v1beta1", "ingresses"): `{"spec": {"backend": {"serviceName": "service2"}}}`,
gvr("networking.k8s.io", "v1", "ingresses"): `{"spec": {"defaultBackend": {"service": {"name": "service2"}}}}`,
gvr("networking.k8s.io", "v1alpha1", "servicecidrs"): `{}`,
gvr("networking.k8s.io", "v1beta1", "servicecidrs"): `{}`,
gvr("networking.k8s.io", "v1", "servicecidrs"): `{}`,
gvr("policy", "v1", "poddisruptionbudgets"): `{"spec": {"selector": {"matchLabels": {"anokkey2": "anokvalue"}}}}`,
gvr("policy", "v1beta1", "poddisruptionbudgets"): `{"spec": {"selector": {"matchLabels": {"anokkey2": "anokvalue"}}}}`,
gvr("storage.k8s.io", "v1alpha1", "volumeattachments"): `{"metadata": {"name": "va3"}, "spec": {"nodeName": "localhost2"}}`,
gvr("storage.k8s.io", "v1", "volumeattachments"): `{"metadata": {"name": "va3"}, "spec": {"nodeName": "localhost2"}}`,
gvr("apiextensions.k8s.io", "v1", "customresourcedefinitions"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"group": "webconsole22.operator.openshift.io"}}`,
gvr("apiextensions.k8s.io", "v1beta1", "customresourcedefinitions"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"group": "webconsole22.operator.openshift.io"}}`,
gvr("awesome.bears.com", "v1", "pandas"): `{"spec": {"replicas": 102}}`,
gvr("awesome.bears.com", "v3", "pandas"): `{"spec": {"replicas": 302}}`,
gvr("apiregistration.k8s.io", "v1beta1", "apiservices"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"group": "foo2.com"}}`,
gvr("apiregistration.k8s.io", "v1", "apiservices"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"group": "foo2.com"}}`,
gvr("resource.k8s.io", "v1alpha3", "devicetaintrules"): `{"metadata": {"labels":{"a":"c"}}}`,
gvr("resource.k8s.io", "v1beta1", "deviceclasses"): `{"metadata": {"labels":{"a":"c"}}}`,
gvr("resource.k8s.io", "v1beta1", "resourceclaims"): `{"spec": {"devices": {"requests": [{"name": "req-0", "deviceClassName": "other-class"}]}}}`, // spec is immutable, but that doesn't matter for the test.
gvr("resource.k8s.io", "v1beta1", "resourceclaimtemplates"): `{"spec": {"spec": {"resourceClassName": "class2name"}}}`,
gvr("resource.k8s.io", "v1beta2", "deviceclasses"): `{"metadata": {"labels":{"a":"c"}}}`,
gvr("resource.k8s.io", "v1beta2", "devicetaintrules"): `{"metadata": {"labels":{"a":"c"}}}`,
gvr("resource.k8s.io", "v1beta2", "resourceclaims"): `{"spec": {"devices": {"requests": [{"name": "req-0", "exactly": {"deviceClassName": "other-class"}}]}}}`, // spec is immutable, but that doesn't matter for the test.
gvr("resource.k8s.io", "v1beta2", "resourceclaimtemplates"): `{"spec": {"spec": {"resourceClassName": "class2name"}}}`,
gvr("resource.k8s.io", "v1", "deviceclasses"): `{"metadata": {"labels":{"a":"c"}}}`,
gvr("resource.k8s.io", "v1", "resourceclaims"): `{"spec": {"devices": {"requests": [{"name": "req-0", "exactly": {"deviceClassName": "other-class"}}]}}}`, // spec is immutable, but that doesn't matter for the test.
gvr("resource.k8s.io", "v1", "resourceclaimtemplates"): `{"spec": {"spec": {"resourceClassName": "class2name"}}}`,
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{}`,
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"paramKind": {"apiVersion": "apps/v1", "kind": "Deployment"}}}`,
gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"paramKind": {"apiVersion": "apps/v1", "kind": "Deployment"}}}`,
gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"paramKind": {"apiVersion": "apps/v1", "kind": "Deployment"}}}`,
}
// TestResetFields makes sure that fieldManager does not own fields reset by the storage strategy.
// It takes 2 objects obj1 and obj2 that differ by one field in the spec and one field in the status.
// It applies obj1 to the spec endpoint and obj2 to the status endpoint, the lack of conflicts
// confirms that the fieldmanager1 is wiped of the status and fieldmanager2 is wiped of the spec.
// We then attempt to apply obj2 to the spec endpoint which fails with an expected conflict.
func TestApplyResetFields(t *testing.T) {
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), []string{"--disable-admission-plugins", "ServiceAccount,TaintNodesByCondition"}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
client, err := kubernetes.NewForConfig(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
// create CRDs so we can make sure that custom resources do not get lost
etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: resetFieldsNamespace}}, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
createData := etcd.GetEtcdStorageDataForNamespace(resetFieldsNamespace)
// gather resources to test
_, resourceLists, err := client.Discovery().ServerGroupsAndResources()
if err != nil {
t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
}
for _, resourceList := range resourceLists {
for _, resource := range resourceList.APIResources {
if !strings.HasSuffix(resource.Name, "/status") {
continue
}
mapping, err := createMapping(resourceList.GroupVersion, resource)
if err != nil {
t.Fatal(err)
}
t.Run(mapping.Resource.String(), func(t *testing.T) {
if _, ok := resetFieldsSkippedResources[mapping.Resource.Resource]; ok {
t.Skip()
}
namespace := resetFieldsNamespace
if mapping.Scope == meta.RESTScopeRoot {
namespace = ""
}
// assemble first object
status, ok := statusData[mapping.Resource]
if !ok {
status = statusDefault
}
resource, ok := createData[mapping.Resource]
if !ok {
t.Fatalf("no test data for %s. Please add a test for your new type to etcd.GetEtcdStorageData() or getResetFieldsEtcdStorageData()", mapping.Resource)
}
obj1 := unstructured.Unstructured{}
if err := json.Unmarshal([]byte(resource.Stub), &obj1.Object); err != nil {
t.Fatal(err)
}
if err := json.Unmarshal([]byte(status), &obj1.Object); err != nil {
t.Fatal(err)
}
name := obj1.GetName()
obj1.SetAPIVersion(mapping.GroupVersionKind.GroupVersion().String())
obj1.SetKind(mapping.GroupVersionKind.Kind)
obj1.SetName(name)
// apply the spec of the first object
_, err = dynamicClient.
Resource(mapping.Resource).
Namespace(namespace).
Apply(context.TODO(), name, &obj1, metav1.ApplyOptions{FieldManager: "fieldmanager1"})
if err != nil {
t.Fatalf("Failed to apply obj1: %v", err)
}
// create second object
obj2 := &unstructured.Unstructured{}
obj1.DeepCopyInto(obj2)
if err := json.Unmarshal([]byte(resetFieldsSpecData[mapping.Resource]), &obj2.Object); err != nil {
t.Fatal(err)
}
status2, ok := resetFieldsStatusData[mapping.Resource]
if !ok {
status2 = resetFieldsStatusDefault
}
if err := json.Unmarshal([]byte(status2), &obj2.Object); err != nil {
t.Fatal(err)
}
if reflect.DeepEqual(obj1, obj2) {
t.Fatalf("obj1 and obj2 should not be equal %v", obj2)
}
// apply the status of the second object
// this won't conflict if resetfields are set correctly
// and will conflict if they are not
_, err = dynamicClient.
Resource(mapping.Resource).
Namespace(namespace).
ApplyStatus(context.TODO(), name, obj2, metav1.ApplyOptions{FieldManager: "fieldmanager2"})
if err != nil {
t.Fatalf("Failed to apply obj2: %v", err)
}
// skip checking for conflicts on resources
// that will never have conflicts
if _, ok = noConflicts[mapping.Resource.Resource]; !ok {
var objRet *unstructured.Unstructured
// reapply second object to the spec endpoint
// that should fail with a conflict
objRet, err = dynamicClient.
Resource(mapping.Resource).
Namespace(namespace).
Apply(context.TODO(), name, obj2, metav1.ApplyOptions{FieldManager: "fieldmanager2"})
err = expectConflict(objRet, err, dynamicClient, mapping.Resource, namespace, name)
if err != nil {
t.Fatalf("Did not get expected conflict in spec of %s %s/%s: %v", mapping.Resource, namespace, name, err)
}
// reapply first object to the status endpoint
// that should fail with a conflict
objRet, err = dynamicClient.
Resource(mapping.Resource).
Namespace(namespace).
ApplyStatus(context.TODO(), name, &obj1, metav1.ApplyOptions{FieldManager: "fieldmanager1"})
err = expectConflict(objRet, err, dynamicClient, mapping.Resource, namespace, name)
if err != nil {
t.Fatalf("Did not get expected conflict in status of %s %s/%s: %v", mapping.Resource, namespace, name, err)
}
}
// cleanup
rsc := dynamicClient.Resource(mapping.Resource).Namespace(namespace)
if err := rsc.Delete(context.TODO(), name, *metav1.NewDeleteOptions(0)); err != nil {
t.Fatalf("deleting final object failed: %v", err)
}
})
apidefinitions.TestAllDefinitions(t, "reset-fields-test", func(t *testing.T, api apidefinitions.Definition) {
if !api.HasStatus() {
t.Skip()
}
}
}
// TestFieldsWipingConsistency verifies that field wiping is applied consistently across the API
// and that field wiping is consistent GetResetFields.
func TestFieldsWipingConsistency(t *testing.T) {
// DO NOT ADD NEW ENTRIES HERE.
// This tracks pre-existing APIs where status is allowed to update metadata.
// All new APIs should use ResetObjectMetaForStatus.
statusDoesNotWipeMetadataAllowed := sets.New(
// https://github.com/kubernetes/kubernetes/issues/137681
"apiextensions.k8s.io/customresourcedefinitions",
// APIs that do not use ResetObjectMetaForStatus:
"apps/daemonsets",
"apps/replicasets",
"apps/statefulsets",
"batch/cronjobs",
"batch/jobs",
"autoscaling/horizontalpodautoscalers",
"networking.k8s.io/ingresses",
"nodes",
"persistentvolumes",
"persistentvolumeclaims",
"pods",
"replicationcontrollers",
"resourcequotas",
"services",
"policy/poddisruptionbudgets",
"namespaces",
"certificates.k8s.io/certificatesigningrequests",
)
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), []string{"--disable-admission-plugins", "ServiceAccount,TaintNodesByCondition"}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
client, err := kubernetes.NewForConfig(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
ns := "field-wiping-consistency-ns"
if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}}, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
storageData := etcd.GetEtcdStorageDataForNamespace(ns)
_, resourceLists, err := client.Discovery().ServerGroupsAndResources()
if err != nil {
t.Fatalf("Failed to get ServerGroupsAndResources: %v", err)
}
for _, resourceList := range resourceLists {
for _, resource := range resourceList.APIResources {
// Only test resources that have a /status subresource, since the
// test verifies consistency between / and /status strategies.
if !strings.HasSuffix(resource.Name, "/status") {
continue
}
mapping, err := createMapping(resourceList.GroupVersion, resource)
if err != nil {
t.Fatal(err)
}
t.Run(mapping.Resource.String(), func(t *testing.T) {
if _, ok := resetFieldsSkippedResources[mapping.Resource.Resource]; ok {
t.Skip()
}
resourceStub, ok := storageData[mapping.Resource]
if !ok {
t.Fatalf("no test data for %s for type in etcd.GetEtcdStorageData", mapping.Resource)
}
status, ok := statusData[mapping.Resource]
if !ok {
status = statusDefault
}
obj := testObj(t, resourceStub.Stub, status, mapping.GroupVersionKind)
name := obj.GetName()
namespace := ns
if mapping.Scope == meta.RESTScopeRoot {
namespace = ""
}
rsc := dynamicClient.Resource(mapping.Resource).Namespace(namespace)
// Step 1: Create the resource
_, err = rsc.Apply(context.TODO(), name, obj, metav1.ApplyOptions{FieldManager: "spec-manager"})
if err != nil {
t.Fatalf("Failed to create via SSA: %v", err)
}
// Step 2: Apply to /status endpoint with spec, status and metadata field changes.
statusObj := testObj(t, resourceStub.Stub, status, mapping.GroupVersionKind)
statusObj.SetName(name)
statusLabels := statusObj.GetLabels()
if statusLabels == nil {
statusLabels = map[string]string{}
}
statusLabels["test-status-ssa"] = "true"
statusObj.SetLabels(statusLabels)
_, err = rsc.ApplyStatus(context.TODO(), name, statusObj, metav1.ApplyOptions{FieldManager: "status-manager", Force: true})
if err != nil {
t.Fatalf("Failed to apply status via SSA: %v", err)
}
// Step 3: Read after writing to observe field wiping behavior and managedField state
baseline, err := rsc.Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
t.Fatalf("Failed to get baseline: %v", err)
}
baselineStatus := baseline.Object["status"]
baselineSpec := baseline.Object["spec"]
// Infer GetResetFields behavior from managedFields.
ssaMainResetsStatus := true
ssaStatusResetsSpec := true
ssaStatusResetsMetadata := true
for _, mf := range baseline.GetManagedFields() {
if mf.Manager == "spec-manager" && mf.Subresource == "" {
ssaMainResetsStatus = !managedFieldsOwnTopLevelField(t, mf.FieldsV1, "status")
}
if mf.Manager == "status-manager" && mf.Subresource == "status" {
ssaStatusResetsSpec = !managedFieldsOwnTopLevelField(t, mf.FieldsV1, "spec")
ssaStatusResetsMetadata = !managedFieldsOwnLabel(t, mf.FieldsV1, "test-status-ssa")
}
}
// Check / PrepareForUpdate status wiping
var mainWipesStatus bool
if baselineStatus != nil {
differentStatus, ok := resetFieldsStatusData[mapping.Resource]
if !ok {
differentStatus = resetFieldsStatusDefault
}
result, err := rsc.Patch(context.TODO(), name, types.MergePatchType, []byte(differentStatus), metav1.PatchOptions{})
if err != nil {
t.Fatalf("Failed to patch main endpoint with different status: %v", err)
}
mainWipesStatus = !checkPatch(t, differentStatus, "status", result.Object)
} else {
mainWipesStatus = true
}
// Check /status PrepareForUpdate spec wiping
var statusWipesSpec bool
differentSpec, hasSpecData := resetFieldsSpecData[mapping.Resource]
if baselineSpec != nil && hasSpecData {
result, err := rsc.Patch(context.TODO(), name, types.MergePatchType, []byte(differentSpec), metav1.PatchOptions{}, "status")
if err != nil {
statusWipesSpec = true
t.Logf("Patch to status endpoint with different spec returned an error (OK if validation rejects it): %v", err)
} else {
statusWipesSpec = !checkPatch(t, differentSpec, "spec", result.Object)
}
} else {
statusWipesSpec = true
}
// Check /status PrepareForUpdate metadata wiping
var statusWipesMetadata bool
labelPatch := []byte(`{"metadata": {"labels": {"test-wipe-label": "test-value"}}}`)
result, err := rsc.Patch(context.TODO(), name, types.MergePatchType, labelPatch, metav1.PatchOptions{}, "status")
if err != nil {
t.Logf("Label patch to status endpoint failed: %v", err)
statusWipesMetadata = false
} else {
statusWipesMetadata = result.GetLabels()["test-wipe-label"] != "test-value"
}
// Check consistency between field wiping and field resetting
checkConsistency := func(endpoint, field string, wipes, resets bool) {
if wipes == resets {
return
}
direction := "PrepareForUpdate wipes the field but GetResetFields does not declare it"
if resets && !wipes {
direction = "GetResetFields declares the field but PrepareForUpdate does not wipe it"
}
t.Errorf("Mismatch between PrepareForUpdate and GetResetFields (%s endpoint, %s field): %s (wipes=%v, resets=%v)",
endpoint, field, direction, wipes, resets)
}
checkConsistency("/", "status", mainWipesStatus, ssaMainResetsStatus)
checkConsistency("/status", "spec", statusWipesSpec, ssaStatusResetsSpec)
checkConsistency("/status", "metadata", statusWipesMetadata, ssaStatusResetsMetadata)
requireWiped := func(wipes bool, endpoint, field string) {
if wipes {
return
}
t.Errorf("%s did NOT wipe %s via PrepareForUpdate", endpoint, field)
}
requireWiped(mainWipesStatus, "/", "status")
requireWiped(statusWipesSpec, "/status", "spec")
if !statusWipesMetadata && !statusDoesNotWipeMetadataAllowed.Has(groupResource(mapping.Resource)) {
t.Errorf("/status does not wipe metadata. Add ResetObjectMetaForStatus to status strategy, or add %q to statusDoesNotWipeMetadataAllowed", groupResource(mapping.Resource))
}
if err := rsc.Delete(context.TODO(), name, *metav1.NewDeleteOptions(0)); err != nil {
t.Fatalf("deleting final object failed: %v", err)
}
})
if !api.HasVerb("patch") || !api.HasVerb("get") || !api.HasVerb("update") {
t.Skip("Resource does not support patch, get, and update")
}
}
}
func testObj(t *testing.T, stub, status string, gvk schema.GroupVersionKind) *unstructured.Unstructured {
t.Helper()
obj := &unstructured.Unstructured{}
if err := json.Unmarshal([]byte(stub), &obj.Object); err != nil {
t.Fatal(err)
}
if err := json.Unmarshal([]byte(status), &obj.Object); err != nil {
t.Fatal(err)
}
obj.SetAPIVersion(gvk.GroupVersion().String())
obj.SetKind(gvk.Kind)
return obj
}
// assemble first object
status := api.StorageData.GetStatusStub()
obj1 := apidefinitions.TestObj(t, api.StorageData.Stub, status, api.Mapping.GroupVersionKind)
name := obj1.GetName()
func groupResource(gvr schema.GroupVersionResource) string {
if gvr.Group == "" {
return gvr.Resource
}
return gvr.Group + "/" + gvr.Resource
}
rsc := api.ResourceClient()
// checkPatch checks if field values under fieldScope (e.g. spec, status, metdata) in objData match the values
// in the applyManifest.
func checkPatch(t *testing.T, applyManifest string, fieldScope string, objData map[string]interface{}) bool {
t.Helper()
var applyObj map[string]interface{}
if err := json.Unmarshal([]byte(applyManifest), &applyObj); err != nil {
t.Fatalf("Failed to parse apply JSON: %v", err)
}
applyValue, ok := applyObj[fieldScope]
if !ok {
return false
}
objValue, ok := objData[fieldScope]
if !ok {
return false
}
return containsAll(applyValue, objValue)
}
// apply the spec of the first object
_, err := rsc.Apply(context.TODO(), name, obj1, metav1.ApplyOptions{FieldManager: "fieldmanager1"})
if err != nil {
t.Fatalf("Failed to apply obj1: %v", err)
}
// containsAll checks if all keys in want are present in got and if the values of those keys are equal.
func containsAll(want, got any) bool {
wantMap, wantIsMap := want.(map[string]any)
gotMap, gotIsMap := got.(map[string]any)
if wantIsMap && gotIsMap {
for k, wv := range wantMap {
gv, exists := gotMap[k]
if !exists || !containsAll(wv, gv) {
return false
// create second object
status2 := api.StorageData.GetMutatedStatusStub()
differentSpec := api.StorageData.MutatedStub
if differentSpec == "" {
t.Skipf("No conflicting spec data defined for %v to test reset fields", api.Mapping.Resource)
}
obj2 := apidefinitions.TestObj(t, api.StorageData.Stub, status2, api.Mapping.GroupVersionKind)
if err := json.Unmarshal([]byte(differentSpec), &obj2.Object); err != nil {
t.Fatal(err)
}
obj2.SetName(name)
if reflect.DeepEqual(obj1, obj2) {
t.Fatalf("obj1 and obj2 should not be equal %v", obj2)
}
// apply the status of the second object
_, err = rsc.ApplyStatus(context.TODO(), name, obj2, metav1.ApplyOptions{FieldManager: "fieldmanager2"})
if err != nil {
t.Fatalf("Failed to apply obj2: %v", err)
}
if _, ok := noConflicts[api.Mapping.Resource.Resource]; !ok {
// reapply second object to the spec endpoint
_, err = rsc.Apply(context.TODO(), name, obj2, metav1.ApplyOptions{FieldManager: "fieldmanager2"})
if err := expectConflict(nil, err, rsc, name); err != nil {
t.Fatalf("Did not get expected conflict on spec apply: %v", err)
}
}
return true
}
return reflect.DeepEqual(want, got)
}
// managedFieldsOwnTopLevelField checks whether a FieldsV1 set contains a given top-level field.
func managedFieldsOwnTopLevelField(t *testing.T, fieldsV1 *metav1.FieldsV1, field string) bool {
t.Helper()
if fieldsV1 == nil {
return false
}
var fields map[string]interface{}
if err := json.Unmarshal(fieldsV1.GetRawBytes(), &fields); err != nil {
t.Logf("Failed to unmarshal FieldsV1: %v", err)
return false
}
_, ok := fields["f:"+field]
return ok
}
// managedFieldsOwnLabel checks whether a FieldsV1 set contains a metadata label.
func managedFieldsOwnLabel(t *testing.T, fieldsV1 *metav1.FieldsV1, labelKey string) bool {
t.Helper()
if fieldsV1 == nil {
return false
}
var fields map[string]interface{}
if err := json.Unmarshal(fieldsV1.GetRawBytes(), &fields); err != nil {
t.Logf("Failed to unmarshal FieldsV1: %v", err)
return false
}
metadata, ok := fields["f:metadata"].(map[string]interface{})
if !ok {
return false
}
labels, ok := metadata["f:labels"].(map[string]interface{})
if !ok {
return false
}
_, ok = labels["f:"+labelKey]
return ok
}
// TestUpdateStatusWithOldVersion tests that apply with resetFields works correctly when updating
// a custom resource's status subresource using an older API version while maintaining field ownership.
func TestUpdateStatusWithOldVersion(t *testing.T) {
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), []string{"--disable-admission-plugins", "ServiceAccount,TaintNodesByCondition"}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
client, err := kubernetes.NewForConfig(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
apiExtensionClient, err := apiextensionsclientset.NewForConfig(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
noxuBetaDefinition := nearlyRemovedBetaMultipleVersionNoxuCRDWithStatus(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err := fixtures.CreateCRDUsingRemovedAPI(server.EtcdClient, server.EtcdStoragePrefix, noxuBetaDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
kind := noxuDefinition.Spec.Names.Kind
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Versions[1].Name
name := "mytest"
rest := apiExtensionClient.Discovery().RESTClient()
// create namespace ns test
if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: resetFieldsNamespace}}, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
// Create the resource using the v1 CRD API.
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: %s
namespace: %s
spec:
a: value-for-a
b: value-for-b`, apiVersion, kind, name, resetFieldsNamespace))
result, err := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[1].Name, "/namespaces", resetFieldsNamespace, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw(context.TODO())
if err != nil {
t.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(result))
}
t.Logf("result: %s", string(result))
oldManagedFields, err := getManagedFields(result)
if err != nil {
t.Fatalf("failed to get managed fields: %v", err)
}
// When updating the status subresource via the v1beta1 CRD API,
// we assign a value to the spec field for testing purposes.
// However, in this case, the operation should NOT trigger any field manager updates
// related to server-side apply tracking.
updateStatusBytes := []byte(`{
"spec": { "a": "value-for-a-update" },
"status": {
"a": "status-for-a"
}
}`)
result, err = rest.Patch(types.MergePatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Versions[0].Name, "/namespaces", resetFieldsNamespace, noxuDefinition.Spec.Names.Plural).
Name(name).
SubResource("status").
Param("fieldManager", "subresource_test").
Body(updateStatusBytes).
DoRaw(context.TODO())
if err != nil {
t.Fatalf("Error updating subresource: %v ", err)
}
t.Logf("result: %s", string(result))
newManagedFields, err := getManagedFields(result)
if err != nil {
t.Fatalf("failed to get managed fields: %v", err)
}
// newManagedFields should include oldManagedFields
var applyManagerFound, subresourceManagerFound bool
for i, field := range newManagedFields {
if field.Manager == "apply_test" {
if !reflect.DeepEqual(newManagedFields[i], oldManagedFields[0]) {
t.Fatalf("Expected managed fields to not have changed when trying manually setting them via subresoures.\n\nExpected: %#v\n\nGot: %#v", oldManagedFields[0], newManagedFields[i])
}
applyManagerFound = true
if err := rsc.Delete(context.TODO(), name, *metav1.NewDeleteOptions(0)); err != nil {
t.Fatalf("deleting final object failed: %v", err)
}
if field.Manager == "subresource_test" {
subresourceManagerFound = true
}
}
if !applyManagerFound {
t.Errorf("expected field manager 'apply_test' to be present in newManagedFields")
}
if !subresourceManagerFound {
t.Errorf("expected field manager 'subresource_test' to be present in newManagedFields")
}
})
}
func expectConflict(objRet *unstructured.Unstructured, err error, dynamicClient dynamic.Interface, resource schema.GroupVersionResource, namespace, name string) error {
func expectConflict(objRet *unstructured.Unstructured, err error, rsc dynamic.ResourceInterface, name string) error {
if err != nil && strings.Contains(err.Error(), "conflict") {
return nil
}
@ -756,10 +120,7 @@ func expectConflict(objRet *unstructured.Unstructured, err error, dynamicClient
if objRet == nil {
which = "subsequently fetched"
var err2 error
objRet, err2 = dynamicClient.
Resource(resource).
Namespace(namespace).
Get(context.TODO(), name, metav1.GetOptions{})
objRet, err2 = rsc.Get(context.TODO(), name, metav1.GetOptions{})
if err2 != nil {
return fmt.Errorf("instead got error %w, and failed to Get object: %v", err, err2)
}

View file

@ -18,88 +18,27 @@ package apiserver
import (
"context"
"strings"
"testing"
v1 "k8s.io/api/core/v1"
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/kubernetes/test/integration/apiserver/apidefinitions"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/dynamic"
clientfeatures "k8s.io/client-go/features"
clientfeaturestesting "k8s.io/client-go/features/testing"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
featuregatetesting "k8s.io/component-base/featuregate/testing"
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration/etcd"
"k8s.io/kubernetes/test/integration/framework"
)
// namespace used for all tests, do not change this
const testNamespace = "statusnamespace"
var statusData = map[schema.GroupVersionResource]string{
gvr("", "v1", "persistentvolumes"): `{"status": {"message": "hello"}}`,
gvr("", "v1", "resourcequotas"): `{"status": {"used": {"cpu": "5M"}}}`,
gvr("", "v1", "services"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.1"}]}}}`,
gvr("extensions", "v1beta1", "ingresses"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.1"}]}}}`,
gvr("networking.k8s.io", "v1beta1", "ingresses"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.1"}]}}}`,
gvr("networking.k8s.io", "v1", "ingresses"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.1"}]}}}`,
gvr("autoscaling", "v1", "horizontalpodautoscalers"): `{"status": {"currentReplicas": 5}}`,
gvr("autoscaling", "v2", "horizontalpodautoscalers"): `{"status": {"currentReplicas": 5}}`,
gvr("batch", "v1", "cronjobs"): `{"status": {"lastScheduleTime": null}}`,
gvr("batch", "v1beta1", "cronjobs"): `{"status": {"lastScheduleTime": null}}`,
gvr("storage.k8s.io", "v1", "volumeattachments"): `{"status": {"attached": true}}`,
gvr("policy", "v1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 5}}`,
gvr("policy", "v1beta1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 5}}`,
gvr("resource.k8s.io", "v1alpha3", "devicetaintrules"): `{"status": {"conditions": [{"type": "EvictionInProgress", "status": "True", "reason: "PodsLeft", "message: "100 pods left", "lastTransitionTime": "2020-01-01T00:00:00Z"}]}}`,
gvr("resource.k8s.io", "v1beta1", "resourceclaims"): `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-value"]}] }]}}}}`,
gvr("resource.k8s.io", "v1beta2", "devicetaintrules"): `{"status": {"conditions": [{"type": "EvictionInProgress", "status": "True", "reason: "PodsLeft", "message: "100 pods left", "lastTransitionTime": "2020-01-01T00:00:00Z"}]}}`,
gvr("resource.k8s.io", "v1beta2", "resourceclaims"): `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-value"]}] }]}}}}`,
gvr("resource.k8s.io", "v1", "resourceclaims"): `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-value"]}] }]}}}}`,
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`,
// standard for []metav1.Condition
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): `{"status": {"conditions":[{"type":"Accepted","status":"False","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies"): `{"status": {"conditions":[{"type":"Accepted","status":"False","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies"): `{"status": {"conditions":[{"type":"Accepted","status":"False","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
}
const statusDefault = `{"status": {"conditions": [{"type": "MyStatus", "status":"True"}]}}`
func gvr(g, v, r string) schema.GroupVersionResource {
return schema.GroupVersionResource{Group: g, Version: v, Resource: r}
}
func createMapping(groupVersion string, resource metav1.APIResource) (*meta.RESTMapping, error) {
gv, err := schema.ParseGroupVersion(groupVersion)
if err != nil {
return nil, err
}
if len(resource.Group) > 0 || len(resource.Version) > 0 {
gv = schema.GroupVersion{
Group: resource.Group,
Version: resource.Version,
}
}
gvk := gv.WithKind(resource.Kind)
gvr := gv.WithResource(strings.TrimSuffix(resource.Name, "/status"))
scope := meta.RESTScopeRoot
if resource.Namespaced {
scope = meta.RESTScopeNamespace
}
return &meta.RESTMapping{
Resource: gvr,
GroupVersionKind: gvk,
Scope: scope,
}, nil
}
// TestApplyStatus makes sure that applying the status works for all known types.
func TestApplyStatus(t *testing.T) {
testApplyStatus(t, func(testing.TB, *rest.Config) {})
@ -116,121 +55,84 @@ func TestApplyStatusWithCBOR(t *testing.T) {
}
func testApplyStatus(t *testing.T, reconfigureClient func(testing.TB, *rest.Config)) {
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), []string{"--disable-admission-plugins", "ServiceAccount,TaintNodesByCondition"}, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
client, err := kubernetes.NewForConfig(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
// create CRDs so we can make sure that custom resources do not get lost
etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
createData := etcd.GetEtcdStorageData()
// gather resources to test
_, resourceLists, err := client.Discovery().ServerGroupsAndResources()
if err != nil {
t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
}
for _, resourceList := range resourceLists {
for _, resource := range resourceList.APIResources {
if !strings.HasSuffix(resource.Name, "/status") {
continue
}
mapping, err := createMapping(resourceList.GroupVersion, resource)
if err != nil {
t.Fatal(err)
}
t.Run(mapping.Resource.String(), func(t *testing.T) {
// both spec and status get wiped for CSRs,
// nothing is expected to be managed for it, skip it
if mapping.Resource.Resource == "certificatesigningrequests" {
t.SkipNow()
}
status, ok := statusData[mapping.Resource]
if !ok {
status = statusDefault
}
newResource, ok := createData[mapping.Resource]
if !ok {
t.Fatalf("no test data for %s. Please add a test for your new type to etcd.GetEtcdStorageData().", mapping.Resource)
}
newObj := unstructured.Unstructured{}
if err := json.Unmarshal([]byte(newResource.Stub), &newObj.Object); err != nil {
t.Fatal(err)
}
namespace := testNamespace
if mapping.Scope == meta.RESTScopeRoot {
namespace = ""
}
name := newObj.GetName()
// etcd test stub data doesn't contain apiVersion/kind (!), but apply requires it
newObj.SetGroupVersionKind(mapping.GroupVersionKind)
dynamicClientConfig := rest.CopyConfig(server.ClientConfig)
reconfigureClient(t, dynamicClientConfig)
dynamicClient, err := dynamic.NewForConfig(dynamicClientConfig)
if err != nil {
t.Fatal(err)
}
rsc := dynamicClient.Resource(mapping.Resource).Namespace(namespace)
// apply to create
_, err = rsc.Apply(context.TODO(), name, &newObj, metav1.ApplyOptions{FieldManager: "create_test"})
if err != nil {
t.Fatal(err)
}
statusObj := unstructured.Unstructured{}
if err := json.Unmarshal([]byte(status), &statusObj.Object); err != nil {
t.Fatal(err)
}
statusObj.SetAPIVersion(mapping.GroupVersionKind.GroupVersion().String())
statusObj.SetKind(mapping.GroupVersionKind.Kind)
statusObj.SetName(name)
obj, err := dynamicClient.
Resource(mapping.Resource).
Namespace(namespace).
ApplyStatus(context.TODO(), name, &statusObj, metav1.ApplyOptions{FieldManager: "apply_status_test", Force: true})
if err != nil {
t.Fatalf("Failed to apply: %v", err)
}
accessor, err := meta.Accessor(obj)
if err != nil {
t.Fatalf("Failed to get meta accessor: %v:\n%v", err, obj)
}
managedFields := accessor.GetManagedFields()
if managedFields == nil {
t.Fatal("Empty managed fields")
}
if !findManager(managedFields, "apply_status_test") {
t.Fatalf("Couldn't find apply_status_test: %v", managedFields)
}
if !findManager(managedFields, "create_test") {
t.Fatalf("Couldn't find create_test: %v", managedFields)
}
if err := rsc.Delete(context.TODO(), name, *metav1.NewDeleteOptions(0)); err != nil {
t.Fatalf("deleting final object failed: %v", err)
}
})
apidefinitions.TestAllDefinitions(t, testNamespace, func(t *testing.T, api apidefinitions.Definition) {
if !api.HasStatus() {
t.Skip()
}
}
if !api.HasVerb("patch") || !api.HasVerb("get") || !api.HasVerb("update") {
t.Skip("Resource does not support patch, get, and update")
}
// both spec and status get wiped for CSRs,
// nothing is expected to be managed for it, skip it
if api.Mapping.Resource.Resource == "certificatesigningrequests" {
t.Skip()
}
status := api.StorageData.GetStatusStub()
newObj := unstructured.Unstructured{}
if err := json.Unmarshal([]byte(api.StorageData.Stub), &newObj.Object); err != nil {
t.Fatal(err)
}
namespace := api.Namespace
if api.Mapping.Scope == meta.RESTScopeRoot {
namespace = ""
}
name := newObj.GetName()
// etcd test stub data doesn't contain apiVersion/kind (!), but apply requires it
newObj.SetGroupVersionKind(api.Mapping.GroupVersionKind)
dynamicClientConfig := rest.CopyConfig(api.Config)
reconfigureClient(t, dynamicClientConfig)
dynamicClient, err := dynamic.NewForConfig(dynamicClientConfig)
if err != nil {
t.Fatal(err)
}
rsc := dynamicClient.Resource(api.Mapping.Resource).Namespace(namespace)
// apply to create
_, err = rsc.Apply(context.TODO(), name, &newObj, metav1.ApplyOptions{FieldManager: "create_test"})
if err != nil {
t.Fatal(err)
}
statusObj := unstructured.Unstructured{}
if err := json.Unmarshal([]byte(status), &statusObj.Object); err != nil {
t.Fatal(err)
}
statusObj.SetAPIVersion(api.Mapping.GroupVersionKind.GroupVersion().String())
statusObj.SetKind(api.Mapping.GroupVersionKind.Kind)
statusObj.SetName(name)
obj, err := dynamicClient.
Resource(api.Mapping.Resource).
Namespace(namespace).
ApplyStatus(context.TODO(), name, &statusObj, metav1.ApplyOptions{FieldManager: "apply_status_test", Force: true})
if err != nil {
t.Fatalf("Failed to apply: %v", err)
}
accessor, err := meta.Accessor(obj)
if err != nil {
t.Fatalf("Failed to get meta accessor: %v:\n%v", err, obj)
}
managedFields := accessor.GetManagedFields()
if managedFields == nil {
t.Fatal("Empty managed fields")
}
if !findManager(managedFields, "apply_status_test") {
t.Fatalf("Couldn't find apply_status_test: %v", managedFields)
}
if !findManager(managedFields, "create_test") {
t.Fatalf("Couldn't find create_test: %v", managedFields)
}
if err := rsc.Delete(context.TODO(), name, *metav1.NewDeleteOptions(0)); err != nil {
t.Fatalf("deleting final object failed: %v", err)
}
})
}
func findManager(managedFields []metav1.ManagedFieldsEntry, manager string) bool {

View file

@ -70,6 +70,7 @@ func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionRes
// It returns a new map on every invocation to prevent different tests from mutating shared state.
// Namespaced objects keys are computed for the specified namespace.
func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulation bool) map[schema.GroupVersionResource]StorageData {
alternateImage := image.GetE2EImage(image.Etcd)
image := image.GetE2EImage(image.BusyBox)
etcdStorageData := map[schema.GroupVersionResource]StorageData{
// k8s.io/kubernetes/pkg/api/v1
@ -80,8 +81,11 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
},
gvr("", "v1", "services"): {
Stub: `{"metadata": {"name": "service1"}, "spec": {"type": "LoadBalancer", "ports": [{"port": 10000, "targetPort": 11000}], "selector": {"test": "data"}}}`,
MutatedStub: `{"spec": {"type": "ClusterIP"}}`,
ExpectedEtcdPath: "/registry/services/specs/" + namespace + "/service1",
IntroducedVersion: "1.0",
StatusStub: `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.1", "ipMode": "VIP"}]}}}`,
MutatedStatusStub: `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.2", "ipMode": "VIP"}]}}}`,
},
gvr("", "v1", "podtemplates"): {
Stub: `{"metadata": {"name": "pt1name"}, "template": {"metadata": {"labels": {"pt": "01"}}, "spec": {"containers": [{"image": "` + image + `", "name": "container9"}]}}}`,
@ -90,6 +94,7 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
},
gvr("", "v1", "pods"): {
Stub: `{"metadata": {"name": "pod1"}, "spec": {"containers": [{"image": "` + image + `", "name": "container7", "resources": {"limits": {"cpu": "1M"}, "requests": {"cpu": "1M"}}}]}}`,
MutatedStub: `{"metadata": {"deletionTimestamp": "2020-01-01T00:00:00Z", "ownerReferences":[]}, "spec": {"containers": [{"image": "` + alternateImage + `", "name": "container7"}]}}`,
ExpectedEtcdPath: "/registry/pods/" + namespace + "/pod1",
IntroducedVersion: "1.0",
},
@ -100,8 +105,11 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
},
gvr("", "v1", "resourcequotas"): {
Stub: `{"metadata": {"name": "rq1name"}, "spec": {"hard": {"cpu": "5M"}}}`,
MutatedStub: `{"spec": {"hard": {"cpu": "25M"}}}`,
ExpectedEtcdPath: "/registry/resourcequotas/" + namespace + "/rq1name",
IntroducedVersion: "1.0",
StatusStub: `{"status": {"used": {"cpu": "5M"}}}`,
MutatedStatusStub: `{"status": {"used": {"cpu": "25M"}}}`,
},
gvr("", "v1", "limitranges"): {
Stub: `{"metadata": {"name": "lr1name"}, "spec": {"limits": [{"type": "Pod"}]}}`,
@ -110,18 +118,23 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
},
gvr("", "v1", "namespaces"): {
Stub: `{"metadata": {"name": "namespace1"}, "spec": {"finalizers": ["kubernetes"]}}`,
MutatedStub: `{"spec": {"finalizers": ["kubernetes2"]}}`,
ExpectedEtcdPath: "/registry/namespaces/namespace1",
IntroducedVersion: "1.0",
},
gvr("", "v1", "nodes"): {
Stub: `{"metadata": {"name": "node1"}, "spec": {"unschedulable": true}}`,
MutatedStub: `{"spec": {"unschedulable": false}}`,
ExpectedEtcdPath: "/registry/minions/node1",
IntroducedVersion: "1.0",
},
gvr("", "v1", "persistentvolumes"): {
Stub: `{"metadata": {"name": "pv1name"}, "spec": {"accessModes": ["ReadWriteOnce"], "capacity": {"storage": "3M"}, "hostPath": {"path": "/tmp/test/"}}}`,
MutatedStub: `{"spec": {"capacity": {"storage": "23M"}}}`,
ExpectedEtcdPath: "/registry/persistentvolumes/pv1name",
IntroducedVersion: "1.0",
StatusStub: `{"status": {"message": "hello"}}`,
MutatedStatusStub: `{"status": {"message": "hello2"}}`,
},
gvr("", "v1", "events"): {
Stub: `{"involvedObject": {"namespace": "` + namespace + `"}, "message": "some data here", "metadata": {"name": "event1"}}`,
@ -130,6 +143,7 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
},
gvr("", "v1", "persistentvolumeclaims"): {
Stub: `{"metadata": {"name": "pvc1"}, "spec": {"accessModes": ["ReadWriteOnce"], "resources": {"limits": {"storage": "1M"}, "requests": {"storage": "2M"}}, "selector": {"matchLabels": {"pvc": "stuff"}}}}`,
MutatedStub: `{"spec": {"resources": {"requests": {"storage": "21M"}}}}`,
ExpectedEtcdPath: "/registry/persistentvolumeclaims/" + namespace + "/pvc1",
IntroducedVersion: "1.0",
},
@ -145,6 +159,7 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
},
gvr("", "v1", "replicationcontrollers"): {
Stub: `{"metadata": {"name": "rc1"}, "spec": {"selector": {"new": "stuff"}, "template": {"metadata": {"labels": {"new": "stuff"}}, "spec": {"containers": [{"image": "` + image + `", "name": "container8"}]}}}}`,
MutatedStub: `{"spec": {"selector": {"new": "stuff2"}}}`,
ExpectedEtcdPath: "/registry/controllers/" + namespace + "/rc1",
IntroducedVersion: "1.0",
},
@ -153,21 +168,25 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
// k8s.io/kubernetes/pkg/apis/apps/v1
gvr("apps", "v1", "daemonsets"): {
Stub: `{"metadata": {"name": "ds6"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}, "spec": {"containers": [{"image": "` + image + `", "name": "container6"}]}}}}`,
MutatedStub: `{"spec": {"template": {"spec": {"containers": [{"image": "` + alternateImage + `", "name": "container6"}]}}}}`,
ExpectedEtcdPath: "/registry/daemonsets/" + namespace + "/ds6",
IntroducedVersion: "1.9",
},
gvr("apps", "v1", "deployments"): {
Stub: `{"metadata": {"name": "deployment4"}, "spec": {"selector": {"matchLabels": {"f": "z"}}, "template": {"metadata": {"labels": {"f": "z"}}, "spec": {"containers": [{"image": "` + image + `", "name": "container6"}]}}}}`,
MutatedStub: `{"metadata": {"labels": {"a":"c"}}, "spec": {"template": {"spec": {"containers": [{"image": "` + alternateImage + `", "name": "container6"}]}}}}`,
ExpectedEtcdPath: "/registry/deployments/" + namespace + "/deployment4",
IntroducedVersion: "1.9",
},
gvr("apps", "v1", "statefulsets"): {
Stub: `{"metadata": {"name": "ss3"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}, "spec": {"restartPolicy": "Always", "terminationGracePeriodSeconds": 30, "containers": [{"image": "` + image + `", "name": "container6", "terminationMessagePolicy": "File"}]}}}}`,
MutatedStub: `{"spec": {"selector": {"matchLabels": {"a2": "b2"}}}}`,
ExpectedEtcdPath: "/registry/statefulsets/" + namespace + "/ss3",
IntroducedVersion: "1.9",
},
gvr("apps", "v1", "replicasets"): {
Stub: `{"metadata": {"name": "rs3"}, "spec": {"selector": {"matchLabels": {"g": "h"}}, "template": {"metadata": {"labels": {"g": "h"}}, "spec": {"containers": [{"image": "` + image + `", "name": "container4"}]}}}}`,
MutatedStub: `{"spec": {"template": {"spec": {"containers": [{"image": "` + alternateImage + `", "name": "container4"}]}}}}`,
ExpectedEtcdPath: "/registry/replicasets/" + namespace + "/rs3",
IntroducedVersion: "1.9",
},
@ -181,36 +200,47 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
// k8s.io/kubernetes/pkg/apis/autoscaling/v1
gvr("autoscaling", "v1", "horizontalpodautoscalers"): {
Stub: `{"metadata": {"name": "hpa2"}, "spec": {"maxReplicas": 3, "scaleTargetRef": {"kind": "something", "name": "cross", "apiVersion": "apps/v1"}}}`,
MutatedStub: `{"spec": {"maxReplicas": 23}}`,
ExpectedEtcdPath: "/registry/horizontalpodautoscalers/" + namespace + "/hpa2",
ExpectedGVK: gvkP("autoscaling", "v2", "HorizontalPodAutoscaler"),
IntroducedVersion: "1.2",
StatusStub: `{"status": {"currentReplicas": 5}}`,
MutatedStatusStub: `{"status": {"currentReplicas": 25}}`,
},
// --
// k8s.io/kubernetes/pkg/apis/autoscaling/v2
gvr("autoscaling", "v2", "horizontalpodautoscalers"): {
Stub: `{"metadata": {"name": "hpa4"}, "spec": {"maxReplicas": 3, "scaleTargetRef": {"kind": "something", "name": "cross", "apiVersion": "apps/v1"}}}`,
MutatedStub: `{"spec": {"maxReplicas": 23}}`,
ExpectedEtcdPath: "/registry/horizontalpodautoscalers/" + namespace + "/hpa4",
IntroducedVersion: "1.23",
StatusStub: `{"status": {"currentReplicas": 5}}`,
MutatedStatusStub: `{"status": {"currentReplicas": 25}}`,
},
// --
// k8s.io/kubernetes/pkg/apis/batch/v1
gvr("batch", "v1", "jobs"): {
Stub: `{"metadata": {"name": "job1"}, "spec": {"manualSelector": true, "selector": {"matchLabels": {"controller-uid": "uid1"}}, "template": {"metadata": {"labels": {"controller-uid": "uid1"}}, "spec": {"containers": [{"image": "` + image + `", "name": "container1"}], "dnsPolicy": "ClusterFirst", "restartPolicy": "Never"}}}}`,
MutatedStub: `{"spec": {"template": {"spec": {"containers": [{"image": "` + alternateImage + `", "name": "container1"}]}}}}`,
ExpectedEtcdPath: "/registry/jobs/" + namespace + "/job1",
IntroducedVersion: "1.2",
},
gvr("batch", "v1", "cronjobs"): {
Stub: `{"metadata": {"name": "cjv1"}, "spec": {"jobTemplate": {"spec": {"template": {"metadata": {"labels": {"controller-uid": "uid0"}}, "spec": {"containers": [{"image": "` + image + `", "name": "container0"}], "dnsPolicy": "ClusterFirst", "restartPolicy": "Never"}}}}, "schedule": "* * * * *"}}`,
MutatedStub: `{"spec": {"jobTemplate": {"spec": {"template": {"spec": {"containers": [{"image": "` + alternateImage + `", "name": "container0"}]}}}}}}`,
ExpectedEtcdPath: "/registry/cronjobs/" + namespace + "/cjv1",
IntroducedVersion: "1.21",
StatusStub: `{"status": {"lastScheduleTime": null}}`,
MutatedStatusStub: `{"status": {"lastScheduleTime": "2020-01-01T00:00:00Z"}}`,
},
// --
// k8s.io/kubernetes/pkg/apis/certificates/v1
gvr("certificates.k8s.io", "v1", "certificatesigningrequests"): {
Stub: `{"metadata": {"name": "csr2"}, "spec": {"signerName":"example.com/signer", "usages":["any"], "request": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQnlqQ0NBVE1DQVFBd2dZa3hDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJRXdwRFlXeHBabTl5Ym1saApNUll3RkFZRFZRUUhFdzFOYjNWdWRHRnBiaUJXYVdWM01STXdFUVlEVlFRS0V3cEhiMjluYkdVZ1NXNWpNUjh3CkhRWURWUVFMRXhaSmJtWnZjbTFoZEdsdmJpQlVaV05vYm05c2IyZDVNUmN3RlFZRFZRUURFdzUzZDNjdVoyOXYKWjJ4bExtTnZiVENCbnpBTkJna3Foa2lHOXcwQkFRRUZBQU9CalFBd2dZa0NnWUVBcFp0WUpDSEo0VnBWWEhmVgpJbHN0UVRsTzRxQzAzaGpYK1prUHl2ZFlkMVE0K3FiQWVUd1htQ1VLWUhUaFZSZDVhWFNxbFB6eUlCd2llTVpyCldGbFJRZGRaMUl6WEFsVlJEV3dBbzYwS2VjcWVBWG5uVUsrNWZYb1RJL1VnV3NocmU4dEoreC9UTUhhUUtSL0oKY0lXUGhxYVFoc0p1elpidkFkR0E4MEJMeGRNQ0F3RUFBYUFBTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUlobAo0UHZGcStlN2lwQVJnSTVaTStHWng2bXBDejQ0RFRvMEprd2ZSRGYrQnRyc2FDMHE2OGVUZjJYaFlPc3E0ZmtIClEwdUEwYVZvZzNmNWlKeENhM0hwNWd4YkpRNnpWNmtKMFRFc3VhYU9oRWtvOXNkcENvUE9uUkJtMmkvWFJEMkQKNmlOaDhmOHowU2hHc0ZxakRnRkh5RjNvK2xVeWorVUM2SDFRVzdibgotLS0tLUVORCBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0="}}`,
MutatedStub: `{}`,
ExpectedEtcdPath: "/registry/certificatesigningrequests/csr2",
IntroducedVersion: "1.19",
},
@ -296,8 +326,11 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
// k8s.io/kubernetes/pkg/apis/networking/v1
gvr("networking.k8s.io", "v1", "ingresses"): {
Stub: `{"metadata": {"name": "ingress3"}, "spec": {"defaultBackend": {"service":{"name":"service", "port":{"number": 5000}}}}}`,
MutatedStub: `{"spec": {"defaultBackend": {"service": {"name": "service2"}}}}`,
ExpectedEtcdPath: "/registry/ingress/" + namespace + "/ingress3",
IntroducedVersion: "1.19",
StatusStub: `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.1"}]}}}`,
MutatedStatusStub: `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.2"}]}}}`,
},
gvr("networking.k8s.io", "v1", "ingressclasses"): {
Stub: `{"metadata": {"name": "ingressclass3"}, "spec": {"controller": "example.com/controller"}}`,
@ -317,9 +350,11 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
},
gvr("networking.k8s.io", "v1", "servicecidrs"): {
Stub: `{"metadata": {"name": "range-b2"}, "spec": {"cidrs": ["192.168.0.0/16","fd00:1::/120"]}}`,
MutatedStub: `{}`,
ExpectedEtcdPath: "/registry/servicecidrs/range-b2",
ExpectedGVK: gvkP("networking.k8s.io", "v1", "ServiceCIDR"),
IntroducedVersion: "1.33",
MutatedStatusStub: `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
},
// --
@ -336,18 +371,23 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
// k8s.io/kubernetes/pkg/apis/networking/v1beta1
gvr("networking.k8s.io", "v1beta1", "servicecidrs"): {
Stub: `{"metadata": {"name": "range-b1"}, "spec": {"cidrs": ["192.168.0.0/16","fd00:1::/120"]}}`,
MutatedStub: `{}`,
ExpectedEtcdPath: "/registry/servicecidrs/range-b1",
ExpectedGVK: gvkP("networking.k8s.io", "v1", "ServiceCIDR"),
IntroducedVersion: "1.31",
RemovedVersion: "1.37",
MutatedStatusStub: `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
},
// --
// k8s.io/kubernetes/pkg/apis/policy/v1
gvr("policy", "v1", "poddisruptionbudgets"): {
Stub: `{"metadata": {"name": "pdbv1"}, "spec": {"selector": {"matchLabels": {"anokkey": "anokvalue"}}}}`,
MutatedStub: `{"spec": {"selector": {"matchLabels": {"anokkey2": "anokvalue"}}}}`,
ExpectedEtcdPath: "/registry/poddisruptionbudgets/" + namespace + "/pdbv1",
IntroducedVersion: "1.21",
StatusStub: `{"status": {"currentHealthy": 5}}`,
MutatedStatusStub: `{"status": {"currentHealthy": 25}}`,
},
// --
@ -363,6 +403,7 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
// k8s.io/kubernetes/pkg/apis/flowcontrol/v1
gvr("flowcontrol.apiserver.k8s.io", "v1", "flowschemas"): {
Stub: `{"metadata": {"name": "fs-3"}, "spec": {"priorityLevelConfiguration": {"name": "name1"}}}`,
MutatedStub: `{"metadata": {"labels":{"a":"c"}}, "spec": {"priorityLevelConfiguration": {"name": "name2"}}}`,
ExpectedEtcdPath: "/registry/flowschemas/fs-3",
IntroducedVersion: "1.29",
},
@ -371,6 +412,7 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
// k8s.io/kubernetes/pkg/apis/flowcontrol/v1
gvr("flowcontrol.apiserver.k8s.io", "v1", "prioritylevelconfigurations"): {
Stub: `{"metadata": {"name": "conf5"}, "spec": {"type": "Limited", "limited": {"nominalConcurrencyShares":3, "limitResponse": {"type": "Reject"}}}}`,
MutatedStub: `{"metadata": {"labels":{"a":"c"}}, "spec": {"limited": {"nominalConcurrencyShares": 23}}}`,
ExpectedEtcdPath: "/registry/prioritylevelconfigurations/conf5",
IntroducedVersion: "1.29",
},
@ -379,8 +421,11 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
// k8s.io/kubernetes/pkg/apis/storage/v1
gvr("storage.k8s.io", "v1", "volumeattachments"): {
Stub: `{"metadata": {"name": "va3"}, "spec": {"attacher": "gce", "nodeName": "localhost", "source": {"persistentVolumeName": "pv3"}}}`,
MutatedStub: `{"metadata": {"name": "va3"}, "spec": {"nodeName": "localhost2"}}`,
ExpectedEtcdPath: "/registry/volumeattachments/va3",
IntroducedVersion: "1.13",
StatusStub: `{"status": {"attached": true}}`,
MutatedStatusStub: `{"status": {"attached": false}}`,
},
// --
@ -452,6 +497,7 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
},
gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies"): {
Stub: `{"metadata":{"name":"vap1"},"spec":{"paramKind":{"apiVersion":"test.example.com/v1","kind":"Example"},"matchConstraints":{"resourceRules": [{"resourceNames": ["fakeName"], "apiGroups":["apps"],"apiVersions":["v1"],"operations":["CREATE", "UPDATE"], "resources":["deployments"]}]},"validations":[{"expression":"object.spec.replicas <= params.maxReplicas","message":"Too many replicas"}]}}`,
MutatedStub: `{"metadata": {"labels": {"a":"c"}}, "spec": {"paramKind": {"apiVersion": "apps/v1", "kind": "Deployment"}}}`,
ExpectedEtcdPath: "/registry/validatingadmissionpolicies/vap1",
IntroducedVersion: "1.30",
},
@ -547,6 +593,7 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
// depends on aggregator using the same ungrouped RESTOptionsGetter as the kube apiserver, not SimpleRestOptionsFactory in aggregator.go
gvr("apiregistration.k8s.io", "v1", "apiservices"): {
Stub: `{"metadata": {"name": "as2.foo.com"}, "spec": {"group": "foo.com", "version": "as2", "groupPriorityMinimum":100, "versionPriority":10}}`,
MutatedStub: `{"metadata": {"labels": {"a":"c"}}, "spec": {"versionPriority": 100}}`,
ExpectedEtcdPath: "/registry/apiregistration.k8s.io/apiservices/as2.foo.com",
IntroducedVersion: "1.10",
},
@ -558,6 +605,7 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
`"scope": "Cluster","group": "webconsole2.operator.openshift.io",` +
`"versions": [{"name":"v1alpha1","storage":true,"served":true,"schema":{"openAPIV3Schema":{"type":"object"}}}],` +
`"names": {"kind": "OpenShiftWebConsoleConfig","plural": "openshiftwebconsoleconfigs","singular": "openshiftwebconsoleconfig"}}}`,
MutatedStub: `{"metadata": {"labels":{"a":"c"}}, "spec": {"group": "webconsole22.operator.openshift.io"}}`,
ExpectedEtcdPath: "/registry/apiextensions.k8s.io/customresourcedefinitions/openshiftwebconsoleconfigs.webconsole2.operator.openshift.io",
ExpectedGVK: gvkP("apiextensions.k8s.io", "v1beta1", "CustomResourceDefinition"),
IntroducedVersion: "1.16",
@ -574,11 +622,13 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
},
gvr("awesome.bears.com", "v1", "pandas"): {
Stub: `{"kind": "Panda", "apiVersion": "awesome.bears.com/v1", "metadata": {"name": "cr3panda"}, "spec":{"replicas": 100}}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper
MutatedStub: `{"spec": {"replicas": 102}}`,
ExpectedEtcdPath: "/registry/awesome.bears.com/pandas/cr3panda",
IntroducedVersion: "1.0",
},
gvr("awesome.bears.com", "v3", "pandas"): {
Stub: `{"kind": "Panda", "apiVersion": "awesome.bears.com/v3", "metadata": {"name": "cr4panda"}, "spec":{"replicas": 300}}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper
MutatedStub: `{"spec": {"replicas": 302}}`,
ExpectedEtcdPath: "/registry/awesome.bears.com/pandas/cr4panda",
ExpectedGVK: gvkP("awesome.bears.com", "v1", "Panda"),
IntroducedVersion: "1.0",
@ -601,6 +651,7 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
// k8s.io/kubernetes/pkg/apis/resource/v1alpha3
gvr("resource.k8s.io", "v1alpha3", "devicetaintrules"): {
Stub: `{"metadata": {"name": "taint1name"}, "spec": {"taint": {"key": "example.com/taintkey", "value": "taintvalue", "effect": "NoSchedule"}}}`,
MutatedStub: `{"metadata": {"labels":{"a":"c"}}}`,
ExpectedEtcdPath: "/registry/devicetaintrules/taint1name",
IntroducedVersion: "1.33",
RemovedVersion: "1.39",
@ -616,6 +667,7 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
// k8s.io/kubernetes/pkg/apis/resource/v1beta1
gvr("resource.k8s.io", "v1beta1", "deviceclasses"): {
Stub: `{"metadata": {"name": "class2name"}}`,
MutatedStub: `{"metadata": {"labels":{"a":"c"}}}`,
ExpectedEtcdPath: "/registry/deviceclasses/class2name",
ExpectedGVK: gvkP("resource.k8s.io", "v1", "DeviceClass"),
IntroducedVersion: "1.32",
@ -623,13 +675,17 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
},
gvr("resource.k8s.io", "v1beta1", "resourceclaims"): {
Stub: `{"metadata": {"name": "claim2name"}, "spec": {"devices": {"requests": [{"name": "req-0", "deviceClassName": "example-class", "allocationMode": "ExactCount", "count": 1}]}}}`,
MutatedStub: `{"spec": {"devices": {"requests": [{"name": "req-0", "deviceClassName": "other-class"}]}}}`,
ExpectedEtcdPath: "/registry/resourceclaims/" + namespace + "/claim2name",
ExpectedGVK: gvkP("resource.k8s.io", "v1", "ResourceClaim"),
IntroducedVersion: "1.32",
RemovedVersion: "1.38",
StatusStub: `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-value"]}] }]}}}}`,
MutatedStatusStub: `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-other-value"]}] }]}}}}`,
},
gvr("resource.k8s.io", "v1beta1", "resourceclaimtemplates"): {
Stub: `{"metadata": {"name": "claimtemplate2name"}, "spec": {"spec": {"devices": {"requests": [{"name": "req-0", "deviceClassName": "example-class", "allocationMode": "ExactCount", "count": 1}]}}}}`,
MutatedStub: `{"spec": {"spec": {"resourceClassName": "class2name"}}}`,
ExpectedEtcdPath: "/registry/resourceclaimtemplates/" + namespace + "/claimtemplate2name",
ExpectedGVK: gvkP("resource.k8s.io", "v1", "ResourceClaimTemplate"),
IntroducedVersion: "1.32",
@ -650,6 +706,7 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
// v1beta1 is the default although the actual storage version is v1beta2.
gvr("resource.k8s.io", "v1beta2", "deviceclasses"): {
Stub: `{"metadata": {"name": "class3name"}}`,
MutatedStub: `{"metadata": {"labels":{"a":"c"}}}`,
ExpectedEtcdPath: "/registry/deviceclasses/class3name",
ExpectedGVK: gvkP("resource.k8s.io", "v1", "DeviceClass"),
IntroducedVersion: "1.33",
@ -657,6 +714,7 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
},
gvr("resource.k8s.io", "v1beta2", "devicetaintrules"): {
Stub: `{"metadata": {"name": "taint2name"}, "spec": {"taint": {"key": "example.com/taintkey", "value": "taintvalue", "effect": "NoSchedule"}}}`,
MutatedStub: `{"metadata": {"labels":{"a":"c"}}}`,
ExpectedEtcdPath: "/registry/devicetaintrules/taint2name",
ExpectedGVK: gvkP("resource.k8s.io", "v1alpha3", "DeviceTaintRule"), // v1beta2 has higher priority, but to support downgrades v1alpha3 is picked automatically.
IntroducedVersion: "1.36",
@ -664,13 +722,17 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
},
gvr("resource.k8s.io", "v1beta2", "resourceclaims"): {
Stub: `{"metadata": {"name": "claim3name"}, "spec": {"devices": {"requests": [{"name": "req-0", "exactly": {"deviceClassName": "example-class", "allocationMode": "ExactCount", "count": 1}}]}}}`,
MutatedStub: `{"spec": {"devices": {"requests": [{"name": "req-0", "exactly": {"deviceClassName": "other-class"}}]}}}`,
ExpectedEtcdPath: "/registry/resourceclaims/" + namespace + "/claim3name",
ExpectedGVK: gvkP("resource.k8s.io", "v1", "ResourceClaim"),
IntroducedVersion: "1.33",
RemovedVersion: "1.39",
StatusStub: `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-value"]}] }]}}}}`,
MutatedStatusStub: `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-other-value"]}] }]}}}}`,
},
gvr("resource.k8s.io", "v1beta2", "resourceclaimtemplates"): {
Stub: `{"metadata": {"name": "claimtemplate3name"}, "spec": {"spec": {"devices": {"requests": [{"name": "req-0", "exactly": {"deviceClassName": "example-class", "allocationMode": "ExactCount", "count": 1}}]}}}}`,
MutatedStub: `{"spec": {"spec": {"resourceClassName": "class2name"}}}`,
ExpectedEtcdPath: "/registry/resourceclaimtemplates/" + namespace + "/claimtemplate3name",
ExpectedGVK: gvkP("resource.k8s.io", "v1", "ResourceClaimTemplate"),
IntroducedVersion: "1.33",
@ -688,18 +750,23 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
// k8s.io/kubernetes/pkg/apis/resource/v1
gvr("resource.k8s.io", "v1", "deviceclasses"): {
Stub: `{"metadata": {"name": "class4name"}}`,
MutatedStub: `{"metadata": {"labels":{"a":"c"}}}`,
ExpectedEtcdPath: "/registry/deviceclasses/class4name",
ExpectedGVK: gvkP("resource.k8s.io", "v1", "DeviceClass"),
IntroducedVersion: "1.34",
},
gvr("resource.k8s.io", "v1", "resourceclaims"): {
Stub: `{"metadata": {"name": "claim4name"}, "spec": {"devices": {"requests": [{"name": "req-0", "exactly": {"deviceClassName": "example-class", "allocationMode": "ExactCount", "count": 1}}]}}}`,
MutatedStub: `{"spec": {"devices": {"requests": [{"name": "req-0", "exactly": {"deviceClassName": "other-class"}}]}}}`,
ExpectedEtcdPath: "/registry/resourceclaims/" + namespace + "/claim4name",
ExpectedGVK: gvkP("resource.k8s.io", "v1", "ResourceClaim"),
IntroducedVersion: "1.34",
StatusStub: `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-value"]}] }]}}}}`,
MutatedStatusStub: `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-other-value"]}] }]}}}}`,
},
gvr("resource.k8s.io", "v1", "resourceclaimtemplates"): {
Stub: `{"metadata": {"name": "claimtemplate4name"}, "spec": {"spec": {"devices": {"requests": [{"name": "req-0", "exactly": {"deviceClassName": "example-class", "allocationMode": "ExactCount", "count": 1}}]}}}}`,
MutatedStub: `{"spec": {"spec": {"resourceClassName": "class2name"}}}`,
ExpectedEtcdPath: "/registry/resourceclaimtemplates/" + namespace + "/claimtemplate4name",
ExpectedGVK: gvkP("resource.k8s.io", "v1", "ResourceClaimTemplate"),
IntroducedVersion: "1.34",
@ -714,8 +781,11 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
// k8s.io/apiserver/pkg/apis/apiserverinternal/v1alpha1
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): {
Stub: `{"metadata":{"name":"sv1.test"},"spec":{}}`,
ExpectedEtcdPath: "/registry/storageversions/sv1.test",
Stub: `{"metadata":{"name":"sv1.test"},"spec":{}}`,
MutatedStub: `{}`,
ExpectedEtcdPath: "/registry/storageversions/sv1.test",
StatusStub: `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`,
MutatedStatusStub: `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"False","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`,
},
// --
@ -839,7 +909,24 @@ func storageVersionAtEmulationVersion(key schema.GroupVersionResource, expectedG
// StorageData contains information required to create an object and verify its storage in etcd
// It must be paired with a specific resource
type StorageData struct {
Stub string // Valid JSON stub to use during create
// Stub is a valid JSON object used to create the resource.
Stub string
// MutatedStub is Stub with at least one spec or metadata field changed to a different value.
// This should mutate a spec field if possible. Only mutate a metadata field if the entire spec is immutable.
// This mutation is applied using a server-side apply patch.
// Required for resources that have a status subresource; leave empty otherwise.
// Example: if Stub has `"replicas": 1`, MutatedStub might have `"replicas": 2`.
MutatedStub string
// StatusStub is a valid JSON status payload used to write to the /status subresource.
// If empty, a metav1.Condition style status condition is used. Required only
// for resources without status conditions.
StatusStub string
// MutatedStatusStub is StatusStub with at least one status field changed to a different value.
// If empty, the default status payload with a flipped condition value is used.
// This mutation is applied using a server-side apply patch.
// Must be set whenever StatusStub is set.
MutatedStatusStub string
Prerequisites []Prerequisite // Optional, ordered list of JSON objects to create before stub
ExpectedEtcdPath string // Expected location of object in etcd, do not use any variables, constants, etc to derive this value - always supply the full raw string
ExpectedGVK *schema.GroupVersionKind // The GVK that we expect this object to be stored as - leave this nil to use the default
@ -847,6 +934,26 @@ type StorageData struct {
RemovedVersion string // The version that this type is removed. May be empty for stable resources
}
const defaultStatusStub = `{"status": {"conditions": [{"type": "MyStatus", "status":"True", "lastTransitionTime": "2020-01-01T00:00:00Z", "reason": "MyReason", "message": "some message"}]}}`
const defaultMutatedStatusStub = `{"status": {"conditions": [{"type": "MyStatus", "status":"False", "lastTransitionTime": "2020-01-01T00:00:00Z", "reason": "MyReason", "message": "some message"}]}}`
// GetStatusStub returns the StatusStub, or a default conditions-based status if empty.
func (s StorageData) GetStatusStub() string {
if s.StatusStub != "" {
return s.StatusStub
}
return defaultStatusStub
}
// GetMutatedStatusStub returns the MutatedStatusStub, or a default conditions-based status
// with a flipped condition value if empty.
func (s StorageData) GetMutatedStatusStub() string {
if s.MutatedStatusStub != "" {
return s.MutatedStatusStub
}
return defaultMutatedStatusStub
}
// Prerequisite contains information required to create a resource (but not verify it)
type Prerequisite struct {
GvrData schema.GroupVersionResource