Skip to content

Commit 30c2b09

Browse files
committed
Dedicated controller for cleaning up bundle targets
Signed-off-by: Erik Godding Boye <[email protected]>
1 parent bc336d5 commit 30c2b09

File tree

4 files changed

+147
-79
lines changed

4 files changed

+147
-79
lines changed

pkg/bundle/bundle.go

Lines changed: 7 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
corev1 "k8s.io/api/core/v1"
2626
apierrors "k8s.io/apimachinery/pkg/api/errors"
2727
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28-
"k8s.io/apimachinery/pkg/labels"
2928
"k8s.io/apimachinery/pkg/types"
3029
utilerrors "k8s.io/apimachinery/pkg/util/errors"
3130
"k8s.io/client-go/tools/record"
@@ -169,9 +168,9 @@ func (b *bundle) reconcileBundle(ctx context.Context, req ctrl.Request) (result
169168
return ctrl.Result{}, statusPatch, nil
170169
}
171170

172-
targetResources := map[target.Resource]struct{}{}
171+
var targetResources []target.Resource
173172

174-
namespaceSelector, err := b.bundleTargetNamespaceSelector(&bundle)
173+
namespaceSelector, err := target.NamespaceSelector(&bundle)
175174
if err != nil {
176175
b.recorder.Eventf(&bundle, corev1.EventTypeWarning, "NamespaceSelectorError", "Failed to build namespace match labels selector: %s", err)
177176
return ctrl.Result{}, nil, fmt.Errorf("failed to build NamespaceSelector: %w", err)
@@ -202,87 +201,30 @@ func (b *bundle) reconcileBundle(ctx context.Context, req ctrl.Request) (result
202201
}
203202

204203
if bundle.Spec.Target.Secret != nil {
205-
targetResources[target.Resource{Kind: target.KindSecret, NamespacedName: namespacedName}] = struct{}{}
204+
targetResources = append(targetResources, target.Resource{Kind: target.KindSecret, NamespacedName: namespacedName})
206205
}
207206
if bundle.Spec.Target.ConfigMap != nil {
208-
targetResources[target.Resource{Kind: target.KindConfigMap, NamespacedName: namespacedName}] = struct{}{}
209-
}
210-
}
211-
}
212-
213-
// Find all old existing target resources.
214-
targetKinds := []target.Kind{target.KindConfigMap}
215-
if b.Options.SecretTargetsEnabled {
216-
targetKinds = append(targetKinds, target.KindSecret)
217-
}
218-
for _, kind := range targetKinds {
219-
targetList := &metav1.PartialObjectMetadataList{
220-
TypeMeta: metav1.TypeMeta{
221-
APIVersion: "v1",
222-
Kind: string(kind),
223-
},
224-
}
225-
err := b.targetReconciler.Cache.List(ctx, targetList, &client.ListOptions{
226-
LabelSelector: labels.SelectorFromSet(map[string]string{
227-
trustapi.BundleLabelKey: bundle.Name,
228-
}),
229-
})
230-
if err != nil {
231-
log.Error(err, "failed to list targets", "kind", kind)
232-
b.recorder.Eventf(&bundle, corev1.EventTypeWarning, fmt.Sprintf("%sListError", kind), "Failed to list %ss: %s", strings.ToLower(string(kind)), err)
233-
return ctrl.Result{}, nil, fmt.Errorf("failed to list %ss: %w", kind, err)
234-
}
235-
236-
for _, t := range targetList.Items {
237-
key := target.Resource{
238-
Kind: kind,
239-
NamespacedName: types.NamespacedName{
240-
Name: t.Name,
241-
Namespace: t.Namespace,
242-
},
243-
}
244-
245-
targetLog := log.WithValues("target", key)
246-
247-
if _, ok := targetResources[key]; ok {
248-
// This target is still a target, so we don't need to remove it.
249-
continue
250-
}
251-
252-
// Don't reconcile target for targets that are being deleted.
253-
if t.GetDeletionTimestamp() != nil {
254-
targetLog.V(2).WithValues("deletionTimestamp", t.GetDeletionTimestamp()).Info("skipping sync for target as it is being deleted")
255-
continue
256-
}
257-
258-
if !metav1.IsControlledBy(&t, &bundle) /* #nosec G601 -- False positive. See https://github.com/golang/go/discussions/56010 */ {
259-
targetLog.V(2).Info("skipping sync for target as it is not controlled by bundle")
260-
continue
261-
}
262-
263-
if _, err := b.targetReconciler.CleanupTarget(ctx, key, &bundle); err != nil {
264-
// Failing target cleanup is not considered critical, log error and continue.
265-
targetLog.Error(err, "failed to cleanup bundle target")
207+
targetResources = append(targetResources, target.Resource{Kind: target.KindConfigMap, NamespacedName: namespacedName})
266208
}
267209
}
268210
}
269211

270212
var needsUpdate bool
271213

272-
for t := range targetResources {
214+
for _, t := range targetResources {
273215
targetLog := log.WithValues("target", t)
274216
synced, err := b.targetReconciler.ApplyTarget(logf.IntoContext(ctx, targetLog), t, &bundle, resolvedBundle)
275217
if err != nil {
276218
targetLog.Error(err, "failed sync bundle to target namespace")
277-
b.recorder.Eventf(&bundle, corev1.EventTypeWarning, fmt.Sprintf("Sync%sTargetFailed", t.Kind), "Failed to sync target %s in Namespace %q: %s", t.Kind, t.Namespace, err)
219+
b.recorder.Eventf(&bundle, corev1.EventTypeWarning, fmt.Sprintf("ApplyTarget%sTargetFailed", t.Kind), "Failed to sync target %s in Namespace %q: %s", t.Kind, t.Namespace, err)
278220

279221
b.setBundleCondition(
280222
bundle.Status.Conditions,
281223
&statusPatch.Conditions,
282224
metav1.Condition{
283225
Type: trustapi.BundleConditionSynced,
284226
Status: metav1.ConditionFalse,
285-
Reason: fmt.Sprintf("Sync%sTargetFailed", t.Kind),
227+
Reason: fmt.Sprintf("ApplyTarget%sTargetFailed", t.Kind),
286228
Message: fmt.Sprintf("Failed to sync bundle %s to namespace %q: %s", t.Kind, t.Namespace, err),
287229
ObservedGeneration: bundle.Generation,
288230
},
@@ -330,16 +272,3 @@ func (b *bundle) reconcileBundle(ctx context.Context, req ctrl.Request) (result
330272

331273
return ctrl.Result{}, statusPatch, nil
332274
}
333-
334-
func (b *bundle) bundleTargetNamespaceSelector(bundleObj *trustapi.Bundle) (labels.Selector, error) {
335-
nsSelector := bundleObj.Spec.Target.NamespaceSelector
336-
337-
// LabelSelectorAsSelector returns a Selector selecting nothing if LabelSelector is nil,
338-
// while our current default is to select everything. But this is subject to change.
339-
// See https://github.com/cert-manager/trust-manager/issues/39
340-
if nsSelector == nil {
341-
return labels.Everything(), nil
342-
}
343-
344-
return metav1.LabelSelectorAsSelector(nsSelector)
345-
}

pkg/bundle/controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ func AddBundleController(
128128
// Reconcile all Bundles on a Namespace change.
129129
Watches(&corev1.Namespace{}, b.enqueueRequestsFromBundleFunc(
130130
func(obj client.Object, bundle trustapi.Bundle) bool {
131-
namespaceSelector, err := b.bundleTargetNamespaceSelector(&bundle)
131+
namespaceSelector, err := target.NamespaceSelector(&bundle)
132132
if err != nil {
133133
// We have an invalid selector, so we can skip this Bundle.
134134
return false
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package target
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
corev1 "k8s.io/api/core/v1"
8+
apierrors "k8s.io/apimachinery/pkg/api/errors"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/labels"
11+
ctrl "sigs.k8s.io/controller-runtime"
12+
"sigs.k8s.io/controller-runtime/pkg/client"
13+
14+
trustapi "github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1"
15+
"github.com/cert-manager/trust-manager/pkg/bundle/controller"
16+
)
17+
18+
// targetCleanupController is responsible for cleaning up obsolete bundle target resources.
19+
type targetCleanupController struct {
20+
*Reconciler
21+
22+
// Options holds options for the Bundle controller.
23+
controller.Options
24+
}
25+
26+
// Reconcile is the top level function for the controller,
27+
// and will be called whenever a Bundle event happens.
28+
func (t *targetCleanupController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
29+
bundleObj := &trustapi.Bundle{}
30+
err := t.Client.Get(ctx, req.NamespacedName, bundleObj)
31+
if apierrors.IsNotFound(err) || bundleObj.GetDeletionTimestamp() != nil {
32+
// Bundle doesn't exist or is about to be deleted.
33+
// Kubernetes garbage collector will delete obsolete bundle targets.
34+
return ctrl.Result{}, nil
35+
}
36+
if err != nil {
37+
return ctrl.Result{}, fmt.Errorf("failed to get %q: %s", req.NamespacedName, err)
38+
}
39+
40+
return ctrl.Result{}, t.reconcile(ctx, bundleObj)
41+
}
42+
43+
func (t *targetCleanupController) reconcile(ctx context.Context, bundle *trustapi.Bundle) error {
44+
targetKinds := []Kind{KindConfigMap}
45+
if t.Options.SecretTargetsEnabled {
46+
targetKinds = append(targetKinds, KindSecret)
47+
}
48+
49+
// Convert metav1.LabelSelector to labels.Selector
50+
nsSelector, err := NamespaceSelector(bundle)
51+
if err != nil {
52+
return fmt.Errorf("failed to build NamespaceSelector: %w", err)
53+
}
54+
55+
for _, kind := range targetKinds {
56+
var targetTemplate *trustapi.TargetTemplate
57+
switch kind {
58+
case KindConfigMap:
59+
targetTemplate = bundle.Spec.Target.ConfigMap
60+
case KindSecret:
61+
targetTemplate = bundle.Spec.Target.Secret
62+
default:
63+
return fmt.Errorf("unknown targetType: %s", kind)
64+
}
65+
66+
targetList := &metav1.PartialObjectMetadataList{
67+
TypeMeta: metav1.TypeMeta{
68+
APIVersion: "v1",
69+
Kind: string(kind),
70+
},
71+
}
72+
err := t.Cache.List(ctx, targetList, &client.ListOptions{
73+
LabelSelector: labels.SelectorFromSet(map[string]string{
74+
trustapi.BundleLabelKey: bundle.Name,
75+
}),
76+
})
77+
if err != nil {
78+
return fmt.Errorf("failed to list %ss: %w", kind, err)
79+
}
80+
81+
processTarget := func(targetObj *metav1.PartialObjectMetadata) error {
82+
targetResource := Resource{
83+
Kind: kind,
84+
NamespacedName: client.ObjectKeyFromObject(targetObj),
85+
}
86+
87+
if targetObj.GetDeletionTimestamp() != nil {
88+
// Don't reconcile target for targets that are being deleted.
89+
return nil
90+
}
91+
if !metav1.IsControlledBy(targetObj, bundle) /* #nosec G601 -- False positive. See https://github.com/golang/go/discussions/56010 */ {
92+
// Skipping delete of target not controlled by bundle
93+
return nil
94+
}
95+
96+
if targetTemplate == nil {
97+
// No targets of this kind should exist
98+
_, err := t.CleanupTarget(ctx, targetResource, bundle)
99+
return err
100+
}
101+
if !nsSelector.Empty() {
102+
// Target namespace selector limits target namespaces. We have to check if target namespace matches selector.
103+
ns := &corev1.Namespace{}
104+
if err := t.Client.Get(ctx, client.ObjectKey{Name: targetObj.Namespace}, ns); err != nil {
105+
return fmt.Errorf("failed to get %s namespace: %w", targetObj.Namespace, err)
106+
}
107+
if !nsSelector.Matches(labels.Set(ns.Labels)) {
108+
// Target namespace does not match selector, and should be cleaned.
109+
_, err := t.CleanupTarget(ctx, targetResource, bundle)
110+
return err
111+
}
112+
}
113+
return nil
114+
}
115+
116+
for _, targetObj := range targetList.Items {
117+
err := processTarget(&targetObj)
118+
if err != nil {
119+
return err
120+
}
121+
}
122+
}
123+
124+
return nil
125+
}

pkg/bundle/internal/target/target.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
corev1 "k8s.io/api/core/v1"
2929
apierrors "k8s.io/apimachinery/pkg/api/errors"
3030
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/labels"
3132
"k8s.io/apimachinery/pkg/types"
3233
"k8s.io/apimachinery/pkg/util/json"
3334
"k8s.io/apimachinery/pkg/util/sets"
@@ -423,3 +424,16 @@ func TrustBundleHash(data []byte, additionalFormats *trustapi.AdditionalFormats,
423424

424425
return hex.EncodeToString(hashValue[:])
425426
}
427+
428+
func NamespaceSelector(bundleObj *trustapi.Bundle) (labels.Selector, error) {
429+
nsSelector := bundleObj.Spec.Target.NamespaceSelector
430+
431+
// LabelSelectorAsSelector returns a Selector selecting nothing if LabelSelector is nil,
432+
// while our current default is to select everything. But this is subject to change.
433+
// See https://github.com/cert-manager/trust-manager/issues/39
434+
if nsSelector == nil {
435+
return labels.Everything(), nil
436+
}
437+
438+
return metav1.LabelSelectorAsSelector(nsSelector)
439+
}

0 commit comments

Comments
 (0)