Skip to content

Commit 7e9a601

Browse files
committed
Split reconcile function into step-based oriented approach
The logic present in private `reconcile` function of `ClusterExtensionReconciler` is refactored into step-oriented approach for increased modularity, so that Helm and Boxcutter based approaches can be wired, implemented, and tested in an easier way.
1 parent 8511722 commit 7e9a601

File tree

6 files changed

+597
-372
lines changed

6 files changed

+597
-372
lines changed

cmd/operator-controller/main.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,14 @@ func setupBoxcutter(
571571
ActionClientGetter: acg,
572572
RevisionGenerator: rg,
573573
}
574+
ceReconciler.ReconcileSteps = []controllers.ReconcileStepFunc{
575+
controllers.HandleFinalizers(ceReconciler.Finalizers),
576+
controllers.MigrateStorage(ceReconciler.StorageMigrator),
577+
controllers.RetrieveRevisionStates(ceReconciler.RevisionStatesGetter),
578+
controllers.RetrieveRevisionMetadata(ceReconciler.Resolver),
579+
controllers.UnpackBundle(ceReconciler.ImagePuller, ceReconciler.ImageCache),
580+
controllers.ApplyBundleWithBoxcutter(ceReconciler.Applier),
581+
}
574582

575583
discoveryClient, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig())
576584
if err != nil {
@@ -679,6 +687,14 @@ func setupHelm(
679687
Manager: cm,
680688
}
681689
ceReconciler.RevisionStatesGetter = &controllers.HelmRevisionStatesGetter{ActionClientGetter: acg}
690+
ceReconciler.ReconcileSteps = []controllers.ReconcileStepFunc{
691+
controllers.HandleFinalizers(ceReconciler.Finalizers),
692+
controllers.RetrieveRevisionStates(ceReconciler.RevisionStatesGetter),
693+
controllers.RetrieveRevisionMetadata(ceReconciler.Resolver),
694+
controllers.UnpackBundle(ceReconciler.ImagePuller, ceReconciler.ImageCache),
695+
controllers.ApplyBundle(ceReconciler.Applier),
696+
}
697+
682698
return nil
683699
}
684700

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"cmp"
21+
"context"
22+
"fmt"
23+
"io/fs"
24+
"slices"
25+
26+
apimeta "k8s.io/apimachinery/pkg/api/meta"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
ctrl "sigs.k8s.io/controller-runtime"
29+
"sigs.k8s.io/controller-runtime/pkg/client"
30+
"sigs.k8s.io/controller-runtime/pkg/log"
31+
32+
ocv1 "github.com/operator-framework/operator-controller/api/v1"
33+
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
34+
)
35+
36+
type BoxcutterRevisionStatesGetter struct {
37+
Reader client.Reader
38+
}
39+
40+
func (d *BoxcutterRevisionStatesGetter) GetRevisionStates(ctx context.Context, ext *ocv1.ClusterExtension) (*RevisionStates, error) {
41+
// TODO: boxcutter applier has a nearly identical bit of code for listing and sorting revisions
42+
// only difference here is that it sorts in reverse order to start iterating with the most
43+
// recent revisions. We should consolidate to avoid code duplication.
44+
existingRevisionList := &ocv1.ClusterExtensionRevisionList{}
45+
if err := d.Reader.List(ctx, existingRevisionList, client.MatchingLabels{
46+
ClusterExtensionRevisionOwnerLabel: ext.Name,
47+
}); err != nil {
48+
return nil, fmt.Errorf("listing revisions: %w", err)
49+
}
50+
slices.SortFunc(existingRevisionList.Items, func(a, b ocv1.ClusterExtensionRevision) int {
51+
return cmp.Compare(a.Spec.Revision, b.Spec.Revision)
52+
})
53+
54+
rs := &RevisionStates{}
55+
for _, rev := range existingRevisionList.Items {
56+
switch rev.Spec.LifecycleState {
57+
case ocv1.ClusterExtensionRevisionLifecycleStateActive,
58+
ocv1.ClusterExtensionRevisionLifecycleStatePaused:
59+
default:
60+
// Skip anything not active or paused, which should only be "Archived".
61+
continue
62+
}
63+
64+
// TODO: the setting of these annotations (happens in boxcutter applier when we pass in "storageLabels")
65+
// is fairly decoupled from this code where we get the annotations back out. We may want to co-locate
66+
// the set/get logic a bit better to make it more maintainable and less likely to get out of sync.
67+
rm := &RevisionMetadata{
68+
RevName: rev.Name,
69+
Package: rev.Labels[labels.PackageNameKey],
70+
Image: rev.Annotations[labels.BundleReferenceKey],
71+
Conditions: rev.Status.Conditions,
72+
BundleMetadata: ocv1.BundleMetadata{
73+
Name: rev.Annotations[labels.BundleNameKey],
74+
Version: rev.Annotations[labels.BundleVersionKey],
75+
},
76+
}
77+
78+
if apimeta.IsStatusConditionTrue(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) {
79+
rs.Installed = rm
80+
} else {
81+
rs.RollingOut = append(rs.RollingOut, rm)
82+
}
83+
}
84+
85+
return rs, nil
86+
}
87+
88+
func MigrateStorage(m StorageMigrator) ReconcileStepFunc {
89+
return func(ctx context.Context, ext *ocv1.ClusterExtension) (context.Context, *ctrl.Result, error) {
90+
objLbls := map[string]string{
91+
labels.OwnerKindKey: ocv1.ClusterExtensionKind,
92+
labels.OwnerNameKey: ext.GetName(),
93+
}
94+
95+
if err := m.Migrate(ctx, ext, objLbls); err != nil {
96+
return ctx, nil, fmt.Errorf("migrating storage: %w", err)
97+
}
98+
return ctx, nil, nil
99+
}
100+
}
101+
102+
func ApplyBundleWithBoxcutter(a Applier) ReconcileStepFunc {
103+
return func(ctx context.Context, ext *ocv1.ClusterExtension) (context.Context, *ctrl.Result, error) {
104+
l := log.FromContext(ctx)
105+
resolvedRevisionMetadata := ctx.Value(resolvedRevisionMetadataKey{}).(*RevisionMetadata)
106+
imageFS := ctx.Value(imageFSKey{}).(fs.FS)
107+
revisionStates := ctx.Value(revisionStatesKey{}).(*RevisionStates)
108+
storeLbls := map[string]string{
109+
labels.BundleNameKey: resolvedRevisionMetadata.Name,
110+
labels.PackageNameKey: resolvedRevisionMetadata.Package,
111+
labels.BundleVersionKey: resolvedRevisionMetadata.Version,
112+
labels.BundleReferenceKey: resolvedRevisionMetadata.Image,
113+
}
114+
objLbls := map[string]string{
115+
labels.OwnerKindKey: ocv1.ClusterExtensionKind,
116+
labels.OwnerNameKey: ext.GetName(),
117+
}
118+
119+
l.Info("applying bundle contents")
120+
if _, _, err := a.Apply(ctx, imageFS, ext, objLbls, storeLbls); err != nil {
121+
// If there was an error applying the resolved bundle,
122+
// report the error via the Progressing condition.
123+
setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedRevisionMetadata.BundleMetadata, err))
124+
return ctx, nil, err
125+
}
126+
127+
// Mirror Available/Progressing conditions from the installed revision
128+
if i := revisionStates.Installed; i != nil {
129+
for _, cndType := range []string{ocv1.ClusterExtensionRevisionTypeAvailable, ocv1.ClusterExtensionRevisionTypeProgressing} {
130+
cnd := *apimeta.FindStatusCondition(i.Conditions, cndType)
131+
apimeta.SetStatusCondition(&ext.Status.Conditions, cnd)
132+
}
133+
ext.Status.Install = &ocv1.ClusterExtensionInstallStatus{
134+
Bundle: i.BundleMetadata,
135+
}
136+
ext.Status.ActiveRevisions = []ocv1.RevisionStatus{{Name: i.RevName}}
137+
}
138+
for idx, r := range revisionStates.RollingOut {
139+
rs := ocv1.RevisionStatus{Name: r.RevName}
140+
// Mirror Progressing condition from the latest active revision
141+
if idx == len(revisionStates.RollingOut)-1 {
142+
pcnd := apimeta.FindStatusCondition(r.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing)
143+
if pcnd != nil {
144+
apimeta.SetStatusCondition(&ext.Status.Conditions, *pcnd)
145+
}
146+
if acnd := apimeta.FindStatusCondition(r.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable); pcnd.Status == metav1.ConditionFalse && acnd != nil && acnd.Status != metav1.ConditionTrue {
147+
apimeta.SetStatusCondition(&rs.Conditions, *acnd)
148+
}
149+
}
150+
if len(ext.Status.ActiveRevisions) == 0 {
151+
ext.Status.ActiveRevisions = []ocv1.RevisionStatus{rs}
152+
} else {
153+
ext.Status.ActiveRevisions = append(ext.Status.ActiveRevisions, rs)
154+
}
155+
}
156+
return ctx, nil, nil
157+
}
158+
}

0 commit comments

Comments
 (0)