m/n/k/reconciler: refactor resource interface

Replace interface{} with meta.Object, an interface which provides 
accessors for and is implemented by meta.ObjectMeta. List now returns 
the objects themselves instead of their names. This makes the reconciler 
slightly less generic, as it now only supports kubernetes objects.

This is a refactoring in preparation for implementing updates in the 
reconciler. There should be no change in behavior.

Change-Id: I97a4b1c0166a1e6fd0f247ee04e7c44cff570fd7
Reviewed-on: https://review.monogon.dev/c/monogon/+/2891
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
Vouch-Run-CI: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/node/kubernetes/reconciler/BUILD.bazel b/metropolis/node/kubernetes/reconciler/BUILD.bazel
index bba9f4c..e4bc41d 100644
--- a/metropolis/node/kubernetes/reconciler/BUILD.bazel
+++ b/metropolis/node/kubernetes/reconciler/BUILD.bazel
@@ -28,11 +28,5 @@
     name = "reconciler_test",
     srcs = ["reconciler_test.go"],
     embed = [":reconciler"],
-    deps = [
-        "@io_k8s_api//node/v1beta1",
-        "@io_k8s_api//policy/v1beta1",
-        "@io_k8s_api//rbac/v1:rbac",
-        "@io_k8s_api//storage/v1:storage",
-        "@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
-    ],
+    deps = ["@io_k8s_apimachinery//pkg/apis/meta/v1:meta"],
 )
diff --git a/metropolis/node/kubernetes/reconciler/reconciler.go b/metropolis/node/kubernetes/reconciler/reconciler.go
index dfbb42d..6c13df0 100644
--- a/metropolis/node/kubernetes/reconciler/reconciler.go
+++ b/metropolis/node/kubernetes/reconciler/reconciler.go
@@ -96,23 +96,20 @@
 }
 
 // resource is a type of resource to be managed by the reconciler. All
-// builti-ins/reconciled objects must implement this interface to be managed
+// built-ins/reconciled objects must implement this interface to be managed
 // correctly by the reconciler.
 type resource interface {
-	// List returns a list of names of objects current present on the target
+	// List returns a list of objects currently present on the target
 	// (ie. k8s API server).
-	List(ctx context.Context) ([]string, error)
-	// Create creates an object on the target. The el interface{} argument is
-	// the black box object returned by the Expected() call.
-	Create(ctx context.Context, el interface{}) error
-	// Delete delete an object, by name, from the target.
+	List(ctx context.Context) ([]meta.Object, error)
+	// Create creates an object on the target. The el argument is
+	// an object returned by the Expected() call.
+	Create(ctx context.Context, el meta.Object) error
+	// Delete deletes an object, by name, from the target.
 	Delete(ctx context.Context, name string) error
-	// Expected returns a map of all objects expected to be present on the
-	// target. The keys are names (which must correspond to the names returned
-	// by List() and used by Delete(), and the values are blackboxes that will
-	// then be passed to the Create() call if their corresponding key (name)
-	// does not exist on the target.
-	Expected() map[string]interface{}
+	// Expected returns a list of all objects expected to be present on the
+	// target. Objects are identified by their name, as returned by GetName.
+	Expected() []meta.Object
 }
 
 func allResources(clientSet kubernetes.Interface) map[string]resource {
@@ -162,19 +159,25 @@
 	if err != nil {
 		return err
 	}
-	presentSet := make(map[string]bool)
+	presentMap := make(map[string]meta.Object)
 	for _, el := range present {
-		presentSet[el] = true
+		presentMap[el.GetName()] = el
 	}
-	expectedMap := r.Expected()
-	for name, el := range expectedMap {
-		if !presentSet[name] {
-			if err := r.Create(ctx, el); err != nil {
+	expected := r.Expected()
+	expectedMap := make(map[string]meta.Object)
+	for _, el := range expected {
+		expectedMap[el.GetName()] = el
+	}
+	for name, expectedEl := range expectedMap {
+		if _, ok := presentMap[name]; ok {
+			// TODO(#288): update the object if it is different than expected.
+		} else {
+			if err := r.Create(ctx, expectedEl); err != nil {
 				return err
 			}
 		}
 	}
-	for name, _ := range presentSet {
+	for name, _ := range presentMap {
 		if _, ok := expectedMap[name]; !ok {
 			if err := r.Delete(ctx, name); err != nil {
 				return err
diff --git a/metropolis/node/kubernetes/reconciler/reconciler_test.go b/metropolis/node/kubernetes/reconciler/reconciler_test.go
index ba2f4e8..b72ccb9 100644
--- a/metropolis/node/kubernetes/reconciler/reconciler_test.go
+++ b/metropolis/node/kubernetes/reconciler/reconciler_test.go
@@ -21,72 +21,19 @@
 	"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.
+// selector corresponding to this label.
 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 {
+			if data := v.GetLabels()[BuiltinLabelKey]; data != BuiltinLabelValue {
 				t.Errorf("reconciler %q, object %q: %q=%q, wanted =%q", reconciler, outer, BuiltinLabelKey, data, BuiltinLabelValue)
 				continue
 			}
@@ -94,27 +41,41 @@
 	}
 }
 
-// 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).
+// testObject is the object type managed by testResource.
+type testObject struct {
+	meta.ObjectMeta
+}
+
+func makeTestObject(name string) *testObject {
+	return &testObject{
+		ObjectMeta: meta.ObjectMeta{
+			Name:   name,
+			Labels: builtinLabels(nil),
+		},
+	}
+}
+
+// testResource is a resource type used for testing. 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
+	current map[string]*testObject
 	// expected is what this type will report as the Expected() resources.
-	expected map[string]string
+	expected map[string]*testObject
 }
 
-func (r *testResource) List(ctx context.Context) ([]string, error) {
-	var keys []string
-	for k, _ := range r.current {
-		keys = append(keys, k)
+func (r *testResource) List(ctx context.Context) ([]meta.Object, error) {
+	var cur []meta.Object
+	for _, v := range r.current {
+		v_copy := *v
+		cur = append(cur, &v_copy)
 	}
-	return keys, nil
+	return cur, nil
 }
 
-func (r *testResource) Create(ctx context.Context, el interface{}) error {
-	r.current[el.(string)] = el.(string)
+func (r *testResource) Create(ctx context.Context, el meta.Object) error {
+	r.current[el.GetName()] = el.(*testObject)
 	return nil
 }
 
@@ -123,41 +84,41 @@
 	return nil
 }
 
-func (r *testResource) Expected() map[string]interface{} {
-	exp := make(map[string]interface{})
-	for k, v := range r.expected {
-		exp[k] = v
+func (r *testResource) Expected() []meta.Object {
+	var exp []meta.Object
+	for _, v := range r.expected {
+		v_copy := *v
+		exp = append(exp, &v_copy)
 	}
 	return exp
 }
 
-// newTestResource creates a test resource with a list of expected resource
-// strings.
-func newTestResource(want ...string) *testResource {
-	expected := make(map[string]string)
+// newTestResource creates a test resource with a list of expected objects.
+func newTestResource(want ...*testObject) *testResource {
+	expected := make(map[string]*testObject)
 	for _, w := range want {
-		expected[w] = w
+		expected[w.GetName()] = w
 	}
 	return &testResource{
-		current:  make(map[string]string),
+		current:  make(map[string]*testObject),
 		expected: expected,
 	}
 }
 
-// currentDiff returns a human-readable string showing the different between
-// the current state and the given resource strings. If no difference is
+// currentDiff returns a human-readable string showing the difference between
+// the current state and the given objects. If no difference is
 // present, the returned string is empty.
-func (r *testResource) currentDiff(want ...string) string {
-	expected := make(map[string]string)
+func (r *testResource) currentDiff(want ...*testObject) string {
+	expected := make(map[string]*testObject)
 	for _, w := range want {
-		if _, ok := r.current[w]; !ok {
-			return fmt.Sprintf("%q missing in current", w)
+		if _, ok := r.current[w.GetName()]; !ok {
+			return fmt.Sprintf("%q missing in current", w.GetName())
 		}
-		expected[w] = w
+		expected[w.GetName()] = w
 	}
 	for _, g := range r.current {
-		if _, ok := expected[g]; !ok {
-			return fmt.Sprintf("%q spurious in current", g)
+		if _, ok := expected[g.GetName()]; !ok {
+			return fmt.Sprintf("%q spurious in current", g.GetName())
 		}
 	}
 	return ""
@@ -167,7 +128,7 @@
 // a target state based on a set of expected resources.
 func TestBasicReconciliation(t *testing.T) {
 	ctx := context.Background()
-	r := newTestResource("foo", "bar", "baz")
+	r := newTestResource(makeTestObject("foo"), makeTestObject("bar"), makeTestObject("baz"))
 
 	// nothing should have happened yet (testing the test)
 	if diff := r.currentDiff(); diff != "" {
@@ -178,7 +139,7 @@
 		t.Fatalf("reconcile: %v", err)
 	}
 	// everything requested should have been created
-	if diff := r.currentDiff("foo", "bar", "baz"); diff != "" {
+	if diff := r.currentDiff(makeTestObject("foo"), makeTestObject("bar"), makeTestObject("baz")); diff != "" {
 		t.Fatalf("wrong state after reconciliation: %s", diff)
 	}
 
@@ -186,8 +147,8 @@
 	if err := reconcile(ctx, r); err != nil {
 		t.Fatalf("reconcile: %v", err)
 	}
-	// foo should not be missing
-	if diff := r.currentDiff("bar", "baz"); diff != "" {
+	// foo should now be missing
+	if diff := r.currentDiff(makeTestObject("bar"), makeTestObject("baz")); diff != "" {
 		t.Fatalf("wrong state after deleting foo: %s", diff)
 	}
 }
diff --git a/metropolis/node/kubernetes/reconciler/resources_csi.go b/metropolis/node/kubernetes/reconciler/resources_csi.go
index 24939f0..cec00fd 100644
--- a/metropolis/node/kubernetes/reconciler/resources_csi.go
+++ b/metropolis/node/kubernetes/reconciler/resources_csi.go
@@ -35,19 +35,19 @@
 	kubernetes.Interface
 }
 
-func (r resourceCSIDrivers) List(ctx context.Context) ([]string, error) {
+func (r resourceCSIDrivers) List(ctx context.Context) ([]meta.Object, error) {
 	res, err := r.StorageV1().CSIDrivers().List(ctx, listBuiltins)
 	if err != nil {
 		return nil, err
 	}
-	objs := make([]string, len(res.Items))
-	for i, el := range res.Items {
-		objs[i] = el.ObjectMeta.Name
+	objs := make([]meta.Object, len(res.Items))
+	for i := range res.Items {
+		objs[i] = &res.Items[i]
 	}
 	return objs, nil
 }
 
-func (r resourceCSIDrivers) Create(ctx context.Context, el interface{}) error {
+func (r resourceCSIDrivers) Create(ctx context.Context, el meta.Object) error {
 	_, err := r.StorageV1().CSIDrivers().Create(ctx, el.(*storage.CSIDriver), meta.CreateOptions{})
 	return err
 }
@@ -56,10 +56,10 @@
 	return r.StorageV1().CSIDrivers().Delete(ctx, name, meta.DeleteOptions{})
 }
 
-func (r resourceCSIDrivers) Expected() map[string]interface{} {
+func (r resourceCSIDrivers) Expected() []meta.Object {
 	fsGroupPolicy := storage.FileFSGroupPolicy
-	return map[string]interface{}{
-		csiProvisionerName: &storage.CSIDriver{
+	return []meta.Object{
+		&storage.CSIDriver{
 			ObjectMeta: meta.ObjectMeta{
 				Name:   csiProvisionerName,
 				Labels: builtinLabels(nil),
diff --git a/metropolis/node/kubernetes/reconciler/resources_podsecuritypolicy.go b/metropolis/node/kubernetes/reconciler/resources_podsecuritypolicy.go
index 507089f..97a38dd 100644
--- a/metropolis/node/kubernetes/reconciler/resources_podsecuritypolicy.go
+++ b/metropolis/node/kubernetes/reconciler/resources_podsecuritypolicy.go
@@ -29,19 +29,19 @@
 	kubernetes.Interface
 }
 
-func (r resourcePodSecurityPolicies) List(ctx context.Context) ([]string, error) {
+func (r resourcePodSecurityPolicies) List(ctx context.Context) ([]meta.Object, error) {
 	res, err := r.PolicyV1beta1().PodSecurityPolicies().List(ctx, listBuiltins)
 	if err != nil {
 		return nil, err
 	}
-	objs := make([]string, len(res.Items))
-	for i, el := range res.Items {
-		objs[i] = el.ObjectMeta.Name
+	objs := make([]meta.Object, len(res.Items))
+	for i := range res.Items {
+		objs[i] = &res.Items[i]
 	}
 	return objs, nil
 }
 
-func (r resourcePodSecurityPolicies) Create(ctx context.Context, el interface{}) error {
+func (r resourcePodSecurityPolicies) Create(ctx context.Context, el meta.Object) error {
 	_, err := r.PolicyV1beta1().PodSecurityPolicies().Create(ctx, el.(*policy.PodSecurityPolicy), meta.CreateOptions{})
 	return err
 }
@@ -50,9 +50,9 @@
 	return r.PolicyV1beta1().PodSecurityPolicies().Delete(ctx, name, meta.DeleteOptions{})
 }
 
-func (r resourcePodSecurityPolicies) Expected() map[string]interface{} {
-	return map[string]interface{}{
-		"default": &policy.PodSecurityPolicy{
+func (r resourcePodSecurityPolicies) Expected() []meta.Object {
+	return []meta.Object{
+		&policy.PodSecurityPolicy{
 			ObjectMeta: meta.ObjectMeta{
 				Name:   "default",
 				Labels: builtinLabels(nil),
diff --git a/metropolis/node/kubernetes/reconciler/resources_rbac.go b/metropolis/node/kubernetes/reconciler/resources_rbac.go
index 4eab82e..702ee6b 100644
--- a/metropolis/node/kubernetes/reconciler/resources_rbac.go
+++ b/metropolis/node/kubernetes/reconciler/resources_rbac.go
@@ -39,19 +39,19 @@
 	kubernetes.Interface
 }
 
-func (r resourceClusterRoles) List(ctx context.Context) ([]string, error) {
+func (r resourceClusterRoles) List(ctx context.Context) ([]meta.Object, error) {
 	res, err := r.RbacV1().ClusterRoles().List(ctx, listBuiltins)
 	if err != nil {
 		return nil, err
 	}
-	objs := make([]string, len(res.Items))
-	for i, el := range res.Items {
-		objs[i] = el.ObjectMeta.Name
+	objs := make([]meta.Object, len(res.Items))
+	for i := range res.Items {
+		objs[i] = &res.Items[i]
 	}
 	return objs, nil
 }
 
-func (r resourceClusterRoles) Create(ctx context.Context, el interface{}) error {
+func (r resourceClusterRoles) Create(ctx context.Context, el meta.Object) error {
 	_, err := r.RbacV1().ClusterRoles().Create(ctx, el.(*rbac.ClusterRole), meta.CreateOptions{})
 	return err
 }
@@ -60,9 +60,9 @@
 	return r.RbacV1().ClusterRoles().Delete(ctx, name, meta.DeleteOptions{})
 }
 
-func (r resourceClusterRoles) Expected() map[string]interface{} {
-	return map[string]interface{}{
-		clusterRolePSPDefault: &rbac.ClusterRole{
+func (r resourceClusterRoles) Expected() []meta.Object {
+	return []meta.Object{
+		&rbac.ClusterRole{
 			ObjectMeta: meta.ObjectMeta{
 				Name:   clusterRolePSPDefault,
 				Labels: builtinLabels(nil),
@@ -79,7 +79,7 @@
 				},
 			},
 		},
-		clusterRoleCSIProvisioner: &rbac.ClusterRole{
+		&rbac.ClusterRole{
 			ObjectMeta: meta.ObjectMeta{
 				Name:   clusterRoleCSIProvisioner,
 				Labels: builtinLabels(nil),
@@ -105,7 +105,7 @@
 				},
 			},
 		},
-		clusterRoleNetServices: &rbac.ClusterRole{
+		&rbac.ClusterRole{
 			ObjectMeta: meta.ObjectMeta{
 				Name:   clusterRoleNetServices,
 				Labels: builtinLabels(nil),
@@ -133,19 +133,19 @@
 	kubernetes.Interface
 }
 
-func (r resourceClusterRoleBindings) List(ctx context.Context) ([]string, error) {
+func (r resourceClusterRoleBindings) List(ctx context.Context) ([]meta.Object, error) {
 	res, err := r.RbacV1().ClusterRoleBindings().List(ctx, listBuiltins)
 	if err != nil {
 		return nil, err
 	}
-	objs := make([]string, len(res.Items))
-	for i, el := range res.Items {
-		objs[i] = el.ObjectMeta.Name
+	objs := make([]meta.Object, len(res.Items))
+	for i := range res.Items {
+		objs[i] = &res.Items[i]
 	}
 	return objs, nil
 }
 
-func (r resourceClusterRoleBindings) Create(ctx context.Context, el interface{}) error {
+func (r resourceClusterRoleBindings) Create(ctx context.Context, el meta.Object) error {
 	_, err := r.RbacV1().ClusterRoleBindings().Create(ctx, el.(*rbac.ClusterRoleBinding), meta.CreateOptions{})
 	return err
 }
@@ -154,9 +154,9 @@
 	return r.RbacV1().ClusterRoleBindings().Delete(ctx, name, meta.DeleteOptions{})
 }
 
-func (r resourceClusterRoleBindings) Expected() map[string]interface{} {
-	return map[string]interface{}{
-		clusterRoleBindingDefaultPSP: &rbac.ClusterRoleBinding{
+func (r resourceClusterRoleBindings) Expected() []meta.Object {
+	return []meta.Object{
+		&rbac.ClusterRoleBinding{
 			ObjectMeta: meta.ObjectMeta{
 				Name:   clusterRoleBindingDefaultPSP,
 				Labels: builtinLabels(nil),
@@ -179,7 +179,7 @@
 				},
 			},
 		},
-		clusterRoleBindingAPIServerKubeletClient: &rbac.ClusterRoleBinding{
+		&rbac.ClusterRoleBinding{
 			ObjectMeta: meta.ObjectMeta{
 				Name:   clusterRoleBindingAPIServerKubeletClient,
 				Labels: builtinLabels(nil),
@@ -202,7 +202,7 @@
 				},
 			},
 		},
-		clusterRoleBindingOwnerAdmin: &rbac.ClusterRoleBinding{
+		&rbac.ClusterRoleBinding{
 			ObjectMeta: meta.ObjectMeta{
 				Name:   clusterRoleBindingOwnerAdmin,
 				Labels: builtinLabels(nil),
@@ -224,7 +224,7 @@
 				},
 			},
 		},
-		clusterRoleBindingCSIProvisioners: &rbac.ClusterRoleBinding{
+		&rbac.ClusterRoleBinding{
 			ObjectMeta: meta.ObjectMeta{
 				Name:   clusterRoleBindingCSIProvisioners,
 				Labels: builtinLabels(nil),
@@ -245,7 +245,7 @@
 				},
 			},
 		},
-		clusterRoleBindingNetServices: &rbac.ClusterRoleBinding{
+		&rbac.ClusterRoleBinding{
 			ObjectMeta: meta.ObjectMeta{
 				Name:   clusterRoleBindingNetServices,
 				Labels: builtinLabels(nil),
diff --git a/metropolis/node/kubernetes/reconciler/resources_runtimeclass.go b/metropolis/node/kubernetes/reconciler/resources_runtimeclass.go
index c202c0e..11c2fa0 100644
--- a/metropolis/node/kubernetes/reconciler/resources_runtimeclass.go
+++ b/metropolis/node/kubernetes/reconciler/resources_runtimeclass.go
@@ -28,19 +28,19 @@
 	kubernetes.Interface
 }
 
-func (r resourceRuntimeClasses) List(ctx context.Context) ([]string, error) {
+func (r resourceRuntimeClasses) List(ctx context.Context) ([]meta.Object, error) {
 	res, err := r.NodeV1beta1().RuntimeClasses().List(ctx, listBuiltins)
 	if err != nil {
 		return nil, err
 	}
-	objs := make([]string, len(res.Items))
-	for i, el := range res.Items {
-		objs[i] = el.ObjectMeta.Name
+	objs := make([]meta.Object, len(res.Items))
+	for i := range res.Items {
+		objs[i] = &res.Items[i]
 	}
 	return objs, nil
 }
 
-func (r resourceRuntimeClasses) Create(ctx context.Context, el interface{}) error {
+func (r resourceRuntimeClasses) Create(ctx context.Context, el meta.Object) error {
 	_, err := r.NodeV1beta1().RuntimeClasses().Create(ctx, el.(*node.RuntimeClass), meta.CreateOptions{})
 	return err
 }
@@ -49,16 +49,16 @@
 	return r.NodeV1beta1().RuntimeClasses().Delete(ctx, name, meta.DeleteOptions{})
 }
 
-func (r resourceRuntimeClasses) Expected() map[string]interface{} {
-	return map[string]interface{}{
-		"gvisor": &node.RuntimeClass{
+func (r resourceRuntimeClasses) Expected() []meta.Object {
+	return []meta.Object{
+		&node.RuntimeClass{
 			ObjectMeta: meta.ObjectMeta{
 				Name:   "gvisor",
 				Labels: builtinLabels(nil),
 			},
 			Handler: "runsc",
 		},
-		"runc": &node.RuntimeClass{
+		&node.RuntimeClass{
 			ObjectMeta: meta.ObjectMeta{
 				Name:   "runc",
 				Labels: builtinLabels(nil),
diff --git a/metropolis/node/kubernetes/reconciler/resources_storageclass.go b/metropolis/node/kubernetes/reconciler/resources_storageclass.go
index 72476ec..d8191ce 100644
--- a/metropolis/node/kubernetes/reconciler/resources_storageclass.go
+++ b/metropolis/node/kubernetes/reconciler/resources_storageclass.go
@@ -32,19 +32,19 @@
 	kubernetes.Interface
 }
 
-func (r resourceStorageClasses) List(ctx context.Context) ([]string, error) {
+func (r resourceStorageClasses) List(ctx context.Context) ([]meta.Object, error) {
 	res, err := r.StorageV1().StorageClasses().List(ctx, listBuiltins)
 	if err != nil {
 		return nil, err
 	}
-	objs := make([]string, len(res.Items))
-	for i, el := range res.Items {
-		objs[i] = el.ObjectMeta.Name
+	objs := make([]meta.Object, len(res.Items))
+	for i := range res.Items {
+		objs[i] = &res.Items[i]
 	}
 	return objs, nil
 }
 
-func (r resourceStorageClasses) Create(ctx context.Context, el interface{}) error {
+func (r resourceStorageClasses) Create(ctx context.Context, el meta.Object) error {
 	_, err := r.StorageV1().StorageClasses().Create(ctx, el.(*storage.StorageClass), meta.CreateOptions{})
 	return err
 }
@@ -53,9 +53,9 @@
 	return r.StorageV1().StorageClasses().Delete(ctx, name, meta.DeleteOptions{})
 }
 
-func (r resourceStorageClasses) Expected() map[string]interface{} {
-	return map[string]interface{}{
-		"local": &storage.StorageClass{
+func (r resourceStorageClasses) Expected() []meta.Object {
+	return []meta.Object{
+		&storage.StorageClass{
 			ObjectMeta: meta.ObjectMeta{
 				Name:   "local",
 				Labels: builtinLabels(nil),