1
16
17 package internal_test
18
19 import (
20 "encoding/json"
21 "fmt"
22 "strings"
23 "testing"
24
25 "k8s.io/apimachinery/pkg/api/meta"
26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
27 "k8s.io/apimachinery/pkg/runtime"
28 "k8s.io/apimachinery/pkg/runtime/schema"
29 "k8s.io/apimachinery/pkg/util/managedfields/internal"
30 internaltesting "k8s.io/apimachinery/pkg/util/managedfields/internal/testing"
31 "sigs.k8s.io/yaml"
32 )
33
34 func TestLastAppliedUpdater(t *testing.T) {
35 f := internaltesting.NewTestFieldManagerImpl(fakeTypeConverter, schema.FromAPIVersionAndKind("apps/v1", "Deployment"),
36 "",
37 func(m internal.Manager) internal.Manager {
38 return internal.NewLastAppliedUpdater(m)
39 })
40
41 originalLastApplied := `nonempty`
42 appliedObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
43 appliedDeployment := []byte(`
44 apiVersion: apps/v1
45 kind: Deployment
46 metadata:
47 name: my-deployment
48 annotations:
49 "kubectl.kubernetes.io/last-applied-configuration": "` + originalLastApplied + `"
50 labels:
51 app: my-app
52 spec:
53 replicas: 20
54 selector:
55 matchLabels:
56 app: my-app
57 template:
58 metadata:
59 labels:
60 app: my-app
61 spec:
62 containers:
63 - name: my-c
64 image: my-image
65 `)
66 if err := yaml.Unmarshal(appliedDeployment, &appliedObj.Object); err != nil {
67 t.Errorf("error decoding YAML: %v", err)
68 }
69
70 if err := f.Apply(appliedObj, "NOT-KUBECTL", false); err != nil {
71 t.Errorf("error applying object: %v", err)
72 }
73
74 lastApplied, err := getLastApplied(f.Live())
75 if err != nil {
76 t.Errorf("failed to get last applied: %v", err)
77 }
78
79 if lastApplied != originalLastApplied {
80 t.Errorf("expected last applied annotation to be %q and NOT be updated, but got: %q", originalLastApplied, lastApplied)
81 }
82
83 if err := f.Apply(appliedObj, "kubectl", false); err != nil {
84 t.Errorf("error applying object: %v", err)
85 }
86
87 lastApplied, err = getLastApplied(f.Live())
88 if err != nil {
89 t.Errorf("failed to get last applied: %v", err)
90 }
91
92 if lastApplied == originalLastApplied ||
93 !strings.Contains(lastApplied, "my-app") ||
94 !strings.Contains(lastApplied, "my-image") {
95 t.Errorf("expected last applied annotation to be updated, but got: %q", lastApplied)
96 }
97 }
98
99 func TestLargeLastApplied(t *testing.T) {
100 tests := []struct {
101 name string
102 oldObject *unstructured.Unstructured
103 newObject *unstructured.Unstructured
104 }{
105 {
106 name: "old object + new object last-applied annotation is too big",
107 oldObject: func() *unstructured.Unstructured {
108 u := &unstructured.Unstructured{}
109 err := json.Unmarshal([]byte(`
110 {
111 "metadata": {
112 "name": "large-update-test-cm",
113 "namespace": "default",
114 "annotations": {
115 "kubectl.kubernetes.io/last-applied-configuration": "nonempty"
116 }
117 },
118 "apiVersion": "v1",
119 "kind": "ConfigMap",
120 "data": {
121 "k": "v"
122 }
123 }`), &u)
124 if err != nil {
125 panic(err)
126 }
127 return u
128 }(),
129 newObject: func() *unstructured.Unstructured {
130 u := &unstructured.Unstructured{}
131 err := json.Unmarshal([]byte(`
132 {
133 "metadata": {
134 "name": "large-update-test-cm",
135 "namespace": "default",
136 "annotations": {
137 "kubectl.kubernetes.io/last-applied-configuration": "nonempty"
138 }
139 },
140 "apiVersion": "v1",
141 "kind": "ConfigMap",
142 "data": {
143 "k": "v"
144 }
145 }`), &u)
146 if err != nil {
147 panic(err)
148 }
149 for i := 0; i < 9999; i++ {
150 unique := fmt.Sprintf("this-key-is-very-long-so-as-to-create-a-very-large-serialized-fieldset-%v", i)
151 unstructured.SetNestedField(u.Object, "A", "data", unique)
152 }
153 return u
154 }(),
155 },
156 {
157 name: "old object + new object annotations + new object last-applied annotation is too big",
158 oldObject: func() *unstructured.Unstructured {
159 u := &unstructured.Unstructured{}
160 err := json.Unmarshal([]byte(`
161 {
162 "metadata": {
163 "name": "large-update-test-cm",
164 "namespace": "default",
165 "annotations": {
166 "kubectl.kubernetes.io/last-applied-configuration": "nonempty"
167 }
168 },
169 "apiVersion": "v1",
170 "kind": "ConfigMap",
171 "data": {
172 "k": "v"
173 }
174 }`), &u)
175 if err != nil {
176 panic(err)
177 }
178 for i := 0; i < 2000; i++ {
179 unique := fmt.Sprintf("this-key-is-very-long-so-as-to-create-a-very-large-serialized-fieldset-%v", i)
180 unstructured.SetNestedField(u.Object, "A", "data", unique)
181 }
182 return u
183 }(),
184 newObject: func() *unstructured.Unstructured {
185 u := &unstructured.Unstructured{}
186 err := json.Unmarshal([]byte(`
187 {
188 "metadata": {
189 "name": "large-update-test-cm",
190 "namespace": "default",
191 "annotations": {
192 "kubectl.kubernetes.io/last-applied-configuration": "nonempty"
193 }
194 },
195 "apiVersion": "v1",
196 "kind": "ConfigMap",
197 "data": {
198 "k": "v"
199 }
200 }`), &u)
201 if err != nil {
202 panic(err)
203 }
204 for i := 0; i < 2000; i++ {
205 unique := fmt.Sprintf("this-key-is-very-long-so-as-to-create-a-very-large-serialized-fieldset-%v", i)
206 unstructured.SetNestedField(u.Object, "A", "data", unique)
207 unstructured.SetNestedField(u.Object, "A", "metadata", "annotations", unique)
208 }
209 return u
210 }(),
211 },
212 }
213 for _, test := range tests {
214 t.Run(test.name, func(t *testing.T) {
215 f := internaltesting.NewTestFieldManagerImpl(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "ConfigMap"),
216 "",
217 func(m internal.Manager) internal.Manager {
218 return internal.NewLastAppliedUpdater(m)
219 })
220
221 if err := f.Apply(test.oldObject, "kubectl", false); err != nil {
222 t.Errorf("Error applying object: %v", err)
223 }
224
225 lastApplied, err := getLastApplied(f.Live())
226 if err != nil {
227 t.Errorf("Failed to access last applied annotation: %v", err)
228 }
229 if len(lastApplied) == 0 || lastApplied == "nonempty" {
230 t.Errorf("Expected an updated last-applied annotation, but got: %q", lastApplied)
231 }
232
233 if err := f.Apply(test.newObject, "kubectl", false); err != nil {
234 t.Errorf("Error applying object: %v", err)
235 }
236
237 accessor := meta.NewAccessor()
238 annotations, err := accessor.Annotations(f.Live())
239 if err != nil {
240 t.Errorf("Failed to access annotations: %v", err)
241 }
242 if annotations == nil {
243 t.Errorf("No annotations on obj: %v", f.Live())
244 }
245 lastApplied, ok := annotations[internal.LastAppliedConfigAnnotation]
246 if ok || len(lastApplied) > 0 {
247 t.Errorf("Expected no last applied annotation, but got last applied with length: %d", len(lastApplied))
248 }
249 })
250 }
251 }
252
253 func getLastApplied(obj runtime.Object) (string, error) {
254 accessor := meta.NewAccessor()
255 annotations, err := accessor.Annotations(obj)
256 if err != nil {
257 return "", fmt.Errorf("failed to access annotations: %v", err)
258 }
259 if annotations == nil {
260 return "", fmt.Errorf("no annotations on obj: %v", obj)
261 }
262
263 lastApplied, ok := annotations[internal.LastAppliedConfigAnnotation]
264 if !ok {
265 return "", fmt.Errorf("expected last applied annotation, but got none for object: %v", obj)
266 }
267 return lastApplied, nil
268 }
269
View as plain text