blob: 3bfaa4d659369ee3e194e96d3850c6a72bb489be [file] [log] [blame]
Serge Bazanskie6030f62020-06-03 17:52:59 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
Serge Bazanski662b5b32020-12-21 13:49:00 +010017// The reconciler ensures that a base set of K8s resources is always available
18// in the cluster. These are necessary to ensure correct out-of-the-box
19// functionality. All resources containing the
20// metropolis.monogon.dev/builtin=true label are assumed to be managed by the
21// reconciler.
22// It currently does not revert modifications made by admins, it is planned to
23// create an admission plugin prohibiting such modifications to resources with
24// the metropolis.monogon.dev/builtin label to deal with that problem. This
25// would also solve a potential issue where you could delete resources just by
26// adding the metropolis.monogon.dev/builtin=true label.
Serge Bazanskie6030f62020-06-03 17:52:59 +020027package reconciler
28
29import (
30 "context"
Jan Schär69f5f4e2024-05-15 10:32:07 +020031 "errors"
Serge Bazanskie6030f62020-06-03 17:52:59 +020032 "fmt"
Jan Schär69f5f4e2024-05-15 10:32:07 +020033 "strings"
Serge Bazanskie6030f62020-06-03 17:52:59 +020034
Jan Schär69f5f4e2024-05-15 10:32:07 +020035 apiequality "k8s.io/apimachinery/pkg/api/equality"
36 apierrors "k8s.io/apimachinery/pkg/api/errors"
37 apivalidation "k8s.io/apimachinery/pkg/api/validation"
Serge Bazanskie6030f62020-06-03 17:52:59 +020038 meta "k8s.io/apimachinery/pkg/apis/meta/v1"
Serge Bazanskie6030f62020-06-03 17:52:59 +020039 "k8s.io/client-go/kubernetes"
Jan Schär69f5f4e2024-05-15 10:32:07 +020040
41 "source.monogon.dev/metropolis/pkg/supervisor"
Serge Bazanskie6030f62020-06-03 17:52:59 +020042)
43
Tim Windelschmidt51daf252024-04-18 23:18:43 +020044// True is a sad workaround for all the pointer booleans in K8s specs
Serge Bazanskie6030f62020-06-03 17:52:59 +020045func True() *bool {
46 val := true
47 return &val
48}
49func False() *bool {
50 val := false
51 return &val
52}
53
54const (
Serge Bazanski216fe7b2021-05-21 18:36:16 +020055 // BuiltinLabelKey is used as a k8s label to mark built-in objects (ie.,
56 // managed by the reconciler)
Serge Bazanski662b5b32020-12-21 13:49:00 +010057 BuiltinLabelKey = "metropolis.monogon.dev/builtin"
Serge Bazanski216fe7b2021-05-21 18:36:16 +020058 // BuiltinLabelValue is used as a k8s label value, under the
59 // BuiltinLabelKey key.
Serge Bazanskie6030f62020-06-03 17:52:59 +020060 BuiltinLabelValue = "true"
Serge Bazanski216fe7b2021-05-21 18:36:16 +020061 // BuiltinRBACPrefix is used to prefix all built-in objects that are part
62 // of the rbac/v1 API (eg. {Cluster,}Role{Binding,} objects). This
63 // corresponds to the colon-separated 'namespaces' notation used by
Serge Bazanskie6030f62020-06-03 17:52:59 +020064 // Kubernetes system (system:) objects.
Serge Bazanski662b5b32020-12-21 13:49:00 +010065 BuiltinRBACPrefix = "metropolis:"
Serge Bazanskie6030f62020-06-03 17:52:59 +020066)
67
Serge Bazanski216fe7b2021-05-21 18:36:16 +020068// builtinLabels makes a kubernetes-compatible label dictionary (key->value)
69// that is used to mark objects that are built-in into Metropolis (ie., managed
70// by the reconciler). These are then subsequently retrieved by listBuiltins.
71// The extra argument specifies what other labels are to be merged into the the
72// labels dictionary, for convenience. If nil or empty, no extra labels will be
73// applied.
Serge Bazanskie6030f62020-06-03 17:52:59 +020074func builtinLabels(extra map[string]string) map[string]string {
75 l := map[string]string{
76 BuiltinLabelKey: BuiltinLabelValue,
77 }
Tim Windelschmidt24ce66f2024-04-18 23:59:24 +020078 for k, v := range extra {
79 l[k] = v
Serge Bazanskie6030f62020-06-03 17:52:59 +020080 }
81 return l
82}
83
Serge Bazanski216fe7b2021-05-21 18:36:16 +020084// listBuiltins returns a k8s client ListOptions structure that allows to
85// retrieve all objects that are built-in into Metropolis currently present in
86// the API server (ie., ones that are to be managed by the reconciler). These
87// are created by applying builtinLabels to their metadata labels.
Serge Bazanskie6030f62020-06-03 17:52:59 +020088var listBuiltins = meta.ListOptions{
89 LabelSelector: fmt.Sprintf("%s=%s", BuiltinLabelKey, BuiltinLabelValue),
90}
91
Serge Bazanski216fe7b2021-05-21 18:36:16 +020092// builtinRBACName returns a name that is compatible with colon-delimited
93// 'namespaced' objects, a la system:*.
94// These names are to be used by all builtins created as part of the rbac/v1
95// Kubernetes API.
Serge Bazanskie6030f62020-06-03 17:52:59 +020096func builtinRBACName(name string) string {
97 return BuiltinRBACPrefix + name
98}
99
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200100// resource is a type of resource to be managed by the reconciler. All
Jan Schär7f727482024-03-25 13:03:51 +0100101// built-ins/reconciled objects must implement this interface to be managed
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200102// correctly by the reconciler.
Serge Bazanskie6030f62020-06-03 17:52:59 +0200103type resource interface {
Jan Schär7f727482024-03-25 13:03:51 +0100104 // List returns a list of objects currently present on the target
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200105 // (ie. k8s API server).
Jan Schär7f727482024-03-25 13:03:51 +0100106 List(ctx context.Context) ([]meta.Object, error)
107 // Create creates an object on the target. The el argument is
108 // an object returned by the Expected() call.
109 Create(ctx context.Context, el meta.Object) error
Jan Schär69f5f4e2024-05-15 10:32:07 +0200110 // Update updates an existing object, by name, on the target.
111 // The el argument is an object returned by the Expected() call.
112 Update(ctx context.Context, el meta.Object) error
Jan Schär7f727482024-03-25 13:03:51 +0100113 // Delete deletes an object, by name, from the target.
Jan Schär69f5f4e2024-05-15 10:32:07 +0200114 Delete(ctx context.Context, name string, opts meta.DeleteOptions) error
Jan Schär7f727482024-03-25 13:03:51 +0100115 // Expected returns a list of all objects expected to be present on the
116 // target. Objects are identified by their name, as returned by GetName.
117 Expected() []meta.Object
Serge Bazanskie6030f62020-06-03 17:52:59 +0200118}
119
120func allResources(clientSet kubernetes.Interface) map[string]resource {
121 return map[string]resource{
Serge Bazanskie6030f62020-06-03 17:52:59 +0200122 "clusterroles": resourceClusterRoles{clientSet},
123 "clusterrolebindings": resourceClusterRoleBindings{clientSet},
124 "storageclasses": resourceStorageClasses{clientSet},
125 "csidrivers": resourceCSIDrivers{clientSet},
Lorenz Brun5e4fc2d2020-09-22 18:35:15 +0200126 "runtimeclasses": resourceRuntimeClasses{clientSet},
Serge Bazanskie6030f62020-06-03 17:52:59 +0200127 }
128}
129
Jan Schärd20ddcc2024-05-08 14:18:29 +0200130func reconcileAll(ctx context.Context, clientSet kubernetes.Interface) error {
Serge Bazanski356cbf32023-03-16 17:52:20 +0100131 resources := allResources(clientSet)
132 for name, resource := range resources {
Jan Schär69f5f4e2024-05-15 10:32:07 +0200133 err := reconcile(ctx, resource, name)
Serge Bazanski356cbf32023-03-16 17:52:20 +0100134 if err != nil {
135 return fmt.Errorf("resource %s: %w", name, err)
136 }
137 }
138 return nil
139}
140
Jan Schär69f5f4e2024-05-15 10:32:07 +0200141func reconcile(ctx context.Context, r resource, rname string) error {
142 log := supervisor.Logger(ctx)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200143 present, err := r.List(ctx)
144 if err != nil {
145 return err
146 }
Jan Schär7f727482024-03-25 13:03:51 +0100147 presentMap := make(map[string]meta.Object)
Serge Bazanskie6030f62020-06-03 17:52:59 +0200148 for _, el := range present {
Jan Schär7f727482024-03-25 13:03:51 +0100149 presentMap[el.GetName()] = el
Serge Bazanskie6030f62020-06-03 17:52:59 +0200150 }
Jan Schär7f727482024-03-25 13:03:51 +0100151 expected := r.Expected()
152 expectedMap := make(map[string]meta.Object)
153 for _, el := range expected {
154 expectedMap[el.GetName()] = el
155 }
156 for name, expectedEl := range expectedMap {
Jan Schär69f5f4e2024-05-15 10:32:07 +0200157 if presentEl, ok := presentMap[name]; ok {
158 // The object already exists. Update it if it is different than expected.
159
160 // The server rejects updates which don't have an up to date ResourceVersion.
161 expectedEl.SetResourceVersion(presentEl.GetResourceVersion())
162
163 // Clear out fields set by the server, such that comparison succeeds if
164 // there are no other changes.
165 presentEl.SetUID("")
166 presentEl.SetGeneration(0)
167 presentEl.SetCreationTimestamp(meta.Time{})
168 presentEl.SetManagedFields(nil)
169
170 if !apiequality.Semantic.DeepEqual(presentEl, expectedEl) {
171 log.Infof("Updating %s object %q", rname, name)
172 if err := r.Update(ctx, expectedEl); err != nil {
173 if !isImmutableError(err) {
174 return err
175 }
176 log.Infof("Failed to update object due to immutable fields; deleting and recreating: %v", err)
177
178 // Only delete if the ResourceVersion has not changed. If it has
179 // changed, that means another reconciler was faster than us and
180 // has already recreated the object.
181 resourceVersion := presentEl.GetResourceVersion()
182 deleteOpts := meta.DeleteOptions{
183 Preconditions: &meta.Preconditions{
184 ResourceVersion: &resourceVersion,
185 },
186 }
187 // ResourceVersion must be cleared when creating.
188 expectedEl.SetResourceVersion("")
189
190 if err := r.Delete(ctx, name, deleteOpts); err != nil {
191 return err
192 }
193 if err := r.Create(ctx, expectedEl); err != nil {
194 return err
195 }
196 }
197 }
Jan Schär7f727482024-03-25 13:03:51 +0100198 } else {
Jan Schär69f5f4e2024-05-15 10:32:07 +0200199 log.Infof("Creating %s object %q", rname, name)
Jan Schär7f727482024-03-25 13:03:51 +0100200 if err := r.Create(ctx, expectedEl); err != nil {
Serge Bazanskie6030f62020-06-03 17:52:59 +0200201 return err
202 }
203 }
204 }
Tim Windelschmidt6b6428d2024-04-11 01:35:41 +0200205 for name := range presentMap {
Serge Bazanskie6030f62020-06-03 17:52:59 +0200206 if _, ok := expectedMap[name]; !ok {
Jan Schär69f5f4e2024-05-15 10:32:07 +0200207 log.Infof("Deleting %s object %q", rname, name)
208 if err := r.Delete(ctx, name, meta.DeleteOptions{}); err != nil {
Serge Bazanskie6030f62020-06-03 17:52:59 +0200209 return err
210 }
211 }
212 }
213 return nil
214}
Jan Schär69f5f4e2024-05-15 10:32:07 +0200215
216// isImmutableError returns true if err indicates that an update failed because
217// of an attempt to update one or more immutable fields.
218func isImmutableError(err error) bool {
219 if !apierrors.IsInvalid(err) {
220 return false
221 }
222 var status apierrors.APIStatus
223 if !errors.As(err, &status) {
224 return false
225 }
226 details := status.Status().Details
227 if details == nil || len(details.Causes) == 0 {
228 return false
229 }
230 for _, cause := range details.Causes {
231 if !strings.Contains(cause.Message, apivalidation.FieldImmutableErrorMsg) {
232 return false
233 }
234 }
235 return true
236}