blob: ba2f4e8836bb81eb32e8ccda64b25533b66d7eb5 [file] [log] [blame]
// 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)
}
}