blob: ba2f4e8836bb81eb32e8ccda64b25533b66d7eb5 [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
Lorenz Brun5e4fc2d2020-09-22 18:35:15 +020024 node "k8s.io/api/node/v1beta1"
Serge Bazanskie6030f62020-06-03 17:52:59 +020025 policy "k8s.io/api/policy/v1beta1"
26 rbac "k8s.io/api/rbac/v1"
27 storage "k8s.io/api/storage/v1"
28 meta "k8s.io/apimachinery/pkg/apis/meta/v1"
29)
30
Serge Bazanski216fe7b2021-05-21 18:36:16 +020031// kubernetesMeta unwraps an interface{} that might contain a Kubernetes
32// resource of type that is managed by the reconciler. Any time a new
33// Kubernetes type is managed by the reconciler, the following switch should be
34// extended to cover that type.
Serge Bazanskie6030f62020-06-03 17:52:59 +020035func kubernetesMeta(v interface{}) *meta.ObjectMeta {
36 switch v2 := v.(type) {
37 case *rbac.ClusterRole:
38 return &v2.ObjectMeta
39 case *rbac.ClusterRoleBinding:
40 return &v2.ObjectMeta
41 case *storage.CSIDriver:
42 return &v2.ObjectMeta
43 case *storage.StorageClass:
44 return &v2.ObjectMeta
45 case *policy.PodSecurityPolicy:
46 return &v2.ObjectMeta
Lorenz Brun5e4fc2d2020-09-22 18:35:15 +020047 case *node.RuntimeClass:
48 return &v2.ObjectMeta
Serge Bazanskie6030f62020-06-03 17:52:59 +020049 }
50 return nil
51}
52
Serge Bazanski216fe7b2021-05-21 18:36:16 +020053// TestExpectedNamedCorrectly ensures that all the Expected objects of all
54// resource types have a correspondence between their returned key and inner
55// name. This contract must be met in order for the reconciler to not create
56// runaway resources. This assumes all managed resources are Kubernetes
57// resources.
Serge Bazanskie6030f62020-06-03 17:52:59 +020058func TestExpectedNamedCorrectly(t *testing.T) {
59 for reconciler, r := range allResources(nil) {
60 for outer, v := range r.Expected() {
61 meta := kubernetesMeta(v)
62 if meta == nil {
63 t.Errorf("reconciler %q, object %q: could not decode kubernetes metadata", reconciler, outer)
64 continue
65 }
66 if inner := meta.Name; outer != inner {
67 t.Errorf("reconciler %q, object %q: inner name mismatch (%q)", reconciler, outer, inner)
68 continue
69 }
70 }
71 }
72}
73
Serge Bazanski216fe7b2021-05-21 18:36:16 +020074// TestExpectedLabeledCorrectly ensures that all the Expected objects of all
75// resource types have a Kubernetes metadata label that signifies it's a
76// builtin object, to be retrieved afterwards. This contract must be met in
77// order for the reconciler to not keep overwriting objects (and possibly
78// failing), when a newly created object is not then retrievable using a
79// selector corresponding to this label. This assumes all managed resources are
80// Kubernetes objects.
Serge Bazanskie6030f62020-06-03 17:52:59 +020081func TestExpectedLabeledCorrectly(t *testing.T) {
82 for reconciler, r := range allResources(nil) {
83 for outer, v := range r.Expected() {
84 meta := kubernetesMeta(v)
85 if meta == nil {
86 t.Errorf("reconciler %q, object %q: could not decode kubernetes metadata", reconciler, outer)
87 continue
88 }
89 if data := meta.Labels[BuiltinLabelKey]; data != BuiltinLabelValue {
90 t.Errorf("reconciler %q, object %q: %q=%q, wanted =%q", reconciler, outer, BuiltinLabelKey, data, BuiltinLabelValue)
91 continue
92 }
93 }
94 }
95}
96
Serge Bazanski216fe7b2021-05-21 18:36:16 +020097// testResource is a resource type used for testing. The inner type is a string
98// that is equal to its name (key). It simulates a target (ie. k8s apiserver
99// mock) that always acts nominally (all resources are created, deleted as
Serge Bazanskie6030f62020-06-03 17:52:59 +0200100// requested, and the state is consistent with requests).
101type testResource struct {
102 // current is the simulated state of resources in the target.
103 current map[string]string
104 // expected is what this type will report as the Expected() resources.
105 expected map[string]string
106}
107
108func (r *testResource) List(ctx context.Context) ([]string, error) {
109 var keys []string
110 for k, _ := range r.current {
111 keys = append(keys, k)
112 }
113 return keys, nil
114}
115
116func (r *testResource) Create(ctx context.Context, el interface{}) error {
117 r.current[el.(string)] = el.(string)
118 return nil
119}
120
121func (r *testResource) Delete(ctx context.Context, name string) error {
122 delete(r.current, name)
123 return nil
124}
125
126func (r *testResource) Expected() map[string]interface{} {
127 exp := make(map[string]interface{})
128 for k, v := range r.expected {
129 exp[k] = v
130 }
131 return exp
132}
133
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200134// newTestResource creates a test resource with a list of expected resource
135// strings.
Serge Bazanskie6030f62020-06-03 17:52:59 +0200136func newTestResource(want ...string) *testResource {
137 expected := make(map[string]string)
138 for _, w := range want {
139 expected[w] = w
140 }
141 return &testResource{
142 current: make(map[string]string),
143 expected: expected,
144 }
145}
146
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200147// currentDiff returns a human-readable string showing the different between
148// the current state and the given resource strings. If no difference is
149// present, the returned string is empty.
Serge Bazanskie6030f62020-06-03 17:52:59 +0200150func (r *testResource) currentDiff(want ...string) string {
151 expected := make(map[string]string)
152 for _, w := range want {
153 if _, ok := r.current[w]; !ok {
154 return fmt.Sprintf("%q missing in current", w)
155 }
156 expected[w] = w
157 }
158 for _, g := range r.current {
159 if _, ok := expected[g]; !ok {
160 return fmt.Sprintf("%q spurious in current", g)
161 }
162 }
163 return ""
164}
165
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200166// TestBasicReconciliation ensures that the reconcile function does manipulate
167// a target state based on a set of expected resources.
Serge Bazanskie6030f62020-06-03 17:52:59 +0200168func TestBasicReconciliation(t *testing.T) {
169 ctx := context.Background()
170 r := newTestResource("foo", "bar", "baz")
171
172 // nothing should have happened yet (testing the test)
173 if diff := r.currentDiff(); diff != "" {
174 t.Fatalf("wrong state after creation: %s", diff)
175 }
176
177 if err := reconcile(ctx, r); err != nil {
178 t.Fatalf("reconcile: %v", err)
179 }
180 // everything requested should have been created
181 if diff := r.currentDiff("foo", "bar", "baz"); diff != "" {
182 t.Fatalf("wrong state after reconciliation: %s", diff)
183 }
184
185 delete(r.expected, "foo")
186 if err := reconcile(ctx, r); err != nil {
187 t.Fatalf("reconcile: %v", err)
188 }
189 // foo should not be missing
190 if diff := r.currentDiff("bar", "baz"); diff != "" {
191 t.Fatalf("wrong state after deleting foo: %s", diff)
192 }
193}