Skip to content

Commit ede025a

Browse files
authored
Merge pull request kubernetes#89232 from apelisse/test-apply-status
Use discovery to test apply all status
2 parents 9ba2bd5 + dfe1703 commit ede025a

File tree

7 files changed

+240
-1
lines changed

7 files changed

+240
-1
lines changed

pkg/registry/flowcontrol/flowschema/strategy.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@ var StatusStrategy = flowSchemaStatusStrategy{Strategy}
9494
func (flowSchemaStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
9595
newFlowSchema := obj.(*flowcontrol.FlowSchema)
9696
oldFlowSchema := old.(*flowcontrol.FlowSchema)
97+
98+
// managedFields must be preserved since it's been modified to
99+
// track changed fields in the status update.
100+
managedFields := newFlowSchema.ManagedFields
97101
newFlowSchema.ObjectMeta = oldFlowSchema.ObjectMeta
102+
newFlowSchema.ManagedFields = managedFields
98103
newFlowSchema.Spec = oldFlowSchema.Spec
99104
}
100105

pkg/registry/flowcontrol/prioritylevelconfiguration/strategy.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@ var StatusStrategy = priorityLevelConfigurationStatusStrategy{Strategy}
9494
func (priorityLevelConfigurationStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
9595
newPriorityLevelConfiguration := obj.(*flowcontrol.PriorityLevelConfiguration)
9696
oldPriorityLevelConfiguration := old.(*flowcontrol.PriorityLevelConfiguration)
97+
98+
// managedFields must be preserved since it's been modified to
99+
// track changed fields in the status update.
100+
managedFields := newPriorityLevelConfiguration.ManagedFields
97101
newPriorityLevelConfiguration.ObjectMeta = oldPriorityLevelConfiguration.ObjectMeta
102+
newPriorityLevelConfiguration.ManagedFields = managedFields
98103
newPriorityLevelConfiguration.Spec = oldPriorityLevelConfiguration.Spec
99104
}
100105

staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/status_strategy.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,18 @@ func (a statusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.O
3838
newCustomResource := newCustomResourceObject.UnstructuredContent()
3939
status, ok := newCustomResource["status"]
4040

41+
// managedFields must be preserved since it's been modified to
42+
// track changed fields in the status update.
43+
managedFields := newCustomResourceObject.GetManagedFields()
44+
4145
// copy old object into new object
4246
oldCustomResourceObject := old.(*unstructured.Unstructured)
4347
// overridding the resourceVersion in metadata is safe here, we have already checked that
4448
// new object and old object have the same resourceVersion.
4549
*newCustomResourceObject = *oldCustomResourceObject.DeepCopy()
4650

4751
// set status
52+
newCustomResourceObject.SetManagedFields(managedFields)
4853
newCustomResource = newCustomResourceObject.UnstructuredContent()
4954
if ok {
5055
newCustomResource["status"] = status

staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/helpers.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,9 @@ func ResetObjectMetaForStatus(meta, existingMeta Object) {
252252
meta.SetAnnotations(existingMeta.GetAnnotations())
253253
meta.SetFinalizers(existingMeta.GetFinalizers())
254254
meta.SetOwnerReferences(existingMeta.GetOwnerReferences())
255-
meta.SetManagedFields(existingMeta.GetManagedFields())
255+
// managedFields must be preserved since it's been modified to
256+
// track changed fields in the status update.
257+
//meta.SetManagedFields(existingMeta.GetManagedFields())
256258
}
257259

258260
// MarshalJSON implements json.Marshaler

staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/helpers_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ func TestResetObjectMetaForStatus(t *testing.T) {
189189
existingMeta.SetCreationTimestamp(Time{})
190190
existingMeta.SetDeletionTimestamp(nil)
191191
existingMeta.SetDeletionGracePeriodSeconds(nil)
192+
existingMeta.SetManagedFields(nil)
192193

193194
if !reflect.DeepEqual(meta, existingMeta) {
194195
t.Error(diff.ObjectDiff(meta, existingMeta))

test/integration/apiserver/apply/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ go_test(
77
"apply_crd_test.go",
88
"apply_test.go",
99
"main_test.go",
10+
"status_test.go",
1011
],
1112
tags = [
1213
"etcd",
@@ -32,6 +33,7 @@ go_test(
3233
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
3334
"//staging/src/k8s.io/client-go/rest:go_default_library",
3435
"//staging/src/k8s.io/component-base/featuregate/testing:go_default_library",
36+
"//test/integration/etcd:go_default_library",
3537
"//test/integration/framework:go_default_library",
3638
"//vendor/sigs.k8s.io/yaml:go_default_library",
3739
],
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package apiserver
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"strings"
23+
"testing"
24+
25+
v1 "k8s.io/api/core/v1"
26+
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
27+
"k8s.io/apimachinery/pkg/api/meta"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
30+
"k8s.io/apimachinery/pkg/runtime/schema"
31+
"k8s.io/apimachinery/pkg/types"
32+
genericfeatures "k8s.io/apiserver/pkg/features"
33+
utilfeature "k8s.io/apiserver/pkg/util/feature"
34+
"k8s.io/client-go/dynamic"
35+
"k8s.io/client-go/kubernetes"
36+
featuregatetesting "k8s.io/component-base/featuregate/testing"
37+
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
38+
"k8s.io/kubernetes/test/integration/etcd"
39+
"k8s.io/kubernetes/test/integration/framework"
40+
"sigs.k8s.io/yaml"
41+
)
42+
43+
// namespace used for all tests, do not change this
44+
const testNamespace = "statusnamespace"
45+
46+
var statusData = map[schema.GroupVersionResource]string{
47+
gvr("", "v1", "persistentvolumes"): `{"status": {"message": "hello"}}`,
48+
gvr("", "v1", "resourcequotas"): `{"status": {"used": {"cpu": "5M"}}}`,
49+
gvr("", "v1", "services"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.1"}]}}}`,
50+
gvr("extensions", "v1beta1", "ingresses"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.1"}]}}}`,
51+
gvr("networking.k8s.io", "v1beta1", "ingresses"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.1"}]}}}`,
52+
gvr("autoscaling", "v1", "horizontalpodautoscalers"): `{"status": {"currentReplicas": 5}}`,
53+
gvr("batch", "v1beta1", "cronjobs"): `{"status": {"lastScheduleTime": null}}`,
54+
gvr("batch", "v2alpha1", "cronjobs"): `{"status": {"lastScheduleTime": null}}`,
55+
gvr("storage.k8s.io", "v1", "volumeattachments"): `{"status": {"attached": true}}`,
56+
gvr("policy", "v1beta1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 5}}`,
57+
gvr("certificates.k8s.io", "v1beta1", "certificatesigningrequests"): `{"status": {"conditions": [{"type": "MyStatus"}]}}`,
58+
}
59+
60+
const statusDefault = `{"status": {"conditions": [{"type": "MyStatus", "status":"true"}]}}`
61+
62+
// DO NOT ADD TO THIS LIST.
63+
// This list is used to ignore known bugs. We shouldn't introduce new bugs.
64+
var ignoreList = map[schema.GroupVersionResource]struct{}{
65+
// TODO(#89264): apiservices doesn't work because the openapi is not routed properly.
66+
gvr("apiregistration.k8s.io", "v1beta1", "apiservices"): {},
67+
gvr("apiregistration.k8s.io", "v1", "apiservices"): {},
68+
}
69+
70+
func gvr(g, v, r string) schema.GroupVersionResource {
71+
return schema.GroupVersionResource{Group: g, Version: v, Resource: r}
72+
}
73+
74+
func createMapping(groupVersion string, resource metav1.APIResource) (*meta.RESTMapping, error) {
75+
gv, err := schema.ParseGroupVersion(groupVersion)
76+
if err != nil {
77+
return nil, err
78+
}
79+
if len(resource.Group) > 0 || len(resource.Version) > 0 {
80+
gv = schema.GroupVersion{
81+
Group: resource.Group,
82+
Version: resource.Version,
83+
}
84+
}
85+
gvk := gv.WithKind(resource.Kind)
86+
gvr := gv.WithResource(strings.TrimSuffix(resource.Name, "/status"))
87+
scope := meta.RESTScopeRoot
88+
if resource.Namespaced {
89+
scope = meta.RESTScopeNamespace
90+
}
91+
return &meta.RESTMapping{
92+
Resource: gvr,
93+
GroupVersionKind: gvk,
94+
Scope: scope,
95+
}, nil
96+
}
97+
98+
// TestApplyStatus makes sure that applying the status works for all known types.
99+
func TestApplyStatus(t *testing.T) {
100+
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
101+
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), []string{"--disable-admission-plugins", "ServiceAccount,TaintNodesByCondition"}, framework.SharedEtcd())
102+
if err != nil {
103+
t.Fatal(err)
104+
}
105+
defer server.TearDownFn()
106+
107+
client, err := kubernetes.NewForConfig(server.ClientConfig)
108+
if err != nil {
109+
t.Fatal(err)
110+
}
111+
dynamicClient, err := dynamic.NewForConfig(server.ClientConfig)
112+
if err != nil {
113+
t.Fatal(err)
114+
}
115+
116+
// create CRDs so we can make sure that custom resources do not get lost
117+
etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
118+
if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); err != nil {
119+
t.Fatal(err)
120+
}
121+
122+
createData := etcd.GetEtcdStorageData()
123+
124+
// gather resources to test
125+
_, resourceLists, err := client.Discovery().ServerGroupsAndResources()
126+
if err != nil {
127+
t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
128+
}
129+
130+
for _, resourceList := range resourceLists {
131+
for _, resource := range resourceList.APIResources {
132+
if !strings.HasSuffix(resource.Name, "/status") {
133+
continue
134+
}
135+
mapping, err := createMapping(resourceList.GroupVersion, resource)
136+
if err != nil {
137+
t.Fatal(err)
138+
}
139+
t.Run(mapping.Resource.String(), func(t *testing.T) {
140+
if _, ok := ignoreList[mapping.Resource]; ok {
141+
t.Skip()
142+
}
143+
status, ok := statusData[mapping.Resource]
144+
if !ok {
145+
status = statusDefault
146+
}
147+
newResource, ok := createData[mapping.Resource]
148+
if !ok {
149+
t.Fatalf("no test data for %s. Please add a test for your new type to etcd.GetEtcdStorageData().", mapping.Resource)
150+
}
151+
newObj := unstructured.Unstructured{}
152+
if err := json.Unmarshal([]byte(newResource.Stub), &newObj.Object); err != nil {
153+
t.Fatal(err)
154+
}
155+
156+
namespace := testNamespace
157+
if mapping.Scope == meta.RESTScopeRoot {
158+
namespace = ""
159+
}
160+
name := newObj.GetName()
161+
rsc := dynamicClient.Resource(mapping.Resource).Namespace(namespace)
162+
_, err := rsc.Create(context.TODO(), &newObj, metav1.CreateOptions{FieldManager: "create_test"})
163+
if err != nil {
164+
t.Fatal(err)
165+
}
166+
167+
statusObj := unstructured.Unstructured{}
168+
if err := json.Unmarshal([]byte(status), &statusObj.Object); err != nil {
169+
t.Fatal(err)
170+
}
171+
statusObj.SetAPIVersion(mapping.GroupVersionKind.GroupVersion().String())
172+
statusObj.SetKind(mapping.GroupVersionKind.Kind)
173+
statusObj.SetName(name)
174+
statusYAML, err := yaml.Marshal(statusObj.Object)
175+
if err != nil {
176+
t.Fatal(err)
177+
}
178+
179+
True := true
180+
obj, err := dynamicClient.
181+
Resource(mapping.Resource).
182+
Namespace(namespace).
183+
Patch(context.TODO(), name, types.ApplyPatchType, statusYAML, metav1.PatchOptions{FieldManager: "apply_status_test", Force: &True}, "status")
184+
if err != nil {
185+
t.Fatalf("Failed to apply: %v", err)
186+
}
187+
188+
accessor, err := meta.Accessor(obj)
189+
if err != nil {
190+
t.Fatalf("Failed to get meta accessor: %v:\n%v", err, obj)
191+
}
192+
193+
managedFields := accessor.GetManagedFields()
194+
if managedFields == nil {
195+
t.Fatal("Empty managed fields")
196+
}
197+
if !findManager(managedFields, "apply_status_test") {
198+
t.Fatalf("Couldn't find apply_status_test: %v", managedFields)
199+
}
200+
if !findManager(managedFields, "create_test") {
201+
t.Fatalf("Couldn't find create_test: %v", managedFields)
202+
}
203+
204+
if err := rsc.Delete(context.TODO(), name, *metav1.NewDeleteOptions(0)); err != nil {
205+
t.Fatalf("deleting final object failed: %v", err)
206+
}
207+
})
208+
}
209+
}
210+
}
211+
212+
func findManager(managedFields []metav1.ManagedFieldsEntry, manager string) bool {
213+
for _, entry := range managedFields {
214+
if entry.Manager == manager {
215+
return true
216+
}
217+
}
218+
return false
219+
}

0 commit comments

Comments
 (0)