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