From 346cfeb956cca8f3985325cb442f1553797e37ff Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Wed, 9 Oct 2024 18:45:11 +0200 Subject: [PATCH 1/6] add helmer Signed-off-by: Per Goncalves da Silva --- cmd/manager/main.go | 19 +- config/samples/argocd-helm.yaml | 39 + config/samples/catalogd_operatorcatalog.yaml | 2 +- internal/applier/helmer.go | 173 ++ .../clusterextension_controller.go | 12 +- .../clusterextension_controller_test.go | 1402 ----------------- internal/rukpak/source/tgz.go | 195 +++ 7 files changed, 432 insertions(+), 1410 deletions(-) create mode 100644 config/samples/argocd-helm.yaml create mode 100644 internal/applier/helmer.go delete mode 100644 internal/controllers/clusterextension_controller_test.go create mode 100644 internal/rukpak/source/tgz.go diff --git a/cmd/manager/main.go b/cmd/manager/main.go index b03472dfc0..adcd94f1e7 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -284,11 +284,25 @@ func main() { crdupgradesafety.NewPreflight(aeClient.CustomResourceDefinitions()), } - applier := &applier.Helm{ + olmApplier := &applier.Helm{ ActionClientGetter: acg, Preflights: preflights, } + helmer := &controllers.Engine{ + Unpacker: &source.TarGZ{ + BaseCachePath: filepath.Join(cachePath, "charts"), + }, + Applier: &applier.Helmer{ + ActionClientGetter: acg, + }, + } + + _ = &controllers.Engine{ + Unpacker: unpacker, + Applier: olmApplier, + } + cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper()) err = clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupContentManagerCacheFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) { ext := obj.(*ocv1alpha1.ClusterExtension) @@ -303,8 +317,7 @@ func main() { if err = (&controllers.ClusterExtensionReconciler{ Client: cl, Resolver: resolver, - Unpacker: unpacker, - Applier: applier, + Engine: helmer, InstalledBundleGetter: &controllers.DefaultInstalledBundleGetter{ActionClientGetter: acg}, Finalizers: clusterExtensionFinalizers, Manager: cm, diff --git a/config/samples/argocd-helm.yaml b/config/samples/argocd-helm.yaml new file mode 100644 index 0000000000..a33533de6f --- /dev/null +++ b/config/samples/argocd-helm.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: argocd-helm +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: argocd-helm-installer + namespace: argocd-helm +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: argocd-helm-cluster-admin-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: argocd-helm-installer + namespace: argocd-helm +--- +apiVersion: olm.operatorframework.io/v1alpha1 +kind: ClusterExtension +metadata: + name: argocd-helm +spec: + source: + sourceType: Catalog + catalog: + packageName: argocd-helm + version: 7.6.6 + install: + namespace: argocd-helm + serviceAccount: + name: argocd-helm-installer \ No newline at end of file diff --git a/config/samples/catalogd_operatorcatalog.yaml b/config/samples/catalogd_operatorcatalog.yaml index 48f1da5734..c5b4e8efa6 100644 --- a/config/samples/catalogd_operatorcatalog.yaml +++ b/config/samples/catalogd_operatorcatalog.yaml @@ -6,5 +6,5 @@ spec: source: type: Image image: - ref: quay.io/operatorhubio/catalog:latest + ref: docker.io/perdasilva/catalog:test pollInterval: 10m diff --git a/internal/applier/helmer.go b/internal/applier/helmer.go new file mode 100644 index 0000000000..807696c91e --- /dev/null +++ b/internal/applier/helmer.go @@ -0,0 +1,173 @@ +package applier + +import ( + "context" + "errors" + "fmt" + "helm.sh/helm/v3/pkg/chart/loader" + "io" + "io/fs" + "path/filepath" + "strings" + + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/postrender" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/storage/driver" + "sigs.k8s.io/controller-runtime/pkg/client" + + helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client" + + ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" + "github.com/operator-framework/operator-controller/internal/rukpak/util" +) + +type Helmer struct { + ActionClientGetter helmclient.ActionClientGetter +} + +func loadChartFromFS(fsys fs.FS, chartDir string) (*chart.Chart, error) { + var files []*loader.BufferedFile + + // Walk through the file system and gather the chart files + err := fs.WalkDir(fsys, chartDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Ignore directories + if d.IsDir() { + return nil + } + + // Open the file from fs.FS + file, err := fsys.Open(path) + if err != nil { + return err + } + defer file.Close() + + // Read the file content + content, err := io.ReadAll(file) + if err != nil { + return err + } + + // Create a BufferedFile with the content + relativePath, err := filepath.Rel(chartDir, path) + if err != nil { + return err + } + files = append(files, &loader.BufferedFile{Name: relativePath, Data: content}) + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("error walking file system: %v", err) + } + + // Load the chart from the in-memory files + chart, err := loader.LoadFiles(files) + if err != nil { + return nil, fmt.Errorf("failed to load chart: %v", err) + } + + return chart, nil +} + +func (h *Helmer) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1alpha1.ClusterExtension, objectLabels map[string]string, storageLabels map[string]string) ([]client.Object, string, error) { + chrt, err := loadChartFromFS(contentFS, ".") + if err != nil { + return nil, "", err + } + values := chartutil.Values{} + + ac, err := h.ActionClientGetter.ActionClientFor(ctx, ext) + if err != nil { + return nil, "", err + } + + post := &postrenderer{ + labels: objectLabels, + } + + rel, _, state, err := h.getReleaseState(ac, ext, chrt, values, post) + if err != nil { + return nil, "", err + } + + switch state { + case StateNeedsInstall: + rel, err = ac.Install(ext.GetName(), ext.Spec.Install.Namespace, chrt, values, func(install *action.Install) error { + install.CreateNamespace = false + install.Labels = storageLabels + return nil + }, helmclient.AppendInstallPostRenderer(post)) + if err != nil { + return nil, state, err + } + case StateNeedsUpgrade: + rel, err = ac.Upgrade(ext.GetName(), ext.Spec.Install.Namespace, chrt, values, func(upgrade *action.Upgrade) error { + upgrade.MaxHistory = maxHelmReleaseHistory + upgrade.Labels = storageLabels + return nil + }, helmclient.AppendUpgradePostRenderer(post)) + if err != nil { + return nil, state, err + } + case StateUnchanged: + if err := ac.Reconcile(rel); err != nil { + return nil, state, err + } + default: + return nil, state, fmt.Errorf("unexpected release state %q", state) + } + + relObjects, err := util.ManifestObjects(strings.NewReader(rel.Manifest), fmt.Sprintf("%s-release-manifest", rel.Name)) + if err != nil { + return nil, state, err + } + + return relObjects, state, nil +} + +func (h *Helmer) getReleaseState(cl helmclient.ActionInterface, ext *ocv1alpha1.ClusterExtension, chrt *chart.Chart, values chartutil.Values, post postrender.PostRenderer) (*release.Release, *release.Release, string, error) { + currentRelease, err := cl.Get(ext.GetName()) + if err != nil && !errors.Is(err, driver.ErrReleaseNotFound) { + return nil, nil, StateError, err + } + if errors.Is(err, driver.ErrReleaseNotFound) { + return nil, nil, StateNeedsInstall, nil + } + + if errors.Is(err, driver.ErrReleaseNotFound) { + desiredRelease, err := cl.Install(ext.GetName(), ext.Spec.Install.Namespace, chrt, values, func(i *action.Install) error { + i.DryRun = true + i.DryRunOption = "server" + return nil + }, helmclient.AppendInstallPostRenderer(post)) + if err != nil { + return nil, nil, StateError, err + } + return nil, desiredRelease, StateNeedsInstall, nil + } + desiredRelease, err := cl.Upgrade(ext.GetName(), ext.Spec.Install.Namespace, chrt, values, func(upgrade *action.Upgrade) error { + upgrade.MaxHistory = maxHelmReleaseHistory + upgrade.DryRun = true + upgrade.DryRunOption = "server" + return nil + }, helmclient.AppendUpgradePostRenderer(post)) + if err != nil { + return currentRelease, nil, StateError, err + } + relState := StateUnchanged + if desiredRelease.Manifest != currentRelease.Manifest || + currentRelease.Info.Status == release.StatusFailed || + currentRelease.Info.Status == release.StatusSuperseded { + relState = StateNeedsUpgrade + } + return currentRelease, desiredRelease, relState, nil +} diff --git a/internal/controllers/clusterextension_controller.go b/internal/controllers/clusterextension_controller.go index 2601d97f08..6e097bfa78 100644 --- a/internal/controllers/clusterextension_controller.go +++ b/internal/controllers/clusterextension_controller.go @@ -62,12 +62,16 @@ const ( ClusterExtensionCleanupContentManagerCacheFinalizer = "olm.operatorframework.io/cleanup-contentmanager-cache" ) +type Engine struct { + rukpaksource.Unpacker + Applier +} + // ClusterExtensionReconciler reconciles a ClusterExtension object type ClusterExtensionReconciler struct { client.Client Resolver resolve.Resolver - Unpacker rukpaksource.Unpacker - Applier Applier + Engine *Engine Manager contentmanager.Manager controller crcontroller.Controller cache cache.Cache @@ -247,7 +251,7 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp }, } l.Info("unpacking resolved bundle") - unpackResult, err := r.Unpacker.Unpack(ctx, bundleSource) + unpackResult, err := r.Engine.Unpack(ctx, bundleSource) if err != nil { // Wrap the error passed to this with the resolution information until we have successfully // installed since we intend for the progressing condition to replace the resolved condition @@ -281,7 +285,7 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp // to ensure exponential backoff can occur: // - Permission errors (it is not possible to watch changes to permissions. // The only way to eventually recover from permission errors is to keep retrying). - managedObjs, _, err := r.Applier.Apply(ctx, unpackResult.Bundle, ext, objLbls, storeLbls) + managedObjs, _, err := r.Engine.Apply(ctx, unpackResult.Bundle, ext, objLbls, storeLbls) if err != nil { setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedBundleMetadata, err)) // If bundle is not already installed, set Installed status condition to False diff --git a/internal/controllers/clusterextension_controller_test.go b/internal/controllers/clusterextension_controller_test.go deleted file mode 100644 index 8f7331384e..0000000000 --- a/internal/controllers/clusterextension_controller_test.go +++ /dev/null @@ -1,1402 +0,0 @@ -package controllers_test - -import ( - "context" - "errors" - "fmt" - "testing" - "testing/fstest" - - bsemver "github.com/blang/semver/v4" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - apimeta "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/rand" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/operator-framework/operator-registry/alpha/declcfg" - - ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" - "github.com/operator-framework/operator-controller/internal/conditionsets" - "github.com/operator-framework/operator-controller/internal/controllers" - "github.com/operator-framework/operator-controller/internal/finalizers" - "github.com/operator-framework/operator-controller/internal/resolve" - "github.com/operator-framework/operator-controller/internal/rukpak/source" -) - -// Describe: ClusterExtension Controller Test -func TestClusterExtensionDoesNotExist(t *testing.T) { - _, reconciler := newClientAndReconciler(t) - - t.Log("When the cluster extension does not exist") - t.Log("It returns no error") - res, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{Name: "non-existent"}}) - require.Equal(t, ctrl.Result{}, res) - require.NoError(t, err) -} - -func TestClusterExtensionResolutionFails(t *testing.T) { - pkgName := fmt.Sprintf("non-existent-%s", rand.String(6)) - cl, reconciler := newClientAndReconciler(t) - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1alpha1.ClusterExtension, _ *ocv1alpha1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - return nil, nil, nil, fmt.Errorf("no package %q found", pkgName) - }) - ctx := context.Background() - extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} - - t.Log("When the cluster extension specifies a non-existent package") - t.Log("By initializing cluster state") - clusterExtension := &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - PackageName: pkgName, - }, - }, - Install: ocv1alpha1.ClusterExtensionInstallConfig{ - Namespace: "default", - ServiceAccount: ocv1alpha1.ServiceAccountReference{ - Name: "default", - }, - }, - }, - } - require.NoError(t, cl.Create(ctx, clusterExtension)) - - t.Log("It sets resolution failure status") - t.Log("By running reconcile") - res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - require.Equal(t, ctrl.Result{}, res) - require.EqualError(t, err, fmt.Sprintf("no package %q found", pkgName)) - - t.Log("By fetching updated cluster extension after reconcile") - require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) - - t.Log("By checking the status fields") - require.Empty(t, clusterExtension.Status.Install) - - t.Log("By checking the expected conditions") - cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeProgressing) - require.NotNil(t, cond) - require.Equal(t, metav1.ConditionTrue, cond.Status) - require.Equal(t, ocv1alpha1.ReasonRetrying, cond.Reason) - require.Equal(t, fmt.Sprintf("no package %q found", pkgName), cond.Message) - - verifyInvariants(ctx, t, reconciler.Client, clusterExtension) - require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) -} - -func TestClusterExtensionResolutionSuccessfulUnpackFails(t *testing.T) { - type testCase struct { - name string - unpackErr error - expectTerminal bool - } - for _, tc := range []testCase{ - { - name: "non-terminal unpack failure", - unpackErr: errors.New("unpack failure"), - }, - { - name: "terminal unpack failure", - unpackErr: reconcile.TerminalError(errors.New("terminal unpack failure")), - expectTerminal: true, - }, - } { - t.Run(tc.name, func(t *testing.T) { - cl, reconciler := newClientAndReconciler(t) - reconciler.Unpacker = &MockUnpacker{ - err: tc.unpackErr, - } - - ctx := context.Background() - extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} - - t.Log("When the cluster extension specifies a channel with version that exist") - t.Log("By initializing cluster state") - pkgName := "prometheus" - pkgVer := "1.0.0" - pkgChan := "beta" - namespace := fmt.Sprintf("test-ns-%s", rand.String(8)) - serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) - - clusterExtension := &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - PackageName: pkgName, - Version: pkgVer, - Channels: []string{pkgChan}, - }, - }, - Install: ocv1alpha1.ClusterExtensionInstallConfig{ - Namespace: namespace, - ServiceAccount: ocv1alpha1.ServiceAccountReference{ - Name: serviceAccount, - }, - }, - }, - } - err := cl.Create(ctx, clusterExtension) - require.NoError(t, err) - - t.Log("It sets resolution success status") - t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1alpha1.ClusterExtension, _ *ocv1alpha1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") - return &declcfg.Bundle{ - Name: "prometheus.v1.0.0", - Package: "prometheus", - Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil - }) - res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - require.Equal(t, ctrl.Result{}, res) - require.Error(t, err) - - isTerminal := errors.Is(err, reconcile.TerminalError(nil)) - assert.Equal(t, tc.expectTerminal, isTerminal, "expected terminal error: %v, got: %v", tc.expectTerminal, isTerminal) - require.ErrorContains(t, err, tc.unpackErr.Error()) - - t.Log("By fetching updated cluster extension after reconcile") - require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) - - t.Log("By checking the status fields") - expectedBundleMetadata := ocv1alpha1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"} - require.Empty(t, clusterExtension.Status.Install) - - t.Log("By checking the expected conditions") - expectStatus := metav1.ConditionTrue - expectReason := ocv1alpha1.ReasonRetrying - if tc.expectTerminal { - expectStatus = metav1.ConditionFalse - expectReason = ocv1alpha1.ReasonBlocked - } - progressingCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeProgressing) - require.NotNil(t, progressingCond) - require.Equal(t, expectStatus, progressingCond.Status) - require.Equal(t, expectReason, progressingCond.Reason) - require.Contains(t, progressingCond.Message, fmt.Sprintf("for resolved bundle %q with version %q", expectedBundleMetadata.Name, expectedBundleMetadata.Version)) - - require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) - }) - } -} - -func TestClusterExtensionUnpackUnexpectedState(t *testing.T) { - cl, reconciler := newClientAndReconciler(t) - reconciler.Unpacker = &MockUnpacker{ - result: &source.Result{ - State: "unexpected", - }, - } - - ctx := context.Background() - extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} - - t.Log("When the cluster extension specifies a channel with version that exist") - t.Log("By initializing cluster state") - pkgName := "prometheus" - pkgVer := "1.0.0" - pkgChan := "beta" - namespace := fmt.Sprintf("test-ns-%s", rand.String(8)) - serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) - - clusterExtension := &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - PackageName: pkgName, - Version: pkgVer, - Channels: []string{pkgChan}, - }, - }, - Install: ocv1alpha1.ClusterExtensionInstallConfig{ - Namespace: namespace, - ServiceAccount: ocv1alpha1.ServiceAccountReference{ - Name: serviceAccount, - }, - }, - }, - } - err := cl.Create(ctx, clusterExtension) - require.NoError(t, err) - - t.Log("It sets resolution success status") - t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1alpha1.ClusterExtension, _ *ocv1alpha1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") - return &declcfg.Bundle{ - Name: "prometheus.v1.0.0", - Package: "prometheus", - Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil - }) - - require.Panics(t, func() { - _, _ = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - }, "reconciliation should panic on unknown unpack state") - - require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) -} - -func TestClusterExtensionResolutionAndUnpackSuccessfulApplierFails(t *testing.T) { - cl, reconciler := newClientAndReconciler(t) - reconciler.Unpacker = &MockUnpacker{ - result: &source.Result{ - State: source.StateUnpacked, - Bundle: fstest.MapFS{}, - }, - } - - ctx := context.Background() - extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} - - t.Log("When the cluster extension specifies a channel with version that exist") - t.Log("By initializing cluster state") - pkgName := "prometheus" - pkgVer := "1.0.0" - pkgChan := "beta" - namespace := fmt.Sprintf("test-ns-%s", rand.String(8)) - serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) - - clusterExtension := &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - PackageName: pkgName, - Version: pkgVer, - Channels: []string{pkgChan}, - }, - }, - Install: ocv1alpha1.ClusterExtensionInstallConfig{ - Namespace: namespace, - ServiceAccount: ocv1alpha1.ServiceAccountReference{ - Name: serviceAccount, - }, - }, - }, - } - err := cl.Create(ctx, clusterExtension) - require.NoError(t, err) - - t.Log("It sets resolution success status") - t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1alpha1.ClusterExtension, _ *ocv1alpha1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") - return &declcfg.Bundle{ - Name: "prometheus.v1.0.0", - Package: "prometheus", - Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil - }) - reconciler.Applier = &MockApplier{ - err: errors.New("apply failure"), - } - res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - require.Equal(t, ctrl.Result{}, res) - require.Error(t, err) - - t.Log("By fetching updated cluster extension after reconcile") - require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) - - t.Log("By checking the status fields") - expectedBundleMetadata := ocv1alpha1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"} - require.Empty(t, clusterExtension.Status.Install) - - t.Log("By checking the expected installed conditions") - installedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeInstalled) - require.NotNil(t, installedCond) - require.Equal(t, metav1.ConditionFalse, installedCond.Status) - require.Equal(t, ocv1alpha1.ReasonFailed, installedCond.Reason) - - t.Log("By checking the expected progressing conditions") - progressingCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeProgressing) - require.NotNil(t, progressingCond) - require.Equal(t, metav1.ConditionTrue, progressingCond.Status) - require.Equal(t, ocv1alpha1.ReasonRetrying, progressingCond.Reason) - require.Contains(t, progressingCond.Message, fmt.Sprintf("for resolved bundle %q with version %q", expectedBundleMetadata.Name, expectedBundleMetadata.Version)) - - require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) -} - -func TestClusterExtensionApplierFailsWithBundleInstalled(t *testing.T) { - cl, reconciler := newClientAndReconciler(t) - reconciler.Unpacker = &MockUnpacker{ - result: &source.Result{ - State: source.StateUnpacked, - Bundle: fstest.MapFS{}, - }, - } - - ctx := context.Background() - extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} - - t.Log("When the cluster extension specifies a channel with version that exist") - t.Log("By initializing cluster state") - pkgName := "prometheus" - pkgVer := "1.0.0" - pkgChan := "beta" - namespace := fmt.Sprintf("test-ns-%s", rand.String(8)) - serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) - - clusterExtension := &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - PackageName: pkgName, - Version: pkgVer, - Channels: []string{pkgChan}, - }, - }, - Install: ocv1alpha1.ClusterExtensionInstallConfig{ - Namespace: namespace, - ServiceAccount: ocv1alpha1.ServiceAccountReference{ - Name: serviceAccount, - }, - }, - }, - } - err := cl.Create(ctx, clusterExtension) - require.NoError(t, err) - - t.Log("It sets resolution success status") - t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1alpha1.ClusterExtension, _ *ocv1alpha1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") - return &declcfg.Bundle{ - Name: "prometheus.v1.0.0", - Package: "prometheus", - Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil - }) - - reconciler.Manager = &MockManagedContentCacheManager{ - cache: &MockManagedContentCache{}, - } - reconciler.InstalledBundleGetter = &MockInstalledBundleGetter{ - bundle: &ocv1alpha1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"}, - } - reconciler.Applier = &MockApplier{ - objs: []client.Object{}, - } - - res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - require.Equal(t, ctrl.Result{}, res) - require.NoError(t, err) - - reconciler.Applier = &MockApplier{ - err: errors.New("apply failure"), - } - - res, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - require.Equal(t, ctrl.Result{}, res) - require.Error(t, err) - - t.Log("By fetching updated cluster extension after reconcile") - require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) - - t.Log("By checking the status fields") - expectedBundleMetadata := ocv1alpha1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"} - require.Equal(t, expectedBundleMetadata, clusterExtension.Status.Install.Bundle) - - t.Log("By checking the expected installed conditions") - installedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeInstalled) - require.NotNil(t, installedCond) - require.Equal(t, metav1.ConditionTrue, installedCond.Status) - require.Equal(t, ocv1alpha1.ReasonSucceeded, installedCond.Reason) - - t.Log("By checking the expected progressing conditions") - progressingCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeProgressing) - require.NotNil(t, progressingCond) - require.Equal(t, metav1.ConditionTrue, progressingCond.Status) - require.Equal(t, ocv1alpha1.ReasonRetrying, progressingCond.Reason) - require.Contains(t, progressingCond.Message, fmt.Sprintf("for resolved bundle %q with version %q", expectedBundleMetadata.Name, expectedBundleMetadata.Version)) - - require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) -} - -func TestClusterExtensionManagerFailed(t *testing.T) { - cl, reconciler := newClientAndReconciler(t) - reconciler.Unpacker = &MockUnpacker{ - result: &source.Result{ - State: source.StateUnpacked, - Bundle: fstest.MapFS{}, - }, - } - - ctx := context.Background() - extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} - - t.Log("When the cluster extension specifies a channel with version that exist") - t.Log("By initializing cluster state") - pkgName := "prometheus" - pkgVer := "1.0.0" - pkgChan := "beta" - namespace := fmt.Sprintf("test-ns-%s", rand.String(8)) - serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) - - clusterExtension := &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - PackageName: pkgName, - Version: pkgVer, - Channels: []string{pkgChan}, - }, - }, - Install: ocv1alpha1.ClusterExtensionInstallConfig{ - Namespace: namespace, - ServiceAccount: ocv1alpha1.ServiceAccountReference{ - Name: serviceAccount, - }, - }, - }, - } - err := cl.Create(ctx, clusterExtension) - require.NoError(t, err) - - t.Log("It sets resolution success status") - t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1alpha1.ClusterExtension, _ *ocv1alpha1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") - return &declcfg.Bundle{ - Name: "prometheus.v1.0.0", - Package: "prometheus", - Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil - }) - reconciler.Applier = &MockApplier{ - objs: []client.Object{}, - } - reconciler.Manager = &MockManagedContentCacheManager{ - err: errors.New("manager fail"), - } - res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - require.Equal(t, ctrl.Result{}, res) - require.Error(t, err) - - t.Log("By fetching updated cluster extension after reconcile") - require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) - - t.Log("By checking the status fields") - require.Equal(t, ocv1alpha1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"}, clusterExtension.Status.Install.Bundle) - - t.Log("By checking the expected installed conditions") - installedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeInstalled) - require.NotNil(t, installedCond) - require.Equal(t, metav1.ConditionTrue, installedCond.Status) - require.Equal(t, ocv1alpha1.ReasonSucceeded, installedCond.Reason) - - t.Log("By checking the expected progressing conditions") - progressingCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeProgressing) - require.NotNil(t, progressingCond) - require.Equal(t, metav1.ConditionTrue, progressingCond.Status) - require.Equal(t, ocv1alpha1.ReasonRetrying, progressingCond.Reason) - - require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) -} - -func TestClusterExtensionManagedContentCacheWatchFail(t *testing.T) { - cl, reconciler := newClientAndReconciler(t) - reconciler.Unpacker = &MockUnpacker{ - result: &source.Result{ - State: source.StateUnpacked, - Bundle: fstest.MapFS{}, - }, - } - - ctx := context.Background() - extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} - - t.Log("When the cluster extension specifies a channel with version that exist") - t.Log("By initializing cluster state") - pkgName := "prometheus" - pkgVer := "1.0.0" - pkgChan := "beta" - installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) - serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) - - clusterExtension := &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: ocv1alpha1.SourceTypeCatalog, - - Catalog: &ocv1alpha1.CatalogSource{ - PackageName: pkgName, - Version: pkgVer, - Channels: []string{pkgChan}, - }, - }, - Install: ocv1alpha1.ClusterExtensionInstallConfig{ - Namespace: installNamespace, - ServiceAccount: ocv1alpha1.ServiceAccountReference{ - Name: serviceAccount, - }, - }, - }, - } - err := cl.Create(ctx, clusterExtension) - require.NoError(t, err) - - t.Log("It sets resolution success status") - t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1alpha1.ClusterExtension, _ *ocv1alpha1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") - return &declcfg.Bundle{ - Name: "prometheus.v1.0.0", - Package: "prometheus", - Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil - }) - reconciler.Applier = &MockApplier{ - objs: []client.Object{}, - } - reconciler.Manager = &MockManagedContentCacheManager{ - cache: &MockManagedContentCache{ - err: errors.New("watch error"), - }, - } - res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - require.Equal(t, ctrl.Result{}, res) - require.Error(t, err) - - t.Log("By fetching updated cluster extension after reconcile") - require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) - - t.Log("By checking the status fields") - require.Equal(t, ocv1alpha1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"}, clusterExtension.Status.Install.Bundle) - - t.Log("By checking the expected installed conditions") - installedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeInstalled) - require.NotNil(t, installedCond) - require.Equal(t, metav1.ConditionTrue, installedCond.Status) - require.Equal(t, ocv1alpha1.ReasonSucceeded, installedCond.Reason) - - t.Log("By checking the expected progressing conditions") - progressingCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeProgressing) - require.NotNil(t, progressingCond) - require.Equal(t, metav1.ConditionTrue, progressingCond.Status) - require.Equal(t, ocv1alpha1.ReasonRetrying, progressingCond.Reason) - - require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) -} - -func TestClusterExtensionInstallationSucceeds(t *testing.T) { - cl, reconciler := newClientAndReconciler(t) - reconciler.Unpacker = &MockUnpacker{ - result: &source.Result{ - State: source.StateUnpacked, - Bundle: fstest.MapFS{}, - }, - } - - ctx := context.Background() - extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} - - t.Log("When the cluster extension specifies a channel with version that exist") - t.Log("By initializing cluster state") - pkgName := "prometheus" - pkgVer := "1.0.0" - pkgChan := "beta" - namespace := fmt.Sprintf("test-ns-%s", rand.String(8)) - serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) - - clusterExtension := &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - PackageName: pkgName, - Version: pkgVer, - Channels: []string{pkgChan}, - }, - }, - Install: ocv1alpha1.ClusterExtensionInstallConfig{ - Namespace: namespace, - ServiceAccount: ocv1alpha1.ServiceAccountReference{ - Name: serviceAccount, - }, - }, - }, - } - err := cl.Create(ctx, clusterExtension) - require.NoError(t, err) - - t.Log("It sets resolution success status") - t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1alpha1.ClusterExtension, _ *ocv1alpha1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") - return &declcfg.Bundle{ - Name: "prometheus.v1.0.0", - Package: "prometheus", - Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil - }) - reconciler.Applier = &MockApplier{ - objs: []client.Object{}, - } - reconciler.Manager = &MockManagedContentCacheManager{ - cache: &MockManagedContentCache{}, - } - res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - require.Equal(t, ctrl.Result{}, res) - require.NoError(t, err) - - t.Log("By fetching updated cluster extension after reconcile") - require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) - - t.Log("By checking the status fields") - require.Equal(t, ocv1alpha1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"}, clusterExtension.Status.Install.Bundle) - - t.Log("By checking the expected installed conditions") - installedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeInstalled) - require.NotNil(t, installedCond) - require.Equal(t, metav1.ConditionTrue, installedCond.Status) - require.Equal(t, ocv1alpha1.ReasonSucceeded, installedCond.Reason) - - t.Log("By checking the expected progressing conditions") - progressingCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeProgressing) - require.NotNil(t, progressingCond) - require.Equal(t, metav1.ConditionFalse, progressingCond.Status) - require.Equal(t, ocv1alpha1.ReasonSucceeded, progressingCond.Reason) - - require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) -} - -func TestClusterExtensionDeleteFinalizerFails(t *testing.T) { - cl, reconciler := newClientAndReconciler(t) - reconciler.Unpacker = &MockUnpacker{ - result: &source.Result{ - State: source.StateUnpacked, - Bundle: fstest.MapFS{}, - }, - } - - ctx := context.Background() - extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} - - t.Log("When the cluster extension specifies a channel with version that exist") - t.Log("By initializing cluster state") - pkgName := "prometheus" - pkgVer := "1.0.0" - pkgChan := "beta" - namespace := fmt.Sprintf("test-ns-%s", rand.String(8)) - serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) - - clusterExtension := &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - PackageName: pkgName, - Version: pkgVer, - Channels: []string{pkgChan}, - }, - }, - Install: ocv1alpha1.ClusterExtensionInstallConfig{ - Namespace: namespace, - ServiceAccount: ocv1alpha1.ServiceAccountReference{ - Name: serviceAccount, - }, - }, - }, - } - err := cl.Create(ctx, clusterExtension) - require.NoError(t, err) - t.Log("It sets resolution success status") - t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1alpha1.ClusterExtension, _ *ocv1alpha1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") - return &declcfg.Bundle{ - Name: "prometheus.v1.0.0", - Package: "prometheus", - Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil - }) - fakeFinalizer := "fake.testfinalizer.io" - finalizersMessage := "still have finalizers" - reconciler.Applier = &MockApplier{ - objs: []client.Object{}, - } - reconciler.Manager = &MockManagedContentCacheManager{ - cache: &MockManagedContentCache{}, - } - reconciler.InstalledBundleGetter = &MockInstalledBundleGetter{ - bundle: &ocv1alpha1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"}, - } - err = reconciler.Finalizers.Register(fakeFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) { - return crfinalizer.Result{}, errors.New(finalizersMessage) - })) - - require.NoError(t, err) - - // Reconcile twice to simulate installing the ClusterExtension and loading in the finalizers - res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - require.Equal(t, ctrl.Result{}, res) - require.NoError(t, err) - res, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - require.Equal(t, ctrl.Result{}, res) - require.NoError(t, err) - - t.Log("By fetching updated cluster extension after first reconcile") - require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) - cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeInstalled) - expectedBundleMetadata := ocv1alpha1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"} - require.Equal(t, expectedBundleMetadata, clusterExtension.Status.Install.Bundle) - require.NotNil(t, cond) - require.Equal(t, metav1.ConditionTrue, cond.Status) - - require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) - res, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - require.Error(t, err, res) - - t.Log("By fetching updated cluster extension after second reconcile") - require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) - cond = apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeInstalled) - require.Equal(t, expectedBundleMetadata, clusterExtension.Status.Install.Bundle) - require.NotNil(t, cond) - require.Equal(t, metav1.ConditionTrue, cond.Status) - require.Equal(t, fakeFinalizer, clusterExtension.Finalizers[0]) - cond = apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeProgressing) - require.NotNil(t, cond) - require.Equal(t, metav1.ConditionTrue, cond.Status) - require.Contains(t, cond.Message, finalizersMessage) -} - -func verifyInvariants(ctx context.Context, t *testing.T, c client.Client, ext *ocv1alpha1.ClusterExtension) { - key := client.ObjectKeyFromObject(ext) - require.NoError(t, c.Get(ctx, key, ext)) - - verifyConditionsInvariants(t, ext) -} - -func verifyConditionsInvariants(t *testing.T, ext *ocv1alpha1.ClusterExtension) { - // Expect that the cluster extension's set of conditions contains all defined - // condition types for the ClusterExtension API. Every reconcile should always - // ensure every condition type's status/reason/message reflects the state - // read during _this_ reconcile call. - require.Len(t, ext.Status.Conditions, len(conditionsets.ConditionTypes)) - for _, tt := range conditionsets.ConditionTypes { - cond := apimeta.FindStatusCondition(ext.Status.Conditions, tt) - require.NotNil(t, cond) - require.NotEmpty(t, cond.Status) - require.Contains(t, conditionsets.ConditionReasons, cond.Reason) - require.Equal(t, ext.GetGeneration(), cond.ObservedGeneration) - } -} - -func TestSetDeprecationStatus(t *testing.T) { - for _, tc := range []struct { - name string - clusterExtension *ocv1alpha1.ClusterExtension - expectedClusterExtension *ocv1alpha1.ClusterExtension - bundle *declcfg.Bundle - deprecation *declcfg.Deprecation - }{ - { - name: "no deprecations, all deprecation statuses set to False", - clusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{}, - }, - }, - expectedClusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{ - { - Type: ocv1alpha1.TypeDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypePackageDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeChannelDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeBundleDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - }, - }, - }, - bundle: &declcfg.Bundle{}, - deprecation: nil, - }, - { - name: "deprecated channel, but no channel specified, all deprecation statuses set to False", - clusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{}, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{}, - }, - }, - expectedClusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{}, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{ - { - Type: ocv1alpha1.TypeDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypePackageDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeChannelDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeBundleDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - }, - }, - }, - bundle: &declcfg.Bundle{}, - deprecation: &declcfg.Deprecation{ - Entries: []declcfg.DeprecationEntry{{ - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaChannel, - Name: "badchannel", - }, - }}, - }, - }, - { - name: "deprecated channel, but a non-deprecated channel specified, all deprecation statuses set to False", - clusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - Channels: []string{"nondeprecated"}, - }, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{}, - }, - }, - expectedClusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - Channels: []string{"nondeprecated"}, - }, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{ - { - Type: ocv1alpha1.TypeDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypePackageDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeChannelDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeBundleDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - }, - }, - }, - bundle: &declcfg.Bundle{}, - deprecation: &declcfg.Deprecation{ - Entries: []declcfg.DeprecationEntry{ - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaChannel, - Name: "badchannel", - }, - }, - }, - }, - }, - { - name: "deprecated channel specified, ChannelDeprecated and Deprecated status set to true, others set to false", - clusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - Channels: []string{"badchannel"}, - }, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{}, - }, - }, - expectedClusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - Channels: []string{"badchannel"}, - }, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{ - { - Type: ocv1alpha1.TypeDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypePackageDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeChannelDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeBundleDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - }, - }, - }, - bundle: &declcfg.Bundle{}, - deprecation: &declcfg.Deprecation{ - Entries: []declcfg.DeprecationEntry{ - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaChannel, - Name: "badchannel", - }, - Message: "bad channel!", - }, - }, - }, - }, - { - name: "deprecated package and channel specified, deprecated bundle, all deprecation statuses set to true", - clusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - Channels: []string{"badchannel"}, - }, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{}, - }, - }, - expectedClusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - Channels: []string{"badchannel"}, - }, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{ - { - Type: ocv1alpha1.TypeDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypePackageDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeChannelDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeBundleDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - }, - }, - }, - bundle: &declcfg.Bundle{Name: "badbundle"}, - deprecation: &declcfg.Deprecation{ - Entries: []declcfg.DeprecationEntry{ - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaChannel, - Name: "badchannel", - }, - Message: "bad channel!", - }, - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaPackage, - }, - Message: "bad package!", - }, - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaBundle, - Name: "badbundle", - }, - Message: "bad bundle!", - }, - }, - }, - }, - { - name: "deprecated channel specified, deprecated bundle, all deprecation statuses set to true, all deprecation statuses set to true except PackageDeprecated", - clusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - Channels: []string{"badchannel"}, - }, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{}, - }, - }, - expectedClusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - Channels: []string{"badchannel"}, - }, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{ - { - Type: ocv1alpha1.TypeDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypePackageDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeChannelDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeBundleDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - }, - }, - }, - bundle: &declcfg.Bundle{Name: "badbundle"}, - deprecation: &declcfg.Deprecation{ - Entries: []declcfg.DeprecationEntry{ - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaChannel, - Name: "badchannel", - }, - Message: "bad channel!", - }, - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaBundle, - Name: "badbundle", - }, - Message: "bad bundle!", - }, - }, - }, - }, - { - name: "deprecated package and channel specified, all deprecation statuses set to true except BundleDeprecated", - clusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - Channels: []string{"badchannel"}, - }, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{}, - }, - }, - expectedClusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - Channels: []string{"badchannel"}, - }, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{ - { - Type: ocv1alpha1.TypeDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypePackageDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeChannelDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeBundleDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - }, - }, - }, - bundle: &declcfg.Bundle{}, - deprecation: &declcfg.Deprecation{ - Entries: []declcfg.DeprecationEntry{ - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaChannel, - Name: "badchannel", - }, - Message: "bad channel!", - }, - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaPackage, - }, - Message: "bad package!", - }, - }, - }, - }, - { - name: "deprecated channels specified, ChannelDeprecated and Deprecated status set to true, others set to false", - clusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - Channels: []string{"badchannel", "anotherbadchannel"}, - }, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{}, - }, - }, - expectedClusterExtension: &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Generation: 1, - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - Source: ocv1alpha1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1alpha1.CatalogSource{ - Channels: []string{"badchannel", "anotherbadchannel"}, - }, - }, - }, - Status: ocv1alpha1.ClusterExtensionStatus{ - Conditions: []metav1.Condition{ - { - Type: ocv1alpha1.TypeDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypePackageDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeChannelDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - ObservedGeneration: 1, - }, - { - Type: ocv1alpha1.TypeBundleDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - ObservedGeneration: 1, - }, - }, - }, - }, - bundle: &declcfg.Bundle{}, - deprecation: &declcfg.Deprecation{ - Entries: []declcfg.DeprecationEntry{ - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaChannel, - Name: "badchannel", - }, - Message: "bad channel!", - }, - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaChannel, - Name: "anotherbadchannel", - }, - Message: "another bad channedl!", - }, - }, - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - controllers.SetDeprecationStatus(tc.clusterExtension, tc.bundle.Name, tc.deprecation) - // TODO: we should test for unexpected changes to lastTransitionTime. We only expect - // lastTransitionTime to change when the status of the condition changes. - assert.Equal(t, "", cmp.Diff(tc.expectedClusterExtension, tc.clusterExtension, cmpopts.IgnoreFields(metav1.Condition{}, "Message", "LastTransitionTime"))) - }) - } -} diff --git a/internal/rukpak/source/tgz.go b/internal/rukpak/source/tgz.go new file mode 100644 index 0000000000..3646b4c669 --- /dev/null +++ b/internal/rukpak/source/tgz.go @@ -0,0 +1,195 @@ +package source + +import ( + "archive/tar" + "compress/gzip" + "context" + "errors" + "fmt" + "io" + "net/http" + "os" + "path" + "path/filepath" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/containers/image/v5/pkg/blobinfocache/none" + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type TarGZ struct { + BaseCachePath string +} + +func (i *TarGZ) Unpack(ctx context.Context, bundle *BundleSource) (*Result, error) { + l := log.FromContext(ctx) + + if bundle.Image == nil { + return nil, reconcile.TerminalError(fmt.Errorf("error parsing bundle, bundle %s has a nil image source", bundle.Name)) + } + + // Download the .tgz file + resp, err := http.Get(bundle.Image.Ref) + if err != nil { + return nil, reconcile.TerminalError(fmt.Errorf("error downloading bundle '%s': %v", bundle.Name, err)) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, reconcile.TerminalError(fmt.Errorf("error downloading bundle '%s': got status code '%d'", bundle.Name, resp.StatusCode)) + } + + // Open a gzip reader + gzReader, err := gzip.NewReader(resp.Body) + if err != nil { + return nil, reconcile.TerminalError(fmt.Errorf("error unpacking bundle '%s': %v", bundle.Name, err)) + } + defer gzReader.Close() + + unpackDir := path.Join(i.BaseCachePath, bundle.Name) + err = os.MkdirAll(unpackDir, 0700) + if err != nil { + return nil, fmt.Errorf("error creating temporary directory: %w", err) + } + defer func() { + if err := os.RemoveAll(unpackDir); err != nil { + l.Error(err, "error removing temporary OCI layout directory") + } + }() + + // Open a tar reader + tarReader := tar.NewReader(gzReader) + + _hack := "" + + // Extract tar contents + for { + header, err := tarReader.Next() + if err == io.EOF { + break // End of archive + } + if err != nil { + return nil, reconcile.TerminalError(fmt.Errorf("error unpaking bundle '%s': %v", bundle.Name, err)) + } + + // Construct the target file path + targetPath := filepath.Join(unpackDir, header.Name) + + switch header.Typeflag { + case tar.TypeDir: + // Create directory + if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil { + return nil, reconcile.TerminalError(fmt.Errorf("error unpacking bundle '%s': %v", bundle.Name, err)) + } + case tar.TypeReg: + // Ensure the directory for the file exists + if err := os.MkdirAll(filepath.Dir(targetPath), os.FileMode(0700)); err != nil { + return nil, reconcile.TerminalError(fmt.Errorf("error unpacking bundle '%s': %v", bundle.Name, err)) + } + if _hack == "" { + _hack = filepath.Join(unpackDir, filepath.Dir(targetPath)) + } + + // Create a file + outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode)) + if err != nil { + return nil, reconcile.TerminalError(fmt.Errorf("error unpacking bundle '%s': %v", bundle.Name, err)) + } + if _, err := io.Copy(outFile, tarReader); err != nil { + outFile.Close() + return nil, reconcile.TerminalError(fmt.Errorf("error unpacking bundle '%s': %v", bundle.Name, err)) + } + outFile.Close() + default: + // Handle other types of files if necessary (e.g., symlinks, etc.) + l.V(2).Info("Skipping unsupported file type in tar: %s\n", header.Name) + } + } + + return successHelmUnpackResult(bundle.Name, _hack, bundle.Image.Ref), nil +} + +func successHelmUnpackResult(bundleName, unpackPath string, chartgz string) *Result { + return &Result{ + Bundle: os.DirFS(unpackPath), + ResolvedSource: &BundleSource{Type: SourceTypeImage, Name: bundleName, Image: &ImageSource{Ref: chartgz}}, + State: StateUnpacked, + Message: fmt.Sprintf("unpacked %q successfully", chartgz), + } +} + +func (i *TarGZ) Cleanup(_ context.Context, bundle *BundleSource) error { + return deleteRecursive(i.bundlePath(bundle.Name)) +} + +func (i *TarGZ) bundlePath(bundleName string) string { + return filepath.Join(i.BaseCachePath, bundleName) +} + +func (i *TarGZ) unpackPath(bundleName string, digest digest.Digest) string { + return filepath.Join(i.bundlePath(bundleName), digest.String()) +} + +func (i *TarGZ) unpackImage(ctx context.Context, unpackPath string, imageReference types.ImageReference, sourceContext *types.SystemContext) error { + img, err := imageReference.NewImage(ctx, sourceContext) + if err != nil { + return fmt.Errorf("error reading image: %w", err) + } + defer func() { + if err := img.Close(); err != nil { + panic(err) + } + }() + + layoutSrc, err := imageReference.NewImageSource(ctx, sourceContext) + if err != nil { + return fmt.Errorf("error creating image source: %w", err) + } + + if err := os.MkdirAll(unpackPath, 0700); err != nil { + return fmt.Errorf("error creating unpack directory: %w", err) + } + l := log.FromContext(ctx) + l.Info("unpacking image", "path", unpackPath) + for i, layerInfo := range img.LayerInfos() { + if err := func() error { + layerReader, _, err := layoutSrc.GetBlob(ctx, layerInfo, none.NoCache) + if err != nil { + return fmt.Errorf("error getting blob for layer[%d]: %w", i, err) + } + defer layerReader.Close() + + if err := applyLayer(ctx, unpackPath, layerReader); err != nil { + return fmt.Errorf("error applying layer[%d]: %w", i, err) + } + l.Info("applied layer", "layer", i) + return nil + }(); err != nil { + return errors.Join(err, deleteRecursive(unpackPath)) + } + } + if err := setReadOnlyRecursive(unpackPath); err != nil { + return fmt.Errorf("error making unpack directory read-only: %w", err) + } + return nil +} + +func (i *TarGZ) deleteOtherImages(bundleName string, digestToKeep digest.Digest) error { + bundlePath := i.bundlePath(bundleName) + imgDirs, err := os.ReadDir(bundlePath) + if err != nil { + return fmt.Errorf("error reading image directories: %w", err) + } + for _, imgDir := range imgDirs { + if imgDir.Name() == digestToKeep.String() { + continue + } + imgDirPath := filepath.Join(bundlePath, imgDir.Name()) + if err := deleteRecursive(imgDirPath); err != nil { + return fmt.Errorf("error removing image directory: %w", err) + } + } + return nil +} From 20106c7b9049fbd30418a798c90946e5ed80ed6e Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Wed, 9 Oct 2024 18:49:35 +0200 Subject: [PATCH 2/6] do stuff Signed-off-by: Per Goncalves da Silva --- cmd/manager/main.go | 13 ++++-- config/samples/argocd-helm.yaml | 2 +- config/samples/catalogd_operatorcatalog.yaml | 2 +- config/samples/metrics-server.yaml | 39 ++++++++++++++++++ internal/applier/helmer.go | 13 ++---- .../clusterextension_controller.go | 41 +++++++++++++++++-- internal/rukpak/source/tgz.go | 40 +++++++++++------- 7 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 config/samples/metrics-server.yaml diff --git a/cmd/manager/main.go b/cmd/manager/main.go index adcd94f1e7..e48f901b21 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -289,7 +289,7 @@ func main() { Preflights: preflights, } - helmer := &controllers.Engine{ + helmEngine := &controllers.Engine{ Unpacker: &source.TarGZ{ BaseCachePath: filepath.Join(cachePath, "charts"), }, @@ -298,11 +298,18 @@ func main() { }, } - _ = &controllers.Engine{ + olmEngine := &controllers.Engine{ Unpacker: unpacker, Applier: olmApplier, } + enginator := &controllers.Enginator{ + Router: map[string]*controllers.Engine{ + "helm": helmEngine, + }, + DefaultEngine: olmEngine, + } + cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper()) err = clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupContentManagerCacheFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) { ext := obj.(*ocv1alpha1.ClusterExtension) @@ -317,7 +324,7 @@ func main() { if err = (&controllers.ClusterExtensionReconciler{ Client: cl, Resolver: resolver, - Engine: helmer, + Enginator: enginator, InstalledBundleGetter: &controllers.DefaultInstalledBundleGetter{ActionClientGetter: acg}, Finalizers: clusterExtensionFinalizers, Manager: cm, diff --git a/config/samples/argocd-helm.yaml b/config/samples/argocd-helm.yaml index a33533de6f..df5348b829 100644 --- a/config/samples/argocd-helm.yaml +++ b/config/samples/argocd-helm.yaml @@ -32,7 +32,7 @@ spec: sourceType: Catalog catalog: packageName: argocd-helm - version: 7.6.6 + version: 7.6.8 install: namespace: argocd-helm serviceAccount: diff --git a/config/samples/catalogd_operatorcatalog.yaml b/config/samples/catalogd_operatorcatalog.yaml index c5b4e8efa6..2dc84d1e14 100644 --- a/config/samples/catalogd_operatorcatalog.yaml +++ b/config/samples/catalogd_operatorcatalog.yaml @@ -6,5 +6,5 @@ spec: source: type: Image image: - ref: docker.io/perdasilva/catalog:test + ref: docker.io/perdasilva/catalog:2 pollInterval: 10m diff --git a/config/samples/metrics-server.yaml b/config/samples/metrics-server.yaml new file mode 100644 index 0000000000..4ed89fb2d8 --- /dev/null +++ b/config/samples/metrics-server.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: metrics-server +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: metrics-server-installer + namespace: metrics-server +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: metrics-server-cluster-admin-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: metrics-server-installer + namespace: metrics-server +--- +apiVersion: olm.operatorframework.io/v1alpha1 +kind: ClusterExtension +metadata: + name: metrics-server +spec: + source: + sourceType: Catalog + catalog: + packageName: metrics-server + version: 3.12.0 + install: + namespace: metrics-server + serviceAccount: + name: metrics-server-installer \ No newline at end of file diff --git a/internal/applier/helmer.go b/internal/applier/helmer.go index 807696c91e..e6ffe21702 100644 --- a/internal/applier/helmer.go +++ b/internal/applier/helmer.go @@ -7,7 +7,6 @@ import ( "helm.sh/helm/v3/pkg/chart/loader" "io" "io/fs" - "path/filepath" "strings" "helm.sh/helm/v3/pkg/action" @@ -28,11 +27,11 @@ type Helmer struct { ActionClientGetter helmclient.ActionClientGetter } -func loadChartFromFS(fsys fs.FS, chartDir string) (*chart.Chart, error) { +func loadChartFromFS(fsys fs.FS) (*chart.Chart, error) { var files []*loader.BufferedFile // Walk through the file system and gather the chart files - err := fs.WalkDir(fsys, chartDir, func(path string, d fs.DirEntry, err error) error { + err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } @@ -56,11 +55,7 @@ func loadChartFromFS(fsys fs.FS, chartDir string) (*chart.Chart, error) { } // Create a BufferedFile with the content - relativePath, err := filepath.Rel(chartDir, path) - if err != nil { - return err - } - files = append(files, &loader.BufferedFile{Name: relativePath, Data: content}) + files = append(files, &loader.BufferedFile{Name: path, Data: content}) return nil }) @@ -79,7 +74,7 @@ func loadChartFromFS(fsys fs.FS, chartDir string) (*chart.Chart, error) { } func (h *Helmer) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1alpha1.ClusterExtension, objectLabels map[string]string, storageLabels map[string]string) ([]client.Object, string, error) { - chrt, err := loadChartFromFS(contentFS, ".") + chrt, err := loadChartFromFS(contentFS) if err != nil { return nil, "", err } diff --git a/internal/controllers/clusterextension_controller.go b/internal/controllers/clusterextension_controller.go index 6e097bfa78..43ef47a54a 100644 --- a/internal/controllers/clusterextension_controller.go +++ b/internal/controllers/clusterextension_controller.go @@ -18,6 +18,7 @@ package controllers import ( "context" + "encoding/json" "errors" "fmt" "io/fs" @@ -67,11 +68,35 @@ type Engine struct { Applier } +type Enginator struct { + Router map[string]*Engine + DefaultEngine *Engine +} + +func (e *Enginator) GetEngine(bundle *declcfg.Bundle) (*Engine, error) { + contentType := "" + for _, property := range bundle.Properties { + if property.Type == "olm.content-type" { + if err := json.Unmarshal(property.Value, &contentType); err != nil { + return nil, fmt.Errorf("error unmarshalling package property: %w", err) + } + break + } + } + if contentType == "" { + return e.DefaultEngine, nil + } + if _, ok := e.Router[contentType]; !ok { + return nil, fmt.Errorf("unknown content type: %s", contentType) + } + return e.Router[contentType], nil +} + // ClusterExtensionReconciler reconciles a ClusterExtension object type ClusterExtensionReconciler struct { client.Client Resolver resolve.Resolver - Engine *Engine + Enginator *Enginator Manager contentmanager.Manager controller crcontroller.Controller cache cache.Cache @@ -250,8 +275,18 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp Ref: resolvedBundle.Image, }, } + + engine, err := r.Enginator.GetEngine(resolvedBundle) + if err != nil { + // Wrap the error passed to this with the resolution information until we have successfully + // installed since we intend for the progressing condition to replace the resolved condition + // and will be removing the .status.resolution field from the ClusterExtension status API + setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedBundleMetadata, err)) + return ctrl.Result{}, err + } + l.Info("unpacking resolved bundle") - unpackResult, err := r.Engine.Unpack(ctx, bundleSource) + unpackResult, err := engine.Unpack(ctx, bundleSource) if err != nil { // Wrap the error passed to this with the resolution information until we have successfully // installed since we intend for the progressing condition to replace the resolved condition @@ -285,7 +320,7 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp // to ensure exponential backoff can occur: // - Permission errors (it is not possible to watch changes to permissions. // The only way to eventually recover from permission errors is to keep retrying). - managedObjs, _, err := r.Engine.Apply(ctx, unpackResult.Bundle, ext, objLbls, storeLbls) + managedObjs, _, err := engine.Apply(ctx, unpackResult.Bundle, ext, objLbls, storeLbls) if err != nil { setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedBundleMetadata, err)) // If bundle is not already installed, set Installed status condition to False diff --git a/internal/rukpak/source/tgz.go b/internal/rukpak/source/tgz.go index 3646b4c669..cd00cf4bfd 100644 --- a/internal/rukpak/source/tgz.go +++ b/internal/rukpak/source/tgz.go @@ -8,10 +8,12 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "path" "path/filepath" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "strings" "github.com/containers/image/v5/pkg/blobinfocache/none" "github.com/containers/image/v5/types" @@ -30,6 +32,13 @@ func (i *TarGZ) Unpack(ctx context.Context, bundle *BundleSource) (*Result, erro return nil, reconcile.TerminalError(fmt.Errorf("error parsing bundle, bundle %s has a nil image source", bundle.Name)) } + // Parse the URL + parsedURL, err := url.Parse(bundle.Image.Ref) + if err != nil { + return nil, reconcile.TerminalError(fmt.Errorf("error downloading bundle '%s': %v", bundle.Name, err)) + } + fileName := path.Base(parsedURL.Path) + // Download the .tgz file resp, err := http.Get(bundle.Image.Ref) if err != nil { @@ -48,22 +57,15 @@ func (i *TarGZ) Unpack(ctx context.Context, bundle *BundleSource) (*Result, erro } defer gzReader.Close() - unpackDir := path.Join(i.BaseCachePath, bundle.Name) + unpackDir := path.Join(i.BaseCachePath, bundle.Name, fileName) err = os.MkdirAll(unpackDir, 0700) if err != nil { return nil, fmt.Errorf("error creating temporary directory: %w", err) } - defer func() { - if err := os.RemoveAll(unpackDir); err != nil { - l.Error(err, "error removing temporary OCI layout directory") - } - }() // Open a tar reader tarReader := tar.NewReader(gzReader) - - _hack := "" - + topLevelDir := "" // Extract tar contents for { header, err := tarReader.Next() @@ -74,8 +76,21 @@ func (i *TarGZ) Unpack(ctx context.Context, bundle *BundleSource) (*Result, erro return nil, reconcile.TerminalError(fmt.Errorf("error unpaking bundle '%s': %v", bundle.Name, err)) } + // On the first entry, capture the top-level directory + if topLevelDir == "" { + topLevelDir = strings.Split(header.Name, "/")[0] + } + + // Strip the top-level directory from the path + relativePath := strings.TrimPrefix(header.Name, topLevelDir+"/") + + if relativePath == "" { + // Skip the top-level directory itself + continue + } + // Construct the target file path - targetPath := filepath.Join(unpackDir, header.Name) + targetPath := filepath.Join(unpackDir, relativePath) switch header.Typeflag { case tar.TypeDir: @@ -88,9 +103,6 @@ func (i *TarGZ) Unpack(ctx context.Context, bundle *BundleSource) (*Result, erro if err := os.MkdirAll(filepath.Dir(targetPath), os.FileMode(0700)); err != nil { return nil, reconcile.TerminalError(fmt.Errorf("error unpacking bundle '%s': %v", bundle.Name, err)) } - if _hack == "" { - _hack = filepath.Join(unpackDir, filepath.Dir(targetPath)) - } // Create a file outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode)) @@ -108,7 +120,7 @@ func (i *TarGZ) Unpack(ctx context.Context, bundle *BundleSource) (*Result, erro } } - return successHelmUnpackResult(bundle.Name, _hack, bundle.Image.Ref), nil + return successHelmUnpackResult(bundle.Name, unpackDir, bundle.Image.Ref), nil } func successHelmUnpackResult(bundleName, unpackPath string, chartgz string) *Result { From c24cb97756f9598cc5c6211a0724311a3a21d58a Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Thu, 10 Oct 2024 15:11:36 +0200 Subject: [PATCH 3/6] more stuff Signed-off-by: Per Goncalves da Silva --- cmd/manager/main.go | 24 ++++++++++++++++++- internal/action/storagedriver.go | 23 +++++++++++++++++- .../clusterextension_controller.go | 3 --- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/cmd/manager/main.go b/cmd/manager/main.go index e48f901b21..0155a28ca4 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -206,6 +206,20 @@ func main() { os.Exit(1) } + pureHelmGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), + helmclient.StorageDriverMapper(action.PureHelmStorageDriverMapper(clientRestConfigMapper, mgr.GetAPIReader())), + helmclient.ClientNamespaceMapper(func(obj client.Object) (string, error) { + ext := obj.(*ocv1alpha1.ClusterExtension) + return ext.Spec.Install.Namespace, nil + }), + // helmclient.StorageRestConfigMapper(clientRestConfigMapper), + helmclient.ClientRestConfigMapper(clientRestConfigMapper), + ) + if err != nil { + setupLog.Error(err, "unable to config for creating helm client") + os.Exit(1) + } + acg, err := action.NewWrappedActionClientGetter(cfgGetter, helmclient.WithFailureRollbacks(false), ) @@ -214,6 +228,14 @@ func main() { os.Exit(1) } + phg, err := action.NewWrappedActionClientGetter(pureHelmGetter, + helmclient.WithFailureRollbacks(false), + ) + if err != nil { + setupLog.Error(err, "unable to create helm client") + os.Exit(1) + } + certPoolWatcher, err := httputil.NewCertPoolWatcher(caCertDir, ctrl.Log.WithName("cert-pool")) if err != nil { setupLog.Error(err, "unable to create CA certificate pool") @@ -294,7 +316,7 @@ func main() { BaseCachePath: filepath.Join(cachePath, "charts"), }, Applier: &applier.Helmer{ - ActionClientGetter: acg, + ActionClientGetter: phg, }, } diff --git a/internal/action/storagedriver.go b/internal/action/storagedriver.go index db8c02ddb2..d34ebfd431 100644 --- a/internal/action/storagedriver.go +++ b/internal/action/storagedriver.go @@ -3,7 +3,6 @@ package action import ( "context" "fmt" - "helm.sh/helm/v3/pkg/storage/driver" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,6 +18,28 @@ import ( "github.com/operator-framework/helm-operator-plugins/pkg/storage" ) +// ObjectToRestConfigMapper +func PureHelmStorageDriverMapper(mapper helmclient.ObjectToRestConfigMapper, reader client.Reader) helmclient.ObjectToStorageDriverMapper { + return func(ctx context.Context, object client.Object, config *rest.Config) (driver.Driver, error) { + //ext := object.(*ocv1alpha1.ClusterExtension) + //namespace := ext.Spec.Install.Namespace + cfg, err := mapper(ctx, object, config) + extSaClient, err := clientcorev1.NewForConfig(cfg) + if err != nil { + return nil, err + } + secretsClient := newSecretsDelegatingClient(extSaClient, reader, "olmv1-system") + log := logf.FromContext(ctx).V(2) + ownerRefs := []metav1.OwnerReference{*metav1.NewControllerRef(object, object.GetObjectKind().GroupVersionKind())} + ownerRefSecretClient := helmclient.NewOwnerRefSecretClient(secretsClient, ownerRefs, nil) + s := driver.NewSecrets(ownerRefSecretClient) + s.Log = func(s string, i ...interface{}) { + log.Info(s, i...) + } + return s, nil + } +} + func ChunkedStorageDriverMapper(secretsGetter clientcorev1.SecretsGetter, reader client.Reader, namespace string) helmclient.ObjectToStorageDriverMapper { secretsClient := newSecretsDelegatingClient(secretsGetter, reader, namespace) return func(ctx context.Context, object client.Object, config *rest.Config) (driver.Driver, error) { diff --git a/internal/controllers/clusterextension_controller.go b/internal/controllers/clusterextension_controller.go index 43ef47a54a..e02f69c723 100644 --- a/internal/controllers/clusterextension_controller.go +++ b/internal/controllers/clusterextension_controller.go @@ -278,9 +278,6 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp engine, err := r.Enginator.GetEngine(resolvedBundle) if err != nil { - // Wrap the error passed to this with the resolution information until we have successfully - // installed since we intend for the progressing condition to replace the resolved condition - // and will be removing the .status.resolution field from the ClusterExtension status API setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedBundleMetadata, err)) return ctrl.Result{}, err } From d1f7fc236fb1f0a4f170383e4bc14c55fa757cd1 Mon Sep 17 00:00:00 2001 From: yashoza19 Date: Thu, 31 Oct 2024 03:42:48 +0400 Subject: [PATCH 4/6] adding support functions for injecting values.yaml via configMap Signed-off-by: yashoza19 --- Makefile | 2 +- cmd/manager/main.go | 2 ++ internal/applier/helmer.go | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 49a707b3c3..90c4af06cf 100644 --- a/Makefile +++ b/Makefile @@ -283,7 +283,7 @@ run: docker-build kind-cluster kind-load kind-deploy #HELP Build the operator-co .PHONY: docker-build docker-build: build-linux #EXHELP Build docker image for operator-controller with GOOS=linux and local GOARCH. - $(CONTAINER_RUNTIME) build -t $(IMG) -f Dockerfile ./bin/linux + $(CONTAINER_RUNTIME) build --load -t $(IMG) -f Dockerfile ./bin/linux #SECTION Release ifeq ($(origin ENABLE_RELEASE_PIPELINE), undefined) diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 0155a28ca4..5ad5bae4bb 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -317,6 +317,8 @@ func main() { }, Applier: &applier.Helmer{ ActionClientGetter: phg, + ConfigMapName: "", + Namespace: "", }, } diff --git a/internal/applier/helmer.go b/internal/applier/helmer.go index e6ffe21702..9b109a4f92 100644 --- a/internal/applier/helmer.go +++ b/internal/applier/helmer.go @@ -4,9 +4,13 @@ import ( "context" "errors" "fmt" + "gopkg.in/yaml.v2" "helm.sh/helm/v3/pkg/chart/loader" "io" "io/fs" + corev1 "k8s.io/api/core/v1" + errv1 "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" "strings" "helm.sh/helm/v3/pkg/action" @@ -25,6 +29,9 @@ import ( type Helmer struct { ActionClientGetter helmclient.ActionClientGetter + ConfigMapName string + Namespace string + Client client.Client } func loadChartFromFS(fsys fs.FS) (*chart.Chart, error) { @@ -80,6 +87,28 @@ func (h *Helmer) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1alpha1.Clu } values := chartutil.Values{} + // Look for the ConfigMap with the specified name and namespace + // here for testing I'm using a pre-configured test configMap in test namespace + // TODO: find a way to pass this config map through the ClusterExtension specs + var userValuesMap map[string]interface{} + configMap := &corev1.ConfigMap{} + err = h.Client.Get(ctx, types.NamespacedName{Name: h.ConfigMapName, Namespace: h.Namespace}, configMap) + if err != nil && !errv1.IsNotFound(err) { + return nil, "", fmt.Errorf("failed to retrieve ConfigMap: %v", err) + } + + // If the ConfigMap is found, parse the values.yaml from the data + if err == nil { + valuesYaml, found := configMap.Data["values.yaml"] + if found { + userValuesMap, err = parseValuesYaml([]byte(valuesYaml)) + if err != nil { + return nil, "", fmt.Errorf("failed to parse values.yaml from ConfigMap: %v", err) + } + values = chartutil.CoalesceTables(values, userValuesMap) + } + } + ac, err := h.ActionClientGetter.ActionClientFor(ctx, ext) if err != nil { return nil, "", err @@ -166,3 +195,12 @@ func (h *Helmer) getReleaseState(cl helmclient.ActionInterface, ext *ocv1alpha1. } return currentRelease, desiredRelease, relState, nil } + +func parseValuesYaml(yamlContent []byte) (map[string]interface{}, error) { + var values map[string]interface{} + err := yaml.Unmarshal(yamlContent, &values) + if err != nil { + return nil, fmt.Errorf("failed to parse values.yaml: %v", err) + } + return values, nil +} From c9fcf4209c7f814accdc0587ab4a2b503c6139d6 Mon Sep 17 00:00:00 2001 From: Sid Kattoju Date: Mon, 4 Nov 2024 22:06:25 -0500 Subject: [PATCH 5/6] rough sketch for helm values support --- api/v1alpha1/clusterextension_types.go | 11 + api/v1alpha1/zz_generated.deepcopy.go | 20 ++ cmd/manager/main.go | 1 + ...peratorframework.io_clusterextensions.yaml | 10 + config/samples/metrics-server.yaml | 4 +- config/samples/values.yaml | 200 ++++++++++++++++++ internal/applier/helmer.go | 109 ++++++++-- 7 files changed, 339 insertions(+), 16 deletions(-) create mode 100644 config/samples/values.yaml diff --git a/api/v1alpha1/clusterextension_types.go b/api/v1alpha1/clusterextension_types.go index ad99e72511..11ee6ed0d2 100644 --- a/api/v1alpha1/clusterextension_types.go +++ b/api/v1alpha1/clusterextension_types.go @@ -142,6 +142,12 @@ type ClusterExtensionInstallConfig struct { // resources that are included in the bundle of content being applied. ServiceAccount ServiceAccountReference `json:"serviceAccount"` + // configMap is an optional field that can be used to reference a configMap + // containing helm values when installing an extension that is packaged as a helm chart + // + //+optional + ConfigMap *ConfigMapReference `json:"configMap,omitempty"` + // preflight is an optional field that can be used to configure the preflight checks run before installation or upgrade of the content for the package specified in the packageName field. // // When specified, it overrides the default configuration of the preflight checks that are required to execute successfully during an install/upgrade operation. @@ -376,6 +382,11 @@ type ServiceAccountReference struct { Name string `json:"name"` } +// ConfigMapReference references a configMap +type ConfigMapReference struct { + Name string `json:"name"` +} + // PreflightConfig holds the configuration for the preflight checks. If used, at least one preflight check must be non-nil. // +kubebuilder:validation:XValidation:rule="has(self.crdUpgradeSafety)",message="at least one of [crdUpgradeSafety] are required when preflight is specified" type PreflightConfig struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ccd143aec5..42f06e97d1 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -107,6 +107,11 @@ func (in *ClusterExtension) DeepCopyObject() runtime.Object { func (in *ClusterExtensionInstallConfig) DeepCopyInto(out *ClusterExtensionInstallConfig) { *out = *in out.ServiceAccount = in.ServiceAccount + if in.ConfigMap != nil { + in, out := &in.ConfigMap, &out.ConfigMap + *out = new(ConfigMapReference) + **out = **in + } if in.Preflight != nil { in, out := &in.Preflight, &out.Preflight *out = new(PreflightConfig) @@ -216,6 +221,21 @@ func (in *ClusterExtensionStatus) DeepCopy() *ClusterExtensionStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigMapReference) DeepCopyInto(out *ConfigMapReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapReference. +func (in *ConfigMapReference) DeepCopy() *ConfigMapReference { + if in == nil { + return nil + } + out := new(ConfigMapReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PreflightConfig) DeepCopyInto(out *PreflightConfig) { *out = *in diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 5ad5bae4bb..c978a9c010 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -319,6 +319,7 @@ func main() { ActionClientGetter: phg, ConfigMapName: "", Namespace: "", + Client: cl, }, } diff --git a/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml b/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml index 61b81606bf..f0d6af9226 100644 --- a/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml +++ b/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml @@ -51,6 +51,16 @@ spec: serviceAccount: name: example-sa properties: + configMap: + description: |- + configMap is an optional field that can be used to reference a configMap + containing helm values when installing an extension that is packaged as a helm chart + properties: + name: + type: string + required: + - name + type: object namespace: description: |- namespace is a reference to the Namespace in which the bundle of diff --git a/config/samples/metrics-server.yaml b/config/samples/metrics-server.yaml index 4ed89fb2d8..41116e4c11 100644 --- a/config/samples/metrics-server.yaml +++ b/config/samples/metrics-server.yaml @@ -36,4 +36,6 @@ spec: install: namespace: metrics-server serviceAccount: - name: metrics-server-installer \ No newline at end of file + name: metrics-server-installer + configMap: + name: values \ No newline at end of file diff --git a/config/samples/values.yaml b/config/samples/values.yaml new file mode 100644 index 0000000000..be843db413 --- /dev/null +++ b/config/samples/values.yaml @@ -0,0 +1,200 @@ +# Default values for metrics-server. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +image: + repository: registry.k8s.io/metrics-server/metrics-server + # Overrides the image tag whose default is v{{ .Chart.AppVersion }} + tag: "" + pullPolicy: IfNotPresent + +imagePullSecrets: [] +# - name: registrySecretName + +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + # The list of secrets mountable by this service account. + # See https://kubernetes.io/docs/reference/labels-annotations-taints/#enforce-mountable-secrets + secrets: [] + +rbac: + # Specifies whether RBAC resources should be created + create: true + # Note: PodSecurityPolicy will not be created when Kubernetes version is 1.25 or later. + pspEnabled: false + +apiService: + # Specifies if the v1beta1.metrics.k8s.io API service should be created. + # + # You typically want this enabled! If you disable API service creation you have to + # manage it outside of this chart for e.g horizontal pod autoscaling to + # work with this release. + create: true + # Annotations to add to the API service + annotations: {} + # Specifies whether to skip TLS verification + insecureSkipTLSVerify: true + # The PEM encoded CA bundle for TLS verification + caBundle: "" + +commonLabels: {} +podLabels: {} +podAnnotations: {} + +podSecurityContext: {} + +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + capabilities: + drop: + - ALL + +priorityClassName: system-cluster-critical + +containerPort: 10250 + +hostNetwork: + # Specifies if metrics-server should be started in hostNetwork mode. + # + # You would require this enabled if you use alternate overlay networking for pods and + # API server unable to communicate with metrics-server. As an example, this is required + # if you use Weave network on EKS + enabled: false + +replicas: 1 + +revisionHistoryLimit: + +updateStrategy: {} +# type: RollingUpdate +# rollingUpdate: +# maxSurge: 0 +# maxUnavailable: 1 + +podDisruptionBudget: + # https://kubernetes.io/docs/tasks/run-application/configure-pdb/ + enabled: false + minAvailable: + maxUnavailable: + +defaultArgs: + - --cert-dir=/tmp + - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname + - --kubelet-use-node-status-port + - --metric-resolution=15s + +args: [] + +livenessProbe: + httpGet: + path: /livez + port: https + scheme: HTTPS + initialDelaySeconds: 0 + periodSeconds: 10 + failureThreshold: 3 + +readinessProbe: + httpGet: + path: /readyz + port: https + scheme: HTTPS + initialDelaySeconds: 20 + periodSeconds: 10 + failureThreshold: 3 + +service: + type: ClusterIP + port: 443 + annotations: {} + labels: {} + # Add these labels to have metrics-server show up in `kubectl cluster-info` + # kubernetes.io/cluster-service: "true" + # kubernetes.io/name: "Metrics-server" + +addonResizer: + enabled: false + image: + repository: registry.k8s.io/autoscaling/addon-resizer + tag: 1.8.21 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + capabilities: + drop: + - ALL + resources: + requests: + cpu: 40m + memory: 25Mi + limits: + cpu: 40m + memory: 25Mi + nanny: + cpu: 0m + extraCpu: 1m + memory: 0Mi + extraMemory: 2Mi + minClusterSize: 100 + pollPeriod: 300000 + threshold: 5 + +metrics: + enabled: false + +serviceMonitor: + enabled: false + additionalLabels: {} + interval: 1m + scrapeTimeout: 10s + metricRelabelings: [] + relabelings: [] + +# See https://github.com/kubernetes-sigs/metrics-server#scaling +resources: + requests: + cpu: 100m + memory: 200Mi + # limits: + # cpu: + # memory: + +extraVolumeMounts: [] + +extraVolumes: [] + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +topologySpreadConstraints: [] + +dnsConfig: {} + +# Annotations to add to the deployment +deploymentAnnotations: {} + +schedulerName: "" + +tmpVolume: + emptyDir: {} diff --git a/internal/applier/helmer.go b/internal/applier/helmer.go index 9b109a4f92..8825000a5e 100644 --- a/internal/applier/helmer.go +++ b/internal/applier/helmer.go @@ -4,14 +4,20 @@ import ( "context" "errors" "fmt" - "gopkg.in/yaml.v2" - "helm.sh/helm/v3/pkg/chart/loader" "io" "io/fs" + "log" + "strings" + "time" + + "helm.sh/helm/v3/pkg/chart/loader" + authv1 "k8s.io/api/authentication/v1" corev1 "k8s.io/api/core/v1" errv1 "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "strings" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/utils/ptr" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" @@ -87,25 +93,58 @@ func (h *Helmer) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1alpha1.Clu } values := chartutil.Values{} + // Create a client with an SA token + // HACK >>>> + // Initialize the client to communicate with the cluster + + // Get the default config + cfg, err := rest.InClusterConfig() + if err != nil { + log.Fatalf("failed to get Kubernetes config: %v", err) + } + + // Create the Kubernetes client + defaultClient, err := kubernetes.NewForConfig(cfg) + if err != nil { + log.Fatalf("failed to create Kubernetes client: %v", err) + } + + // Create a token request for the specified service account + token, err := getServiceAccountToken(defaultClient, ext.Spec.Install.Namespace, ext.Spec.Install.ServiceAccount.Name) + if err != nil { + log.Fatalf("failed to get token for service account %s/%s: %v", ext.Spec.Install.Namespace, ext.Spec.Install.ServiceAccount.Name, err) + } + + // Now, create a client that uses this token to authenticate + authdClientSet, err := createClientWithToken(cfg, token) + if err != nil { + log.Fatalf("failed to create client with token: %v", err) + } + // Look for the ConfigMap with the specified name and namespace // here for testing I'm using a pre-configured test configMap in test namespace // TODO: find a way to pass this config map through the ClusterExtension specs - var userValuesMap map[string]interface{} configMap := &corev1.ConfigMap{} - err = h.Client.Get(ctx, types.NamespacedName{Name: h.ConfigMapName, Namespace: h.Namespace}, configMap) - if err != nil && !errv1.IsNotFound(err) { - return nil, "", fmt.Errorf("failed to retrieve ConfigMap: %v", err) + if ext.Spec.Install.ConfigMap != nil { + configMap, err = authdClientSet.CoreV1().ConfigMaps(ext.Spec.Install.Namespace).Get(ctx, ext.Spec.Install.ConfigMap.Name, metav1.GetOptions{}) + if err != nil && !errv1.IsNotFound(err) { + return nil, "", fmt.Errorf("failed to retrieve ConfigMap: %v", err) + } } // If the ConfigMap is found, parse the values.yaml from the data if err == nil { + var userValuesMap map[string]interface{} valuesYaml, found := configMap.Data["values.yaml"] if found { - userValuesMap, err = parseValuesYaml([]byte(valuesYaml)) + userValuesMap, err = chartutil.ReadValues([]byte(valuesYaml)) if err != nil { return nil, "", fmt.Errorf("failed to parse values.yaml from ConfigMap: %v", err) } - values = chartutil.CoalesceTables(values, userValuesMap) + values, err = chartutil.CoalesceValues(chrt, userValuesMap) + if err != nil { + return nil, "", fmt.Errorf("failed to coalesce values from ConfigMap: %v", err) + } } } @@ -118,6 +157,10 @@ func (h *Helmer) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1alpha1.Clu labels: objectLabels, } + // DEBUG + // stringValues, err := values.YAML() + // log.Printf("attempting install with:\n%v\n%v\n", stringValues, err) + rel, _, state, err := h.getReleaseState(ac, ext, chrt, values, post) if err != nil { return nil, "", err @@ -196,11 +239,47 @@ func (h *Helmer) getReleaseState(cl helmclient.ActionInterface, ext *ocv1alpha1. return currentRelease, desiredRelease, relState, nil } -func parseValuesYaml(yamlContent []byte) (map[string]interface{}, error) { - var values map[string]interface{} - err := yaml.Unmarshal(yamlContent, &values) +//func parseValuesYaml(yamlContent []byte) (map[string]interface{}, error) { +// var values map[string]interface{} +// err := yaml.Unmarshal(yamlContent, &values) +// if err != nil { +// return nil, fmt.Errorf("failed to parse values.yaml: %v", err) +// } +// return values, nil +//} + +// getServiceAccountToken requests a token for the given service account. +func getServiceAccountToken(clientSet *kubernetes.Clientset, namespace, serviceAccountName string) (string, error) { + tokenRequest := &authv1.TokenRequest{ + Spec: authv1.TokenRequestSpec{ + ExpirationSeconds: ptr.To[int64](int64(10 * time.Minute / time.Second)), + }, + } + + // Make the TokenRequest API call + token, err := clientSet.CoreV1().ServiceAccounts(namespace).CreateToken(context.TODO(), serviceAccountName, tokenRequest, metav1.CreateOptions{}) if err != nil { - return nil, fmt.Errorf("failed to parse values.yaml: %v", err) + return "", fmt.Errorf("failed to create token for service account: %w", err) } - return values, nil + + // Return the token + return token.Status.Token, nil +} + +// createClientWithToken creates a new client that uses the specified token. +func createClientWithToken(cfg *rest.Config, token string) (*kubernetes.Clientset, error) { + + // Remove existing credentials + anonCfg := rest.AnonymousClientConfig(cfg) + + // Create a custom rest config using the token + cfgCopy := rest.CopyConfig(anonCfg) + cfgCopy.BearerToken = token + + clientSet, err := kubernetes.NewForConfig(cfgCopy) + if err != nil { + return nil, fmt.Errorf("failed to create Kubernetes client with token: %w", err) + } + + return clientSet, nil } From b05a2f0e10b6dbfe2fccd64af6fe0311e3cd9135 Mon Sep 17 00:00:00 2001 From: Sid Kattoju Date: Wed, 20 Nov 2024 15:17:47 -0500 Subject: [PATCH 6/6] add support for multiple config sources --- api/v1alpha1/clusterextension_types.go | 12 +- api/v1alpha1/zz_generated.deepcopy.go | 31 ++- cmd/manager/main.go | 4 +- ...peratorframework.io_clusterextensions.yaml | 18 +- config/samples/metrics-server.yaml | 5 +- internal/applier/helmer.go | 215 ++++++++++-------- 6 files changed, 161 insertions(+), 124 deletions(-) diff --git a/api/v1alpha1/clusterextension_types.go b/api/v1alpha1/clusterextension_types.go index 11ee6ed0d2..e1c4f6248a 100644 --- a/api/v1alpha1/clusterextension_types.go +++ b/api/v1alpha1/clusterextension_types.go @@ -142,11 +142,11 @@ type ClusterExtensionInstallConfig struct { // resources that are included in the bundle of content being applied. ServiceAccount ServiceAccountReference `json:"serviceAccount"` - // configMap is an optional field that can be used to reference a configMap + // configSource is an optional field that can be used to reference a configMap // containing helm values when installing an extension that is packaged as a helm chart // //+optional - ConfigMap *ConfigMapReference `json:"configMap,omitempty"` + ConfigSources *ConfigSourceReferences `json:"configMap,omitempty"` // preflight is an optional field that can be used to configure the preflight checks run before installation or upgrade of the content for the package specified in the packageName field. // @@ -382,9 +382,11 @@ type ServiceAccountReference struct { Name string `json:"name"` } -// ConfigMapReference references a configMap -type ConfigMapReference struct { - Name string `json:"name"` +// ConfigSourceReference can references a configMap, a secret, plain text config +type ConfigSourceReferences struct { + ConfigMapNames []string `json:"configMapNames,omitempty"` + SecretNames []string `json:"secretNames,omitempty"` + TextConfigs []string `json:"textConfigs,omitempty"` } // PreflightConfig holds the configuration for the preflight checks. If used, at least one preflight check must be non-nil. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 42f06e97d1..db6c0dc0b6 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -107,10 +107,10 @@ func (in *ClusterExtension) DeepCopyObject() runtime.Object { func (in *ClusterExtensionInstallConfig) DeepCopyInto(out *ClusterExtensionInstallConfig) { *out = *in out.ServiceAccount = in.ServiceAccount - if in.ConfigMap != nil { - in, out := &in.ConfigMap, &out.ConfigMap - *out = new(ConfigMapReference) - **out = **in + if in.ConfigSources != nil { + in, out := &in.ConfigSources, &out.ConfigSources + *out = new(ConfigSourceReferences) + (*in).DeepCopyInto(*out) } if in.Preflight != nil { in, out := &in.Preflight, &out.Preflight @@ -222,16 +222,31 @@ func (in *ClusterExtensionStatus) DeepCopy() *ClusterExtensionStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ConfigMapReference) DeepCopyInto(out *ConfigMapReference) { +func (in *ConfigSourceReferences) DeepCopyInto(out *ConfigSourceReferences) { *out = *in + if in.ConfigMapNames != nil { + in, out := &in.ConfigMapNames, &out.ConfigMapNames + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.SecretNames != nil { + in, out := &in.SecretNames, &out.SecretNames + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TextConfigs != nil { + in, out := &in.TextConfigs, &out.TextConfigs + *out = make([]string, len(*in)) + copy(*out, *in) + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapReference. -func (in *ConfigMapReference) DeepCopy() *ConfigMapReference { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigSourceReferences. +func (in *ConfigSourceReferences) DeepCopy() *ConfigSourceReferences { if in == nil { return nil } - out := new(ConfigMapReference) + out := new(ConfigSourceReferences) in.DeepCopyInto(out) return out } diff --git a/cmd/manager/main.go b/cmd/manager/main.go index c978a9c010..7e81dda0af 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -317,9 +317,7 @@ func main() { }, Applier: &applier.Helmer{ ActionClientGetter: phg, - ConfigMapName: "", - Namespace: "", - Client: cl, + TokenGetter: tokenGetter, }, } diff --git a/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml b/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml index f0d6af9226..63c27777c6 100644 --- a/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml +++ b/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml @@ -53,13 +53,21 @@ spec: properties: configMap: description: |- - configMap is an optional field that can be used to reference a configMap + configSource is an optional field that can be used to reference a configMap containing helm values when installing an extension that is packaged as a helm chart properties: - name: - type: string - required: - - name + configMapNames: + items: + type: string + type: array + secretNames: + items: + type: string + type: array + textConfigs: + items: + type: string + type: array type: object namespace: description: |- diff --git a/config/samples/metrics-server.yaml b/config/samples/metrics-server.yaml index 41116e4c11..bbe1b41995 100644 --- a/config/samples/metrics-server.yaml +++ b/config/samples/metrics-server.yaml @@ -37,5 +37,6 @@ spec: namespace: metrics-server serviceAccount: name: metrics-server-installer - configMap: - name: values \ No newline at end of file + configSources: + configMaps: + - "values" diff --git a/internal/applier/helmer.go b/internal/applier/helmer.go index 8825000a5e..25b84c47be 100644 --- a/internal/applier/helmer.go +++ b/internal/applier/helmer.go @@ -8,23 +8,21 @@ import ( "io/fs" "log" "strings" - "time" - - "helm.sh/helm/v3/pkg/chart/loader" - authv1 "k8s.io/api/authentication/v1" - corev1 "k8s.io/api/core/v1" - errv1 "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/utils/ptr" + "github.com/operator-framework/operator-controller/internal/authentication" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/postrender" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" + corev1 "k8s.io/api/core/v1" + errv1 "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client" @@ -35,9 +33,7 @@ import ( type Helmer struct { ActionClientGetter helmclient.ActionClientGetter - ConfigMapName string - Namespace string - Client client.Client + TokenGetter *authentication.TokenGetter } func loadChartFromFS(fsys fs.FS) (*chart.Chart, error) { @@ -87,65 +83,15 @@ func loadChartFromFS(fsys fs.FS) (*chart.Chart, error) { } func (h *Helmer) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1alpha1.ClusterExtension, objectLabels map[string]string, storageLabels map[string]string) ([]client.Object, string, error) { - chrt, err := loadChartFromFS(contentFS) + chartFromFS, err := loadChartFromFS(contentFS) if err != nil { return nil, "", err } values := chartutil.Values{} - // Create a client with an SA token - // HACK >>>> - // Initialize the client to communicate with the cluster - - // Get the default config - cfg, err := rest.InClusterConfig() - if err != nil { - log.Fatalf("failed to get Kubernetes config: %v", err) - } - - // Create the Kubernetes client - defaultClient, err := kubernetes.NewForConfig(cfg) + values, err = processConfig(ctx, h.TokenGetter, ext, values) if err != nil { - log.Fatalf("failed to create Kubernetes client: %v", err) - } - - // Create a token request for the specified service account - token, err := getServiceAccountToken(defaultClient, ext.Spec.Install.Namespace, ext.Spec.Install.ServiceAccount.Name) - if err != nil { - log.Fatalf("failed to get token for service account %s/%s: %v", ext.Spec.Install.Namespace, ext.Spec.Install.ServiceAccount.Name, err) - } - - // Now, create a client that uses this token to authenticate - authdClientSet, err := createClientWithToken(cfg, token) - if err != nil { - log.Fatalf("failed to create client with token: %v", err) - } - - // Look for the ConfigMap with the specified name and namespace - // here for testing I'm using a pre-configured test configMap in test namespace - // TODO: find a way to pass this config map through the ClusterExtension specs - configMap := &corev1.ConfigMap{} - if ext.Spec.Install.ConfigMap != nil { - configMap, err = authdClientSet.CoreV1().ConfigMaps(ext.Spec.Install.Namespace).Get(ctx, ext.Spec.Install.ConfigMap.Name, metav1.GetOptions{}) - if err != nil && !errv1.IsNotFound(err) { - return nil, "", fmt.Errorf("failed to retrieve ConfigMap: %v", err) - } - } - - // If the ConfigMap is found, parse the values.yaml from the data - if err == nil { - var userValuesMap map[string]interface{} - valuesYaml, found := configMap.Data["values.yaml"] - if found { - userValuesMap, err = chartutil.ReadValues([]byte(valuesYaml)) - if err != nil { - return nil, "", fmt.Errorf("failed to parse values.yaml from ConfigMap: %v", err) - } - values, err = chartutil.CoalesceValues(chrt, userValuesMap) - if err != nil { - return nil, "", fmt.Errorf("failed to coalesce values from ConfigMap: %v", err) - } - } + return nil, "", fmt.Errorf("Unable to process config: %v", err) } ac, err := h.ActionClientGetter.ActionClientFor(ctx, ext) @@ -157,18 +103,14 @@ func (h *Helmer) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1alpha1.Clu labels: objectLabels, } - // DEBUG - // stringValues, err := values.YAML() - // log.Printf("attempting install with:\n%v\n%v\n", stringValues, err) - - rel, _, state, err := h.getReleaseState(ac, ext, chrt, values, post) + rel, _, state, err := h.getReleaseState(ac, ext, chartFromFS, values, post) if err != nil { return nil, "", err } switch state { case StateNeedsInstall: - rel, err = ac.Install(ext.GetName(), ext.Spec.Install.Namespace, chrt, values, func(install *action.Install) error { + rel, err = ac.Install(ext.GetName(), ext.Spec.Install.Namespace, chartFromFS, values, func(install *action.Install) error { install.CreateNamespace = false install.Labels = storageLabels return nil @@ -177,7 +119,7 @@ func (h *Helmer) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1alpha1.Clu return nil, state, err } case StateNeedsUpgrade: - rel, err = ac.Upgrade(ext.GetName(), ext.Spec.Install.Namespace, chrt, values, func(upgrade *action.Upgrade) error { + rel, err = ac.Upgrade(ext.GetName(), ext.Spec.Install.Namespace, chartFromFS, values, func(upgrade *action.Upgrade) error { upgrade.MaxHistory = maxHelmReleaseHistory upgrade.Labels = storageLabels return nil @@ -239,35 +181,11 @@ func (h *Helmer) getReleaseState(cl helmclient.ActionInterface, ext *ocv1alpha1. return currentRelease, desiredRelease, relState, nil } -//func parseValuesYaml(yamlContent []byte) (map[string]interface{}, error) { -// var values map[string]interface{} -// err := yaml.Unmarshal(yamlContent, &values) -// if err != nil { -// return nil, fmt.Errorf("failed to parse values.yaml: %v", err) -// } -// return values, nil -//} - -// getServiceAccountToken requests a token for the given service account. -func getServiceAccountToken(clientSet *kubernetes.Clientset, namespace, serviceAccountName string) (string, error) { - tokenRequest := &authv1.TokenRequest{ - Spec: authv1.TokenRequestSpec{ - ExpirationSeconds: ptr.To[int64](int64(10 * time.Minute / time.Second)), - }, - } - - // Make the TokenRequest API call - token, err := clientSet.CoreV1().ServiceAccounts(namespace).CreateToken(context.TODO(), serviceAccountName, tokenRequest, metav1.CreateOptions{}) - if err != nil { - return "", fmt.Errorf("failed to create token for service account: %w", err) - } - - // Return the token - return token.Status.Token, nil -} - // createClientWithToken creates a new client that uses the specified token. -func createClientWithToken(cfg *rest.Config, token string) (*kubernetes.Clientset, error) { +func createClientWithToken(token string) (*kubernetes.Clientset, error) { + + // Get the default config + cfg, err := rest.InClusterConfig() // Remove existing credentials anonCfg := rest.AnonymousClientConfig(cfg) @@ -283,3 +201,98 @@ func createClientWithToken(cfg *rest.Config, token string) (*kubernetes.Clientse return clientSet, nil } + +// Looks for the ConfigSources by name in the install namespace and gathers values +func processConfig(ctx context.Context, tokenGetter *authentication.TokenGetter, ext *ocv1alpha1.ClusterExtension, values chartutil.Values) (chartutil.Values, error) { + + // Create or get a token for the provided service account + token, err := tokenGetter.Get(ctx, types.NamespacedName{Namespace: ext.Spec.Install.Namespace, Name: ext.Spec.Install.ServiceAccount.Name}) + if err != nil { + log.Fatalf("failed to get token for service account %s/%s: %v", ext.Spec.Install.Namespace, ext.Spec.Install.ServiceAccount.Name, err) + } + + // Create a client with the provided service account token + authedClientSet, err := createClientWithToken(token) + if err != nil { + log.Fatalf("failed to create client with token: %v", err) + } + + var configMapList []corev1.ConfigMap + var secretList []corev1.Secret + var textConfigList []string + + if ext.Spec.Install.ConfigSources != nil { + configSources := *ext.Spec.Install.ConfigSources + // Process config map names + if configSources.ConfigMapNames != nil && len(configSources.ConfigMapNames) > 0 { + for _, configMapName := range configSources.ConfigMapNames { + configMap := &corev1.ConfigMap{} + configMap, err := authedClientSet.CoreV1().ConfigMaps(ext.Spec.Install.Namespace).Get(ctx, configMapName, metav1.GetOptions{}) + if err != nil && !errv1.IsNotFound(err) { + return values, fmt.Errorf("failed to retrieve ConfigMap: %v", err) + } + configMapList = append(configMapList, *configMap) + } + } + // Process secrets + if configSources.SecretNames != nil && len(configSources.SecretNames) > 0 { + for _, secretName := range configSources.SecretNames { + secret := &corev1.Secret{} + secret, err := authedClientSet.CoreV1().Secrets(ext.Spec.Install.Namespace).Get(ctx, secretName, metav1.GetOptions{}) + if err != nil && !errv1.IsNotFound(err) { + return values, fmt.Errorf("failed to retrieve ConfigMap: %v", err) + } + secretList = append(secretList, *secret) + } + } + // Process plain text configs + if configSources.TextConfigs != nil && len(configSources.TextConfigs) > 0 { + for _, textConfig := range configSources.TextConfigs { + textConfigList = append(textConfigList, textConfig) + } + } + } + + // If the ConfigSources have been found, build values from the data + if len(configMapList) > 0 || len(secretList) > 0 || len(textConfigList) > 0 { + // combine all configMaps + for _, configMap := range configMapList { + valuesYaml, found := configMap.Data["values.yaml"] + if found { + userValuesMap, err := chartutil.ReadValues([]byte(valuesYaml)) + if err != nil { + return values, fmt.Errorf("failed to parse values.yaml from ConfigMap %v: %v", configMap.Name, err) + } + // combine all values files + values = chartutil.MergeTables(values, userValuesMap) + } + } + + //combine all secrets + for _, secret := range secretList { + valuesYaml, found := secret.Data["values.yaml"] + if found { + userValuesMap, err := chartutil.ReadValues([]byte(valuesYaml)) + if err != nil { + return nil, fmt.Errorf("failed to parse values.yaml from secret %v: %v", secret.Name, err) + } + // combine all values files + values = chartutil.MergeTables(values, userValuesMap) + } + } + + //combine all secrets + for _, textConfig := range textConfigList { + if len(textConfig) > 0 { + userValuesMap, err := chartutil.ReadValues([]byte(textConfig)) + if err != nil { + return nil, fmt.Errorf("failed to parse values from textConfig: %v", err) + } + // combine all values files + values = chartutil.MergeTables(values, userValuesMap) + } + } + } + + return values, nil +}