blob: efc513edbc8e6b598d37706a34151a8bb61ac186 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Serge Bazanskie6030f62020-06-03 17:52:59 +02002// SPDX-License-Identifier: Apache-2.0
Serge Bazanskie6030f62020-06-03 17:52:59 +02003
Serge Bazanski662b5b32020-12-21 13:49:00 +01004// The reconciler ensures that a base set of K8s resources is always available
5// in the cluster. These are necessary to ensure correct out-of-the-box
6// functionality. All resources containing the
7// metropolis.monogon.dev/builtin=true label are assumed to be managed by the
8// reconciler.
9// It currently does not revert modifications made by admins, it is planned to
10// create an admission plugin prohibiting such modifications to resources with
11// the metropolis.monogon.dev/builtin label to deal with that problem. This
12// would also solve a potential issue where you could delete resources just by
13// adding the metropolis.monogon.dev/builtin=true label.
Serge Bazanskie6030f62020-06-03 17:52:59 +020014package reconciler
15
16import (
17 "context"
Jan Schär69f5f4e2024-05-15 10:32:07 +020018 "errors"
Serge Bazanskie6030f62020-06-03 17:52:59 +020019 "fmt"
Jan Schär69f5f4e2024-05-15 10:32:07 +020020 "strings"
Serge Bazanskie6030f62020-06-03 17:52:59 +020021
Jan Schär69f5f4e2024-05-15 10:32:07 +020022 apiequality "k8s.io/apimachinery/pkg/api/equality"
23 apierrors "k8s.io/apimachinery/pkg/api/errors"
24 apivalidation "k8s.io/apimachinery/pkg/api/validation"
Serge Bazanskie6030f62020-06-03 17:52:59 +020025 meta "k8s.io/apimachinery/pkg/apis/meta/v1"
Serge Bazanskie6030f62020-06-03 17:52:59 +020026 "k8s.io/client-go/kubernetes"
Jan Schär69f5f4e2024-05-15 10:32:07 +020027
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020028 "source.monogon.dev/osbase/supervisor"
Serge Bazanskie6030f62020-06-03 17:52:59 +020029)
30
Serge Bazanskie6030f62020-06-03 17:52:59 +020031const (
Serge Bazanski216fe7b2021-05-21 18:36:16 +020032 // BuiltinLabelKey is used as a k8s label to mark built-in objects (ie.,
33 // managed by the reconciler)
Serge Bazanski662b5b32020-12-21 13:49:00 +010034 BuiltinLabelKey = "metropolis.monogon.dev/builtin"
Serge Bazanski216fe7b2021-05-21 18:36:16 +020035 // BuiltinLabelValue is used as a k8s label value, under the
36 // BuiltinLabelKey key.
Serge Bazanskie6030f62020-06-03 17:52:59 +020037 BuiltinLabelValue = "true"
Serge Bazanski216fe7b2021-05-21 18:36:16 +020038 // BuiltinRBACPrefix is used to prefix all built-in objects that are part
39 // of the rbac/v1 API (eg. {Cluster,}Role{Binding,} objects). This
40 // corresponds to the colon-separated 'namespaces' notation used by
Serge Bazanskie6030f62020-06-03 17:52:59 +020041 // Kubernetes system (system:) objects.
Serge Bazanski662b5b32020-12-21 13:49:00 +010042 BuiltinRBACPrefix = "metropolis:"
Serge Bazanskie6030f62020-06-03 17:52:59 +020043)
44
Serge Bazanski216fe7b2021-05-21 18:36:16 +020045// builtinLabels makes a kubernetes-compatible label dictionary (key->value)
46// that is used to mark objects that are built-in into Metropolis (ie., managed
47// by the reconciler). These are then subsequently retrieved by listBuiltins.
48// The extra argument specifies what other labels are to be merged into the the
49// labels dictionary, for convenience. If nil or empty, no extra labels will be
50// applied.
Serge Bazanskie6030f62020-06-03 17:52:59 +020051func builtinLabels(extra map[string]string) map[string]string {
52 l := map[string]string{
53 BuiltinLabelKey: BuiltinLabelValue,
54 }
Tim Windelschmidt24ce66f2024-04-18 23:59:24 +020055 for k, v := range extra {
56 l[k] = v
Serge Bazanskie6030f62020-06-03 17:52:59 +020057 }
58 return l
59}
60
Serge Bazanski216fe7b2021-05-21 18:36:16 +020061// listBuiltins returns a k8s client ListOptions structure that allows to
62// retrieve all objects that are built-in into Metropolis currently present in
63// the API server (ie., ones that are to be managed by the reconciler). These
64// are created by applying builtinLabels to their metadata labels.
Serge Bazanskie6030f62020-06-03 17:52:59 +020065var listBuiltins = meta.ListOptions{
66 LabelSelector: fmt.Sprintf("%s=%s", BuiltinLabelKey, BuiltinLabelValue),
67}
68
Serge Bazanski216fe7b2021-05-21 18:36:16 +020069// builtinRBACName returns a name that is compatible with colon-delimited
70// 'namespaced' objects, a la system:*.
71// These names are to be used by all builtins created as part of the rbac/v1
72// Kubernetes API.
Serge Bazanskie6030f62020-06-03 17:52:59 +020073func builtinRBACName(name string) string {
74 return BuiltinRBACPrefix + name
75}
76
Serge Bazanski216fe7b2021-05-21 18:36:16 +020077// resource is a type of resource to be managed by the reconciler. All
Jan Schär7f727482024-03-25 13:03:51 +010078// built-ins/reconciled objects must implement this interface to be managed
Serge Bazanski216fe7b2021-05-21 18:36:16 +020079// correctly by the reconciler.
Serge Bazanskie6030f62020-06-03 17:52:59 +020080type resource interface {
Jan Schär7f727482024-03-25 13:03:51 +010081 // List returns a list of objects currently present on the target
Serge Bazanski216fe7b2021-05-21 18:36:16 +020082 // (ie. k8s API server).
Jan Schär7f727482024-03-25 13:03:51 +010083 List(ctx context.Context) ([]meta.Object, error)
84 // Create creates an object on the target. The el argument is
85 // an object returned by the Expected() call.
86 Create(ctx context.Context, el meta.Object) error
Jan Schär69f5f4e2024-05-15 10:32:07 +020087 // Update updates an existing object, by name, on the target.
88 // The el argument is an object returned by the Expected() call.
89 Update(ctx context.Context, el meta.Object) error
Jan Schär7f727482024-03-25 13:03:51 +010090 // Delete deletes an object, by name, from the target.
Jan Schär69f5f4e2024-05-15 10:32:07 +020091 Delete(ctx context.Context, name string, opts meta.DeleteOptions) error
Jan Schär7f727482024-03-25 13:03:51 +010092 // Expected returns a list of all objects expected to be present on the
93 // target. Objects are identified by their name, as returned by GetName.
94 Expected() []meta.Object
Serge Bazanskie6030f62020-06-03 17:52:59 +020095}
96
97func allResources(clientSet kubernetes.Interface) map[string]resource {
98 return map[string]resource{
Serge Bazanskie6030f62020-06-03 17:52:59 +020099 "clusterroles": resourceClusterRoles{clientSet},
100 "clusterrolebindings": resourceClusterRoleBindings{clientSet},
101 "storageclasses": resourceStorageClasses{clientSet},
102 "csidrivers": resourceCSIDrivers{clientSet},
Lorenz Brun5e4fc2d2020-09-22 18:35:15 +0200103 "runtimeclasses": resourceRuntimeClasses{clientSet},
Serge Bazanskie6030f62020-06-03 17:52:59 +0200104 }
105}
106
Jan Schärd20ddcc2024-05-08 14:18:29 +0200107func reconcileAll(ctx context.Context, clientSet kubernetes.Interface) error {
Serge Bazanski356cbf32023-03-16 17:52:20 +0100108 resources := allResources(clientSet)
109 for name, resource := range resources {
Jan Schär69f5f4e2024-05-15 10:32:07 +0200110 err := reconcile(ctx, resource, name)
Serge Bazanski356cbf32023-03-16 17:52:20 +0100111 if err != nil {
112 return fmt.Errorf("resource %s: %w", name, err)
113 }
114 }
115 return nil
116}
117
Jan Schär69f5f4e2024-05-15 10:32:07 +0200118func reconcile(ctx context.Context, r resource, rname string) error {
119 log := supervisor.Logger(ctx)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200120 present, err := r.List(ctx)
121 if err != nil {
122 return err
123 }
Jan Schär7f727482024-03-25 13:03:51 +0100124 presentMap := make(map[string]meta.Object)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200125 for _, el := range present {
Jan Schär7f727482024-03-25 13:03:51 +0100126 presentMap[el.GetName()] = el
Serge Bazanskie6030f62020-06-03 17:52:59 +0200127 }
Jan Schär7f727482024-03-25 13:03:51 +0100128 expected := r.Expected()
129 expectedMap := make(map[string]meta.Object)
130 for _, el := range expected {
131 expectedMap[el.GetName()] = el
132 }
133 for name, expectedEl := range expectedMap {
Jan Schär69f5f4e2024-05-15 10:32:07 +0200134 if presentEl, ok := presentMap[name]; ok {
135 // The object already exists. Update it if it is different than expected.
136
137 // The server rejects updates which don't have an up to date ResourceVersion.
138 expectedEl.SetResourceVersion(presentEl.GetResourceVersion())
139
140 // Clear out fields set by the server, such that comparison succeeds if
141 // there are no other changes.
142 presentEl.SetUID("")
143 presentEl.SetGeneration(0)
144 presentEl.SetCreationTimestamp(meta.Time{})
145 presentEl.SetManagedFields(nil)
146
147 if !apiequality.Semantic.DeepEqual(presentEl, expectedEl) {
148 log.Infof("Updating %s object %q", rname, name)
149 if err := r.Update(ctx, expectedEl); err != nil {
150 if !isImmutableError(err) {
151 return err
152 }
153 log.Infof("Failed to update object due to immutable fields; deleting and recreating: %v", err)
154
155 // Only delete if the ResourceVersion has not changed. If it has
156 // changed, that means another reconciler was faster than us and
157 // has already recreated the object.
158 resourceVersion := presentEl.GetResourceVersion()
159 deleteOpts := meta.DeleteOptions{
160 Preconditions: &meta.Preconditions{
161 ResourceVersion: &resourceVersion,
162 },
163 }
164 // ResourceVersion must be cleared when creating.
165 expectedEl.SetResourceVersion("")
166
167 if err := r.Delete(ctx, name, deleteOpts); err != nil {
168 return err
169 }
170 if err := r.Create(ctx, expectedEl); err != nil {
171 return err
172 }
173 }
174 }
Jan Schär7f727482024-03-25 13:03:51 +0100175 } else {
Jan Schär69f5f4e2024-05-15 10:32:07 +0200176 log.Infof("Creating %s object %q", rname, name)
Jan Schär7f727482024-03-25 13:03:51 +0100177 if err := r.Create(ctx, expectedEl); err != nil {
Serge Bazanskie6030f62020-06-03 17:52:59 +0200178 return err
179 }
180 }
181 }
Tim Windelschmidt6b6428d2024-04-11 01:35:41 +0200182 for name := range presentMap {
Serge Bazanskie6030f62020-06-03 17:52:59 +0200183 if _, ok := expectedMap[name]; !ok {
Jan Schär69f5f4e2024-05-15 10:32:07 +0200184 log.Infof("Deleting %s object %q", rname, name)
185 if err := r.Delete(ctx, name, meta.DeleteOptions{}); err != nil {
Serge Bazanskie6030f62020-06-03 17:52:59 +0200186 return err
187 }
188 }
189 }
190 return nil
191}
Jan Schär69f5f4e2024-05-15 10:32:07 +0200192
193// isImmutableError returns true if err indicates that an update failed because
194// of an attempt to update one or more immutable fields.
195func isImmutableError(err error) bool {
196 if !apierrors.IsInvalid(err) {
197 return false
198 }
199 var status apierrors.APIStatus
200 if !errors.As(err, &status) {
201 return false
202 }
203 details := status.Status().Details
204 if details == nil || len(details.Causes) == 0 {
205 return false
206 }
207 for _, cause := range details.Causes {
208 if !strings.Contains(cause.Message, apivalidation.FieldImmutableErrorMsg) {
209 return false
210 }
211 }
212 return true
213}