|  | // Copyright 2020 The Monogon Project Authors. | 
|  | // | 
|  | // SPDX-License-Identifier: Apache-2.0 | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //     http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | package reconciler | 
|  |  | 
|  | import ( | 
|  | "context" | 
|  | "fmt" | 
|  | "testing" | 
|  |  | 
|  | node "k8s.io/api/node/v1beta1" | 
|  | policy "k8s.io/api/policy/v1beta1" | 
|  | rbac "k8s.io/api/rbac/v1" | 
|  | storage "k8s.io/api/storage/v1" | 
|  | meta "k8s.io/apimachinery/pkg/apis/meta/v1" | 
|  | ) | 
|  |  | 
|  | // kubernetesMeta unwraps an interface{} that might contain a Kubernetes | 
|  | // resource of type that is managed by the reconciler. Any time a new | 
|  | // Kubernetes type is managed by the reconciler, the following switch should be | 
|  | // extended to cover that type. | 
|  | func kubernetesMeta(v interface{}) *meta.ObjectMeta { | 
|  | switch v2 := v.(type) { | 
|  | case *rbac.ClusterRole: | 
|  | return &v2.ObjectMeta | 
|  | case *rbac.ClusterRoleBinding: | 
|  | return &v2.ObjectMeta | 
|  | case *storage.CSIDriver: | 
|  | return &v2.ObjectMeta | 
|  | case *storage.StorageClass: | 
|  | return &v2.ObjectMeta | 
|  | case *policy.PodSecurityPolicy: | 
|  | return &v2.ObjectMeta | 
|  | case *node.RuntimeClass: | 
|  | return &v2.ObjectMeta | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // TestExpectedNamedCorrectly ensures that all the Expected objects of all | 
|  | // resource types have a correspondence between their returned key and inner | 
|  | // name. This contract must be met in order for the reconciler to not create | 
|  | // runaway resources. This assumes all managed resources are Kubernetes | 
|  | // resources. | 
|  | func TestExpectedNamedCorrectly(t *testing.T) { | 
|  | for reconciler, r := range allResources(nil) { | 
|  | for outer, v := range r.Expected() { | 
|  | meta := kubernetesMeta(v) | 
|  | if meta == nil { | 
|  | t.Errorf("reconciler %q, object %q: could not decode kubernetes metadata", reconciler, outer) | 
|  | continue | 
|  | } | 
|  | if inner := meta.Name; outer != inner { | 
|  | t.Errorf("reconciler %q, object %q: inner name mismatch (%q)", reconciler, outer, inner) | 
|  | continue | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestExpectedLabeledCorrectly ensures that all the Expected objects of all | 
|  | // resource types have a Kubernetes metadata label that signifies it's a | 
|  | // builtin object, to be retrieved afterwards. This contract must be met in | 
|  | // order for the reconciler to not keep overwriting objects (and possibly | 
|  | // failing), when a newly created object is not then retrievable using a | 
|  | // selector corresponding to this label. This assumes all managed resources are | 
|  | // Kubernetes objects. | 
|  | func TestExpectedLabeledCorrectly(t *testing.T) { | 
|  | for reconciler, r := range allResources(nil) { | 
|  | for outer, v := range r.Expected() { | 
|  | meta := kubernetesMeta(v) | 
|  | if meta == nil { | 
|  | t.Errorf("reconciler %q, object %q: could not decode kubernetes metadata", reconciler, outer) | 
|  | continue | 
|  | } | 
|  | if data := meta.Labels[BuiltinLabelKey]; data != BuiltinLabelValue { | 
|  | t.Errorf("reconciler %q, object %q: %q=%q, wanted =%q", reconciler, outer, BuiltinLabelKey, data, BuiltinLabelValue) | 
|  | continue | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // testResource is a resource type used for testing. The inner type is a string | 
|  | // that is equal to its name (key).  It simulates a target (ie. k8s apiserver | 
|  | // mock) that always acts nominally (all resources are created, deleted as | 
|  | // requested, and the state is consistent with requests). | 
|  | type testResource struct { | 
|  | // current is the simulated state of resources in the target. | 
|  | current map[string]string | 
|  | // expected is what this type will report as the Expected() resources. | 
|  | expected map[string]string | 
|  | } | 
|  |  | 
|  | func (r *testResource) List(ctx context.Context) ([]string, error) { | 
|  | var keys []string | 
|  | for k, _ := range r.current { | 
|  | keys = append(keys, k) | 
|  | } | 
|  | return keys, nil | 
|  | } | 
|  |  | 
|  | func (r *testResource) Create(ctx context.Context, el interface{}) error { | 
|  | r.current[el.(string)] = el.(string) | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func (r *testResource) Delete(ctx context.Context, name string) error { | 
|  | delete(r.current, name) | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func (r *testResource) Expected() map[string]interface{} { | 
|  | exp := make(map[string]interface{}) | 
|  | for k, v := range r.expected { | 
|  | exp[k] = v | 
|  | } | 
|  | return exp | 
|  | } | 
|  |  | 
|  | // newTestResource creates a test resource with a list of expected resource | 
|  | // strings. | 
|  | func newTestResource(want ...string) *testResource { | 
|  | expected := make(map[string]string) | 
|  | for _, w := range want { | 
|  | expected[w] = w | 
|  | } | 
|  | return &testResource{ | 
|  | current:  make(map[string]string), | 
|  | expected: expected, | 
|  | } | 
|  | } | 
|  |  | 
|  | // currentDiff returns a human-readable string showing the different between | 
|  | // the current state and the given resource strings. If no difference is | 
|  | // present, the returned string is empty. | 
|  | func (r *testResource) currentDiff(want ...string) string { | 
|  | expected := make(map[string]string) | 
|  | for _, w := range want { | 
|  | if _, ok := r.current[w]; !ok { | 
|  | return fmt.Sprintf("%q missing in current", w) | 
|  | } | 
|  | expected[w] = w | 
|  | } | 
|  | for _, g := range r.current { | 
|  | if _, ok := expected[g]; !ok { | 
|  | return fmt.Sprintf("%q spurious in current", g) | 
|  | } | 
|  | } | 
|  | return "" | 
|  | } | 
|  |  | 
|  | // TestBasicReconciliation ensures that the reconcile function does manipulate | 
|  | // a target state based on a set of expected resources. | 
|  | func TestBasicReconciliation(t *testing.T) { | 
|  | ctx := context.Background() | 
|  | r := newTestResource("foo", "bar", "baz") | 
|  |  | 
|  | // nothing should have happened yet (testing the test) | 
|  | if diff := r.currentDiff(); diff != "" { | 
|  | t.Fatalf("wrong state after creation: %s", diff) | 
|  | } | 
|  |  | 
|  | if err := reconcile(ctx, r); err != nil { | 
|  | t.Fatalf("reconcile: %v", err) | 
|  | } | 
|  | // everything requested should have been created | 
|  | if diff := r.currentDiff("foo", "bar", "baz"); diff != "" { | 
|  | t.Fatalf("wrong state after reconciliation: %s", diff) | 
|  | } | 
|  |  | 
|  | delete(r.expected, "foo") | 
|  | if err := reconcile(ctx, r); err != nil { | 
|  | t.Fatalf("reconcile: %v", err) | 
|  | } | 
|  | // foo should not be missing | 
|  | if diff := r.currentDiff("bar", "baz"); diff != "" { | 
|  | t.Fatalf("wrong state after deleting foo: %s", diff) | 
|  | } | 
|  | } |