| // 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 | 
 | // metropolis.monogon.dev/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 metropolis.monogon.dev/builtin label to deal with that problem. This | 
 | // would also solve a potential issue where you could delete resources just by | 
 | // adding the metropolis.monogon.dev/builtin=true label. | 
 | package reconciler | 
 |  | 
 | import ( | 
 | 	"context" | 
 | 	"fmt" | 
 | 	"time" | 
 |  | 
 | 	meta "k8s.io/apimachinery/pkg/apis/meta/v1" | 
 | 	"k8s.io/client-go/kubernetes" | 
 |  | 
 | 	"source.monogon.dev/metropolis/pkg/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 = "metropolis.monogon.dev/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 = "metropolis:" | 
 | ) | 
 |  | 
 | // builtinLabels makes a kubernetes-compatible label dictionary (key->value) | 
 | // that is used to mark objects that are built-in into Metropolis (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 Metropolis 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 ReconcileAll(ctx context.Context, clientSet kubernetes.Interface) error { | 
 | 	resources := allResources(clientSet) | 
 | 	for name, resource := range resources { | 
 | 		err := reconcile(ctx, resource) | 
 | 		if err != nil { | 
 | 			return fmt.Errorf("resource %s: %w", name, err) | 
 | 		} | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | func Maintain(clientSet kubernetes.Interface) supervisor.Runnable { | 
 | 	return func(ctx context.Context) error { | 
 | 		log := supervisor.Logger(ctx) | 
 | 		supervisor.Signal(ctx, supervisor.SignalHealthy) | 
 | 		t := time.NewTicker(10 * time.Second) | 
 | 		defer t.Stop() | 
 | 		for { | 
 | 			select { | 
 | 			case <-t.C: | 
 | 				err := ReconcileAll(ctx, clientSet) | 
 | 				if err != nil { | 
 | 					log.Warning(err) | 
 | 				} | 
 | 			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 | 
 | } |