1/*
2Copyright 2021 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package ensurer
18
19import (
20	"context"
21	"reflect"
22	"testing"
23
24	flowcontrolv1beta1 "k8s.io/api/flowcontrol/v1beta1"
25	apierrors "k8s.io/apimachinery/pkg/api/errors"
26	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27	"k8s.io/apiserver/pkg/apis/flowcontrol/bootstrap"
28	"k8s.io/client-go/kubernetes/fake"
29	flowcontrolclient "k8s.io/client-go/kubernetes/typed/flowcontrol/v1beta1"
30	flowcontrolapisv1beta1 "k8s.io/kubernetes/pkg/apis/flowcontrol/v1beta1"
31
32	"github.com/google/go-cmp/cmp"
33	"github.com/stretchr/testify/assert"
34)
35
36func TestEnsureFlowSchema(t *testing.T) {
37	tests := []struct {
38		name      string
39		strategy  func(flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer
40		current   *flowcontrolv1beta1.FlowSchema
41		bootstrap *flowcontrolv1beta1.FlowSchema
42		expected  *flowcontrolv1beta1.FlowSchema
43	}{
44		// for suggested configurations
45		{
46			name: "suggested flow schema does not exist - the object should always be re-created",
47			strategy: func(client flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer {
48				return NewSuggestedFlowSchemaEnsurer(client)
49			},
50			bootstrap: newFlowSchema("fs1", "pl1", 100).Object(),
51			current:   nil,
52			expected:  newFlowSchema("fs1", "pl1", 100).Object(),
53		},
54		{
55			name: "suggested flow schema exists, auto update is enabled, spec does not match - current object should be updated",
56			strategy: func(client flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer {
57				return NewSuggestedFlowSchemaEnsurer(client)
58			},
59			bootstrap: newFlowSchema("fs1", "pl1", 100).Object(),
60			current:   newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("true").Object(),
61			expected:  newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(),
62		},
63		{
64			name: "suggested flow schema exists, auto update is disabled, spec does not match - current object should not be updated",
65			strategy: func(client flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer {
66				return NewSuggestedFlowSchemaEnsurer(client)
67			},
68			bootstrap: newFlowSchema("fs1", "pl1", 100).Object(),
69			current:   newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("false").Object(),
70			expected:  newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("false").Object(),
71		},
72
73		// for mandatory configurations
74		{
75			name: "mandatory flow schema does not exist - new object should be created",
76			strategy: func(client flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer {
77				return NewMandatoryFlowSchemaEnsurer(client)
78			},
79			bootstrap: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(),
80			current:   nil,
81			expected:  newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(),
82		},
83		{
84			name: "mandatory flow schema exists, annotation is missing - annotation should be added",
85			strategy: func(client flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer {
86				return NewMandatoryFlowSchemaEnsurer(client)
87			},
88			bootstrap: newFlowSchema("fs1", "pl1", 100).Object(),
89			current:   newFlowSchema("fs1", "pl1", 100).Object(),
90			expected:  newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(),
91		},
92		{
93			name: "mandatory flow schema exists, auto update is disabled, spec does not match - current object should be updated",
94			strategy: func(client flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer {
95				return NewMandatoryFlowSchemaEnsurer(client)
96			},
97			bootstrap: newFlowSchema("fs1", "pl1", 100).Object(),
98			current:   newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("false").Object(),
99			expected:  newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(),
100		},
101	}
102
103	for _, test := range tests {
104		t.Run(test.name, func(t *testing.T) {
105			client := fake.NewSimpleClientset().FlowcontrolV1beta1().FlowSchemas()
106			if test.current != nil {
107				client.Create(context.TODO(), test.current, metav1.CreateOptions{})
108			}
109
110			ensurer := test.strategy(client)
111
112			err := ensurer.Ensure([]*flowcontrolv1beta1.FlowSchema{test.bootstrap})
113			if err != nil {
114				t.Fatalf("Expected no error, but got: %v", err)
115			}
116
117			fsGot, err := client.Get(context.TODO(), test.bootstrap.Name, metav1.GetOptions{})
118			switch {
119			case test.expected == nil:
120				if !apierrors.IsNotFound(err) {
121					t.Fatalf("Expected GET to return an %q error, but got: %v", metav1.StatusReasonNotFound, err)
122				}
123			case err != nil:
124				t.Fatalf("Expected GET to return no error, but got: %v", err)
125			}
126
127			if !reflect.DeepEqual(test.expected, fsGot) {
128				t.Errorf("FlowSchema does not match - diff: %s", cmp.Diff(test.expected, fsGot))
129			}
130		})
131	}
132}
133
134func TestSuggestedFSEnsureStrategy_ShouldUpdate(t *testing.T) {
135	tests := []struct {
136		name              string
137		current           *flowcontrolv1beta1.FlowSchema
138		bootstrap         *flowcontrolv1beta1.FlowSchema
139		newObjectExpected *flowcontrolv1beta1.FlowSchema
140	}{
141		{
142			name:              "auto update is enabled, first generation, spec does not match - spec update expected",
143			current:           newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").WithGeneration(1).Object(),
144			bootstrap:         newFlowSchema("fs1", "pl1", 200).Object(),
145			newObjectExpected: newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("true").WithGeneration(1).Object(),
146		},
147		{
148			name:              "auto update is enabled, first generation, spec matches - no update expected",
149			current:           newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").WithGeneration(1).Object(),
150			bootstrap:         newFlowSchema("fs1", "pl1", 100).Object(),
151			newObjectExpected: nil,
152		},
153		{
154			name:              "auto update is enabled, second generation, spec does not match - spec update expected",
155			current:           newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").WithGeneration(2).Object(),
156			bootstrap:         newFlowSchema("fs1", "pl2", 200).Object(),
157			newObjectExpected: newFlowSchema("fs1", "pl2", 200).WithAutoUpdateAnnotation("true").WithGeneration(2).Object(),
158		},
159		{
160			name:              "auto update is enabled, second generation, spec matches - no update expected",
161			current:           newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").WithGeneration(2).Object(),
162			bootstrap:         newFlowSchema("fs1", "pl1", 100).Object(),
163			newObjectExpected: nil,
164		},
165		{
166			name:              "auto update is disabled, first generation, spec does not match - no update expected",
167			current:           newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("false").WithGeneration(1).Object(),
168			bootstrap:         newFlowSchema("fs1", "pl1", 200).Object(),
169			newObjectExpected: nil,
170		},
171		{
172			name:              "auto update is disabled, first generation, spec matches - no update expected",
173			current:           newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("false").WithGeneration(1).Object(),
174			bootstrap:         newFlowSchema("fs1", "pl1", 100).Object(),
175			newObjectExpected: nil,
176		},
177		{
178			name:              "auto update is disabled, second generation, spec does not match - no update expected",
179			current:           newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("false").WithGeneration(2).Object(),
180			bootstrap:         newFlowSchema("fs1", "pl2", 200).Object(),
181			newObjectExpected: nil,
182		},
183		{
184			name:              "auto update is disabled, second generation, spec matches - no update expected",
185			current:           newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("false").WithGeneration(2).Object(),
186			bootstrap:         newFlowSchema("fs1", "pl1", 100).Object(),
187			newObjectExpected: nil,
188		},
189		{
190			name:              "annotation is missing, first generation, spec does not match - both annotation and spec update expected",
191			current:           newFlowSchema("fs1", "pl1", 100).WithGeneration(1).Object(),
192			bootstrap:         newFlowSchema("fs1", "pl2", 200).Object(),
193			newObjectExpected: newFlowSchema("fs1", "pl2", 200).WithAutoUpdateAnnotation("true").WithGeneration(1).Object(),
194		},
195		{
196			name:              "annotation is missing, first generation, spec matches - annotation update is expected",
197			current:           newFlowSchema("fs1", "pl1", 100).WithGeneration(1).Object(),
198			bootstrap:         newFlowSchema("fs1", "pl1", 100).Object(),
199			newObjectExpected: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").WithGeneration(1).Object(),
200		},
201		{
202			name:              "annotation is missing, second generation, spec does not match - annotation update is expected",
203			current:           newFlowSchema("fs1", "pl1", 100).WithGeneration(2).Object(),
204			bootstrap:         newFlowSchema("fs1", "pl2", 200).Object(),
205			newObjectExpected: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("false").WithGeneration(2).Object(),
206		},
207		{
208			name:              "annotation is missing, second generation, spec matches - annotation update is expected",
209			current:           newFlowSchema("fs1", "pl1", 100).WithGeneration(2).Object(),
210			bootstrap:         newFlowSchema("fs1", "pl1", 100).Object(),
211			newObjectExpected: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("false").WithGeneration(2).Object(),
212		},
213	}
214
215	for _, test := range tests {
216		t.Run(test.name, func(t *testing.T) {
217			strategy := newSuggestedEnsureStrategy(&flowSchemaWrapper{})
218			newObjectGot, updateGot, err := strategy.ShouldUpdate(test.current, test.bootstrap)
219			if err != nil {
220				t.Errorf("Expected no error, but got: %v", err)
221			}
222
223			if test.newObjectExpected == nil {
224				if newObjectGot != nil {
225					t.Errorf("Expected a nil object, but got: %#v", newObjectGot)
226				}
227				if updateGot {
228					t.Errorf("Expected update=%t but got: %t", false, updateGot)
229				}
230				return
231			}
232
233			if !updateGot {
234				t.Errorf("Expected update=%t but got: %t", true, updateGot)
235			}
236			if !reflect.DeepEqual(test.newObjectExpected, newObjectGot) {
237				t.Errorf("Expected the object to be updated to match - diff: %s", cmp.Diff(test.newObjectExpected, newObjectGot))
238			}
239		})
240	}
241}
242
243func TestFlowSchemaSpecChanged(t *testing.T) {
244	fs1 := &flowcontrolv1beta1.FlowSchema{
245		Spec: flowcontrolv1beta1.FlowSchemaSpec{},
246	}
247	fs2 := &flowcontrolv1beta1.FlowSchema{
248		Spec: flowcontrolv1beta1.FlowSchemaSpec{
249			MatchingPrecedence: 1,
250		},
251	}
252	fs1Defaulted := &flowcontrolv1beta1.FlowSchema{
253		Spec: flowcontrolv1beta1.FlowSchemaSpec{
254			MatchingPrecedence: flowcontrolapisv1beta1.FlowSchemaDefaultMatchingPrecedence,
255		},
256	}
257	testCases := []struct {
258		name        string
259		expected    *flowcontrolv1beta1.FlowSchema
260		actual      *flowcontrolv1beta1.FlowSchema
261		specChanged bool
262	}{
263		{
264			name:        "identical flow-schemas should work",
265			expected:    bootstrap.MandatoryFlowSchemaCatchAll,
266			actual:      bootstrap.MandatoryFlowSchemaCatchAll,
267			specChanged: false,
268		},
269		{
270			name:        "defaulted flow-schemas should work",
271			expected:    fs1,
272			actual:      fs1Defaulted,
273			specChanged: false,
274		},
275		{
276			name:        "non-defaulted flow-schema has wrong spec",
277			expected:    fs1,
278			actual:      fs2,
279			specChanged: true,
280		},
281	}
282	for _, testCase := range testCases {
283		t.Run(testCase.name, func(t *testing.T) {
284			w := flowSchemaSpecChanged(testCase.expected, testCase.actual)
285			assert.Equal(t, testCase.specChanged, w)
286		})
287	}
288}
289
290func TestRemoveFlowSchema(t *testing.T) {
291	tests := []struct {
292		name           string
293		current        *flowcontrolv1beta1.FlowSchema
294		bootstrapName  string
295		removeExpected bool
296	}{
297		{
298			name:          "flow schema does not exist",
299			bootstrapName: "fs1",
300			current:       nil,
301		},
302		{
303			name:           "flow schema exists, auto update is enabled",
304			bootstrapName:  "fs1",
305			current:        newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("true").Object(),
306			removeExpected: true,
307		},
308		{
309			name:           "flow schema exists, auto update is disabled",
310			bootstrapName:  "fs1",
311			current:        newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("false").Object(),
312			removeExpected: false,
313		},
314		{
315			name:           "flow schema exists, the auto-update annotation is malformed",
316			bootstrapName:  "fs1",
317			current:        newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("invalid").Object(),
318			removeExpected: false,
319		},
320	}
321
322	for _, test := range tests {
323		t.Run(test.name, func(t *testing.T) {
324			client := fake.NewSimpleClientset().FlowcontrolV1beta1().FlowSchemas()
325			if test.current != nil {
326				client.Create(context.TODO(), test.current, metav1.CreateOptions{})
327			}
328
329			remover := NewFlowSchemaRemover(client)
330			err := remover.Remove([]string{test.bootstrapName})
331			if err != nil {
332				t.Fatalf("Expected no error, but got: %v", err)
333			}
334
335			if test.current == nil {
336				return
337			}
338			_, err = client.Get(context.TODO(), test.bootstrapName, metav1.GetOptions{})
339			switch {
340			case test.removeExpected:
341				if !apierrors.IsNotFound(err) {
342					t.Errorf("Expected error: %q, but got: %v", metav1.StatusReasonNotFound, err)
343				}
344			default:
345				if err != nil {
346					t.Errorf("Expected no error, but got: %v", err)
347				}
348			}
349		})
350	}
351}
352
353func TestGetFlowSchemaRemoveCandidate(t *testing.T) {
354	tests := []struct {
355		name      string
356		current   []*flowcontrolv1beta1.FlowSchema
357		bootstrap []*flowcontrolv1beta1.FlowSchema
358		expected  []string
359	}{
360		{
361			name: "no object has been removed from the bootstrap configuration",
362			bootstrap: []*flowcontrolv1beta1.FlowSchema{
363				newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(),
364				newFlowSchema("fs2", "pl2", 200).WithAutoUpdateAnnotation("true").Object(),
365				newFlowSchema("fs3", "pl3", 300).WithAutoUpdateAnnotation("true").Object(),
366			},
367			current: []*flowcontrolv1beta1.FlowSchema{
368				newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(),
369				newFlowSchema("fs2", "pl2", 200).WithAutoUpdateAnnotation("true").Object(),
370				newFlowSchema("fs3", "pl3", 300).WithAutoUpdateAnnotation("true").Object(),
371			},
372			expected: []string{},
373		},
374		{
375			name:      "bootstrap is empty, all current objects with the annotation should be candidates",
376			bootstrap: []*flowcontrolv1beta1.FlowSchema{},
377			current: []*flowcontrolv1beta1.FlowSchema{
378				newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(),
379				newFlowSchema("fs2", "pl2", 200).WithAutoUpdateAnnotation("true").Object(),
380				newFlowSchema("fs3", "pl3", 300).Object(),
381			},
382			expected: []string{"fs1", "fs2"},
383		},
384		{
385			name: "object(s) have been removed from the bootstrap configuration",
386			bootstrap: []*flowcontrolv1beta1.FlowSchema{
387				newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(),
388			},
389			current: []*flowcontrolv1beta1.FlowSchema{
390				newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(),
391				newFlowSchema("fs2", "pl2", 200).WithAutoUpdateAnnotation("true").Object(),
392				newFlowSchema("fs3", "pl3", 300).WithAutoUpdateAnnotation("true").Object(),
393			},
394			expected: []string{"fs2", "fs3"},
395		},
396		{
397			name: "object(s) without the annotation key are ignored",
398			bootstrap: []*flowcontrolv1beta1.FlowSchema{
399				newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(),
400			},
401			current: []*flowcontrolv1beta1.FlowSchema{
402				newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(),
403				newFlowSchema("fs2", "pl2", 200).Object(),
404				newFlowSchema("fs3", "pl3", 300).Object(),
405			},
406			expected: []string{},
407		},
408	}
409
410	for _, test := range tests {
411		t.Run(test.name, func(t *testing.T) {
412			client := fake.NewSimpleClientset().FlowcontrolV1beta1().FlowSchemas()
413			for i := range test.current {
414				client.Create(context.TODO(), test.current[i], metav1.CreateOptions{})
415			}
416
417			removeListGot, err := GetFlowSchemaRemoveCandidate(client, test.bootstrap)
418			if err != nil {
419				t.Fatalf("Expected no error, but got: %v", err)
420			}
421
422			if !cmp.Equal(test.expected, removeListGot) {
423				t.Errorf("Remove candidate list does not match - diff: %s", cmp.Diff(test.expected, removeListGot))
424			}
425		})
426	}
427}
428
429type fsBuilder struct {
430	object *flowcontrolv1beta1.FlowSchema
431}
432
433func newFlowSchema(name, plName string, matchingPrecedence int32) *fsBuilder {
434	return &fsBuilder{
435		object: &flowcontrolv1beta1.FlowSchema{
436			ObjectMeta: metav1.ObjectMeta{
437				Name: name,
438			},
439			Spec: flowcontrolv1beta1.FlowSchemaSpec{
440				PriorityLevelConfiguration: flowcontrolv1beta1.PriorityLevelConfigurationReference{
441					Name: plName,
442				},
443				MatchingPrecedence: matchingPrecedence,
444			},
445		},
446	}
447}
448
449func (b *fsBuilder) Object() *flowcontrolv1beta1.FlowSchema {
450	return b.object
451}
452
453func (b *fsBuilder) WithGeneration(value int64) *fsBuilder {
454	b.object.SetGeneration(value)
455	return b
456}
457
458func (b *fsBuilder) WithAutoUpdateAnnotation(value string) *fsBuilder {
459	setAnnotation(b.object, value)
460	return b
461}
462
463func setAnnotation(accessor metav1.Object, value string) {
464	if accessor.GetAnnotations() == nil {
465		accessor.SetAnnotations(map[string]string{})
466	}
467
468	accessor.GetAnnotations()[flowcontrolv1beta1.AutoUpdateAnnotationKey] = value
469}
470