Skip to content

Commit 4ad5221

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

File tree

10 files changed

+312
-67
lines changed

10 files changed

+312
-67
lines changed

api/v1/clusterextensionrevision_types.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,28 @@ type ClusterExtensionRevisionObject struct {
6666
// +kubebuilder:validation:EmbeddedResource
6767
// +kubebuilder:pruning:PreserveUnknownFields
6868
Object unstructured.Unstructured `json:"object"`
69+
70+
// +kubebuilder:default="Prevent"
71+
CollisionProtection CollisionProtection `json:"collisionProtection"`
6972
}
7073

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

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: 119 additions & 7 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,13 +40,64 @@ 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

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+
// Build desired revision
81+
return &ocv1.ClusterExtensionRevision{
82+
ObjectMeta: metav1.ObjectMeta{
83+
Annotations: map[string]string{
84+
labels.BundleNameKey: helmRelease.Labels[labels.BundleNameKey],
85+
labels.PackageNameKey: helmRelease.Labels[labels.PackageNameKey],
86+
labels.BundleVersionKey: helmRelease.Labels[labels.BundleVersionKey],
87+
labels.BundleReferenceKey: helmRelease.Labels[labels.BundleReferenceKey],
88+
},
89+
Labels: map[string]string{
90+
controllers.ClusterExtensionRevisionOwnerLabel: ext.Name,
91+
},
92+
Name: fmt.Sprintf("%s-1", ext.Name),
93+
},
94+
Spec: ocv1.ClusterExtensionRevisionSpec{
95+
Phases: PhaseSort(objs),
96+
Revision: 1,
97+
},
98+
}, nil
99+
}
100+
43101
func (r *SimpleRevisionGenerator) GenerateRevision(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) {
44102
// extract plain manifests
45103
plain, err := r.BundleRenderer.Render(bundleFS, ext)
@@ -50,14 +108,12 @@ func (r *SimpleRevisionGenerator) GenerateRevision(bundleFS fs.FS, ext *ocv1.Clu
50108
// objectLabels
51109
objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(plain))
52110
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)
111+
labels := maps.Clone(obj.GetLabels())
112+
if labels == nil {
113+
labels = map[string]string{}
60114
}
115+
maps.Copy(labels, objectLabels)
116+
obj.SetLabels(labels)
61117

62118
gvk, err := apiutil.GVKForObject(obj, r.Scheme)
63119
if err != nil {
@@ -94,6 +150,49 @@ func (r *SimpleRevisionGenerator) GenerateRevision(bundleFS fs.FS, ext *ocv1.Clu
94150
}, nil
95151
}
96152

153+
type BoxcutterStorageMigrator struct {
154+
ActionClientGetter helmclient.ActionClientGetter
155+
RevisionGenerator ClusterExtensionRevisionGenerator
156+
Client client.Client
157+
}
158+
159+
func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.ClusterExtension, objectLabels map[string]string) error {
160+
existingRevisionList := ocv1.ClusterExtensionRevisionList{}
161+
if err := m.Client.List(ctx, &existingRevisionList, client.MatchingLabels{
162+
controllers.ClusterExtensionRevisionOwnerLabel: ext.Name,
163+
}); err != nil {
164+
return fmt.Errorf("listing ClusterExtensionRevisions before attempting migration: %w", err)
165+
}
166+
if len(existingRevisionList.Items) != 0 {
167+
// No migration needed.
168+
return nil
169+
}
170+
171+
ac, err := m.ActionClientGetter.ActionClientFor(ctx, ext)
172+
if err != nil {
173+
return err
174+
}
175+
176+
helmRelease, err := ac.Get(ext.GetName())
177+
if errors.Is(err, driver.ErrReleaseNotFound) {
178+
// no Helm Release -> no prior installation.
179+
return nil
180+
}
181+
if err != nil {
182+
return err
183+
}
184+
185+
rev, err := m.RevisionGenerator.GenerateRevisionFromHelmRelease(helmRelease, ext, objectLabels)
186+
if err != nil {
187+
return err
188+
}
189+
190+
if err := m.Client.Create(ctx, rev); err != nil {
191+
return err
192+
}
193+
return nil
194+
}
195+
97196
type Boxcutter struct {
98197
Client client.Client
99198
Scheme *runtime.Scheme
@@ -289,3 +388,16 @@ func (r *RegistryV1BundleRenderer) Render(bundleFS fs.FS, ext *ocv1.ClusterExten
289388
}
290389
return r.BundleRenderer.Render(reg, ext.Spec.Namespace, render.WithTargetNamespaces(watchNamespace), render.WithCertificateProvider(r.CertificateProvider))
291390
}
391+
392+
func splitManifestDocuments(file string) []string {
393+
//nolint:prealloc
394+
var docs []string
395+
for _, manifest := range strings.Split(file, "\n") {
396+
manifest = strings.TrimSpace(manifest)
397+
if len(manifest) == 0 {
398+
continue
399+
}
400+
docs = append(docs, manifest)
401+
}
402+
return docs
403+
}

internal/operator-controller/applier/boxcutter_test.go

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

1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
13+
"helm.sh/helm/v3/pkg/release"
1314
appsv1 "k8s.io/api/apps/v1"
1415
corev1 "k8s.io/api/core/v1"
1516
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -268,7 +269,7 @@ func TestBoxcutter_Apply(t *testing.T) {
268269
UID: "test-uid",
269270
},
270271
}
271-
defaultDesiredHash := "faaeb52a1cb7c968c96278bc1cd804e50d3ae9faae08807c9279a5e569933ea0"
272+
defaultDesiredHash := "347b93565df45eb39cc8a7f158d66163da553b78ed5ac89f4b300372db1942a6"
272273
defaultDesiredRevision := &ocv1.ClusterExtensionRevision{
273274
ObjectMeta: metav1.ObjectMeta{
274275
Name: "test-ext-1",
@@ -460,7 +461,7 @@ func TestBoxcutter_Apply(t *testing.T) {
460461

461462
assert.Equal(t, "test-ext-2", newRev.Name)
462463
assert.Equal(t, int64(2), newRev.Spec.Revision)
463-
assert.Equal(t, "ec8213d4061a75b55cd67a009d9cdeb1bdd6f503d4b3bb7b6cfea3a5233aad43", newRev.Annotations[applier.RevisionHashAnnotation])
464+
assert.Equal(t, "6f681b6990c9939556392d2be3eb9389dd71e84fa051773a9aaf8cf8b34ff5b6", newRev.Annotations[applier.RevisionHashAnnotation])
464465
require.Len(t, newRev.Spec.Previous, 1)
465466
assert.Equal(t, "test-ext-1", newRev.Spec.Previous[0].Name)
466467
assert.Equal(t, types.UID("rev-uid-1"), newRev.Spec.Previous[0].UID)
@@ -534,6 +535,13 @@ func (m *mockBundleRevisionBuilder) GenerateRevision(bundleFS fs.FS, ext *ocv1.C
534535
return m.makeRevisionFunc(bundleFS, ext, objectLabels, revisionAnnotations)
535536
}
536537

538+
func (m *mockBundleRevisionBuilder) GenerateRevisionFromHelmRelease(
539+
helmRelease *release.Release, ext *ocv1.ClusterExtension,
540+
objectLabels map[string]string,
541+
) (*ocv1.ClusterExtensionRevision, error) {
542+
return nil, nil
543+
}
544+
537545
type mockBundleRenderer func(bundleFS fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error)
538546

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

0 commit comments

Comments
 (0)