-
Notifications
You must be signed in to change notification settings - Fork 0
✨ Add migration from helm to boxcutter revision #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -489,22 +489,50 @@ func getCertificateProvider() render.CertificateProvider { | |
| func setupBoxcutter(mgr manager.Manager, ceReconciler *controllers.ClusterExtensionReconciler, preflights []applier.Preflight) error { | ||
| certProvider := getCertificateProvider() | ||
|
|
||
| coreClient, err := corev1client.NewForConfig(mgr.GetConfig()) | ||
| if err != nil { | ||
| return fmt.Errorf("unable to create core client: %w", err) | ||
| } | ||
| cfgGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), | ||
| helmclient.StorageDriverMapper(action.ChunkedStorageDriverMapper(coreClient, mgr.GetAPIReader(), cfg.systemNamespace)), | ||
| helmclient.ClientNamespaceMapper(func(obj client.Object) (string, error) { | ||
| ext := obj.(*ocv1.ClusterExtension) | ||
| return ext.Spec.Namespace, nil | ||
| }), | ||
| ) | ||
| if err != nil { | ||
| return fmt.Errorf("unable to create helm action config getter: %w", err) | ||
| } | ||
|
|
||
| acg, err := action.NewWrappedActionClientGetter(cfgGetter, | ||
| helmclient.WithFailureRollbacks(false), | ||
| ) | ||
| if err != nil { | ||
| return fmt.Errorf("unable to create helm action client getter: %w", err) | ||
| } | ||
|
|
||
| // TODO: add support for preflight checks | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this TODO still valid? |
||
| // TODO: better scheme handling - which types do we want to support? | ||
| _ = apiextensionsv1.AddToScheme(mgr.GetScheme()) | ||
| ceReconciler.Applier = &applier.Boxcutter{ | ||
| Client: mgr.GetClient(), | ||
| rg := &applier.SimpleRevisionGenerator{ | ||
| Scheme: mgr.GetScheme(), | ||
| RevisionGenerator: &applier.SimpleRevisionGenerator{ | ||
| Scheme: mgr.GetScheme(), | ||
| BundleRenderer: &applier.RegistryV1BundleRenderer{ | ||
| BundleRenderer: registryv1.Renderer, | ||
| CertificateProvider: certProvider, | ||
| }, | ||
| BundleRenderer: &applier.RegistryV1BundleRenderer{ | ||
| BundleRenderer: registryv1.Renderer, | ||
| CertificateProvider: certProvider, | ||
| }, | ||
| Preflights: preflights, | ||
| } | ||
| ceReconciler.Applier = &applier.Boxcutter{ | ||
| Client: mgr.GetClient(), | ||
| Scheme: mgr.GetScheme(), | ||
| RevisionGenerator: rg, | ||
| Preflights: preflights, | ||
| } | ||
| ceReconciler.RevisionStatesGetter = &controllers.BoxcutterRevisionStatesGetter{Reader: mgr.GetClient()} | ||
| ceReconciler.StorageMigrator = &applier.BoxcutterStorageMigrator{ | ||
| Client: mgr.GetClient(), | ||
| ActionClientGetter: acg, | ||
| RevisionGenerator: rg, | ||
| } | ||
|
|
||
| // Boxcutter | ||
| const ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,18 +11,25 @@ import ( | |
| "io/fs" | ||
| "maps" | ||
| "slices" | ||
| "strings" | ||
|
|
||
| "github.com/davecgh/go-spew/spew" | ||
| "helm.sh/helm/v3/pkg/release" | ||
| "helm.sh/helm/v3/pkg/storage/driver" | ||
| "k8s.io/apimachinery/pkg/api/meta" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
| "k8s.io/apimachinery/pkg/runtime" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| "sigs.k8s.io/controller-runtime/pkg/client/apiutil" | ||
| "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" | ||
| "sigs.k8s.io/yaml" | ||
|
|
||
| helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client" | ||
|
|
||
| ocv1 "github.com/operator-framework/operator-controller/api/v1" | ||
| "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" | ||
| "github.com/operator-framework/operator-controller/internal/operator-controller/labels" | ||
| "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" | ||
| "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" | ||
| ) | ||
|
|
@@ -33,14 +40,58 @@ const ( | |
|
|
||
| type ClusterExtensionRevisionGenerator interface { | ||
| GenerateRevision(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) | ||
| GenerateRevisionFromHelmRelease( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could we be mixing concerns here? Maybe we could use generics in the interface to define the input type? (e.g. helmRelease vs bundleFS)? |
||
| helmRelease *release.Release, ext *ocv1.ClusterExtension, | ||
| objectLabels map[string]string, | ||
| ) (*ocv1.ClusterExtensionRevision, error) | ||
| } | ||
|
|
||
| type SimpleRevisionGenerator struct { | ||
| Scheme *runtime.Scheme | ||
| BundleRenderer BundleRenderer | ||
| } | ||
|
|
||
| func (r *SimpleRevisionGenerator) GenerateRevision(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) { | ||
| func (r *SimpleRevisionGenerator) GenerateRevisionFromHelmRelease( | ||
| helmRelease *release.Release, ext *ocv1.ClusterExtension, | ||
| objectLabels map[string]string, | ||
| ) (*ocv1.ClusterExtensionRevision, error) { | ||
| docs := splitManifestDocuments(helmRelease.Manifest) | ||
| objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(docs)) | ||
| for _, doc := range docs { | ||
| obj := unstructured.Unstructured{} | ||
| if err := yaml.Unmarshal([]byte(doc), &obj); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| labels := maps.Clone(obj.GetLabels()) | ||
| if labels == nil { | ||
| labels = map[string]string{} | ||
| } | ||
| maps.Copy(labels, objectLabels) | ||
| obj.SetLabels(labels) | ||
| obj.SetOwnerReferences(nil) // reset OwnerReferences for migration. | ||
|
|
||
| objs = append(objs, ocv1.ClusterExtensionRevisionObject{ | ||
| Object: obj, | ||
| CollisionProtection: ocv1.CollisionProtectionNone, // allow to adopt objects from previous release | ||
| }) | ||
| } | ||
|
|
||
| rev := r.buildClusterExtensionRevision(objs, ext, map[string]string{ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could make this a revision generator and then just use composition for the bundle and helm release generators....wdyt? |
||
| labels.BundleNameKey: helmRelease.Labels[labels.BundleNameKey], | ||
| labels.PackageNameKey: helmRelease.Labels[labels.PackageNameKey], | ||
| labels.BundleVersionKey: helmRelease.Labels[labels.BundleVersionKey], | ||
| labels.BundleReferenceKey: helmRelease.Labels[labels.BundleReferenceKey], | ||
| }) | ||
| rev.Name = fmt.Sprintf("%s-1", ext.Name) | ||
| rev.Spec.Revision = 1 | ||
| return rev, nil | ||
| } | ||
|
|
||
| func (r *SimpleRevisionGenerator) GenerateRevision( | ||
| bundleFS fs.FS, ext *ocv1.ClusterExtension, | ||
| objectLabels, revisionAnnotations map[string]string, | ||
| ) (*ocv1.ClusterExtensionRevision, error) { | ||
| // extract plain manifests | ||
| plain, err := r.BundleRenderer.Render(bundleFS, ext) | ||
| if err != nil { | ||
|
|
@@ -50,14 +101,12 @@ func (r *SimpleRevisionGenerator) GenerateRevision(bundleFS fs.FS, ext *ocv1.Clu | |
| // objectLabels | ||
| objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(plain)) | ||
| for _, obj := range plain { | ||
| if len(obj.GetLabels()) > 0 { | ||
| labels := maps.Clone(obj.GetLabels()) | ||
| if labels == nil { | ||
| labels = map[string]string{} | ||
| } | ||
| maps.Copy(labels, objectLabels) | ||
| obj.SetLabels(labels) | ||
| labels := maps.Clone(obj.GetLabels()) | ||
| if labels == nil { | ||
| labels = map[string]string{} | ||
| } | ||
| maps.Copy(labels, objectLabels) | ||
| obj.SetLabels(labels) | ||
|
|
||
| gvk, err := apiutil.GVKForObject(obj, r.Scheme) | ||
| if err != nil { | ||
|
|
@@ -80,18 +129,73 @@ func (r *SimpleRevisionGenerator) GenerateRevision(bundleFS fs.FS, ext *ocv1.Clu | |
| revisionAnnotations = map[string]string{} | ||
| } | ||
|
|
||
| // Build desired revision | ||
| return r.buildClusterExtensionRevision(objs, ext, revisionAnnotations), nil | ||
| } | ||
|
|
||
| func (r *SimpleRevisionGenerator) buildClusterExtensionRevision( | ||
| objects []ocv1.ClusterExtensionRevisionObject, | ||
| ext *ocv1.ClusterExtension, | ||
| annotations map[string]string, | ||
| ) *ocv1.ClusterExtensionRevision { | ||
| return &ocv1.ClusterExtensionRevision{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Annotations: revisionAnnotations, | ||
| Annotations: annotations, | ||
| Labels: map[string]string{ | ||
| controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, | ||
| }, | ||
| }, | ||
| Spec: ocv1.ClusterExtensionRevisionSpec{ | ||
| Phases: PhaseSort(objs), | ||
| Phases: PhaseSort(objects), | ||
| }, | ||
| }, nil | ||
| } | ||
| } | ||
|
|
||
| type BoxcutterStorageMigrator struct { | ||
| ActionClientGetter helmclient.ActionClientGetter | ||
| RevisionGenerator ClusterExtensionRevisionGenerator | ||
| Client boxcutterStorageMigratorClient | ||
| } | ||
|
|
||
| type boxcutterStorageMigratorClient interface { | ||
| List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error | ||
| Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error | ||
| } | ||
|
|
||
| func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.ClusterExtension, objectLabels map[string]string) error { | ||
| existingRevisionList := ocv1.ClusterExtensionRevisionList{} | ||
| if err := m.Client.List(ctx, &existingRevisionList, client.MatchingLabels{ | ||
| controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, | ||
| }); err != nil { | ||
| return fmt.Errorf("listing ClusterExtensionRevisions before attempting migration: %w", err) | ||
| } | ||
| if len(existingRevisionList.Items) != 0 { | ||
| // No migration needed. | ||
| return nil | ||
| } | ||
|
|
||
| ac, err := m.ActionClientGetter.ActionClientFor(ctx, ext) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| helmRelease, err := ac.Get(ext.GetName()) | ||
| if errors.Is(err, driver.ErrReleaseNotFound) { | ||
| // no Helm Release -> no prior installation. | ||
| return nil | ||
| } | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| rev, err := m.RevisionGenerator.GenerateRevisionFromHelmRelease(helmRelease, ext, objectLabels) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if err := m.Client.Create(ctx, rev); err != nil { | ||
| return err | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| type Boxcutter struct { | ||
|
|
@@ -288,3 +392,16 @@ func (r *RegistryV1BundleRenderer) Render(bundleFS fs.FS, ext *ocv1.ClusterExten | |
| } | ||
| return r.BundleRenderer.Render(reg, ext.Spec.Namespace, render.WithTargetNamespaces(watchNamespace), render.WithCertificateProvider(r.CertificateProvider)) | ||
| } | ||
|
|
||
| func splitManifestDocuments(file string) []string { | ||
| //nolint:prealloc | ||
| var docs []string | ||
| for _, manifest := range strings.Split(file, "\n") { | ||
| manifest = strings.TrimSpace(manifest) | ||
| if len(manifest) == 0 { | ||
| continue | ||
| } | ||
| docs = append(docs, manifest) | ||
| } | ||
| return docs | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get what we're trying to say with "Revision number orders changes over time". Maybe just "Revision number changes over time"? Maybe "Revision provides historical ordering of revisions, must always be previous +1"?