Skip to content

Commit 8d7de9a

Browse files
committed
Add migration from helm to boxcutter revision
1 parent 7e4f03b commit 8d7de9a

File tree

11 files changed

+529
-79
lines changed

11 files changed

+529
-79
lines changed

api/v1/clusterextensionrevision_types.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,28 @@ const (
4444
// ClusterExtensionRevisionSpec defines the desired state of ClusterExtensionRevision.
4545
type ClusterExtensionRevisionSpec struct {
4646
// Specifies the lifecycle state of the ClusterExtensionRevision.
47+
//
4748
// +kubebuilder:default="Active"
4849
// +kubebuilder:validation:Enum=Active;Paused;Archived
4950
// +kubebuilder:validation:XValidation:rule="oldSelf == 'Active' || oldSelf == 'Paused' || oldSelf == 'Archived' && oldSelf == self", message="can not un-archive"
5051
LifecycleState ClusterExtensionRevisionLifecycleState `json:"lifecycleState,omitempty"`
52+
// Revision number orders changes over time, must always be previous revision +1.
53+
//
5154
// +kubebuilder:validation:Required
5255
// +kubebuilder:validation:XValidation:rule="self == oldSelf", message="revision is immutable"
5356
Revision int64 `json:"revision"`
57+
// Phases are groups of objects that will be applied at the same time.
58+
// All objects in the a phase will have to pass their probes in order to progress to the next phase.
59+
//
5460
// +kubebuilder:validation:Required
5561
// +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf.size() == 0", message="phases is immutable"
62+
// +patchMergeKey=name
63+
// +patchStrategy=merge
64+
// +listType=map
65+
// +listMapKey=name
5666
Phases []ClusterExtensionRevisionPhase `json:"phases"`
67+
// Previous references previous revisions that objects can be adopted from.
68+
//
5769
// +kubebuilder:validation:XValidation:rule="self == oldSelf", message="previous is immutable"
5870
Previous []ClusterExtensionRevisionPrevious `json:"previous,omitempty"`
5971
}
@@ -73,18 +85,47 @@ const (
7385
ClusterExtensionRevisionLifecycleStateArchived ClusterExtensionRevisionLifecycleState = "Archived"
7486
)
7587

88+
// ClusterExtensionRevisionPhase are groups of objects that will be applied at the same time.
89+
// All objects in the a phase will have to pass their probes in order to progress to the next phase.
7690
type ClusterExtensionRevisionPhase struct {
77-
Name string `json:"name"`
91+
// Name identifies this phase.
92+
//
93+
// +kubebuilder:validation:MaxLength=63
94+
// +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$`
95+
Name string `json:"name"`
96+
// Objects are a list of all the objects within this phase.
7897
Objects []ClusterExtensionRevisionObject `json:"objects"`
79-
Slices []string `json:"slices,omitempty"`
8098
}
8199

100+
// ClusterExtensionRevisionObject contains an object and settings for it.
82101
type ClusterExtensionRevisionObject struct {
83102
// +kubebuilder:validation:EmbeddedResource
84103
// +kubebuilder:pruning:PreserveUnknownFields
85104
Object unstructured.Unstructured `json:"object"`
105+
// CollisionProtection controls whether OLM can adopt and modify objects
106+
// already existing on the cluster or even owned by another controller.
107+
//
108+
// +kubebuilder:default="Prevent"
109+
CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"`
86110
}
87111

112+
// CollisionProtection specifies if and how ownership collisions are prevented.
113+
type CollisionProtection string
114+
115+
const (
116+
// CollisionProtectionPrevent prevents owner collisions entirely
117+
// by only allowing to work with objects itself has created.
118+
CollisionProtectionPrevent CollisionProtection = "Prevent"
119+
// CollisionProtectionIfNoController allows to patch and override
120+
// objects already present if they are not owned by another controller.
121+
CollisionProtectionIfNoController CollisionProtection = "IfNoController"
122+
// CollisionProtectionNone allows to patch and override objects
123+
// already present and owned by other controllers.
124+
// Be careful! This setting may cause multiple controllers to fight over a resource,
125+
// causing load on the API server and etcd.
126+
CollisionProtectionNone CollisionProtection = "None"
127+
)
128+
88129
type ClusterExtensionRevisionPrevious struct {
89130
// +kubebuilder:validation:Required
90131
Name string `json:"name"`

api/v1/zz_generated.deepcopy.go

Lines changed: 0 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/operator-controller/main.go

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -489,22 +489,50 @@ func getCertificateProvider() render.CertificateProvider {
489489
func setupBoxcutter(mgr manager.Manager, ceReconciler *controllers.ClusterExtensionReconciler, preflights []applier.Preflight) error {
490490
certProvider := getCertificateProvider()
491491

492+
coreClient, err := corev1client.NewForConfig(mgr.GetConfig())
493+
if err != nil {
494+
return fmt.Errorf("unable to create core client: %w", err)
495+
}
496+
cfgGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(),
497+
helmclient.StorageDriverMapper(action.ChunkedStorageDriverMapper(coreClient, mgr.GetAPIReader(), cfg.systemNamespace)),
498+
helmclient.ClientNamespaceMapper(func(obj client.Object) (string, error) {
499+
ext := obj.(*ocv1.ClusterExtension)
500+
return ext.Spec.Namespace, nil
501+
}),
502+
)
503+
if err != nil {
504+
return fmt.Errorf("unable to create helm action config getter: %w", err)
505+
}
506+
507+
acg, err := action.NewWrappedActionClientGetter(cfgGetter,
508+
helmclient.WithFailureRollbacks(false),
509+
)
510+
if err != nil {
511+
return fmt.Errorf("unable to create helm action client getter: %w", err)
512+
}
513+
492514
// TODO: add support for preflight checks
493515
// TODO: better scheme handling - which types do we want to support?
494516
_ = apiextensionsv1.AddToScheme(mgr.GetScheme())
495-
ceReconciler.Applier = &applier.Boxcutter{
496-
Client: mgr.GetClient(),
517+
rg := &applier.SimpleRevisionGenerator{
497518
Scheme: mgr.GetScheme(),
498-
RevisionGenerator: &applier.SimpleRevisionGenerator{
499-
Scheme: mgr.GetScheme(),
500-
BundleRenderer: &applier.RegistryV1BundleRenderer{
501-
BundleRenderer: registryv1.Renderer,
502-
CertificateProvider: certProvider,
503-
},
519+
BundleRenderer: &applier.RegistryV1BundleRenderer{
520+
BundleRenderer: registryv1.Renderer,
521+
CertificateProvider: certProvider,
504522
},
505-
Preflights: preflights,
523+
}
524+
ceReconciler.Applier = &applier.Boxcutter{
525+
Client: mgr.GetClient(),
526+
Scheme: mgr.GetScheme(),
527+
RevisionGenerator: rg,
528+
Preflights: preflights,
506529
}
507530
ceReconciler.RevisionStatesGetter = &controllers.BoxcutterRevisionStatesGetter{Reader: mgr.GetClient()}
531+
ceReconciler.StorageMigrator = &applier.BoxcutterStorageMigrator{
532+
Client: mgr.GetClient(),
533+
ActionClientGetter: acg,
534+
RevisionGenerator: rg,
535+
}
508536

509537
// Boxcutter
510538
const (

config/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,17 @@ spec:
6262
objects:
6363
items:
6464
properties:
65+
collisionProtection:
66+
default: Prevent
67+
description: CollisionProtection specifies if and how
68+
ownership collisions are prevented.
69+
type: string
6570
object:
6671
type: object
6772
x-kubernetes-embedded-resource: true
6873
x-kubernetes-preserve-unknown-fields: true
6974
required:
75+
- collisionProtection
7076
- object
7177
type: object
7278
type: array

internal/operator-controller/applier/boxcutter.go

Lines changed: 129 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,25 @@ import (
1111
"io/fs"
1212
"maps"
1313
"slices"
14+
"strings"
1415

1516
"github.com/davecgh/go-spew/spew"
17+
"helm.sh/helm/v3/pkg/release"
18+
"helm.sh/helm/v3/pkg/storage/driver"
1619
"k8s.io/apimachinery/pkg/api/meta"
1720
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1821
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1922
"k8s.io/apimachinery/pkg/runtime"
2023
"sigs.k8s.io/controller-runtime/pkg/client"
2124
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
2225
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
26+
"sigs.k8s.io/yaml"
27+
28+
helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
2329

2430
ocv1 "github.com/operator-framework/operator-controller/api/v1"
2531
"github.com/operator-framework/operator-controller/internal/operator-controller/controllers"
32+
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
2633
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source"
2734
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
2835
)
@@ -33,14 +40,58 @@ const (
3340

3441
type ClusterExtensionRevisionGenerator interface {
3542
GenerateRevision(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error)
43+
GenerateRevisionFromHelmRelease(
44+
helmRelease *release.Release, ext *ocv1.ClusterExtension,
45+
objectLabels map[string]string,
46+
) (*ocv1.ClusterExtensionRevision, error)
3647
}
3748

3849
type SimpleRevisionGenerator struct {
3950
Scheme *runtime.Scheme
4051
BundleRenderer BundleRenderer
4152
}
4253

43-
func (r *SimpleRevisionGenerator) GenerateRevision(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) {
54+
func (r *SimpleRevisionGenerator) GenerateRevisionFromHelmRelease(
55+
helmRelease *release.Release, ext *ocv1.ClusterExtension,
56+
objectLabels map[string]string,
57+
) (*ocv1.ClusterExtensionRevision, error) {
58+
docs := splitManifestDocuments(helmRelease.Manifest)
59+
objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(docs))
60+
for _, doc := range docs {
61+
obj := unstructured.Unstructured{}
62+
if err := yaml.Unmarshal([]byte(doc), &obj); err != nil {
63+
return nil, err
64+
}
65+
66+
labels := maps.Clone(obj.GetLabels())
67+
if labels == nil {
68+
labels = map[string]string{}
69+
}
70+
maps.Copy(labels, objectLabels)
71+
obj.SetLabels(labels)
72+
obj.SetOwnerReferences(nil) // reset OwnerReferences for migration.
73+
74+
objs = append(objs, ocv1.ClusterExtensionRevisionObject{
75+
Object: obj,
76+
CollisionProtection: ocv1.CollisionProtectionNone, // allow to adopt objects from previous release
77+
})
78+
}
79+
80+
rev := r.buildClusterExtensionRevision(objs, ext, map[string]string{
81+
labels.BundleNameKey: helmRelease.Labels[labels.BundleNameKey],
82+
labels.PackageNameKey: helmRelease.Labels[labels.PackageNameKey],
83+
labels.BundleVersionKey: helmRelease.Labels[labels.BundleVersionKey],
84+
labels.BundleReferenceKey: helmRelease.Labels[labels.BundleReferenceKey],
85+
})
86+
rev.Name = fmt.Sprintf("%s-1", ext.Name)
87+
rev.Spec.Revision = 1
88+
return rev, nil
89+
}
90+
91+
func (r *SimpleRevisionGenerator) GenerateRevision(
92+
bundleFS fs.FS, ext *ocv1.ClusterExtension,
93+
objectLabels, revisionAnnotations map[string]string,
94+
) (*ocv1.ClusterExtensionRevision, error) {
4495
// extract plain manifests
4596
plain, err := r.BundleRenderer.Render(bundleFS, ext)
4697
if err != nil {
@@ -50,14 +101,12 @@ func (r *SimpleRevisionGenerator) GenerateRevision(bundleFS fs.FS, ext *ocv1.Clu
50101
// objectLabels
51102
objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(plain))
52103
for _, obj := range plain {
53-
if len(obj.GetLabels()) > 0 {
54-
labels := maps.Clone(obj.GetLabels())
55-
if labels == nil {
56-
labels = map[string]string{}
57-
}
58-
maps.Copy(labels, objectLabels)
59-
obj.SetLabels(labels)
104+
labels := maps.Clone(obj.GetLabels())
105+
if labels == nil {
106+
labels = map[string]string{}
60107
}
108+
maps.Copy(labels, objectLabels)
109+
obj.SetLabels(labels)
61110

62111
gvk, err := apiutil.GVKForObject(obj, r.Scheme)
63112
if err != nil {
@@ -80,18 +129,73 @@ func (r *SimpleRevisionGenerator) GenerateRevision(bundleFS fs.FS, ext *ocv1.Clu
80129
revisionAnnotations = map[string]string{}
81130
}
82131

83-
// Build desired revision
132+
return r.buildClusterExtensionRevision(objs, ext, revisionAnnotations), nil
133+
}
134+
135+
func (r *SimpleRevisionGenerator) buildClusterExtensionRevision(
136+
objects []ocv1.ClusterExtensionRevisionObject,
137+
ext *ocv1.ClusterExtension,
138+
annotations map[string]string,
139+
) *ocv1.ClusterExtensionRevision {
84140
return &ocv1.ClusterExtensionRevision{
85141
ObjectMeta: metav1.ObjectMeta{
86-
Annotations: revisionAnnotations,
142+
Annotations: annotations,
87143
Labels: map[string]string{
88144
controllers.ClusterExtensionRevisionOwnerLabel: ext.Name,
89145
},
90146
},
91147
Spec: ocv1.ClusterExtensionRevisionSpec{
92-
Phases: PhaseSort(objs),
148+
Phases: PhaseSort(objects),
93149
},
94-
}, nil
150+
}
151+
}
152+
153+
type BoxcutterStorageMigrator struct {
154+
ActionClientGetter helmclient.ActionClientGetter
155+
RevisionGenerator ClusterExtensionRevisionGenerator
156+
Client boxcutterStorageMigratorClient
157+
}
158+
159+
type boxcutterStorageMigratorClient interface {
160+
List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error
161+
Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error
162+
}
163+
164+
func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.ClusterExtension, objectLabels map[string]string) error {
165+
existingRevisionList := ocv1.ClusterExtensionRevisionList{}
166+
if err := m.Client.List(ctx, &existingRevisionList, client.MatchingLabels{
167+
controllers.ClusterExtensionRevisionOwnerLabel: ext.Name,
168+
}); err != nil {
169+
return fmt.Errorf("listing ClusterExtensionRevisions before attempting migration: %w", err)
170+
}
171+
if len(existingRevisionList.Items) != 0 {
172+
// No migration needed.
173+
return nil
174+
}
175+
176+
ac, err := m.ActionClientGetter.ActionClientFor(ctx, ext)
177+
if err != nil {
178+
return err
179+
}
180+
181+
helmRelease, err := ac.Get(ext.GetName())
182+
if errors.Is(err, driver.ErrReleaseNotFound) {
183+
// no Helm Release -> no prior installation.
184+
return nil
185+
}
186+
if err != nil {
187+
return err
188+
}
189+
190+
rev, err := m.RevisionGenerator.GenerateRevisionFromHelmRelease(helmRelease, ext, objectLabels)
191+
if err != nil {
192+
return err
193+
}
194+
195+
if err := m.Client.Create(ctx, rev); err != nil {
196+
return err
197+
}
198+
return nil
95199
}
96200

97201
type Boxcutter struct {
@@ -288,3 +392,16 @@ func (r *RegistryV1BundleRenderer) Render(bundleFS fs.FS, ext *ocv1.ClusterExten
288392
}
289393
return r.BundleRenderer.Render(reg, ext.Spec.Namespace, render.WithTargetNamespaces(watchNamespace), render.WithCertificateProvider(r.CertificateProvider))
290394
}
395+
396+
func splitManifestDocuments(file string) []string {
397+
//nolint:prealloc
398+
var docs []string
399+
for _, manifest := range strings.Split(file, "\n") {
400+
manifest = strings.TrimSpace(manifest)
401+
if len(manifest) == 0 {
402+
continue
403+
}
404+
docs = append(docs, manifest)
405+
}
406+
return docs
407+
}

0 commit comments

Comments
 (0)