Skip to content

Commit 1d8a803

Browse files
pepovwaynz0rstoadersiliconbrain
authored
Remove controlNamespace from resourceOwner and various improvements (#25)
* move GetControlNamespace to separate interface * add Created desired state * check restmapper while pruning * ignore notfound error while pruning * fix predicates * extend desiredstate to allow more fine grained reconcile - BeforeUpdate signature changed and has the actual desired object - BeforeCreate and BeforeDelete added - create, update, delete options can be set within desired state - annotation and label merge removed - ownership annotation removal also removed * gracefully check cross-namespace ownership * fix silent cross-namespace ownership condition * add create/update/delete pre condition funcs Co-authored-by: Zsolt Varga <[email protected]> Co-authored-by: Toader Sebastian <[email protected]> Co-authored-by: Dudás Ádám <[email protected]>
1 parent 1efe115 commit 1d8a803

File tree

11 files changed

+614
-54
lines changed

11 files changed

+614
-54
lines changed

docs/types/Readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ For more information please click on the name
55

66
| Name | Description |
77
|---|---|
8-
| **[MetaBase](base_types.md)** | |
8+
| **[ObjectKey](base_types.md)** | |
99
| **[Secret](secret_types.md)** | Secret referencing abstraction |
1010
| **[KubernetesVolume](volume_types.md)** | Kubernetes volume abstraction |
1111
</center>

docs/types/base_types.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
### ObjectKey
2+
| Variable Name | Type | Required | Default | Description |
3+
|---|---|---|---|---|
4+
| name | string | No | - | |
5+
| namespace | string | No | - | |
16
### MetaBase
27
| Variable Name | Type | Required | Default | Description |
38
|---|---|---|---|---|

pkg/reconciler/component.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
package reconciler
1616

1717
import (
18+
"emperror.dev/errors"
1819
"github.com/go-logr/logr"
1920
"k8s.io/apimachinery/pkg/runtime"
2021
ctrl "sigs.k8s.io/controller-runtime"
2122
"sigs.k8s.io/controller-runtime/pkg/builder"
2223
"sigs.k8s.io/controller-runtime/pkg/client"
24+
"sigs.k8s.io/controller-runtime/pkg/controller"
2325
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2426
)
2527

@@ -28,6 +30,10 @@ type ComponentReconciler interface {
2830
RegisterWatches(*builder.Builder)
2931
}
3032

33+
type Watches interface {
34+
SetupAdditionalWatches(c controller.Controller) error
35+
}
36+
3137
// Dispatcher orchestrates reconciliation of multiple ComponentReconciler objects
3238
// focusing on handing off reconciled object to all of its components and calculating an aggregated result to return.
3339
// It requires a ResourceGetter callback and optionally can leverage a ResourceFilter and a CompletionHandler
@@ -44,20 +50,20 @@ type Dispatcher struct {
4450
func (r *Dispatcher) Reconcile(req ctrl.Request) (ctrl.Result, error) {
4551
object, err := r.ResourceGetter(req)
4652
if err != nil {
47-
return reconcile.Result{}, err
53+
return reconcile.Result{}, errors.WithStack(err)
4854
}
4955
if r.ResourceFilter != nil {
5056
shouldReconcile, err := r.ResourceFilter(object)
5157
if err != nil || !shouldReconcile {
52-
return reconcile.Result{}, err
58+
return reconcile.Result{}, errors.WithStack(err)
5359
}
5460
}
5561
result, err := r.Handle(object)
5662
if r.CompletionHandler != nil {
57-
return r.CompletionHandler(object, result, err)
63+
return r.CompletionHandler(object, result, errors.WithStack(err))
5864
}
5965
if err != nil {
60-
return result, err
66+
return result, errors.WithStack(err)
6167
}
6268
return result, nil
6369
}
@@ -68,7 +74,12 @@ func (r *Dispatcher) Handle(object runtime.Object) (ctrl.Result, error) {
6874
combinedResult := &CombinedResult{}
6975
for _, cr := range r.ComponentReconcilers {
7076
result, err := cr.Reconcile(object)
71-
combinedResult.Combine(result, err)
77+
combinedResult.Combine(result, errors.WithStack(err))
78+
if cr, ok := cr.(interface{ IsOptional() bool }); ok {
79+
if err != nil && !cr.IsOptional() {
80+
break
81+
}
82+
}
7283
}
7384
return combinedResult.Result, combinedResult.Err
7485
}
@@ -80,3 +91,17 @@ func (r *Dispatcher) RegisterWatches(b *builder.Builder) *builder.Builder {
8091
}
8192
return b
8293
}
94+
95+
// SetupAdditionalWatches dispatches the controller for watch registration to all its components
96+
func (r *Dispatcher) SetupAdditionalWatches(c controller.Controller) error {
97+
for _, cr := range r.ComponentReconcilers {
98+
if cr, ok := cr.(Watches); ok {
99+
err := cr.SetupAdditionalWatches(c)
100+
if err != nil {
101+
return errors.WithStack(err)
102+
}
103+
}
104+
}
105+
106+
return nil
107+
}

pkg/reconciler/handlers.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright © 2020 Banzai Cloud
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package reconciler
16+
17+
import (
18+
"strings"
19+
20+
"k8s.io/apimachinery/pkg/types"
21+
"sigs.k8s.io/controller-runtime/pkg/client"
22+
"sigs.k8s.io/controller-runtime/pkg/handler"
23+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
24+
25+
ottypes "github.com/banzaicloud/operator-tools/pkg/types"
26+
)
27+
28+
func EnqueueByOwnerAnnotationMapper() handler.Mapper {
29+
return handler.ToRequestsFunc(func(a handler.MapObject) []reconcile.Request {
30+
pieces := strings.SplitN(a.Meta.GetAnnotations()[ottypes.BanzaiCloudRelatedTo], string(types.Separator), 2)
31+
if len(pieces) != 2 {
32+
return []reconcile.Request{}
33+
}
34+
35+
return []reconcile.Request{
36+
{NamespacedName: client.ObjectKey{
37+
Name: pieces[1],
38+
Namespace: pieces[0],
39+
}},
40+
}
41+
})
42+
}

pkg/reconciler/native.go

Lines changed: 106 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import (
2121
"emperror.dev/errors"
2222
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2323
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
24+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
2425
"k8s.io/apimachinery/pkg/api/meta"
26+
apimeta "k8s.io/apimachinery/pkg/api/meta"
2527
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2628
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2729
"k8s.io/apimachinery/pkg/runtime"
@@ -32,21 +34,28 @@ import (
3234
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
3335
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3436
"sigs.k8s.io/controller-runtime/pkg/reconcile"
35-
)
3637

37-
const BanzaiCloudManagedComponent = "banzaicloud.io/managed-component"
38+
"github.com/banzaicloud/operator-tools/pkg/types"
39+
"github.com/banzaicloud/operator-tools/pkg/utils"
40+
)
3841

3942
type ResourceOwner interface {
4043
// to be aware of metadata
4144
metav1.Object
4245
// to be aware of the owner's type
4346
runtime.Object
47+
}
48+
49+
type ResourceOwnerWithControlNamespace interface {
50+
ResourceOwner
4451
// control namespace dictates where namespaced objects should belong to
4552
GetControlNamespace() string
4653
}
4754

4855
type ResourceBuilders func(parent ResourceOwner, object interface{}) []ResourceBuilder
4956
type ResourceBuilder func() (runtime.Object, DesiredState, error)
57+
type ResourceTranslate func(runtime.Object) (parent ResourceOwner, config interface{})
58+
type PurgeTypesFunc func() []schema.GroupVersionKind
5059

5160
type NativeReconciledComponent interface {
5261
ResourceBuilders(parent ResourceOwner, object interface{}) []ResourceBuilder
@@ -93,11 +102,13 @@ func (d *DefaultReconciledComponent) PurgeTypes() []schema.GroupVersionKind {
93102
type NativeReconciler struct {
94103
*GenericResourceReconciler
95104
client.Client
96-
scheme *runtime.Scheme
97-
reconciledComponent NativeReconciledComponent
98-
configTranslate func(runtime.Object) (parent ResourceOwner, config interface{})
99-
componentName string
100-
setControllerRef bool
105+
scheme *runtime.Scheme
106+
restMapper meta.RESTMapper
107+
reconciledComponent NativeReconciledComponent
108+
configTranslate ResourceTranslate
109+
componentName string
110+
setControllerRef bool
111+
reconciledObjectStates map[reconciledObjectState][]runtime.Object
101112
}
102113

103114
type NativeReconcilerOpt func(*NativeReconciler)
@@ -114,6 +125,12 @@ func NativeReconcilerSetControllerRef() NativeReconcilerOpt {
114125
}
115126
}
116127

128+
func NativeReconcilerSetRESTMapper(mapper meta.RESTMapper) NativeReconcilerOpt {
129+
return func(r *NativeReconciler) {
130+
r.restMapper = mapper
131+
}
132+
}
133+
117134
func NewNativeReconciler(
118135
componentName string,
119136
rec *GenericResourceReconciler,
@@ -129,6 +146,8 @@ func NewNativeReconciler(
129146
componentName: componentName,
130147
}
131148

149+
reconciler.initReconciledObjectStates()
150+
132151
for _, opt := range opts {
133152
opt(reconciler)
134153
}
@@ -159,11 +178,12 @@ func (rec *NativeReconciler) Reconcile(owner runtime.Object) (*reconcile.Result,
159178
if err != nil {
160179
combinedResult.CombineErr(err)
161180
} else {
162-
objectMeta, err := rec.addAnnotation(o, componentID)
181+
objectMeta, err := rec.addComponentIDAnnotation(o, componentID)
163182
if err != nil {
164183
combinedResult.CombineErr(err)
165184
continue
166185
}
186+
rec.addRelatedToAnnotation(objectMeta, ownerMeta)
167187
if rec.setControllerRef {
168188
isCrd := false
169189
switch o.(type) {
@@ -173,9 +193,12 @@ func (rec *NativeReconciler) Reconcile(owner runtime.Object) (*reconcile.Result,
173193
isCrd = true
174194
}
175195
if !isCrd {
176-
if err := controllerutil.SetControllerReference(ownerMeta, objectMeta, rec.scheme); err != nil {
177-
combinedResult.CombineErr(err)
178-
continue
196+
// namespaced resource can only own resources in the same namespace
197+
if ownerMeta.GetNamespace() == "" || ownerMeta.GetNamespace() == objectMeta.GetNamespace() {
198+
if err := controllerutil.SetControllerReference(ownerMeta, objectMeta, rec.scheme); err != nil {
199+
combinedResult.CombineErr(err)
200+
continue
201+
}
179202
}
180203
}
181204
}
@@ -187,6 +210,12 @@ func (rec *NativeReconciler) Reconcile(owner runtime.Object) (*reconcile.Result,
187210
continue
188211
}
189212
excludeFromPurge[resourceID] = true
213+
214+
s := ReconciledObjectStatePresent
215+
if state == StateAbsent {
216+
s = ReconciledObjectStateAbsent
217+
}
218+
rec.addReconciledObjectState(s, o.DeepCopyObject())
190219
}
191220
combinedResult.Combine(result, err)
192221
}
@@ -260,12 +289,42 @@ func (rec *NativeReconciler) generateResourceID(resource runtime.Object) (string
260289
return strings.Join(identifiers, "-"), nil
261290
}
262291

292+
func (rec *NativeReconciler) gvkExists(gvk schema.GroupVersionKind) bool {
293+
if rec.restMapper == nil {
294+
return true
295+
}
296+
297+
mappings, err := rec.restMapper.RESTMappings(gvk.GroupKind(), gvk.Version)
298+
if apimeta.IsNoMatchError(err) {
299+
return false
300+
}
301+
if err != nil {
302+
return true
303+
}
304+
305+
for _, m := range mappings {
306+
if gvk == m.GroupVersionKind {
307+
return true
308+
}
309+
}
310+
311+
return false
312+
}
313+
263314
func (rec *NativeReconciler) purge(excluded map[string]bool, componentId string) error {
264315
var allErr error
265316
for _, gvk := range rec.reconciledComponent.PurgeTypes() {
317+
rec.Log.V(2).Info("purging GVK", "gvk", gvk)
318+
if !rec.gvkExists(gvk) {
319+
continue
320+
}
266321
objects := &unstructured.UnstructuredList{}
267322
objects.SetGroupVersionKind(gvk)
268323
err := rec.List(context.TODO(), objects)
324+
if apimeta.IsNoMatchError(err) {
325+
// skip unknown GVKs
326+
continue
327+
}
269328
if err != nil {
270329
rec.Log.Error(err, "failed list objects to prune",
271330
"groupversion", gvk.GroupVersion().String(),
@@ -286,23 +345,54 @@ func (rec *NativeReconciler) purge(excluded map[string]bool, componentId string)
286345
if excluded[resourceID] {
287346
continue
288347
}
289-
if objectMeta.GetAnnotations() != nil && objectMeta.GetAnnotations()[BanzaiCloudManagedComponent] == componentId {
348+
if objectMeta.GetAnnotations()[types.BanzaiCloudManagedComponent] == componentId {
290349
rec.Log.Info("pruning unmmanaged resource",
291350
"name", objectMeta.GetName(),
292351
"namespace", objectMeta.GetNamespace(),
293352
"group", gvk.Group,
294353
"version", gvk.Version,
295354
"listKind", gvk.Kind)
296-
if err := rec.Client.Delete(context.TODO(), &o); err != nil {
355+
if err := rec.Client.Delete(context.TODO(), &o); err != nil && !k8serrors.IsNotFound(err) {
297356
allErr = errors.Combine(allErr, err)
357+
} else {
358+
rec.addReconciledObjectState(ReconciledObjectStatePurged, o.DeepCopy())
298359
}
299360
}
300361
}
301362
}
302363
return allErr
303364
}
304365

305-
func (rec *NativeReconciler) addAnnotation(o runtime.Object, componentId string) (metav1.Object, error) {
366+
type reconciledObjectState string
367+
368+
const (
369+
ReconciledObjectStateAbsent reconciledObjectState = "Absent"
370+
ReconciledObjectStatePresent reconciledObjectState = "Present"
371+
ReconciledObjectStatePurged reconciledObjectState = "Purged"
372+
)
373+
374+
func (rec *NativeReconciler) initReconciledObjectStates() {
375+
rec.reconciledObjectStates = make(map[reconciledObjectState][]runtime.Object)
376+
}
377+
378+
func (rec *NativeReconciler) addReconciledObjectState(state reconciledObjectState, o runtime.Object) {
379+
rec.reconciledObjectStates[state] = append(rec.reconciledObjectStates[state], o)
380+
}
381+
382+
func (rec *NativeReconciler) GetReconciledObjectWithState(state reconciledObjectState) []runtime.Object {
383+
return rec.reconciledObjectStates[state]
384+
}
385+
386+
func (rec *NativeReconciler) addRelatedToAnnotation(objectMeta, ownerMeta metav1.Object) {
387+
annotations := objectMeta.GetAnnotations()
388+
if annotations == nil {
389+
annotations = make(map[string]string)
390+
}
391+
annotations[types.BanzaiCloudRelatedTo] = utils.ObjectKeyFromObjectMeta(ownerMeta).String()
392+
objectMeta.SetAnnotations(annotations)
393+
}
394+
395+
func (rec *NativeReconciler) addComponentIDAnnotation(o runtime.Object, componentId string) (metav1.Object, error) {
306396
objectMeta, err := meta.Accessor(o)
307397
if err != nil {
308398
return nil, errors.Wrapf(err, "failed to access object metadata")
@@ -311,14 +401,14 @@ func (rec *NativeReconciler) addAnnotation(o runtime.Object, componentId string)
311401
if annotations == nil {
312402
annotations = make(map[string]string)
313403
}
314-
if currentComponentId, ok := annotations[BanzaiCloudManagedComponent]; ok {
404+
if currentComponentId, ok := annotations[types.BanzaiCloudManagedComponent]; ok {
315405
if currentComponentId != componentId {
316406
return nil, errors.Errorf(
317407
"object actual component id `%s` is different from the one defined by the component `%s`",
318408
currentComponentId, componentId)
319409
}
320410
} else {
321-
annotations[BanzaiCloudManagedComponent] = componentId
411+
annotations[types.BanzaiCloudManagedComponent] = componentId
322412
objectMeta.SetAnnotations(annotations)
323413
}
324414
return objectMeta, nil

0 commit comments

Comments
 (0)