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