blob: 47c8389c5ddad10b42405dbe8bee8f2542d8619d [file] [log] [blame]
Serge Bazanskibe538db2020-11-12 00:22:42 +01001package kubecfg
2
3import (
4 "fmt"
5 "io/ioutil"
6 "path/filepath"
7 "testing"
8
9 pb_proto "github.com/golang/protobuf/proto"
10 openapi_v2 "github.com/googleapis/gnostic/openapiv2"
11 apiequality "k8s.io/apimachinery/pkg/api/equality"
12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14 "k8s.io/apimachinery/pkg/runtime/schema"
15 "k8s.io/apimachinery/pkg/util/diff"
16 "k8s.io/apimachinery/pkg/util/strategicpatch"
17 "k8s.io/kube-openapi/pkg/util/proto"
18 "k8s.io/kubectl/pkg/util/openapi"
19
20 "code.hackerspace.pl/hscloud/cluster/tools/kartongips/utils"
21)
22
23func TestStringListContains(t *testing.T) {
24 t.Parallel()
25 foobar := []string{"foo", "bar"}
26 if stringListContains([]string{}, "") {
27 t.Error("Empty list was not empty")
28 }
29 if !stringListContains(foobar, "foo") {
30 t.Error("Failed to find foo")
31 }
32 if stringListContains(foobar, "baz") {
33 t.Error("Should not contain baz")
34 }
35}
36
37func TestIsValidKindSchema(t *testing.T) {
38 t.Parallel()
39 schemaResources := readSchemaOrDie(filepath.FromSlash("../../testdata/schema.pb"))
40
41 cmgvk := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"}
42 if !isValidKindSchema(schemaResources.LookupResource(cmgvk)) {
43 t.Errorf("%s should have a valid schema", cmgvk)
44 }
45
46 if isValidKindSchema(nil) {
47 t.Error("nil should not be a valid schema")
48 }
49
50 // This is what a schema-less CRD appears as in k8s >= 1.15
51 mapSchema := &proto.Map{
52 BaseSchema: proto.BaseSchema{
53 Extensions: map[string]interface{}{
54 "x-kubernetes-group-version-kind": []interface{}{
55 map[interface{}]interface{}{"group": "bitnami.com", "kind": "SealedSecret", "version": "v1alpha1"},
56 },
57 },
58 },
59 SubType: &proto.Arbitrary{},
60 }
61 if isValidKindSchema(mapSchema) {
62 t.Error("Trivial type:object schema should be invalid")
63 }
64}
65
66func TestEligibleForGc(t *testing.T) {
67 t.Parallel()
68 const myTag = "my-gctag"
69 boolTrue := true
70 o := &unstructured.Unstructured{
71 Object: map[string]interface{}{
72 "apiVersion": "tests/v1alpha1",
73 "kind": "Dummy",
74 },
75 }
76
77 if eligibleForGc(o, myTag) {
78 t.Errorf("%v should not be eligible (no tag)", o)
79 }
80
81 // [gctag-migration]: Remove annotation in phase2
82 utils.SetMetaDataAnnotation(o, AnnotationGcTag, "unknowntag")
83 utils.SetMetaDataLabel(o, LabelGcTag, "unknowntag")
84 if eligibleForGc(o, myTag) {
85 t.Errorf("%v should not be eligible (wrong tag)", o)
86 }
87
88 // [gctag-migration]: Remove annotation in phase2
89 utils.SetMetaDataAnnotation(o, AnnotationGcTag, myTag)
90 utils.SetMetaDataLabel(o, LabelGcTag, myTag)
91 if !eligibleForGc(o, myTag) {
92 t.Errorf("%v should be eligible", o)
93 }
94
95 // [gctag-migration]: Remove testcase in phase2
96 utils.SetMetaDataAnnotation(o, AnnotationGcTag, myTag)
97 utils.DeleteMetaDataLabel(o, LabelGcTag) // no label. ie: pre-migration
98 if !eligibleForGc(o, myTag) {
99 t.Errorf("%v should be eligible (gctag-migration phase1)", o)
100 }
101
102 utils.SetMetaDataAnnotation(o, AnnotationGcStrategy, GcStrategyIgnore)
103 if eligibleForGc(o, myTag) {
104 t.Errorf("%v should not be eligible (strategy=ignore)", o)
105 }
106
107 utils.SetMetaDataAnnotation(o, AnnotationGcStrategy, GcStrategyAuto)
108 if !eligibleForGc(o, myTag) {
109 t.Errorf("%v should be eligible (strategy=auto)", o)
110 }
111
112 // Unstructured.SetOwnerReferences is broken in apimachinery release-1.6
113 // See kubernetes/kubernetes#46817
114 setOwnerRef := func(u *unstructured.Unstructured, ref metav1.OwnerReference) {
115 // This is not a complete nor robust reimplementation
116 c := map[string]interface{}{
117 "kind": ref.Kind,
118 "name": ref.Name,
119 }
120 if ref.Controller != nil {
121 c["controller"] = *ref.Controller
122 }
123 u.Object["metadata"].(map[string]interface{})["ownerReferences"] = []interface{}{c}
124 }
125 setOwnerRef(o, metav1.OwnerReference{Kind: "foo", Name: "bar"})
126 if !eligibleForGc(o, myTag) {
127 t.Errorf("%v should be eligible (non-controller ownerref)", o)
128 }
129
130 setOwnerRef(o, metav1.OwnerReference{Kind: "foo", Name: "bar", Controller: &boolTrue})
131 if eligibleForGc(o, myTag) {
132 t.Errorf("%v should not be eligible (controller ownerref)", o)
133 }
134}
135
136func exampleConfigMap() *unstructured.Unstructured {
137 result := &unstructured.Unstructured{
138 Object: map[string]interface{}{
139 "apiVersion": "v1",
140 "kind": "ConfigMap",
141 "metadata": map[string]interface{}{
142 "name": "myname",
143 "namespace": "mynamespace",
144 "annotations": map[string]interface{}{
145 "myannotation": "somevalue",
146 },
147 },
148 "data": map[string]interface{}{
149 "foo": "bar",
150 },
151 },
152 }
153
154 return result
155}
156
157func addOrigAnnotation(obj *unstructured.Unstructured) {
158 data, err := utils.CompactEncodeObject(obj)
159 if err != nil {
160 panic(fmt.Sprintf("Failed to serialise object: %v", err))
161 }
162 utils.SetMetaDataAnnotation(obj, AnnotationOrigObject, data)
163}
164
165func newPatchMetaFromStructOrDie(dataStruct interface{}) strategicpatch.PatchMetaFromStruct {
166 t, err := strategicpatch.NewPatchMetaFromStruct(dataStruct)
167 if err != nil {
168 panic(fmt.Sprintf("NewPatchMetaFromStruct(%t) failed: %v", dataStruct, err))
169 }
170 return t
171}
172
173func readSchemaOrDie(path string) openapi.Resources {
174 var doc openapi_v2.Document
175 b, err := ioutil.ReadFile(path)
176 if err != nil {
177 panic(fmt.Sprintf("Unable to read %s: %v", path, err))
178 }
179 if err := pb_proto.Unmarshal(b, &doc); err != nil {
180 panic(fmt.Sprintf("Unable to unmarshal %s: %v", path, err))
181 }
182 schemaResources, err := openapi.NewOpenAPIData(&doc)
183 if err != nil {
184 panic(fmt.Sprintf("Unable to parse openapi doc: %v", err))
185 }
186 return schemaResources
187}
188
189func TestPatchNoop(t *testing.T) {
190 t.Parallel()
191 schemaResources := readSchemaOrDie(filepath.FromSlash("../../testdata/schema.pb"))
192
193 existing := exampleConfigMap()
194 new := existing.DeepCopy()
195 addOrigAnnotation(existing)
196
197 result, err := patch(existing, new, schemaResources.LookupResource(existing.GroupVersionKind()))
198 if err != nil {
199 t.Errorf("patch() returned error: %v", err)
200 }
201
202 t.Logf("existing: %#v", existing)
203 t.Logf("result: %#v", result)
204 if !apiequality.Semantic.DeepEqual(existing, result) {
205 t.Error("Objects differed: ", diff.ObjectDiff(existing, result))
206 }
207}
208
209func TestPatchNoopNoAnnotation(t *testing.T) {
210 t.Parallel()
211 schemaResources := readSchemaOrDie(filepath.FromSlash("../../testdata/schema.pb"))
212
213 existing := exampleConfigMap()
214 new := existing.DeepCopy()
215 // Note: no addOrigAnnotation(existing)
216
217 result, err := patch(existing, new, schemaResources.LookupResource(existing.GroupVersionKind()))
218 if err != nil {
219 t.Errorf("patch() returned error: %v", err)
220 }
221
222 // result should == existing, except for annotation
223
224 if result.GetAnnotations()[AnnotationOrigObject] == "" {
225 t.Errorf("result lacks last-applied annotation")
226 }
227
228 utils.DeleteMetaDataAnnotation(result, AnnotationOrigObject)
229 if !apiequality.Semantic.DeepEqual(existing, result) {
230 t.Error("Objects differed: ", diff.ObjectDiff(existing, result))
231 }
232}
233
234func TestPatchNoConflict(t *testing.T) {
235 t.Parallel()
236 schemaResources := readSchemaOrDie(filepath.FromSlash("../../testdata/schema.pb"))
237
238 existing := exampleConfigMap()
239 utils.SetMetaDataAnnotation(existing, "someanno", "origvalue")
240 addOrigAnnotation(existing)
241 utils.SetMetaDataAnnotation(existing, "otheranno", "existingvalue")
242 new := exampleConfigMap()
243 utils.SetMetaDataAnnotation(new, "someanno", "newvalue")
244
245 result, err := patch(existing, new, schemaResources.LookupResource(existing.GroupVersionKind()))
246 if err != nil {
247 t.Errorf("patch() returned error: %v", err)
248 }
249
250 t.Logf("existing: %#v", existing)
251 t.Logf("result: %#v", result)
252 someanno := result.GetAnnotations()["someanno"]
253 if someanno != "newvalue" {
254 t.Errorf("someanno was %q", someanno)
255 }
256
257 otheranno := result.GetAnnotations()["otheranno"]
258 if otheranno != "existingvalue" {
259 t.Errorf("otheranno was %q", otheranno)
260 }
261}
262
263func TestPatchConflict(t *testing.T) {
264 t.Parallel()
265 schemaResources := readSchemaOrDie(filepath.FromSlash("../../testdata/schema.pb"))
266
267 existing := exampleConfigMap()
268 utils.SetMetaDataAnnotation(existing, "someanno", "origvalue")
269 addOrigAnnotation(existing)
270 utils.SetMetaDataAnnotation(existing, "someanno", "existingvalue")
271 new := exampleConfigMap()
272 utils.SetMetaDataAnnotation(new, "someanno", "newvalue")
273
274 result, err := patch(existing, new, schemaResources.LookupResource(existing.GroupVersionKind()))
275 if err != nil {
276 t.Errorf("patch() returned error: %v", err)
277 }
278
279 // `new` should win conflicts
280
281 t.Logf("existing: %#v", existing)
282 t.Logf("result: %#v", result)
283 value := result.GetAnnotations()["someanno"]
284 if value != "newvalue" {
285 t.Errorf("annotation was %q", value)
286 }
287}