Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions cmd/operator-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,14 @@ func setupBoxcutter(
ActionClientGetter: acg,
RevisionGenerator: rg,
}
ceReconciler.ReconcileSteps = []controllers.ReconcileStepFunc{
controllers.HandleFinalizers(ceReconciler.Finalizers),
controllers.MigrateStorage(ceReconciler.StorageMigrator),
controllers.RetrieveRevisionStates(ceReconciler.RevisionStatesGetter),
controllers.RetrieveRevisionMetadata(ceReconciler.Resolver),
controllers.UnpackBundle(ceReconciler.ImagePuller, ceReconciler.ImageCache),
controllers.ApplyBundle(ceReconciler.Applier),
}

baseDiscoveryClient, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig())
if err != nil {
Expand Down Expand Up @@ -700,6 +708,14 @@ func setupHelm(
Manager: cm,
}
ceReconciler.RevisionStatesGetter = &controllers.HelmRevisionStatesGetter{ActionClientGetter: acg}
ceReconciler.ReconcileSteps = []controllers.ReconcileStepFunc{
controllers.HandleFinalizers(ceReconciler.Finalizers),
controllers.RetrieveRevisionStates(ceReconciler.RevisionStatesGetter),
controllers.RetrieveRevisionMetadata(ceReconciler.Resolver),
controllers.UnpackBundle(ceReconciler.ImagePuller, ceReconciler.ImageCache),
controllers.ApplyBundle(ceReconciler.Applier),
}

return nil
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
"cmp"
"context"
"fmt"
"slices"

apimeta "k8s.io/apimachinery/pkg/api/meta"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

ocv1 "github.com/operator-framework/operator-controller/api/v1"
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
)

type BoxcutterRevisionStatesGetter struct {
Reader client.Reader
}

func (d *BoxcutterRevisionStatesGetter) GetRevisionStates(ctx context.Context, ext *ocv1.ClusterExtension) (*RevisionStates, error) {
// TODO: boxcutter applier has a nearly identical bit of code for listing and sorting revisions
// only difference here is that it sorts in reverse order to start iterating with the most
// recent revisions. We should consolidate to avoid code duplication.
existingRevisionList := &ocv1.ClusterExtensionRevisionList{}
if err := d.Reader.List(ctx, existingRevisionList, client.MatchingLabels{
ClusterExtensionRevisionOwnerLabel: ext.Name,
}); err != nil {
return nil, fmt.Errorf("listing revisions: %w", err)
}
slices.SortFunc(existingRevisionList.Items, func(a, b ocv1.ClusterExtensionRevision) int {
return cmp.Compare(a.Spec.Revision, b.Spec.Revision)
})

rs := &RevisionStates{}
for _, rev := range existingRevisionList.Items {
switch rev.Spec.LifecycleState {
case ocv1.ClusterExtensionRevisionLifecycleStateActive,
ocv1.ClusterExtensionRevisionLifecycleStatePaused:
default:
// Skip anything not active or paused, which should only be "Archived".
continue
}

// TODO: the setting of these annotations (happens in boxcutter applier when we pass in "storageLabels")
// is fairly decoupled from this code where we get the annotations back out. We may want to co-locate
// the set/get logic a bit better to make it more maintainable and less likely to get out of sync.
rm := &RevisionMetadata{
Package: rev.Labels[labels.PackageNameKey],
Image: rev.Annotations[labels.BundleReferenceKey],
BundleMetadata: ocv1.BundleMetadata{
Name: rev.Annotations[labels.BundleNameKey],
Version: rev.Annotations[labels.BundleVersionKey],
},
}

if apimeta.IsStatusConditionTrue(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) {
rs.Installed = rm
} else {
rs.RollingOut = append(rs.RollingOut, rm)
}
}

return rs, nil
}

Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MigrateStorage function lacks documentation. As an exported function that returns a ReconcileStepFunc, it should have a doc comment explaining its purpose and behavior.

Consider adding:

// MigrateStorage returns a ReconcileStepFunc that performs storage migration for the ClusterExtension.
// This step is specific to the boxcutter applier workflow and migrates storage from older formats.
func MigrateStorage(m StorageMigrator) ReconcileStepFunc {
Suggested change
// MigrateStorage returns a ReconcileStepFunc that performs storage migration for the ClusterExtension.
// This step is specific to the boxcutter applier workflow and migrates storage from older formats.

Copilot uses AI. Check for mistakes.
func MigrateStorage(m StorageMigrator) ReconcileStepFunc {
return func(ctx context.Context, ext *ocv1.ClusterExtension) (context.Context, *ctrl.Result, error) {
objLbls := map[string]string{
labels.OwnerKindKey: ocv1.ClusterExtensionKind,
labels.OwnerNameKey: ext.GetName(),
}

if err := m.Migrate(ctx, ext, objLbls); err != nil {
return ctx, nil, fmt.Errorf("migrating storage: %w", err)
}
return ctx, nil, nil
}
}
Loading
Loading