blob: a90564881eefcc243724ed67ea3031c4c3436b91 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Serge Bazanskie6030f62020-06-03 17:52:59 +02002// SPDX-License-Identifier: Apache-2.0
Serge Bazanskie6030f62020-06-03 17:52:59 +02003
4package reconciler
5
6import (
7 "context"
8 "fmt"
9 "testing"
10
Jan Schär69f5f4e2024-05-15 10:32:07 +020011 apiequality "k8s.io/apimachinery/pkg/api/equality"
12 apierrors "k8s.io/apimachinery/pkg/api/errors"
13 apivalidation "k8s.io/apimachinery/pkg/api/validation"
Serge Bazanskie6030f62020-06-03 17:52:59 +020014 meta "k8s.io/apimachinery/pkg/apis/meta/v1"
Jan Schär69f5f4e2024-05-15 10:32:07 +020015 "k8s.io/apimachinery/pkg/runtime"
16 "k8s.io/apimachinery/pkg/runtime/schema"
17 "k8s.io/apimachinery/pkg/util/validation/field"
18 installnode "k8s.io/kubernetes/pkg/apis/node/install"
19 installpolicy "k8s.io/kubernetes/pkg/apis/policy/install"
20 installrbac "k8s.io/kubernetes/pkg/apis/rbac/install"
21 installstorage "k8s.io/kubernetes/pkg/apis/storage/install"
22
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020023 "source.monogon.dev/osbase/supervisor"
Serge Bazanskie6030f62020-06-03 17:52:59 +020024)
25
Jan Schär69f5f4e2024-05-15 10:32:07 +020026// TestExpectedUniqueNames ensures that all the Expected objects of any
27// given resource type have a unique name.
28func TestExpectedUniqueNames(t *testing.T) {
29 for reconciler, r := range allResources(nil) {
30 names := make(map[string]bool)
31 for _, v := range r.Expected() {
32 if names[v.GetName()] {
33 t.Errorf("reconciler %q: duplicate name %q", reconciler, v.GetName())
34 continue
35 }
36 names[v.GetName()] = true
37 }
38 }
39}
40
Serge Bazanski216fe7b2021-05-21 18:36:16 +020041// TestExpectedLabeledCorrectly ensures that all the Expected objects of all
42// resource types have a Kubernetes metadata label that signifies it's a
43// builtin object, to be retrieved afterwards. This contract must be met in
44// order for the reconciler to not keep overwriting objects (and possibly
45// failing), when a newly created object is not then retrievable using a
Jan Schär7f727482024-03-25 13:03:51 +010046// selector corresponding to this label.
Serge Bazanskie6030f62020-06-03 17:52:59 +020047func TestExpectedLabeledCorrectly(t *testing.T) {
48 for reconciler, r := range allResources(nil) {
Jan Schär69f5f4e2024-05-15 10:32:07 +020049 for _, v := range r.Expected() {
Jan Schär7f727482024-03-25 13:03:51 +010050 if data := v.GetLabels()[BuiltinLabelKey]; data != BuiltinLabelValue {
Jan Schär69f5f4e2024-05-15 10:32:07 +020051 t.Errorf("reconciler %q, object %q: %q=%q, wanted =%q", reconciler, v.GetName(), BuiltinLabelKey, data, BuiltinLabelValue)
Serge Bazanskie6030f62020-06-03 17:52:59 +020052 continue
53 }
54 }
55 }
56}
57
Jan Schär69f5f4e2024-05-15 10:32:07 +020058// TestExpectedDefaulted ensures that all the Expected objects of all
59// resource types have defaults already applied. If this were not the case,
60// the reconciler would think that the object has changed and try to update it
61// in each iteration. If this test fails, the most likely fix is to add the
62// missing default values to the expected objects.
63func TestExpectedDefaulted(t *testing.T) {
64 scheme := runtime.NewScheme()
65 installnode.Install(scheme)
66 installpolicy.Install(scheme)
67 installrbac.Install(scheme)
68 installstorage.Install(scheme)
69
70 for reconciler, r := range allResources(nil) {
71 for _, v := range r.Expected() {
72 v_defaulted := v.(runtime.Object).DeepCopyObject()
73 if _, ok := scheme.IsUnversioned(v_defaulted); !ok {
74 t.Errorf("reconciler %q: type not installed in scheme", reconciler)
75 }
76 scheme.Default(v_defaulted)
77 if !apiequality.Semantic.DeepEqual(v, v_defaulted) {
78 t.Errorf("reconciler %q, object %q changed after defaulting\ngot: %+v\nwanted: %+v", reconciler, v.GetName(), v, v_defaulted)
79 }
80 }
81 }
82}
83
Jan Schär7f727482024-03-25 13:03:51 +010084// testObject is the object type managed by testResource.
85type testObject struct {
86 meta.ObjectMeta
Jan Schär69f5f4e2024-05-15 10:32:07 +020087 Val int
Jan Schär7f727482024-03-25 13:03:51 +010088}
89
Jan Schär69f5f4e2024-05-15 10:32:07 +020090func makeTestObject(name string, val int) *testObject {
Jan Schär7f727482024-03-25 13:03:51 +010091 return &testObject{
92 ObjectMeta: meta.ObjectMeta{
93 Name: name,
94 Labels: builtinLabels(nil),
95 },
Jan Schär69f5f4e2024-05-15 10:32:07 +020096 Val: val,
Jan Schär7f727482024-03-25 13:03:51 +010097 }
98}
99
100// testResource is a resource type used for testing. It simulates a target
101// (ie. k8s apiserver mock) that always acts nominally (all resources are
102// created, deleted as requested, and the state is consistent with requests).
Serge Bazanskie6030f62020-06-03 17:52:59 +0200103type testResource struct {
104 // current is the simulated state of resources in the target.
Jan Schär7f727482024-03-25 13:03:51 +0100105 current map[string]*testObject
Serge Bazanskie6030f62020-06-03 17:52:59 +0200106 // expected is what this type will report as the Expected() resources.
Jan Schär7f727482024-03-25 13:03:51 +0100107 expected map[string]*testObject
Serge Bazanskie6030f62020-06-03 17:52:59 +0200108}
109
Jan Schär7f727482024-03-25 13:03:51 +0100110func (r *testResource) List(ctx context.Context) ([]meta.Object, error) {
111 var cur []meta.Object
112 for _, v := range r.current {
113 v_copy := *v
114 cur = append(cur, &v_copy)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200115 }
Jan Schär7f727482024-03-25 13:03:51 +0100116 return cur, nil
Serge Bazanskie6030f62020-06-03 17:52:59 +0200117}
118
Jan Schär7f727482024-03-25 13:03:51 +0100119func (r *testResource) Create(ctx context.Context, el meta.Object) error {
120 r.current[el.GetName()] = el.(*testObject)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200121 return nil
122}
123
Jan Schär69f5f4e2024-05-15 10:32:07 +0200124func (r *testResource) Update(ctx context.Context, el meta.Object) error {
125 r.current[el.GetName()] = el.(*testObject)
126 return nil
127}
128
129func (r *testResource) Delete(ctx context.Context, name string, opts meta.DeleteOptions) error {
Serge Bazanskie6030f62020-06-03 17:52:59 +0200130 delete(r.current, name)
131 return nil
132}
133
Jan Schär7f727482024-03-25 13:03:51 +0100134func (r *testResource) Expected() []meta.Object {
135 var exp []meta.Object
136 for _, v := range r.expected {
137 v_copy := *v
138 exp = append(exp, &v_copy)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200139 }
140 return exp
141}
142
Jan Schär7f727482024-03-25 13:03:51 +0100143// newTestResource creates a test resource with a list of expected objects.
144func newTestResource(want ...*testObject) *testResource {
145 expected := make(map[string]*testObject)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200146 for _, w := range want {
Jan Schär7f727482024-03-25 13:03:51 +0100147 expected[w.GetName()] = w
Serge Bazanskie6030f62020-06-03 17:52:59 +0200148 }
149 return &testResource{
Jan Schär7f727482024-03-25 13:03:51 +0100150 current: make(map[string]*testObject),
Serge Bazanskie6030f62020-06-03 17:52:59 +0200151 expected: expected,
152 }
153}
154
Jan Schär7f727482024-03-25 13:03:51 +0100155// currentDiff returns a human-readable string showing the difference between
156// the current state and the given objects. If no difference is
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200157// present, the returned string is empty.
Jan Schär7f727482024-03-25 13:03:51 +0100158func (r *testResource) currentDiff(want ...*testObject) string {
159 expected := make(map[string]*testObject)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200160 for _, w := range want {
Jan Schär7f727482024-03-25 13:03:51 +0100161 if _, ok := r.current[w.GetName()]; !ok {
162 return fmt.Sprintf("%q missing in current", w.GetName())
Serge Bazanskie6030f62020-06-03 17:52:59 +0200163 }
Jan Schär7f727482024-03-25 13:03:51 +0100164 expected[w.GetName()] = w
Serge Bazanskie6030f62020-06-03 17:52:59 +0200165 }
166 for _, g := range r.current {
Jan Schär7f727482024-03-25 13:03:51 +0100167 if _, ok := expected[g.GetName()]; !ok {
168 return fmt.Sprintf("%q spurious in current", g.GetName())
Serge Bazanskie6030f62020-06-03 17:52:59 +0200169 }
170 }
171 return ""
172}
173
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200174// TestBasicReconciliation ensures that the reconcile function does manipulate
175// a target state based on a set of expected resources.
Serge Bazanskie6030f62020-06-03 17:52:59 +0200176func TestBasicReconciliation(t *testing.T) {
Jan Schär69f5f4e2024-05-15 10:32:07 +0200177 // This needs to run in a TestHarness to make logging work.
178 supervisor.TestHarness(t, func(ctx context.Context) error {
179 r := newTestResource(makeTestObject("foo", 0), makeTestObject("bar", 0), makeTestObject("baz", 0))
180 rname := "testresource"
Serge Bazanskie6030f62020-06-03 17:52:59 +0200181
Jan Schär69f5f4e2024-05-15 10:32:07 +0200182 // nothing should have happened yet (testing the test)
183 if diff := r.currentDiff(); diff != "" {
184 return fmt.Errorf("wrong state after creation: %s", diff)
185 }
Serge Bazanskie6030f62020-06-03 17:52:59 +0200186
Jan Schär69f5f4e2024-05-15 10:32:07 +0200187 if err := reconcile(ctx, r, rname); err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200188 return fmt.Errorf("reconcile: %w", err)
Jan Schär69f5f4e2024-05-15 10:32:07 +0200189 }
190 // everything requested should have been created
191 if diff := r.currentDiff(makeTestObject("foo", 0), makeTestObject("bar", 0), makeTestObject("baz", 0)); diff != "" {
192 return fmt.Errorf("wrong state after reconciliation: %s", diff)
193 }
Serge Bazanskie6030f62020-06-03 17:52:59 +0200194
Jan Schär69f5f4e2024-05-15 10:32:07 +0200195 delete(r.expected, "foo")
196 if err := reconcile(ctx, r, rname); err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200197 return fmt.Errorf("reconcile: %w", err)
Jan Schär69f5f4e2024-05-15 10:32:07 +0200198 }
199 // foo should now be missing
200 if diff := r.currentDiff(makeTestObject("bar", 0), makeTestObject("baz", 0)); diff != "" {
201 return fmt.Errorf("wrong state after deleting foo: %s", diff)
202 }
203
204 r.expected["bar"] = makeTestObject("bar", 1)
205 if err := reconcile(ctx, r, rname); err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200206 return fmt.Errorf("reconcile: %w", err)
Jan Schär69f5f4e2024-05-15 10:32:07 +0200207 }
208 // bar should be updated
209 if diff := r.currentDiff(makeTestObject("bar", 1), makeTestObject("baz", 0)); diff != "" {
210 return fmt.Errorf("wrong state after deleting foo: %s", diff)
211 }
212
213 return nil
214 })
215}
216
217func TestIsImmutableError(t *testing.T) {
218 gk := schema.GroupKind{Group: "someGroup", Kind: "someKind"}
219 cases := []struct {
220 err error
221 isImmutable bool
222 }{
223 {fmt.Errorf("something wrong"), false},
224 {apierrors.NewApplyConflict(nil, "conflict"), false},
225 {apierrors.NewInvalid(gk, "name", field.ErrorList{}), false},
226 {apierrors.NewInvalid(gk, "name", field.ErrorList{
227 field.Invalid(field.NewPath("field1"), true, apivalidation.FieldImmutableErrorMsg),
228 field.Invalid(field.NewPath("field2"), true, "some other error"),
229 }), false},
230 {apierrors.NewInvalid(gk, "name", field.ErrorList{
231 field.Invalid(field.NewPath("field1"), true, apivalidation.FieldImmutableErrorMsg),
232 }), true},
233 {apierrors.NewInvalid(gk, "name", field.ErrorList{
234 field.Invalid(field.NewPath("field1"), true, apivalidation.FieldImmutableErrorMsg),
235 field.Invalid(field.NewPath("field2"), true, apivalidation.FieldImmutableErrorMsg),
236 }), true},
Serge Bazanskie6030f62020-06-03 17:52:59 +0200237 }
Jan Schär69f5f4e2024-05-15 10:32:07 +0200238 for _, c := range cases {
239 actual := isImmutableError(c.err)
240 if actual != c.isImmutable {
241 t.Errorf("Expected %v, got %v for error: %v", c.isImmutable, actual, c.err)
242 }
Serge Bazanskie6030f62020-06-03 17:52:59 +0200243 }
244}