mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-06-13 19:01:10 -04:00
Introduce apidefinition test helper and organize tests around it
This commit is contained in:
parent
f5c7b42274
commit
643ec7035b
7 changed files with 698 additions and 872 deletions
128
test/integration/apiserver/apidefinitions/generation_test.go
Normal file
128
test/integration/apiserver/apidefinitions/generation_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
190
test/integration/apiserver/apidefinitions/helper.go
Normal file
190
test/integration/apiserver/apidefinitions/helper.go
Normal 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
|
||||
}
|
||||
27
test/integration/apiserver/apidefinitions/main_test.go
Normal file
27
test/integration/apiserver/apidefinitions/main_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue