core -> metropolis

Smalltown is now called Metropolis!

This is the first commit in a series of cleanup commits that prepare us
for an open source release. This one just some Bazel packages around to
follow a stricter directory layout.

All of Metropolis now lives in `//metropolis`.

All of Metropolis Node code now lives in `//metropolis/node`.

All of the main /init now lives in `//m/n/core`.

All of the Kubernetes functionality/glue now lives in `//m/n/kubernetes`.

Next steps:
     - hunt down all references to Smalltown and replace them appropriately
     - narrow down visibility rules
     - document new code organization
     - move `//build/toolchain` to `//monogon/build/toolchain`
     - do another cleanup pass between `//golibs` and
       `//monogon/node/{core,common}`.
     - remove `//delta` and `//anubis`

Fixes T799.

Test Plan: Just a very large refactor. CI should help us out here.

Bug: T799

X-Origin-Diff: phab/D667
GitOrigin-RevId: 6029b8d4edc42325d50042596b639e8b122d0ded
diff --git a/metropolis/node/kubernetes/reconciler/BUILD.bazel b/metropolis/node/kubernetes/reconciler/BUILD.bazel
new file mode 100644
index 0000000..d8f2db6
--- /dev/null
+++ b/metropolis/node/kubernetes/reconciler/BUILD.bazel
@@ -0,0 +1,38 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "reconciler.go",
+        "resources_csi.go",
+        "resources_podsecuritypolicy.go",
+        "resources_rbac.go",
+        "resources_runtimeclass.go",
+        "resources_storageclass.go",
+    ],
+    importpath = "git.monogon.dev/source/nexantic.git/metropolis/node/kubernetes/reconciler",
+    visibility = ["//metropolis/node:__subpackages__"],
+    deps = [
+        "//metropolis/node/common/supervisor:go_default_library",
+        "@io_k8s_api//core/v1:go_default_library",
+        "@io_k8s_api//node/v1beta1:go_default_library",
+        "@io_k8s_api//policy/v1beta1:go_default_library",
+        "@io_k8s_api//rbac/v1:go_default_library",
+        "@io_k8s_api//storage/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
+        "@io_k8s_client_go//kubernetes:go_default_library",
+    ],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["reconciler_test.go"],
+    embed = [":go_default_library"],
+    deps = [
+        "@io_k8s_api//node/v1beta1:go_default_library",
+        "@io_k8s_api//policy/v1beta1:go_default_library",
+        "@io_k8s_api//rbac/v1:go_default_library",
+        "@io_k8s_api//storage/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
+    ],
+)
diff --git a/metropolis/node/kubernetes/reconciler/reconciler.go b/metropolis/node/kubernetes/reconciler/reconciler.go
new file mode 100644
index 0000000..9c5ba4e
--- /dev/null
+++ b/metropolis/node/kubernetes/reconciler/reconciler.go
@@ -0,0 +1,163 @@
+// 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.
+
+// The reconciler ensures that a base set of K8s resources is always available in the cluster. These are necessary to
+// ensure correct out-of-the-box functionality. All resources containing the smalltown.com/builtin=true label are assumed
+// to be managed by the reconciler.
+// It currently does not revert modifications made by admins, it is  planned to create an admission plugin prohibiting
+// such modifications to resources with the smalltown.com/builtin label to deal with that problem. This would also solve a
+// potential issue where you could delete resources just by adding the smalltown.com/builtin=true label.
+package reconciler
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+
+	"git.monogon.dev/source/nexantic.git/metropolis/node/common/supervisor"
+)
+
+// Sad workaround for all the pointer booleans in K8s specs
+func True() *bool {
+	val := true
+	return &val
+}
+func False() *bool {
+	val := false
+	return &val
+}
+
+const (
+	// BuiltinLabelKey is used as a k8s label to mark built-in objects (ie., managed by the reconciler)
+	BuiltinLabelKey = "smalltown.com/builtin"
+	// BuiltinLabelValue is used as a k8s label value, under the BuiltinLabelKey key.
+	BuiltinLabelValue = "true"
+	// BuiltinRBACPrefix is used to prefix all built-in objects that are part of the rbac/v1 API (eg.
+	// {Cluster,}Role{Binding,} objects). This corresponds to the colon-separated 'namespaces' notation used by
+	// Kubernetes system (system:) objects.
+	BuiltinRBACPrefix = "smalltown:"
+)
+
+// builtinLabels makes a kubernetes-compatible label dictionary (key->value) that is used to mark objects that are
+// built-in into Smalltown (ie., managed by the reconciler). These are then subsequently retrieved by listBuiltins.
+// The extra argument specifies what other labels are to be merged into the the labels dictionary, for convenience. If
+// nil or empty, no extra labels will be applied.
+func builtinLabels(extra map[string]string) map[string]string {
+	l := map[string]string{
+		BuiltinLabelKey: BuiltinLabelValue,
+	}
+	if extra != nil {
+		for k, v := range extra {
+			l[k] = v
+		}
+	}
+	return l
+}
+
+// listBuiltins returns a k8s client ListOptions structure that allows to retrieve all objects that are built-in into
+// Smalltown currently present in the API server (ie., ones that are to be managed by the reconciler). These are created
+// by applying builtinLabels to their metadata labels.
+var listBuiltins = meta.ListOptions{
+	LabelSelector: fmt.Sprintf("%s=%s", BuiltinLabelKey, BuiltinLabelValue),
+}
+
+// builtinRBACName returns a name that is compatible with colon-delimited 'namespaced' objects, a la system:*.
+// These names are to be used by all builtins created as part of the rbac/v1 Kubernetes API.
+func builtinRBACName(name string) string {
+	return BuiltinRBACPrefix + name
+}
+
+// resource is a type of resource to be managed by the reconciler. All builti-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 (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.
+	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{}
+}
+
+func allResources(clientSet kubernetes.Interface) map[string]resource {
+	return map[string]resource{
+		"psps":                resourcePodSecurityPolicies{clientSet},
+		"clusterroles":        resourceClusterRoles{clientSet},
+		"clusterrolebindings": resourceClusterRoleBindings{clientSet},
+		"storageclasses":      resourceStorageClasses{clientSet},
+		"csidrivers":          resourceCSIDrivers{clientSet},
+		"runtimeclasses":      resourceRuntimeClasses{clientSet},
+	}
+}
+
+func Run(clientSet kubernetes.Interface) supervisor.Runnable {
+	return func(ctx context.Context) error {
+		log := supervisor.Logger(ctx)
+		resources := allResources(clientSet)
+		t := time.NewTicker(10 * time.Second)
+		reconcileAll := func() {
+			for name, resource := range resources {
+				if err := reconcile(ctx, resource); err != nil {
+					log.Warningf("Failed to reconcile built-in resources %s: %v", name, err)
+				}
+			}
+		}
+		supervisor.Signal(ctx, supervisor.SignalHealthy)
+		reconcileAll()
+		for {
+			select {
+			case <-t.C:
+				reconcileAll()
+			case <-ctx.Done():
+				return nil
+			}
+		}
+	}
+}
+
+func reconcile(ctx context.Context, r resource) error {
+	present, err := r.List(ctx)
+	if err != nil {
+		return err
+	}
+	presentSet := make(map[string]bool)
+	for _, el := range present {
+		presentSet[el] = true
+	}
+	expectedMap := r.Expected()
+	for name, el := range expectedMap {
+		if !presentSet[name] {
+			if err := r.Create(ctx, el); err != nil {
+				return err
+			}
+		}
+	}
+	for name, _ := range presentSet {
+		if _, ok := expectedMap[name]; !ok {
+			if err := r.Delete(ctx, name); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
diff --git a/metropolis/node/kubernetes/reconciler/reconciler_test.go b/metropolis/node/kubernetes/reconciler/reconciler_test.go
new file mode 100644
index 0000000..b58d4af
--- /dev/null
+++ b/metropolis/node/kubernetes/reconciler/reconciler_test.go
@@ -0,0 +1,184 @@
+// 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)
+	}
+}
diff --git a/metropolis/node/kubernetes/reconciler/resources_csi.go b/metropolis/node/kubernetes/reconciler/resources_csi.go
new file mode 100644
index 0000000..ecbcb4b
--- /dev/null
+++ b/metropolis/node/kubernetes/reconciler/resources_csi.go
@@ -0,0 +1,71 @@
+// 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"
+
+	storage "k8s.io/api/storage/v1"
+	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+// TODO(q3k): this is duplicated with //metropolis/node/kubernetes:provisioner.go; integrate this once provisioner.go
+// gets moved into a subpackage.
+// ONCHANGE(//metropolis/node/kubernetes:provisioner.go): needs to match csiProvisionerName declared.
+const csiProvisionerName = "com.nexantic.smalltown.vfs"
+
+type resourceCSIDrivers struct {
+	kubernetes.Interface
+}
+
+func (r resourceCSIDrivers) List(ctx context.Context) ([]string, 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
+	}
+	return objs, nil
+}
+
+func (r resourceCSIDrivers) Create(ctx context.Context, el interface{}) error {
+	_, err := r.StorageV1().CSIDrivers().Create(ctx, el.(*storage.CSIDriver), meta.CreateOptions{})
+	return err
+}
+
+func (r resourceCSIDrivers) Delete(ctx context.Context, name string) error {
+	return r.StorageV1().CSIDrivers().Delete(ctx, name, meta.DeleteOptions{})
+}
+
+func (r resourceCSIDrivers) Expected() map[string]interface{} {
+	return map[string]interface{}{
+		csiProvisionerName: &storage.CSIDriver{
+			ObjectMeta: meta.ObjectMeta{
+				Name:   csiProvisionerName,
+				Labels: builtinLabels(nil),
+			},
+			Spec: storage.CSIDriverSpec{
+				AttachRequired:       False(),
+				PodInfoOnMount:       False(),
+				VolumeLifecycleModes: []storage.VolumeLifecycleMode{storage.VolumeLifecyclePersistent},
+			},
+		},
+	}
+}
diff --git a/metropolis/node/kubernetes/reconciler/resources_podsecuritypolicy.go b/metropolis/node/kubernetes/reconciler/resources_podsecuritypolicy.go
new file mode 100644
index 0000000..507089f
--- /dev/null
+++ b/metropolis/node/kubernetes/reconciler/resources_podsecuritypolicy.go
@@ -0,0 +1,108 @@
+// 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"
+
+	core "k8s.io/api/core/v1"
+	policy "k8s.io/api/policy/v1beta1"
+	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+type resourcePodSecurityPolicies struct {
+	kubernetes.Interface
+}
+
+func (r resourcePodSecurityPolicies) List(ctx context.Context) ([]string, 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
+	}
+	return objs, nil
+}
+
+func (r resourcePodSecurityPolicies) Create(ctx context.Context, el interface{}) error {
+	_, err := r.PolicyV1beta1().PodSecurityPolicies().Create(ctx, el.(*policy.PodSecurityPolicy), meta.CreateOptions{})
+	return err
+}
+
+func (r resourcePodSecurityPolicies) Delete(ctx context.Context, name string) error {
+	return r.PolicyV1beta1().PodSecurityPolicies().Delete(ctx, name, meta.DeleteOptions{})
+}
+
+func (r resourcePodSecurityPolicies) Expected() map[string]interface{} {
+	return map[string]interface{}{
+		"default": &policy.PodSecurityPolicy{
+			ObjectMeta: meta.ObjectMeta{
+				Name:   "default",
+				Labels: builtinLabels(nil),
+				Annotations: map[string]string{
+					"kubernetes.io/description": "This default PSP allows the creation of pods using features that are" +
+						" generally considered safe against any sort of escape.",
+				},
+			},
+			Spec: policy.PodSecurityPolicySpec{
+				AllowPrivilegeEscalation: True(),
+				AllowedCapabilities: []core.Capability{ // runc's default list of allowed capabilities
+					"SETPCAP",
+					"MKNOD",
+					"AUDIT_WRITE",
+					"CHOWN",
+					"NET_RAW",
+					"DAC_OVERRIDE",
+					"FOWNER",
+					"FSETID",
+					"KILL",
+					"SETGID",
+					"SETUID",
+					"NET_BIND_SERVICE",
+					"SYS_CHROOT",
+					"SETFCAP",
+				},
+				HostNetwork: false,
+				HostIPC:     false,
+				HostPID:     false,
+				FSGroup: policy.FSGroupStrategyOptions{
+					Rule: policy.FSGroupStrategyRunAsAny,
+				},
+				RunAsUser: policy.RunAsUserStrategyOptions{
+					Rule: policy.RunAsUserStrategyRunAsAny,
+				},
+				SELinux: policy.SELinuxStrategyOptions{
+					Rule: policy.SELinuxStrategyRunAsAny,
+				},
+				SupplementalGroups: policy.SupplementalGroupsStrategyOptions{
+					Rule: policy.SupplementalGroupsStrategyRunAsAny,
+				},
+				Volumes: []policy.FSType{ // Volumes considered safe to use
+					policy.ConfigMap,
+					policy.EmptyDir,
+					policy.Projected,
+					policy.Secret,
+					policy.DownwardAPI,
+					policy.PersistentVolumeClaim,
+				},
+			},
+		},
+	}
+}
diff --git a/metropolis/node/kubernetes/reconciler/resources_rbac.go b/metropolis/node/kubernetes/reconciler/resources_rbac.go
new file mode 100644
index 0000000..40ca879
--- /dev/null
+++ b/metropolis/node/kubernetes/reconciler/resources_rbac.go
@@ -0,0 +1,154 @@
+// 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"
+
+	rbac "k8s.io/api/rbac/v1"
+	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+var (
+	clusterRolePSPDefault                    = builtinRBACName("psp-default")
+	clusterRoleBindingDefaultPSP             = builtinRBACName("default-psp-for-sa")
+	clusterRoleBindingAPIServerKubeletClient = builtinRBACName("apiserver-kubelet-client")
+)
+
+type resourceClusterRoles struct {
+	kubernetes.Interface
+}
+
+func (r resourceClusterRoles) List(ctx context.Context) ([]string, 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
+	}
+	return objs, nil
+}
+
+func (r resourceClusterRoles) Create(ctx context.Context, el interface{}) error {
+	_, err := r.RbacV1().ClusterRoles().Create(ctx, el.(*rbac.ClusterRole), meta.CreateOptions{})
+	return err
+}
+
+func (r resourceClusterRoles) Delete(ctx context.Context, name string) error {
+	return r.RbacV1().ClusterRoles().Delete(ctx, name, meta.DeleteOptions{})
+}
+
+func (r resourceClusterRoles) Expected() map[string]interface{} {
+	return map[string]interface{}{
+		clusterRolePSPDefault: &rbac.ClusterRole{
+			ObjectMeta: meta.ObjectMeta{
+				Name:   clusterRolePSPDefault,
+				Labels: builtinLabels(nil),
+				Annotations: map[string]string{
+					"kubernetes.io/description": "This role grants access to the \"default\" PSP.",
+				},
+			},
+			Rules: []rbac.PolicyRule{
+				{
+					APIGroups:     []string{"policy"},
+					Resources:     []string{"podsecuritypolicies"},
+					ResourceNames: []string{"default"},
+					Verbs:         []string{"use"},
+				},
+			},
+		},
+	}
+}
+
+type resourceClusterRoleBindings struct {
+	kubernetes.Interface
+}
+
+func (r resourceClusterRoleBindings) List(ctx context.Context) ([]string, 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
+	}
+	return objs, nil
+}
+
+func (r resourceClusterRoleBindings) Create(ctx context.Context, el interface{}) error {
+	_, err := r.RbacV1().ClusterRoleBindings().Create(ctx, el.(*rbac.ClusterRoleBinding), meta.CreateOptions{})
+	return err
+}
+
+func (r resourceClusterRoleBindings) Delete(ctx context.Context, name string) error {
+	return r.RbacV1().ClusterRoleBindings().Delete(ctx, name, meta.DeleteOptions{})
+}
+
+func (r resourceClusterRoleBindings) Expected() map[string]interface{} {
+	return map[string]interface{}{
+		clusterRoleBindingDefaultPSP: &rbac.ClusterRoleBinding{
+			ObjectMeta: meta.ObjectMeta{
+				Name:   clusterRoleBindingDefaultPSP,
+				Labels: builtinLabels(nil),
+				Annotations: map[string]string{
+					"kubernetes.io/description": "This binding grants every service account access to the \"default\" PSP. " +
+						"Creation of Pods is still restricted by other RBAC roles. Otherwise no pods (unprivileged or not) " +
+						"can be created.",
+				},
+			},
+			RoleRef: rbac.RoleRef{
+				APIGroup: rbac.GroupName,
+				Kind:     "ClusterRole",
+				Name:     clusterRolePSPDefault,
+			},
+			Subjects: []rbac.Subject{
+				{
+					APIGroup: rbac.GroupName,
+					Kind:     "Group",
+					Name:     "system:serviceaccounts",
+				},
+			},
+		},
+		clusterRoleBindingAPIServerKubeletClient: &rbac.ClusterRoleBinding{
+			ObjectMeta: meta.ObjectMeta{
+				Name:   clusterRoleBindingAPIServerKubeletClient,
+				Labels: builtinLabels(nil),
+				Annotations: map[string]string{
+					"kubernetes.io/description": "This binding grants the apiserver access to the kubelets. This enables " +
+						"lots of built-in functionality like reading logs or forwarding ports via the API.",
+				},
+			},
+			RoleRef: rbac.RoleRef{
+				APIGroup: rbac.GroupName,
+				Kind:     "ClusterRole",
+				Name:     "system:kubelet-api-admin",
+			},
+			Subjects: []rbac.Subject{
+				{
+					APIGroup: rbac.GroupName,
+					Kind:     "User",
+					// TODO(q3k): describe this name's contract, or unify with whatever creates this.
+					Name: "smalltown:apiserver-kubelet-client",
+				},
+			},
+		},
+	}
+}
diff --git a/metropolis/node/kubernetes/reconciler/resources_runtimeclass.go b/metropolis/node/kubernetes/reconciler/resources_runtimeclass.go
new file mode 100644
index 0000000..c202c0e
--- /dev/null
+++ b/metropolis/node/kubernetes/reconciler/resources_runtimeclass.go
@@ -0,0 +1,69 @@
+// 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"
+
+	node "k8s.io/api/node/v1beta1"
+	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+type resourceRuntimeClasses struct {
+	kubernetes.Interface
+}
+
+func (r resourceRuntimeClasses) List(ctx context.Context) ([]string, 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
+	}
+	return objs, nil
+}
+
+func (r resourceRuntimeClasses) Create(ctx context.Context, el interface{}) error {
+	_, err := r.NodeV1beta1().RuntimeClasses().Create(ctx, el.(*node.RuntimeClass), meta.CreateOptions{})
+	return err
+}
+
+func (r resourceRuntimeClasses) Delete(ctx context.Context, name string) error {
+	return r.NodeV1beta1().RuntimeClasses().Delete(ctx, name, meta.DeleteOptions{})
+}
+
+func (r resourceRuntimeClasses) Expected() map[string]interface{} {
+	return map[string]interface{}{
+		"gvisor": &node.RuntimeClass{
+			ObjectMeta: meta.ObjectMeta{
+				Name:   "gvisor",
+				Labels: builtinLabels(nil),
+			},
+			Handler: "runsc",
+		},
+		"runc": &node.RuntimeClass{
+			ObjectMeta: meta.ObjectMeta{
+				Name:   "runc",
+				Labels: builtinLabels(nil),
+			},
+			Handler: "runc",
+		},
+	}
+}
diff --git a/metropolis/node/kubernetes/reconciler/resources_storageclass.go b/metropolis/node/kubernetes/reconciler/resources_storageclass.go
new file mode 100644
index 0000000..72476ec
--- /dev/null
+++ b/metropolis/node/kubernetes/reconciler/resources_storageclass.go
@@ -0,0 +1,72 @@
+// 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"
+
+	core "k8s.io/api/core/v1"
+	storage "k8s.io/api/storage/v1"
+	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+var reclaimPolicyDelete = core.PersistentVolumeReclaimDelete
+var waitForConsumerBinding = storage.VolumeBindingWaitForFirstConsumer
+
+type resourceStorageClasses struct {
+	kubernetes.Interface
+}
+
+func (r resourceStorageClasses) List(ctx context.Context) ([]string, 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
+	}
+	return objs, nil
+}
+
+func (r resourceStorageClasses) Create(ctx context.Context, el interface{}) error {
+	_, err := r.StorageV1().StorageClasses().Create(ctx, el.(*storage.StorageClass), meta.CreateOptions{})
+	return err
+}
+
+func (r resourceStorageClasses) Delete(ctx context.Context, name string) error {
+	return r.StorageV1().StorageClasses().Delete(ctx, name, meta.DeleteOptions{})
+}
+
+func (r resourceStorageClasses) Expected() map[string]interface{} {
+	return map[string]interface{}{
+		"local": &storage.StorageClass{
+			ObjectMeta: meta.ObjectMeta{
+				Name:   "local",
+				Labels: builtinLabels(nil),
+				Annotations: map[string]string{
+					"storageclass.kubernetes.io/is-default-class": "true",
+				},
+			},
+			AllowVolumeExpansion: True(),
+			Provisioner:          csiProvisionerName,
+			ReclaimPolicy:        &reclaimPolicyDelete,
+			VolumeBindingMode:    &waitForConsumerBinding,
+		},
+	}
+}