| // 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) | 
 | 	} | 
 | } |