Skip to content

Commit be0beba

Browse files
committed
Add migration from helm to boxcutter revision
1 parent 01cf0d3 commit be0beba

5 files changed

Lines changed: 209 additions & 32 deletions

File tree

api/v1/clusterextensionrevision_types.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,27 @@ type ClusterExtensionRevisionObject struct {
6666
// +kubebuilder:validation:EmbeddedResource
6767
// +kubebuilder:pruning:PreserveUnknownFields
6868
Object unstructured.Unstructured `json:"object"`
69+
70+
CollisionProtection CollisionProtection `json:"collisionProtection"`
6971
}
7072

73+
// CollisionProtection specifies if and how ownership collisions are prevented.
74+
type CollisionProtection string
75+
76+
const (
77+
// CollisionProtectionPrevent prevents owner collisions entirely
78+
// by only allowing to work with objects itself has created.
79+
CollisionProtectionPrevent CollisionProtection = "Prevent"
80+
// CollisionProtectionIfNoController allows to patch and override
81+
// objects already present if they are not owned by another controller.
82+
CollisionProtectionIfNoController CollisionProtection = "IfNoController"
83+
// CollisionProtectionNone allows to patch and override objects
84+
// already present and owned by other controllers.
85+
// Be careful! This setting may cause multiple controllers to fight over a resource,
86+
// causing load on the API server and etcd.
87+
CollisionProtectionNone CollisionProtection = "None"
88+
)
89+
7190
type ClusterExtensionRevisionPrevious struct {
7291
// +kubebuilder:validation:Required
7392
Name string `json:"name"`

cmd/operator-controller/main.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,28 @@ 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())
@@ -502,7 +524,8 @@ func setupBoxcutter(mgr manager.Manager, ceReconciler *controllers.ClusterExtens
502524
CertificateProvider: certProvider,
503525
},
504526
},
505-
Preflights: preflights,
527+
Preflights: preflights,
528+
ActionClientGetter: acg,
506529
}
507530
ceReconciler.RevisionStatesGetter = &controllers.BoxcutterRevisionStatesGetter{Reader: mgr.GetClient()}
508531

internal/operator-controller/applier/boxcutter.go

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,23 @@ import (
1010
"hash"
1111
"io/fs"
1212
"maps"
13+
"regexp"
1314
"slices"
15+
"strings"
1416

1517
"github.com/davecgh/go-spew/spew"
18+
"helm.sh/helm/v3/pkg/release"
19+
"helm.sh/helm/v3/pkg/storage/driver"
1620
"k8s.io/apimachinery/pkg/api/meta"
1721
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1822
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1923
"k8s.io/apimachinery/pkg/runtime"
2024
"sigs.k8s.io/controller-runtime/pkg/client"
2125
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
2226
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
27+
"sigs.k8s.io/yaml"
2328

29+
helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
2430
ocv1 "github.com/operator-framework/operator-controller/api/v1"
2531
"github.com/operator-framework/operator-controller/internal/operator-controller/controllers"
2632
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source"
@@ -33,13 +39,57 @@ const (
3339

3440
type ClusterExtensionRevisionGenerator interface {
3541
GenerateRevision(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error)
42+
GenerateRevisionFromHelmRelease(
43+
helmRelease *release.Release, ext *ocv1.ClusterExtension,
44+
objectLabels, revisionAnnotations map[string]string,
45+
) (*ocv1.ClusterExtensionRevision, error)
3646
}
3747

3848
type SimpleRevisionGenerator struct {
3949
Scheme *runtime.Scheme
4050
BundleRenderer BundleRenderer
4151
}
4252

53+
func (r *SimpleRevisionGenerator) GenerateRevisionFromHelmRelease(
54+
helmRelease *release.Release, ext *ocv1.ClusterExtension,
55+
objectLabels, revisionAnnotations map[string]string,
56+
) (*ocv1.ClusterExtensionRevision, error) {
57+
docs := splitYAMLDocuments(helmRelease.Manifest)
58+
objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(docs))
59+
for _, doc := range docs {
60+
obj := unstructured.Unstructured{}
61+
if err := yaml.Unmarshal([]byte(doc), &obj); err != nil {
62+
return nil, err
63+
}
64+
65+
labels := maps.Clone(obj.GetLabels())
66+
if labels == nil {
67+
labels = map[string]string{}
68+
}
69+
maps.Copy(labels, objectLabels)
70+
obj.SetLabels(labels)
71+
72+
objs = append(objs, ocv1.ClusterExtensionRevisionObject{Object: obj})
73+
}
74+
75+
if revisionAnnotations == nil {
76+
revisionAnnotations = map[string]string{}
77+
}
78+
79+
// Build desired revision
80+
return &ocv1.ClusterExtensionRevision{
81+
ObjectMeta: metav1.ObjectMeta{
82+
Annotations: revisionAnnotations,
83+
Labels: map[string]string{
84+
controllers.ClusterExtensionRevisionOwnerLabel: ext.Name,
85+
},
86+
},
87+
Spec: ocv1.ClusterExtensionRevisionSpec{
88+
Phases: PhaseSort(objs),
89+
},
90+
}, nil
91+
}
92+
4393
func (r *SimpleRevisionGenerator) GenerateRevision(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) {
4494
// extract plain manifests
4595
plain, err := r.BundleRenderer.Render(bundleFS, ext)
@@ -50,14 +100,12 @@ func (r *SimpleRevisionGenerator) GenerateRevision(bundleFS fs.FS, ext *ocv1.Clu
50100
// objectLabels
51101
objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(plain))
52102
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)
103+
labels := maps.Clone(obj.GetLabels())
104+
if labels == nil {
105+
labels = map[string]string{}
60106
}
107+
maps.Copy(labels, objectLabels)
108+
obj.SetLabels(labels)
61109

62110
gvk, err := apiutil.GVKForObject(obj, r.Scheme)
63111
if err != nil {
@@ -99,6 +147,13 @@ type Boxcutter struct {
99147
Scheme *runtime.Scheme
100148
RevisionGenerator ClusterExtensionRevisionGenerator
101149
Preflights []Preflight
150+
151+
// For Migration:
152+
ActionClientGetter helmclient.ActionClientGetter
153+
}
154+
155+
type helmReleaseGetter interface {
156+
Get(name string, opts ...helmclient.GetOption) (*release.Release, error)
102157
}
103158

104159
func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (bool, string, error) {
@@ -145,6 +200,20 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
145200
state = StateNeedsUpgrade
146201
}
147202

203+
if currentRevision == nil && len(existingRevisions) == 0 {
204+
// Helm Migration
205+
// There are no existing revisions, so maybe we have used the Helm Applier in the past.
206+
// Check if there is a Helm Release on for the ClusterExtension and migrate it into an existing revision.
207+
rev, ok, err := bc.migrateFromHelm(ctx, ext, objectLabels, revisionAnnotations)
208+
if err != nil {
209+
return false, "", err
210+
}
211+
212+
if ok {
213+
desiredRevision = rev
214+
}
215+
}
216+
148217
// Preflights
149218
plainObjs := bc.getObjects(desiredRevision)
150219
for _, preflight := range bc.Preflights {
@@ -216,6 +285,43 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
216285
return true, "", nil
217286
}
218287

288+
func (bc *Boxcutter) migrateFromHelm(
289+
ctx context.Context, ext *ocv1.ClusterExtension,
290+
objectLabels, revisionAnnotations map[string]string,
291+
) (
292+
*ocv1.ClusterExtensionRevision, bool, error,
293+
) {
294+
ac, err := bc.ActionClientGetter.ActionClientFor(ctx, ext)
295+
if err != nil {
296+
return nil, false, err
297+
}
298+
299+
helmRelease, err := ac.Get(ext.GetName())
300+
if errors.Is(err, driver.ErrReleaseNotFound) {
301+
// no Helm Release -> no prior installation.
302+
return nil, false, nil
303+
}
304+
if err != nil {
305+
return nil, false, err
306+
}
307+
308+
docs := splitYAMLDocuments(helmRelease.Manifest)
309+
objs := make([]unstructured.Unstructured, 0, len(docs))
310+
for _, doc := range docs {
311+
obj := unstructured.Unstructured{}
312+
if err := yaml.Unmarshal([]byte(doc), &obj); err != nil {
313+
return nil, false, err
314+
}
315+
objs = append(objs, obj)
316+
}
317+
318+
rev, err := bc.RevisionGenerator.GenerateRevisionFromHelmRelease(helmRelease, ext, objectLabels, revisionAnnotations)
319+
if err != nil {
320+
return nil, false, err
321+
}
322+
return rev, true, nil
323+
}
324+
219325
// getExistingRevisions returns the list of ClusterExtensionRevisions for a ClusterExtension with name extName in revision order (oldest to newest)
220326
func (bc *Boxcutter) getExistingRevisions(ctx context.Context, extName string) ([]ocv1.ClusterExtensionRevision, error) {
221327
existingRevisionList := &ocv1.ClusterExtensionRevisionList{}
@@ -289,3 +395,13 @@ func (r *RegistryV1BundleRenderer) Render(bundleFS fs.FS, ext *ocv1.ClusterExten
289395
}
290396
return r.BundleRenderer.Render(reg, ext.Spec.Namespace, render.WithTargetNamespaces(watchNamespace), render.WithCertificateProvider(r.CertificateProvider))
291397
}
398+
399+
var splitYAMLDocumentsRegEx = regexp.MustCompile(`(?m)^---$`)
400+
401+
// Splits a YAML file into multiple documents.
402+
func splitYAMLDocuments(file string) (docs []string) {
403+
for _, yamlDocument := range splitYAMLDocumentsRegEx.Split(string(strings.Trim(file, "---\n")), -1) {
404+
docs = append(docs, strings.TrimSpace(string(yamlDocument)))
405+
}
406+
return docs
407+
}

internal/operator-controller/applier/boxcutter_test.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010

1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
13+
"helm.sh/helm/v3/pkg/release"
14+
"helm.sh/helm/v3/pkg/storage/driver"
1315
appsv1 "k8s.io/api/apps/v1"
1416
corev1 "k8s.io/api/core/v1"
1517
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -268,7 +270,7 @@ func TestBoxcutter_Apply(t *testing.T) {
268270
UID: "test-uid",
269271
},
270272
}
271-
defaultDesiredHash := "faaeb52a1cb7c968c96278bc1cd804e50d3ae9faae08807c9279a5e569933ea0"
273+
defaultDesiredHash := "347b93565df45eb39cc8a7f158d66163da553b78ed5ac89f4b300372db1942a6"
272274
defaultDesiredRevision := &ocv1.ClusterExtensionRevision{
273275
ObjectMeta: metav1.ObjectMeta{
274276
Name: "test-ext-1",
@@ -460,7 +462,7 @@ func TestBoxcutter_Apply(t *testing.T) {
460462

461463
assert.Equal(t, "test-ext-2", newRev.Name)
462464
assert.Equal(t, int64(2), newRev.Spec.Revision)
463-
assert.Equal(t, "ec8213d4061a75b55cd67a009d9cdeb1bdd6f503d4b3bb7b6cfea3a5233aad43", newRev.Annotations[applier.RevisionHashAnnotation])
465+
assert.Equal(t, "6f681b6990c9939556392d2be3eb9389dd71e84fa051773a9aaf8cf8b34ff5b6", newRev.Annotations[applier.RevisionHashAnnotation])
464466
require.Len(t, newRev.Spec.Previous, 1)
465467
assert.Equal(t, "test-ext-1", newRev.Spec.Previous[0].Name)
466468
assert.Equal(t, types.UID("rev-uid-1"), newRev.Spec.Previous[0].UID)
@@ -493,6 +495,9 @@ func TestBoxcutter_Apply(t *testing.T) {
493495
Client: fakeClient,
494496
Scheme: testScheme,
495497
RevisionGenerator: tc.mockBuilder,
498+
ActionClientGetter: &mockActionGetter{
499+
getClientErr: driver.ErrReleaseNotFound,
500+
},
496501
}
497502

498503
// We need a dummy fs.FS
@@ -534,6 +539,13 @@ func (m *mockBundleRevisionBuilder) GenerateRevision(bundleFS fs.FS, ext *ocv1.C
534539
return m.makeRevisionFunc(bundleFS, ext, objectLabels, revisionAnnotations)
535540
}
536541

542+
func (m *mockBundleRevisionBuilder) GenerateRevisionFromHelmRelease(
543+
helmRelease *release.Release, ext *ocv1.ClusterExtension,
544+
objectLabels, revisionAnnotations map[string]string,
545+
) (*ocv1.ClusterExtensionRevision, error) {
546+
return nil, nil
547+
}
548+
537549
type mockBundleRenderer func(bundleFS fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error)
538550

539551
func (f mockBundleRenderer) Render(bundleFS fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error) {

internal/operator-controller/controllers/clusterextensionrevision_controller.go

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -362,28 +362,6 @@ func (c *ClusterExtensionRevisionReconciler) removeFinalizer(ctx context.Context
362362
}
363363

364364
func toBoxcutterRevision(rev *ocv1.ClusterExtensionRevision) (*boxcutter.Revision, []boxcutter.RevisionReconcileOption, []client.Object) {
365-
r := &boxcutter.Revision{
366-
Name: rev.Name,
367-
Owner: rev,
368-
Revision: rev.Spec.Revision,
369-
}
370-
for _, specPhase := range rev.Spec.Phases {
371-
phase := boxcutter.Phase{Name: specPhase.Name}
372-
for _, specObj := range specPhase.Objects {
373-
obj := specObj.Object
374-
375-
labels := obj.GetLabels()
376-
if labels == nil {
377-
labels = map[string]string{}
378-
}
379-
labels[ClusterExtensionRevisionOwnerLabel] = rev.Labels[ClusterExtensionRevisionOwnerLabel]
380-
obj.SetLabels(labels)
381-
382-
phase.Objects = append(phase.Objects, obj)
383-
}
384-
r.Phases = append(r.Phases, phase)
385-
}
386-
387365
previous := make([]client.Object, 0, len(rev.Spec.Previous))
388366
for _, specPrevious := range rev.Spec.Previous {
389367
prev := &unstructured.Unstructured{}
@@ -421,6 +399,35 @@ func toBoxcutterRevision(rev *ocv1.ClusterExtensionRevision) (*boxcutter.Revisio
421399
return false, []string{"not available or not fully updated"}
422400
})),
423401
}
402+
403+
r := &boxcutter.Revision{
404+
Name: rev.Name,
405+
Owner: rev,
406+
Revision: rev.Spec.Revision,
407+
}
408+
for _, specPhase := range rev.Spec.Phases {
409+
phase := boxcutter.Phase{Name: specPhase.Name}
410+
for _, specObj := range specPhase.Objects {
411+
obj := specObj.Object
412+
413+
labels := obj.GetLabels()
414+
if labels == nil {
415+
labels = map[string]string{}
416+
}
417+
labels[ClusterExtensionRevisionOwnerLabel] = rev.Labels[ClusterExtensionRevisionOwnerLabel]
418+
obj.SetLabels(labels)
419+
420+
switch specObj.CollisionProtection {
421+
case ocv1.CollisionProtectionIfNoController, ocv1.CollisionProtectionNone:
422+
opts = append(opts, boxcutter.WithObjectReconcileOptions(
423+
&obj, boxcutter.WithCollisionProtection(specObj.CollisionProtection)))
424+
}
425+
426+
phase.Objects = append(phase.Objects, obj)
427+
}
428+
r.Phases = append(r.Phases, phase)
429+
}
430+
424431
if rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStatePaused {
425432
opts = append(opts, boxcutter.WithPaused{})
426433
}

0 commit comments

Comments
 (0)