blob: b791dbe9650b998a2d027f3c335bea2c71637e6a [file] [log] [blame]
Serge Bazanskie6030f62020-06-03 17:52:59 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package reconciler
18
19import (
20 "context"
21 "fmt"
22 "testing"
23
Jan Schär69f5f4e2024-05-15 10:32:07 +020024 apiequality "k8s.io/apimachinery/pkg/api/equality"
25 apierrors "k8s.io/apimachinery/pkg/api/errors"
26 apivalidation "k8s.io/apimachinery/pkg/api/validation"
Serge Bazanskie6030f62020-06-03 17:52:59 +020027 meta "k8s.io/apimachinery/pkg/apis/meta/v1"
Jan Schär69f5f4e2024-05-15 10:32:07 +020028 "k8s.io/apimachinery/pkg/runtime"
29 "k8s.io/apimachinery/pkg/runtime/schema"
30 "k8s.io/apimachinery/pkg/util/validation/field"
31 installnode "k8s.io/kubernetes/pkg/apis/node/install"
32 installpolicy "k8s.io/kubernetes/pkg/apis/policy/install"
33 installrbac "k8s.io/kubernetes/pkg/apis/rbac/install"
34 installstorage "k8s.io/kubernetes/pkg/apis/storage/install"
35
36 "source.monogon.dev/metropolis/pkg/supervisor"
Serge Bazanskie6030f62020-06-03 17:52:59 +020037)
38
Jan Schär69f5f4e2024-05-15 10:32:07 +020039// TestExpectedUniqueNames ensures that all the Expected objects of any
40// given resource type have a unique name.
41func TestExpectedUniqueNames(t *testing.T) {
42 for reconciler, r := range allResources(nil) {
43 names := make(map[string]bool)
44 for _, v := range r.Expected() {
45 if names[v.GetName()] {
46 t.Errorf("reconciler %q: duplicate name %q", reconciler, v.GetName())
47 continue
48 }
49 names[v.GetName()] = true
50 }
51 }
52}
53
Serge Bazanski216fe7b2021-05-21 18:36:16 +020054// TestExpectedLabeledCorrectly ensures that all the Expected objects of all
55// resource types have a Kubernetes metadata label that signifies it's a
56// builtin object, to be retrieved afterwards. This contract must be met in
57// order for the reconciler to not keep overwriting objects (and possibly
58// failing), when a newly created object is not then retrievable using a
Jan Schär7f727482024-03-25 13:03:51 +010059// selector corresponding to this label.
Serge Bazanskie6030f62020-06-03 17:52:59 +020060func TestExpectedLabeledCorrectly(t *testing.T) {
61 for reconciler, r := range allResources(nil) {
Jan Schär69f5f4e2024-05-15 10:32:07 +020062 for _, v := range r.Expected() {
Jan Schär7f727482024-03-25 13:03:51 +010063 if data := v.GetLabels()[BuiltinLabelKey]; data != BuiltinLabelValue {
Jan Schär69f5f4e2024-05-15 10:32:07 +020064 t.Errorf("reconciler %q, object %q: %q=%q, wanted =%q", reconciler, v.GetName(), BuiltinLabelKey, data, BuiltinLabelValue)
Serge Bazanskie6030f62020-06-03 17:52:59 +020065 continue
66 }
67 }
68 }
69}
70
Jan Schär69f5f4e2024-05-15 10:32:07 +020071// TestExpectedDefaulted ensures that all the Expected objects of all
72// resource types have defaults already applied. If this were not the case,
73// the reconciler would think that the object has changed and try to update it
74// in each iteration. If this test fails, the most likely fix is to add the
75// missing default values to the expected objects.
76func TestExpectedDefaulted(t *testing.T) {
77 scheme := runtime.NewScheme()
78 installnode.Install(scheme)
79 installpolicy.Install(scheme)
80 installrbac.Install(scheme)
81 installstorage.Install(scheme)
82
83 for reconciler, r := range allResources(nil) {
84 for _, v := range r.Expected() {
85 v_defaulted := v.(runtime.Object).DeepCopyObject()
86 if _, ok := scheme.IsUnversioned(v_defaulted); !ok {
87 t.Errorf("reconciler %q: type not installed in scheme", reconciler)
88 }
89 scheme.Default(v_defaulted)
90 if !apiequality.Semantic.DeepEqual(v, v_defaulted) {
91 t.Errorf("reconciler %q, object %q changed after defaulting\ngot: %+v\nwanted: %+v", reconciler, v.GetName(), v, v_defaulted)
92 }
93 }
94 }
95}
96
Jan Schär7f727482024-03-25 13:03:51 +010097// testObject is the object type managed by testResource.
98type testObject struct {
99 meta.ObjectMeta
Jan Schär69f5f4e2024-05-15 10:32:07 +0200100 Val int
Jan Schär7f727482024-03-25 13:03:51 +0100101}
102
Jan Schär69f5f4e2024-05-15 10:32:07 +0200103func makeTestObject(name string, val int) *testObject {
Jan Schär7f727482024-03-25 13:03:51 +0100104 return &testObject{
105 ObjectMeta: meta.ObjectMeta{
106 Name: name,
107 Labels: builtinLabels(nil),
108 },
Jan Schär69f5f4e2024-05-15 10:32:07 +0200109 Val: val,
Jan Schär7f727482024-03-25 13:03:51 +0100110 }
111}
112
113// testResource is a resource type used for testing. It simulates a target
114// (ie. k8s apiserver mock) that always acts nominally (all resources are
115// created, deleted as requested, and the state is consistent with requests).
Serge Bazanskie6030f62020-06-03 17:52:59 +0200116type testResource struct {
117 // current is the simulated state of resources in the target.
Jan Schär7f727482024-03-25 13:03:51 +0100118 current map[string]*testObject
Serge Bazanskie6030f62020-06-03 17:52:59 +0200119 // expected is what this type will report as the Expected() resources.
Jan Schär7f727482024-03-25 13:03:51 +0100120 expected map[string]*testObject
Serge Bazanskie6030f62020-06-03 17:52:59 +0200121}
122
Jan Schär7f727482024-03-25 13:03:51 +0100123func (r *testResource) List(ctx context.Context) ([]meta.Object, error) {
124 var cur []meta.Object
125 for _, v := range r.current {
126 v_copy := *v
127 cur = append(cur, &v_copy)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200128 }
Jan Schär7f727482024-03-25 13:03:51 +0100129 return cur, nil
Serge Bazanskie6030f62020-06-03 17:52:59 +0200130}
131
Jan Schär7f727482024-03-25 13:03:51 +0100132func (r *testResource) Create(ctx context.Context, el meta.Object) error {
133 r.current[el.GetName()] = el.(*testObject)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200134 return nil
135}
136
Jan Schär69f5f4e2024-05-15 10:32:07 +0200137func (r *testResource) Update(ctx context.Context, el meta.Object) error {
138 r.current[el.GetName()] = el.(*testObject)
139 return nil
140}
141
142func (r *testResource) Delete(ctx context.Context, name string, opts meta.DeleteOptions) error {
Serge Bazanskie6030f62020-06-03 17:52:59 +0200143 delete(r.current, name)
144 return nil
145}
146
Jan Schär7f727482024-03-25 13:03:51 +0100147func (r *testResource) Expected() []meta.Object {
148 var exp []meta.Object
149 for _, v := range r.expected {
150 v_copy := *v
151 exp = append(exp, &v_copy)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200152 }
153 return exp
154}
155
Jan Schär7f727482024-03-25 13:03:51 +0100156// newTestResource creates a test resource with a list of expected objects.
157func newTestResource(want ...*testObject) *testResource {
158 expected := make(map[string]*testObject)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200159 for _, w := range want {
Jan Schär7f727482024-03-25 13:03:51 +0100160 expected[w.GetName()] = w
Serge Bazanskie6030f62020-06-03 17:52:59 +0200161 }
162 return &testResource{
Jan Schär7f727482024-03-25 13:03:51 +0100163 current: make(map[string]*testObject),
Serge Bazanskie6030f62020-06-03 17:52:59 +0200164 expected: expected,
165 }
166}
167
Jan Schär7f727482024-03-25 13:03:51 +0100168// currentDiff returns a human-readable string showing the difference between
169// the current state and the given objects. If no difference is
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200170// present, the returned string is empty.
Jan Schär7f727482024-03-25 13:03:51 +0100171func (r *testResource) currentDiff(want ...*testObject) string {
172 expected := make(map[string]*testObject)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200173 for _, w := range want {
Jan Schär7f727482024-03-25 13:03:51 +0100174 if _, ok := r.current[w.GetName()]; !ok {
175 return fmt.Sprintf("%q missing in current", w.GetName())
Serge Bazanskie6030f62020-06-03 17:52:59 +0200176 }
Jan Schär7f727482024-03-25 13:03:51 +0100177 expected[w.GetName()] = w
Serge Bazanskie6030f62020-06-03 17:52:59 +0200178 }
179 for _, g := range r.current {
Jan Schär7f727482024-03-25 13:03:51 +0100180 if _, ok := expected[g.GetName()]; !ok {
181 return fmt.Sprintf("%q spurious in current", g.GetName())
Serge Bazanskie6030f62020-06-03 17:52:59 +0200182 }
183 }
184 return ""
185}
186
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200187// TestBasicReconciliation ensures that the reconcile function does manipulate
188// a target state based on a set of expected resources.
Serge Bazanskie6030f62020-06-03 17:52:59 +0200189func TestBasicReconciliation(t *testing.T) {
Jan Schär69f5f4e2024-05-15 10:32:07 +0200190 // This needs to run in a TestHarness to make logging work.
191 supervisor.TestHarness(t, func(ctx context.Context) error {
192 r := newTestResource(makeTestObject("foo", 0), makeTestObject("bar", 0), makeTestObject("baz", 0))
193 rname := "testresource"
Serge Bazanskie6030f62020-06-03 17:52:59 +0200194
Jan Schär69f5f4e2024-05-15 10:32:07 +0200195 // nothing should have happened yet (testing the test)
196 if diff := r.currentDiff(); diff != "" {
197 return fmt.Errorf("wrong state after creation: %s", diff)
198 }
Serge Bazanskie6030f62020-06-03 17:52:59 +0200199
Jan Schär69f5f4e2024-05-15 10:32:07 +0200200 if err := reconcile(ctx, r, rname); err != nil {
201 return fmt.Errorf("reconcile: %v", err)
202 }
203 // everything requested should have been created
204 if diff := r.currentDiff(makeTestObject("foo", 0), makeTestObject("bar", 0), makeTestObject("baz", 0)); diff != "" {
205 return fmt.Errorf("wrong state after reconciliation: %s", diff)
206 }
Serge Bazanskie6030f62020-06-03 17:52:59 +0200207
Jan Schär69f5f4e2024-05-15 10:32:07 +0200208 delete(r.expected, "foo")
209 if err := reconcile(ctx, r, rname); err != nil {
210 return fmt.Errorf("reconcile: %v", err)
211 }
212 // foo should now be missing
213 if diff := r.currentDiff(makeTestObject("bar", 0), makeTestObject("baz", 0)); diff != "" {
214 return fmt.Errorf("wrong state after deleting foo: %s", diff)
215 }
216
217 r.expected["bar"] = makeTestObject("bar", 1)
218 if err := reconcile(ctx, r, rname); err != nil {
219 return fmt.Errorf("reconcile: %v", err)
220 }
221 // bar should be updated
222 if diff := r.currentDiff(makeTestObject("bar", 1), makeTestObject("baz", 0)); diff != "" {
223 return fmt.Errorf("wrong state after deleting foo: %s", diff)
224 }
225
226 return nil
227 })
228}
229
230func TestIsImmutableError(t *testing.T) {
231 gk := schema.GroupKind{Group: "someGroup", Kind: "someKind"}
232 cases := []struct {
233 err error
234 isImmutable bool
235 }{
236 {fmt.Errorf("something wrong"), false},
237 {apierrors.NewApplyConflict(nil, "conflict"), false},
238 {apierrors.NewInvalid(gk, "name", field.ErrorList{}), false},
239 {apierrors.NewInvalid(gk, "name", field.ErrorList{
240 field.Invalid(field.NewPath("field1"), true, apivalidation.FieldImmutableErrorMsg),
241 field.Invalid(field.NewPath("field2"), true, "some other error"),
242 }), false},
243 {apierrors.NewInvalid(gk, "name", field.ErrorList{
244 field.Invalid(field.NewPath("field1"), true, apivalidation.FieldImmutableErrorMsg),
245 }), true},
246 {apierrors.NewInvalid(gk, "name", field.ErrorList{
247 field.Invalid(field.NewPath("field1"), true, apivalidation.FieldImmutableErrorMsg),
248 field.Invalid(field.NewPath("field2"), true, apivalidation.FieldImmutableErrorMsg),
249 }), true},
Serge Bazanskie6030f62020-06-03 17:52:59 +0200250 }
Jan Schär69f5f4e2024-05-15 10:32:07 +0200251 for _, c := range cases {
252 actual := isImmutableError(c.err)
253 if actual != c.isImmutable {
254 t.Errorf("Expected %v, got %v for error: %v", c.isImmutable, actual, c.err)
255 }
Serge Bazanskie6030f62020-06-03 17:52:59 +0200256 }
257}