diff --git a/go.mod b/go.mod index 79761544..662dc379 100644 --- a/go.mod +++ b/go.mod @@ -12,14 +12,12 @@ require ( github.com/onsi/gomega v1.14.0 github.com/operator-framework/operator-lib v0.3.0 github.com/prometheus/client_golang v1.11.0 - github.com/sergi/go-diff v1.1.0 github.com/sirupsen/logrus v1.8.1 github.com/spf13/afero v1.2.2 github.com/spf13/cobra v1.1.3 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.0 gomodules.xyz/jsonpatch/v2 v2.2.0 - gomodules.xyz/jsonpatch/v3 v3.0.1 helm.sh/helm/v3 v3.6.2 k8s.io/api v0.22.1 k8s.io/apiextensions-apiserver v0.22.1 diff --git a/go.sum b/go.sum index 77a0d1a1..d0491ed3 100644 --- a/go.sum +++ b/go.sum @@ -1263,10 +1263,6 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T gomodules.xyz/jsonpatch/v2 v2.1.0/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= -gomodules.xyz/jsonpatch/v3 v3.0.1 h1:Te7hKxV52TKCbNYq3t84tzKav3xhThdvSsSp/W89IyI= -gomodules.xyz/jsonpatch/v3 v3.0.1/go.mod h1:CBhndykehEwTOlEfnsfJwvkFQbSN8YZFr9M+cIHAJto= -gomodules.xyz/orderedmap v0.1.0 h1:fM/+TGh/O1KkqGR5xjTKg6bU8OKBkg7p0Y+x/J9m8Os= -gomodules.xyz/orderedmap v0.1.0/go.mod h1:g9/TPUCm1t2gwD3j3zfV8uylyYhVdCNSi+xCEIu7yTU= google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= diff --git a/internal/cmd/helm-operator/run/cmd.go b/internal/cmd/helm-operator/run/cmd.go index 5119d511..58af47d7 100644 --- a/internal/cmd/helm-operator/run/cmd.go +++ b/internal/cmd/helm-operator/run/cmd.go @@ -23,12 +23,15 @@ import ( "strings" "github.com/operator-framework/helm-operator-plugins/internal/flags" - "github.com/operator-framework/helm-operator-plugins/internal/legacy/controller" - "github.com/operator-framework/helm-operator-plugins/internal/legacy/release" watches "github.com/operator-framework/helm-operator-plugins/internal/legacy/watches" "github.com/operator-framework/helm-operator-plugins/internal/metrics" "github.com/operator-framework/helm-operator-plugins/internal/version" + "github.com/operator-framework/helm-operator-plugins/pkg/annotation" helmmgr "github.com/operator-framework/helm-operator-plugins/pkg/manager" + "github.com/operator-framework/helm-operator-plugins/pkg/reconciler" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" @@ -167,6 +170,7 @@ func run(cmd *cobra.Command, f *flags.Flags) { os.Exit(1) } + // TODO: remove legacy watches and use watches from lib ws, err := watches.Load(f.WatchesFile) if err != nil { log.Error(err, "Failed to create new manager factories.") @@ -174,23 +178,39 @@ func run(cmd *cobra.Command, f *flags.Flags) { } for _, w := range ws { - // Register the controller with the factory. - err := controller.Add(mgr, controller.WatchOptions{ - Namespace: namespace, - GVK: w.GroupVersionKind, - ManagerFactory: release.NewManagerFactory(mgr, w.ChartDir), - ReconcilePeriod: f.ReconcilePeriod, - WatchDependentResources: *w.WatchDependentResources, - OverrideValues: w.OverrideValues, - MaxConcurrentReconciles: f.MaxConcurrentReconciles, - Selector: w.Selector, - }) + + // TODO: remove this after modifying watches of hybrid lib. + cl, err := getChart(w) if err != nil { - log.Error(err, "Failed to add manager factory to controller.") + log.Error(err, "Unable to read chart") + os.Exit(1) + } + + r, err := reconciler.New( + reconciler.WithChart(*cl), + reconciler.WithGroupVersionKind(w.GroupVersionKind), + reconciler.WithOverrideValues(w.OverrideValues), + reconciler.WithSelector(w.Selector), + reconciler.SkipDependentWatches(*w.WatchDependentResources), + reconciler.WithMaxConcurrentReconciles(f.MaxConcurrentReconciles), + reconciler.WithReconcilePeriod(f.ReconcilePeriod), + reconciler.WithInstallAnnotations(annotation.DefaultInstallAnnotations...), + reconciler.WithUpgradeAnnotations(annotation.DefaultUpgradeAnnotations...), + reconciler.WithUninstallAnnotations(annotation.DefaultUninstallAnnotations...), + ) + if err != nil { + log.Error(err, "unable to creste helm reconciler", "controller", "Helm") + os.Exit(1) + } + + if err := r.SetupWithManager(mgr); err != nil { + log.Error(err, "unable to create controller", "Helm") os.Exit(1) } + log.Info("configured watch", "gvk", w.GroupVersionKind, "chartDir", w.ChartDir, "maxConcurrentReconciles", f.MaxConcurrentReconciles, "reconcilePeriod", f.ReconcilePeriod) } + log.Info("starting manager") // Start the Cmd if err = mgr.Start(signals.SetupSignalHandler()); err != nil { log.Error(err, "Manager exited non-zero.") @@ -219,3 +239,13 @@ func exitIfUnsupported(options manager.Options) { os.Exit(1) } } + +// getChart returns the chart from the chartDir passed to the watches file. +func getChart(w watches.Watch) (*chart.Chart, error) { + c, err := loader.LoadDir(w.ChartDir) + if err != nil { + return nil, fmt.Errorf("failed to load chart dir: %w", err) + } + + return c, nil +} diff --git a/internal/legacy/controller/controller.go b/internal/legacy/controller/controller.go deleted file mode 100644 index 4ce24840..00000000 --- a/internal/legacy/controller/controller.go +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package controller - -import ( - "fmt" - "reflect" - "strings" - "sync" - "time" - - rpb "helm.sh/helm/v3/pkg/release" - "helm.sh/helm/v3/pkg/releaseutil" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/controller" - - crthandler "sigs.k8s.io/controller-runtime/pkg/handler" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/manager" - ctrlpredicate "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/source" - "sigs.k8s.io/yaml" - - "github.com/operator-framework/helm-operator-plugins/internal/legacy/release" - "github.com/operator-framework/helm-operator-plugins/pkg/sdk/controllerutil" - libhandler "github.com/operator-framework/operator-lib/handler" - "github.com/operator-framework/operator-lib/predicate" -) - -var log = logf.Log.WithName("helm.controller") - -// WatchOptions contains the necessary values to create a new controller that -// manages helm releases in a particular namespace based on a GVK watch. -type WatchOptions struct { - Namespace string - GVK schema.GroupVersionKind - ManagerFactory release.ManagerFactory - ReconcilePeriod time.Duration - WatchDependentResources bool - OverrideValues map[string]string - MaxConcurrentReconciles int - Selector metav1.LabelSelector -} - -// Add creates a new helm operator controller and adds it to the manager -func Add(mgr manager.Manager, options WatchOptions) error { - controllerName := fmt.Sprintf("%v-controller", strings.ToLower(options.GVK.Kind)) - - r := &HelmOperatorReconciler{ - Client: mgr.GetClient(), - EventRecorder: mgr.GetEventRecorderFor(controllerName), - GVK: options.GVK, - ManagerFactory: options.ManagerFactory, - ReconcilePeriod: options.ReconcilePeriod, - OverrideValues: options.OverrideValues, - } - - // Register the GVK with the schema - mgr.GetScheme().AddKnownTypeWithName(options.GVK, &unstructured.Unstructured{}) - metav1.AddToGroupVersion(mgr.GetScheme(), options.GVK.GroupVersion()) - - c, err := controller.New(controllerName, mgr, controller.Options{ - Reconciler: r, - MaxConcurrentReconciles: options.MaxConcurrentReconciles, - }) - if err != nil { - return err - } - - o := &unstructured.Unstructured{} - o.SetGroupVersionKind(options.GVK) - - var preds []ctrlpredicate.Predicate - p, err := parsePredicateSelector(options.Selector) - - if err != nil { - return err - } - - if p != nil { - preds = append(preds, p) - } - - if err := c.Watch(&source.Kind{Type: o}, &libhandler.InstrumentedEnqueueRequestForObject{}, preds...); err != nil { - return err - } - - if options.WatchDependentResources { - watchDependentResources(mgr, r, c) - } - - log.Info("Watching resource", "apiVersion", options.GVK.GroupVersion(), "kind", - options.GVK.Kind, "namespace", options.Namespace, "reconcilePeriod", options.ReconcilePeriod.String()) - return nil -} - -// parsePredicateSelector parses the selector in the WatchOptions and creates a predicate -// that is used to filter resources based on the specified selector -func parsePredicateSelector(selector metav1.LabelSelector) (ctrlpredicate.Predicate, error) { - // If a selector has been specified in watches.yaml, add it to the watch's predicates. - if !reflect.ValueOf(selector).IsZero() { - p, err := ctrlpredicate.LabelSelectorPredicate(selector) - if err != nil { - return nil, fmt.Errorf("error constructing predicate from watches selector: %v", err) - } - return p, nil - } - return nil, nil -} - -// watchDependentResources adds a release hook function to the HelmOperatorReconciler -// that adds watches for resources in released Helm charts. -func watchDependentResources(mgr manager.Manager, r *HelmOperatorReconciler, c controller.Controller) { - owner := &unstructured.Unstructured{} - owner.SetGroupVersionKind(r.GVK) - - var m sync.RWMutex - watches := map[schema.GroupVersionKind]struct{}{} - releaseHook := func(release *rpb.Release) error { - resources := releaseutil.SplitManifests(release.Manifest) - for _, resource := range resources { - var u unstructured.Unstructured - if err := yaml.Unmarshal([]byte(resource), &u); err != nil { - return err - } - - gvk := u.GroupVersionKind() - if gvk.Empty() { - continue - } - - var setWatchOnResource = func(dependent runtime.Object) error { - unstructuredObj := dependent.(*unstructured.Unstructured) - gvkDependent := unstructuredObj.GroupVersionKind() - if gvkDependent.Empty() { - return nil - } - - m.RLock() - _, ok := watches[gvkDependent] - m.RUnlock() - if ok { - return nil - } - - restMapper := mgr.GetRESTMapper() - useOwnerRef, err := controllerutil.SupportsOwnerReference(restMapper, owner, unstructuredObj) - if err != nil { - return err - } - - if useOwnerRef { // Setup watch using owner references. - err = c.Watch(&source.Kind{Type: unstructuredObj}, &crthandler.EnqueueRequestForOwner{OwnerType: owner}, - predicate.DependentPredicate{}) - if err != nil { - return err - } - } else { // Setup watch using annotations. - err = c.Watch(&source.Kind{Type: unstructuredObj}, &libhandler.EnqueueRequestForAnnotation{Type: gvkDependent.GroupKind()}, - predicate.DependentPredicate{}) - if err != nil { - return err - } - } - m.Lock() - watches[gvkDependent] = struct{}{} - m.Unlock() - log.Info("Watching dependent resource", "ownerApiVersion", r.GVK.GroupVersion(), - "ownerKind", r.GVK.Kind, "apiVersion", gvkDependent.GroupVersion(), "kind", gvkDependent.Kind) - return nil - } - - // List is not actually a resource and therefore cannot have a - // watch on it. The watch will be on the kinds listed in the list - // and will therefore need to be handled individually. - listGVK := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "List"} - if gvk == listGVK { - errListItem := u.EachListItem(func(obj runtime.Object) error { - return setWatchOnResource(obj) - }) - if errListItem != nil { - return errListItem - } - } else { - err := setWatchOnResource(&u) - if err != nil { - return err - } - } - } - return nil - } - r.releaseHook = releaseHook -} diff --git a/internal/legacy/controller/controller_test.go b/internal/legacy/controller/controller_test.go deleted file mode 100644 index 968fdef1..00000000 --- a/internal/legacy/controller/controller_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package controller - -import ( - "testing" - - "github.com/stretchr/testify/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestFilterPredicate(t *testing.T) { - matchLabelPass := make(map[string]string) - matchLabelPass["testKey"] = "testValue" - selectorPass := metav1.LabelSelector{ - MatchLabels: matchLabelPass, - } - noSelector := metav1.LabelSelector{} - - passPredicate, err := parsePredicateSelector(selectorPass) - assert.Equal(t, nil, err, "Verify that no error is thrown on a valid populated selector") - assert.NotEqual(t, nil, passPredicate, "Verify that a predicate is constructed using a valid selector") - - nilPredicate, err := parsePredicateSelector(noSelector) - assert.Equal(t, nil, err, "Verify that no error is thrown on a valid unpopulated selector") - assert.Equal(t, nil, nilPredicate, "Verify correct parsing of an unpopulated selector") -} diff --git a/internal/legacy/controller/doc.go b/internal/legacy/controller/doc.go deleted file mode 100644 index e466a250..00000000 --- a/internal/legacy/controller/doc.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package controller provides functions for creating and registering a Helm -// controller with a `controller-runtime` manager. It also provides a Helm -// reconciler implementation that can be used to create a Helm-based operator. -package controller diff --git a/internal/legacy/controller/reconcile.go b/internal/legacy/controller/reconcile.go deleted file mode 100644 index cba0fed4..00000000 --- a/internal/legacy/controller/reconcile.go +++ /dev/null @@ -1,439 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package controller - -import ( - "context" - "errors" - "fmt" - "strconv" - "time" - - rpb "helm.sh/helm/v3/pkg/release" - "helm.sh/helm/v3/pkg/storage/driver" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/tools/record" - "k8s.io/client-go/util/retry" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/operator-framework/helm-operator-plugins/internal/legacy/helm/diff" - "github.com/operator-framework/helm-operator-plugins/internal/legacy/helm/types" - "github.com/operator-framework/helm-operator-plugins/internal/legacy/release" -) - -// blank assignment to verify that HelmOperatorReconciler implements reconcile.Reconciler -var _ reconcile.Reconciler = &HelmOperatorReconciler{} - -// ReleaseHookFunc defines a function signature for release hooks. -type ReleaseHookFunc func(*rpb.Release) error - -// HelmOperatorReconciler reconciles custom resources as Helm releases. -type HelmOperatorReconciler struct { - Client client.Client - EventRecorder record.EventRecorder - GVK schema.GroupVersionKind - ManagerFactory release.ManagerFactory - ReconcilePeriod time.Duration - OverrideValues map[string]string - releaseHook ReleaseHookFunc -} - -const ( - // uninstallFinalizer is added to CRs so they are cleaned up after uninstalling a release. - uninstallFinalizer = "helm.sdk.operatorframework.io/uninstall-release" - // Deprecated: use uninstallFinalizer. This will be removed in operator-sdk v2.0.0. - uninstallFinalizerLegacy = "uninstall-helm-release" - - helmUpgradeForceAnnotation = "helm.sdk.operatorframework.io/upgrade-force" - helmUninstallWaitAnnotation = "helm.sdk.operatorframework.io/uninstall-wait" -) - -// Reconcile reconciles the requested resource by installing, updating, or -// uninstalling a Helm release based on the resource's current state. If no -// release changes are necessary, Reconcile will create or patch the underlying -// resources to match the expected release manifest. - -func (r HelmOperatorReconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { //nolint:gocyclo - o := &unstructured.Unstructured{} - o.SetGroupVersionKind(r.GVK) - o.SetNamespace(request.Namespace) - o.SetName(request.Name) - log := log.WithValues( - "namespace", o.GetNamespace(), - "name", o.GetName(), - "apiVersion", o.GetAPIVersion(), - "kind", o.GetKind(), - ) - log.V(1).Info("Reconciling") - - err := r.Client.Get(ctx, request.NamespacedName, o) - if apierrors.IsNotFound(err) { - return reconcile.Result{}, nil - } - if err != nil { - log.Error(err, "Failed to lookup resource") - return reconcile.Result{}, err - } - - manager, err := r.ManagerFactory.NewManager(o, r.OverrideValues) - if err != nil { - log.Error(err, "Failed to get release manager") - return reconcile.Result{}, err - } - - status := types.StatusFor(o) - log = log.WithValues("release", manager.ReleaseName()) - - if o.GetDeletionTimestamp() != nil { - if !(controllerutil.ContainsFinalizer(o, uninstallFinalizer) || - controllerutil.ContainsFinalizer(o, uninstallFinalizerLegacy)) { - - log.Info("Resource is terminated, skipping reconciliation") - return reconcile.Result{}, nil - } - - uninstalledRelease, err := manager.UninstallRelease(ctx) - if err != nil && !errors.Is(err, driver.ErrReleaseNotFound) { - log.Error(err, "Failed to uninstall release") - status.SetCondition(types.HelmAppCondition{ - Type: types.ConditionReleaseFailed, - Status: types.StatusTrue, - Reason: types.ReasonUninstallError, - Message: err.Error(), - }) - if err := r.updateResourceStatus(ctx, o, status); err != nil { - log.Error(err, "Failed to update status after uninstall release failure") - } - return reconcile.Result{}, err - } - status.RemoveCondition(types.ConditionReleaseFailed) - - wait := hasAnnotation(helmUninstallWaitAnnotation, o) - if errors.Is(err, driver.ErrReleaseNotFound) { - log.Info("Release not found") - } else { - log.Info("Uninstalled release") - if log.V(0).Enabled() && uninstalledRelease != nil { - fmt.Println(diff.Generate(uninstalledRelease.Manifest, "")) - } - if !wait { - status.SetCondition(types.HelmAppCondition{ - Type: types.ConditionDeployed, - Status: types.StatusFalse, - Reason: types.ReasonUninstallSuccessful, - }) - status.DeployedRelease = nil - } - } - if wait { - status.SetCondition(types.HelmAppCondition{ - Type: types.ConditionDeployed, - Status: types.StatusFalse, - Reason: types.ReasonUninstallSuccessful, - Message: "Waiting until all resources are deleted.", - }) - } - if err := r.updateResourceStatus(ctx, o, status); err != nil { - log.Info("Failed to update CR status") - return reconcile.Result{}, err - } - - if wait && status.DeployedRelease != nil && status.DeployedRelease.Manifest != "" { - log.Info("Uninstall wait") - isAllResourcesDeleted, err := manager.CleanupRelease(ctx, status.DeployedRelease.Manifest) - if err != nil { - log.Error(err, "Failed to cleanup release") - status.SetCondition(types.HelmAppCondition{ - Type: types.ConditionReleaseFailed, - Status: types.StatusTrue, - Reason: types.ReasonUninstallError, - Message: err.Error(), - }) - _ = r.updateResourceStatus(ctx, o, status) - return reconcile.Result{}, err - } - if !isAllResourcesDeleted { - log.Info("Waiting until all resources are deleted") - return reconcile.Result{RequeueAfter: r.ReconcilePeriod}, nil - } - status.RemoveCondition(types.ConditionReleaseFailed) - } - - log.Info("Removing finalizer") - controllerutil.RemoveFinalizer(o, uninstallFinalizer) - controllerutil.RemoveFinalizer(o, uninstallFinalizerLegacy) - if err := r.updateResource(ctx, o); err != nil { - log.Info("Failed to remove CR uninstall finalizer") - return reconcile.Result{}, err - } - - // Since the client is hitting a cache, waiting for the - // deletion here will guarantee that the next reconciliation - // will see that the CR has been deleted and that there's - // nothing left to do. - if err := r.waitForDeletion(ctx, o); err != nil { - log.Info("Failed waiting for CR deletion") - return reconcile.Result{}, err - } - - return reconcile.Result{}, nil - } - - status.SetCondition(types.HelmAppCondition{ - Type: types.ConditionInitialized, - Status: types.StatusTrue, - }) - - if err := manager.Sync(ctx); err != nil { - log.Error(err, "Failed to sync release") - status.SetCondition(types.HelmAppCondition{ - Type: types.ConditionIrreconcilable, - Status: types.StatusTrue, - Reason: types.ReasonReconcileError, - Message: err.Error(), - }) - if err := r.updateResourceStatus(ctx, o, status); err != nil { - log.Error(err, "Failed to update status after sync release failure") - } - return reconcile.Result{}, err - } - status.RemoveCondition(types.ConditionIrreconcilable) - - if !manager.IsInstalled() { - for k, v := range r.OverrideValues { - r.EventRecorder.Eventf(o, "Warning", "OverrideValuesInUse", - "Chart value %q overridden to %q by operator's watches.yaml", k, v) - } - installedRelease, err := manager.InstallRelease(ctx) - if err != nil { - log.Error(err, "Release failed") - status.SetCondition(types.HelmAppCondition{ - Type: types.ConditionReleaseFailed, - Status: types.StatusTrue, - Reason: types.ReasonInstallError, - Message: err.Error(), - }) - if err := r.updateResourceStatus(ctx, o, status); err != nil { - log.Error(err, "Failed to update status after install release failure") - } - return reconcile.Result{}, err - } - status.RemoveCondition(types.ConditionReleaseFailed) - - log.V(1).Info("Adding finalizer", "finalizer", uninstallFinalizer) - controllerutil.AddFinalizer(o, uninstallFinalizer) - if err := r.updateResource(ctx, o); err != nil { - log.Info("Failed to add CR uninstall finalizer") - return reconcile.Result{}, err - } - - if r.releaseHook != nil { - if err := r.releaseHook(installedRelease); err != nil { - log.Error(err, "Failed to run release hook") - return reconcile.Result{}, err - } - } - - log.Info("Installed release") - if log.V(0).Enabled() { - fmt.Println(diff.Generate("", installedRelease.Manifest)) - } - log.V(1).Info("Config values", "values", installedRelease.Config) - message := "" - if installedRelease.Info != nil { - message = installedRelease.Info.Notes - } - status.SetCondition(types.HelmAppCondition{ - Type: types.ConditionDeployed, - Status: types.StatusTrue, - Reason: types.ReasonInstallSuccessful, - Message: message, - }) - status.DeployedRelease = &types.HelmAppRelease{ - Name: installedRelease.Name, - Manifest: installedRelease.Manifest, - } - err = r.updateResourceStatus(ctx, o, status) - return reconcile.Result{RequeueAfter: r.ReconcilePeriod}, err - } - - if !(controllerutil.ContainsFinalizer(o, uninstallFinalizer) || - controllerutil.ContainsFinalizer(o, uninstallFinalizerLegacy)) { - - log.V(1).Info("Adding finalizer", "finalizer", uninstallFinalizer) - controllerutil.AddFinalizer(o, uninstallFinalizer) - if err := r.updateResource(ctx, o); err != nil { - log.Info("Failed to add CR uninstall finalizer") - return reconcile.Result{}, err - } - } - - if manager.IsUpgradeRequired() { - for k, v := range r.OverrideValues { - r.EventRecorder.Eventf(o, "Warning", "OverrideValuesInUse", - "Chart value %q overridden to %q by operator's watches.yaml", k, v) - } - force := hasAnnotation(helmUpgradeForceAnnotation, o) - previousRelease, upgradedRelease, err := manager.UpgradeRelease(ctx, release.ForceUpgrade(force)) - if err != nil { - log.Error(err, "Release failed") - status.SetCondition(types.HelmAppCondition{ - Type: types.ConditionReleaseFailed, - Status: types.StatusTrue, - Reason: types.ReasonUpgradeError, - Message: err.Error(), - }) - if err := r.updateResourceStatus(ctx, o, status); err != nil { - log.Error(err, "Failed to update status after sync release failure") - } - return reconcile.Result{}, err - } - status.RemoveCondition(types.ConditionReleaseFailed) - - if r.releaseHook != nil { - if err := r.releaseHook(upgradedRelease); err != nil { - log.Error(err, "Failed to run release hook") - return reconcile.Result{}, err - } - } - - log.Info("Upgraded release", "force", force) - if log.V(0).Enabled() { - fmt.Println(diff.Generate(previousRelease.Manifest, upgradedRelease.Manifest)) - } - log.V(1).Info("Config values", "values", upgradedRelease.Config) - message := "" - if upgradedRelease.Info != nil { - message = upgradedRelease.Info.Notes - } - status.SetCondition(types.HelmAppCondition{ - Type: types.ConditionDeployed, - Status: types.StatusTrue, - Reason: types.ReasonUpgradeSuccessful, - Message: message, - }) - status.DeployedRelease = &types.HelmAppRelease{ - Name: upgradedRelease.Name, - Manifest: upgradedRelease.Manifest, - } - err = r.updateResourceStatus(ctx, o, status) - return reconcile.Result{RequeueAfter: r.ReconcilePeriod}, err - } - - // If a change is made to the CR spec that causes a release failure, a - // ConditionReleaseFailed is added to the status conditions. If that change - // is then reverted to its previous state, the operator will stop - // attempting the release and will resume reconciling. In this case, we - // need to remove the ConditionReleaseFailed because the failing release is - // no longer being attempted. - status.RemoveCondition(types.ConditionReleaseFailed) - - expectedRelease, err := manager.ReconcileRelease(ctx) - if err != nil { - log.Error(err, "Failed to reconcile release") - status.SetCondition(types.HelmAppCondition{ - Type: types.ConditionIrreconcilable, - Status: types.StatusTrue, - Reason: types.ReasonReconcileError, - Message: err.Error(), - }) - if err := r.updateResourceStatus(ctx, o, status); err != nil { - log.Error(err, "Failed to update status after reconcile release failure") - } - return reconcile.Result{}, err - } - status.RemoveCondition(types.ConditionIrreconcilable) - - if r.releaseHook != nil { - if err := r.releaseHook(expectedRelease); err != nil { - log.Error(err, "Failed to run release hook") - return reconcile.Result{}, err - } - } - - log.Info("Reconciled release") - reason := types.ReasonUpgradeSuccessful - if expectedRelease.Version == 1 { - reason = types.ReasonInstallSuccessful - } - message := "" - if expectedRelease.Info != nil { - message = expectedRelease.Info.Notes - } - status.SetCondition(types.HelmAppCondition{ - Type: types.ConditionDeployed, - Status: types.StatusTrue, - Reason: reason, - Message: message, - }) - status.DeployedRelease = &types.HelmAppRelease{ - Name: expectedRelease.Name, - Manifest: expectedRelease.Manifest, - } - err = r.updateResourceStatus(ctx, o, status) - return reconcile.Result{RequeueAfter: r.ReconcilePeriod}, err -} - -// returns the boolean representation of the annotation string -// will return false if annotation is not set -func hasAnnotation(anno string, o *unstructured.Unstructured) bool { - boolStr := o.GetAnnotations()[anno] - if boolStr == "" { - return false - } - value := false - if i, err := strconv.ParseBool(boolStr); err != nil { - log.Info("Could not parse annotation as a boolean", - "annotation", anno, "value informed", boolStr) - } else { - value = i - } - return value -} - -func (r HelmOperatorReconciler) updateResource(ctx context.Context, o client.Object) error { - return retry.RetryOnConflict(retry.DefaultBackoff, func() error { - return r.Client.Update(ctx, o) - }) -} - -func (r HelmOperatorReconciler) updateResourceStatus(ctx context.Context, o *unstructured.Unstructured, status *types.HelmAppStatus) error { - return retry.RetryOnConflict(retry.DefaultBackoff, func() error { - o.Object["status"] = status - return r.Client.Status().Update(ctx, o) - }) -} - -func (r HelmOperatorReconciler) waitForDeletion(ctx context.Context, o client.Object) error { - key := client.ObjectKeyFromObject(o) - - tctx, cancel := context.WithTimeout(ctx, time.Second*5) - defer cancel() - return wait.PollImmediateUntil(time.Millisecond*10, func() (bool, error) { - err := r.Client.Get(tctx, key, o) - if apierrors.IsNotFound(err) { - return true, nil - } - if err != nil { - return false, err - } - return false, nil - }, tctx.Done()) -} diff --git a/internal/legacy/controller/reconcile_test.go b/internal/legacy/controller/reconcile_test.go deleted file mode 100644 index 188e936f..00000000 --- a/internal/legacy/controller/reconcile_test.go +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package controller - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func TestHasAnnotation(t *testing.T) { - upgradeForceTests := []struct { - input map[string]interface{} - expectedVal bool - expectedOut string - name string - }{ - { - input: map[string]interface{}{ - "helm.sdk.operatorframework.io/upgrade-force": "True", - }, - expectedVal: true, - name: "upgrade force base case true", - }, - { - input: map[string]interface{}{ - "helm.sdk.operatorframework.io/upgrade-force": "False", - }, - expectedVal: false, - name: "upgrade force base case false", - }, - { - input: map[string]interface{}{ - "helm.sdk.operatorframework.io/upgrade-force": "1", - }, - expectedVal: true, - name: "upgrade force true as int", - }, - { - input: map[string]interface{}{ - "helm.sdk.operatorframework.io/upgrade-force": "0", - }, - expectedVal: false, - name: "upgrade force false as int", - }, - { - input: map[string]interface{}{ - "helm.sdk.operatorframework.io/wrong-annotation": "true", - }, - expectedVal: false, - name: "upgrade force annotation not set", - }, - { - input: map[string]interface{}{ - "helm.sdk.operatorframework.io/upgrade-force": "invalid", - }, - expectedVal: false, - name: "upgrade force invalid value", - }, - } - - for _, test := range upgradeForceTests { - assert.Equal(t, test.expectedVal, hasAnnotation(helmUpgradeForceAnnotation, annotations(test.input)), test.name) - } - - uninstallWaitTests := []struct { - input map[string]interface{} - expectedVal bool - expectedOut string - name string - }{ - { - input: map[string]interface{}{ - "helm.sdk.operatorframework.io/uninstall-wait": "True", - }, - expectedVal: true, - name: "uninstall wait base case true", - }, - { - input: map[string]interface{}{ - "helm.sdk.operatorframework.io/uninstall-wait": "False", - }, - expectedVal: false, - name: "uninstall wait base case false", - }, - { - input: map[string]interface{}{ - "helm.sdk.operatorframework.io/uninstall-wait": "1", - }, - expectedVal: true, - name: "uninstall wait true as int", - }, - { - input: map[string]interface{}{ - "helm.sdk.operatorframework.io/uninstall-wait": "0", - }, - expectedVal: false, - name: "uninstall wait false as int", - }, - { - input: map[string]interface{}{ - "helm.sdk.operatorframework.io/wrong-annotation": "true", - }, - expectedVal: false, - name: "uninstall wait annotation not set", - }, - { - input: map[string]interface{}{ - "helm.sdk.operatorframework.io/uninstall-wait": "invalid", - }, - expectedVal: false, - name: "uninstall wait invalid value", - }, - } - - for _, test := range uninstallWaitTests { - assert.Equal(t, test.expectedVal, hasAnnotation(helmUninstallWaitAnnotation, annotations(test.input)), test.name) - } -} - -func annotations(m map[string]interface{}) *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "annotations": m, - }, - }, - } -} diff --git a/internal/legacy/helm/client/client.go b/internal/legacy/helm/client/client.go deleted file mode 100644 index dbca5a3c..00000000 --- a/internal/legacy/helm/client/client.go +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package client - -import ( - "errors" - "io" - "strings" - - "github.com/operator-framework/helm-operator-plugins/pkg/sdk/controllerutil" - "github.com/operator-framework/operator-lib/handler" - "helm.sh/helm/v3/pkg/kube" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/resource" - "k8s.io/client-go/discovery" - cached "k8s.io/client-go/discovery/cached" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - clientcmdapi "k8s.io/client-go/tools/clientcmd/api" - "sigs.k8s.io/controller-runtime/pkg/manager" -) - -var _ genericclioptions.RESTClientGetter = &restClientGetter{} - -type restClientGetter struct { - restConfig *rest.Config - discoveryClient discovery.CachedDiscoveryInterface - restMapper meta.RESTMapper - namespaceConfig clientcmd.ClientConfig -} - -func (c *restClientGetter) ToRESTConfig() (*rest.Config, error) { - return c.restConfig, nil -} - -func (c *restClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { - return c.discoveryClient, nil -} - -func (c *restClientGetter) ToRESTMapper() (meta.RESTMapper, error) { - return c.restMapper, nil -} - -func (c *restClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig { - return c.namespaceConfig -} - -var _ clientcmd.ClientConfig = &namespaceClientConfig{} - -type namespaceClientConfig struct { - namespace string -} - -func (c namespaceClientConfig) RawConfig() (clientcmdapi.Config, error) { - return clientcmdapi.Config{}, nil -} - -func (c namespaceClientConfig) ClientConfig() (*rest.Config, error) { - return nil, nil -} - -func (c namespaceClientConfig) Namespace() (string, bool, error) { - return c.namespace, false, nil -} - -func (c namespaceClientConfig) ConfigAccess() clientcmd.ConfigAccess { - return nil -} - -func NewRESTClientGetter(mgr manager.Manager, ns string) (genericclioptions.RESTClientGetter, error) { - cfg := mgr.GetConfig() - dc, err := discovery.NewDiscoveryClientForConfig(cfg) - if err != nil { - return nil, err - } - cdc := cached.NewMemCacheClient(dc) - rm := mgr.GetRESTMapper() - - return &restClientGetter{ - restConfig: cfg, - discoveryClient: cdc, - restMapper: rm, - namespaceConfig: &namespaceClientConfig{ns}, - }, nil -} - -var _ kube.Interface = &ownerRefInjectingClient{} - -func NewOwnerRefInjectingClient(base kube.Client, restMapper meta.RESTMapper, - cr *unstructured.Unstructured) (kube.Interface, error) { - - if cr != nil { - if cr.GetObjectKind() != nil { - if cr.GetObjectKind().GroupVersionKind().Empty() || cr.GetName() == "" || cr.GetUID() == "" { - var err = errors.New("owner resource is invalid") - return nil, err - } - } - } - return &ownerRefInjectingClient{ - Client: base, - restMapper: restMapper, - owner: cr, - }, nil -} - -type ownerRefInjectingClient struct { - kube.Client - restMapper meta.RESTMapper - owner *unstructured.Unstructured -} - -func (c *ownerRefInjectingClient) Build(reader io.Reader, validate bool) (kube.ResourceList, error) { - resourceList, err := c.Client.Build(reader, validate) - if err != nil { - return resourceList, err - } - err = resourceList.Visit(func(r *resource.Info, err error) error { - if err != nil { - return err - } - objMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(r.Object) - if err != nil { - return err - } - u := &unstructured.Unstructured{Object: objMap} - useOwnerRef, err := controllerutil.SupportsOwnerReference(c.restMapper, c.owner, u) - if err != nil { - return err - } - - // If the resource contains the Helm resource-policy keep annotation, then do not add - // the owner reference. So when the CR is deleted, Kubernetes won't GCs the resource. - if useOwnerRef && !containsResourcePolicyKeep(u.GetAnnotations()) { - ownerRef := metav1.NewControllerRef(c.owner, c.owner.GroupVersionKind()) - u.SetOwnerReferences([]metav1.OwnerReference{*ownerRef}) - } else { - err := handler.SetOwnerAnnotations(u, c.owner) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - return nil, err - } - return resourceList, nil -} - -func containsResourcePolicyKeep(annotations map[string]string) bool { - if annotations == nil { - return false - } - resourcePolicyType, ok := annotations[kube.ResourcePolicyAnno] - if !ok { - return false - } - resourcePolicyType = strings.ToLower(strings.TrimSpace(resourcePolicyType)) - return resourcePolicyType == kube.KeepPolicy -} diff --git a/internal/legacy/helm/client/client_test.go b/internal/legacy/helm/client/client_test.go deleted file mode 100644 index c223da33..00000000 --- a/internal/legacy/helm/client/client_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2021 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package client - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "helm.sh/helm/v3/pkg/kube" -) - -func TestContainsResourcePolicyKeep(t *testing.T) { - tests := []struct { - input map[string]string - expectedVal bool - expectedOut string - name string - }{ - { - input: map[string]string{ - kube.ResourcePolicyAnno: kube.KeepPolicy, - }, - expectedVal: true, - name: "base case true", - }, - { - input: map[string]string{ - "not-" + kube.ResourcePolicyAnno: kube.KeepPolicy, - }, - expectedVal: false, - name: "base case annotation false", - }, - { - input: map[string]string{ - kube.ResourcePolicyAnno: "not-" + kube.KeepPolicy, - }, - expectedVal: false, - name: "base case value false", - }, - { - input: map[string]string{ - kube.ResourcePolicyAnno: strings.ToUpper(kube.KeepPolicy), - }, - expectedVal: true, - name: "true with upper case", - }, - { - input: map[string]string{ - kube.ResourcePolicyAnno: " " + kube.KeepPolicy + " ", - }, - expectedVal: true, - name: "true with spaces", - }, - { - input: map[string]string{ - kube.ResourcePolicyAnno: " " + strings.ToUpper(kube.KeepPolicy) + " ", - }, - expectedVal: true, - name: "true with upper case and spaces", - }, - } - - for _, test := range tests { - assert.Equal(t, test.expectedVal, containsResourcePolicyKeep(test.input), test.name) - } -} diff --git a/internal/legacy/helm/client/doc.go b/internal/legacy/helm/client/doc.go deleted file mode 100644 index 93e885bc..00000000 --- a/internal/legacy/helm/client/doc.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package client provides helper functions for API clients used by the helm -// operator. -package client diff --git a/internal/legacy/helm/diff/diff.go b/internal/legacy/helm/diff/diff.go deleted file mode 100644 index 6a833fdf..00000000 --- a/internal/legacy/helm/diff/diff.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package diff - -import ( - "bytes" - "regexp" - "strings" - - "github.com/sergi/go-diff/diffmatchpatch" -) - -// Generate generates a diff between a and b, in color. -func Generate(a, b string) string { - dmp := diffmatchpatch.New() - - wSrc, wDst, warray := dmp.DiffLinesToRunes(a, b) - diffs := dmp.DiffMainRunes(wSrc, wDst, false) - diffs = dmp.DiffCharsToLines(diffs, warray) - var buff bytes.Buffer - for _, diff := range diffs { - text := diff.Text - - switch diff.Type { - case diffmatchpatch.DiffInsert: - _, _ = buff.WriteString("\x1b[32m") - _, _ = buff.WriteString(prefixLines(text, "+")) - _, _ = buff.WriteString("\x1b[0m") - case diffmatchpatch.DiffDelete: - _, _ = buff.WriteString("\x1b[31m") - _, _ = buff.WriteString(prefixLines(text, "-")) - _, _ = buff.WriteString("\x1b[0m") - case diffmatchpatch.DiffEqual: - _, _ = buff.WriteString(prefixLines(text, " ")) - } - } - return buff.String() -} - -func prefixLines(s, prefix string) string { - var buf bytes.Buffer - lines := strings.Split(s, "\n") - ls := regexp.MustCompile("^") - for _, line := range lines[:len(lines)-1] { - buf.WriteString(ls.ReplaceAllString(line, prefix)) - buf.WriteString("\n") - } - return buf.String() -} diff --git a/internal/legacy/helm/types/doc.go b/internal/legacy/helm/types/doc.go deleted file mode 100644 index 18255ab3..00000000 --- a/internal/legacy/helm/types/doc.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package types contains types used by various components of the Helm -// operator -package types diff --git a/internal/legacy/helm/types/types.go b/internal/legacy/helm/types/types.go deleted file mode 100644 index b816b57b..00000000 --- a/internal/legacy/helm/types/types.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package types - -import ( - "encoding/json" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -type HelmAppList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata"` - Items []HelmApp `json:"items"` -} - -type HelmApp struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata"` - Spec HelmAppSpec `json:"spec"` - Status HelmAppStatus `json:"status,omitempty"` -} - -type HelmAppSpec map[string]interface{} - -type HelmAppConditionType string -type ConditionStatus string -type HelmAppConditionReason string - -type HelmAppCondition struct { - Type HelmAppConditionType `json:"type"` - Status ConditionStatus `json:"status"` - Reason HelmAppConditionReason `json:"reason,omitempty"` - Message string `json:"message,omitempty"` - - LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` -} - -type HelmAppRelease struct { - Name string `json:"name,omitempty"` - Manifest string `json:"manifest,omitempty"` -} - -const ( - ConditionInitialized HelmAppConditionType = "Initialized" - ConditionDeployed HelmAppConditionType = "Deployed" - ConditionReleaseFailed HelmAppConditionType = "ReleaseFailed" - ConditionIrreconcilable HelmAppConditionType = "Irreconcilable" - - StatusTrue ConditionStatus = "True" - StatusFalse ConditionStatus = "False" - StatusUnknown ConditionStatus = "Unknown" - - ReasonInstallSuccessful HelmAppConditionReason = "InstallSuccessful" - ReasonUpgradeSuccessful HelmAppConditionReason = "UpgradeSuccessful" - ReasonUninstallSuccessful HelmAppConditionReason = "UninstallSuccessful" - ReasonInstallError HelmAppConditionReason = "InstallError" - ReasonUpgradeError HelmAppConditionReason = "UpgradeError" - ReasonReconcileError HelmAppConditionReason = "ReconcileError" - ReasonUninstallError HelmAppConditionReason = "UninstallError" -) - -type HelmAppStatus struct { - Conditions []HelmAppCondition `json:"conditions"` - DeployedRelease *HelmAppRelease `json:"deployedRelease,omitempty"` -} - -func (s *HelmAppStatus) ToMap() (map[string]interface{}, error) { - var out map[string]interface{} - jsonObj, err := json.Marshal(&s) - if err != nil { - return nil, err - } - if err := json.Unmarshal(jsonObj, &out); err != nil { - return nil, err - } - return out, nil -} - -// SetCondition sets a condition on the status object. If the condition already -// exists, it will be replaced. SetCondition does not update the resource in -// the cluster. -func (s *HelmAppStatus) SetCondition(condition HelmAppCondition) *HelmAppStatus { - now := metav1.Now() - for i := range s.Conditions { - if s.Conditions[i].Type == condition.Type { - if s.Conditions[i].Status != condition.Status { - condition.LastTransitionTime = now - } else { - condition.LastTransitionTime = s.Conditions[i].LastTransitionTime - } - s.Conditions[i] = condition - return s - } - } - - // If the condition does not exist, - // initialize the lastTransitionTime - condition.LastTransitionTime = now - s.Conditions = append(s.Conditions, condition) - return s -} - -// RemoveCondition removes the condition with the passed condition type from -// the status object. If the condition is not already present, the returned -// status object is returned unchanged. RemoveCondition does not update the -// resource in the cluster. -func (s *HelmAppStatus) RemoveCondition(conditionType HelmAppConditionType) *HelmAppStatus { - for i := range s.Conditions { - if s.Conditions[i].Type == conditionType { - s.Conditions = append(s.Conditions[:i], s.Conditions[i+1:]...) - return s - } - } - return s -} - -// StatusFor safely returns a typed status block from a custom resource. -func StatusFor(cr *unstructured.Unstructured) *HelmAppStatus { - switch s := cr.Object["status"].(type) { - case *HelmAppStatus: - return s - case map[string]interface{}: - var status *HelmAppStatus - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(s, &status); err != nil { - return &HelmAppStatus{} - } - return status - default: - return &HelmAppStatus{} - } -} diff --git a/internal/legacy/helm/types/types_test.go b/internal/legacy/helm/types/types_test.go deleted file mode 100644 index cd8e84ca..00000000 --- a/internal/legacy/helm/types/types_test.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package types - -import ( - "testing" - - "github.com/stretchr/testify/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -const ( - testNamespaceName = "helm-test" -) - -var now = metav1.Now() - -func TestSetCondition(t *testing.T) { - message := "uninstall was successful" - newStatus, err := newTestStatus().SetCondition(HelmAppCondition{ - Type: ConditionDeployed, - Status: StatusFalse, - Reason: ReasonUninstallSuccessful, - Message: message, - }).ToMap() - assert.NoError(t, err) - - resource := newTestResource() - resource.Object["status"] = newStatus - actual := StatusFor(resource) - - assert.Equal(t, ConditionDeployed, actual.Conditions[0].Type) - assert.Equal(t, StatusFalse, actual.Conditions[0].Status) - assert.Equal(t, ReasonUninstallSuccessful, actual.Conditions[0].Reason) - assert.Equal(t, message, actual.Conditions[0].Message) - assert.NotEqual(t, metav1.Now(), actual.Conditions[0].LastTransitionTime) -} -func TestRemoveCondition(t *testing.T) { - newStatus, err := newTestStatus().RemoveCondition(ConditionDeployed).ToMap() - assert.NoError(t, err) - - resource := newTestResource() - resource.Object["status"] = newStatus - actual := StatusFor(resource) - - assert.Empty(t, actual.Conditions) -} - -func TestStatusForEmpty(t *testing.T) { - status := StatusFor(newTestResource()) - - assert.Equal(t, &HelmAppStatus{}, status) -} - -func TestStatusForFilled(t *testing.T) { - expectedResource := newTestResource() - expectedResource.Object["status"] = newTestStatus() - status := StatusFor(expectedResource) - - assert.EqualValues(t, newTestStatus(), status) -} - -func TestStatusForFilledRaw(t *testing.T) { - expectedResource := newTestResource() - expectedResource.Object["status"] = newTestStatusRaw() - status := StatusFor(expectedResource) - - assert.Equal(t, ConditionDeployed, status.Conditions[0].Type) - assert.Equal(t, StatusTrue, status.Conditions[0].Status) - assert.Equal(t, ReasonInstallSuccessful, status.Conditions[0].Reason) - assert.Equal(t, "some message", status.Conditions[0].Message) - assert.NotEqual(t, metav1.Now(), status.Conditions[0].LastTransitionTime) - assert.Equal(t, "SomeRelease", status.DeployedRelease.Name) -} - -func newTestResource() *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "kind": "Character", - "apiVersion": "stable.nicolerenee.io", - "metadata": map[string]interface{}{ - "name": "dory", - "namespace": testNamespaceName, - }, - "spec": map[string]interface{}{ - "Name": "Dory", - "From": "Finding Nemo", - "By": "Disney", - }, - }, - } -} - -func newTestStatus() *HelmAppStatus { - return &HelmAppStatus{ - Conditions: []HelmAppCondition{ - { - Type: ConditionDeployed, - Status: StatusTrue, - Reason: ReasonInstallSuccessful, - Message: "some message", - LastTransitionTime: now, - }, - }, - DeployedRelease: &HelmAppRelease{Name: "SomeRelease"}, - } -} - -func newTestStatusRaw() map[string]interface{} { - return map[string]interface{}{ - "conditions": []map[string]interface{}{ - { - "type": "Deployed", - "status": "True", - "reason": "InstallSuccessful", - "message": "some message", - "lastTransitionTime": now.UTC(), - }, - }, - "deployedRelease": map[string]interface{}{"name": "SomeRelease"}, - } -} diff --git a/internal/legacy/manifestutil/resource_policy.go b/internal/legacy/manifestutil/resource_policy.go deleted file mode 100644 index 320d9f4d..00000000 --- a/internal/legacy/manifestutil/resource_policy.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2021 The Operator-SDK Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package manifestutil - -import ( - "strings" - - "helm.sh/helm/v3/pkg/kube" - "helm.sh/helm/v3/pkg/releaseutil" -) - -// Source from https://github.com/helm/helm/blob/v3.4.2/pkg/action/resource_policy.go -func FilterManifestsToKeep(manifests []releaseutil.Manifest) (keep, remaining []releaseutil.Manifest) { - for _, m := range manifests { - if m.Head.Metadata == nil || m.Head.Metadata.Annotations == nil || len(m.Head.Metadata.Annotations) == 0 { - remaining = append(remaining, m) - continue - } - - resourcePolicyType, ok := m.Head.Metadata.Annotations[kube.ResourcePolicyAnno] - if !ok { - remaining = append(remaining, m) - continue - } - - resourcePolicyType = strings.ToLower(strings.TrimSpace(resourcePolicyType)) - if resourcePolicyType == kube.KeepPolicy { - keep = append(keep, m) - } - - } - return keep, remaining -} diff --git a/internal/legacy/release/doc.go b/internal/legacy/release/doc.go deleted file mode 100644 index 76c4c344..00000000 --- a/internal/legacy/release/doc.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package release provides interfaces and default implementations for a Helm -// release manager, which is used by the Helm controller and reconciler to -// manage Helm releases in a cluster based on watched custom resources. -package release diff --git a/internal/legacy/release/manager.go b/internal/legacy/release/manager.go deleted file mode 100644 index 6baa26fd..00000000 --- a/internal/legacy/release/manager.go +++ /dev/null @@ -1,424 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package release - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "strings" - - jsonpatch "gomodules.xyz/jsonpatch/v3" - "helm.sh/helm/v3/pkg/action" - cpb "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/kube" - rpb "helm.sh/helm/v3/pkg/release" - "helm.sh/helm/v3/pkg/releaseutil" - "helm.sh/helm/v3/pkg/storage" - "helm.sh/helm/v3/pkg/storage/driver" - apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - apitypes "k8s.io/apimachinery/pkg/types" - apiutilerrors "k8s.io/apimachinery/pkg/util/errors" - "k8s.io/apimachinery/pkg/util/strategicpatch" - "k8s.io/cli-runtime/pkg/resource" - "k8s.io/client-go/discovery" - - "github.com/operator-framework/helm-operator-plugins/internal/legacy/helm/types" - "github.com/operator-framework/helm-operator-plugins/internal/legacy/manifestutil" -) - -// Manager manages a Helm release. It can install, upgrade, reconcile, -// and uninstall a release. -type Manager interface { - ReleaseName() string - IsInstalled() bool - IsUpgradeRequired() bool - Sync(context.Context) error - InstallRelease(context.Context, ...InstallOption) (*rpb.Release, error) - UpgradeRelease(context.Context, ...UpgradeOption) (*rpb.Release, *rpb.Release, error) - ReconcileRelease(context.Context) (*rpb.Release, error) - UninstallRelease(context.Context, ...UninstallOption) (*rpb.Release, error) - CleanupRelease(context.Context, string) (bool, error) -} - -type manager struct { - actionConfig *action.Configuration - storageBackend *storage.Storage - kubeClient kube.Interface - - releaseName string - namespace string - - values map[string]interface{} - status *types.HelmAppStatus - - isInstalled bool - isUpgradeRequired bool - deployedRelease *rpb.Release - chart *cpb.Chart -} - -type InstallOption func(*action.Install) error -type UpgradeOption func(*action.Upgrade) error -type UninstallOption func(*action.Uninstall) error - -// ReleaseName returns the name of the release. -func (m manager) ReleaseName() string { - return m.releaseName -} - -func (m manager) IsInstalled() bool { - return m.isInstalled -} - -func (m manager) IsUpgradeRequired() bool { - return m.isUpgradeRequired -} - -// Sync ensures the Helm storage backend is in sync with the status of the -// custom resource. -func (m *manager) Sync(ctx context.Context) error { - // Get release history for this release name - releases, err := m.storageBackend.History(m.releaseName) - if err != nil && !notFoundErr(err) { - return fmt.Errorf("failed to retrieve release history: %w", err) - } - - // Cleanup non-deployed release versions. If all release versions are - // non-deployed, this will ensure that failed installations are correctly - // retried. - for _, rel := range releases { - if rel.Info != nil && rel.Info.Status != rpb.StatusDeployed { - _, err := m.storageBackend.Delete(rel.Name, rel.Version) - if err != nil && !notFoundErr(err) { - return fmt.Errorf("failed to delete stale release version: %w", err) - } - } - } - - // Load the most recently deployed release from the storage backend. - deployedRelease, err := m.getDeployedRelease() - if errors.Is(err, driver.ErrReleaseNotFound) { - return nil - } - if err != nil { - return fmt.Errorf("failed to get deployed release: %w", err) - } - m.deployedRelease = deployedRelease - m.isInstalled = true - - // Get the next candidate release to determine if an upgrade is necessary. - candidateRelease, err := m.getCandidateRelease(m.namespace, m.releaseName, m.chart, m.values) - if err != nil { - return fmt.Errorf("failed to get candidate release: %w", err) - } - if deployedRelease.Manifest != candidateRelease.Manifest { - m.isUpgradeRequired = true - } - - return nil -} - -func notFoundErr(err error) bool { - return err != nil && strings.Contains(err.Error(), "not found") -} - -func (m manager) getDeployedRelease() (*rpb.Release, error) { - deployedRelease, err := m.storageBackend.Deployed(m.releaseName) - if err != nil { - if strings.Contains(err.Error(), "has no deployed releases") { - return nil, driver.ErrReleaseNotFound - } - return nil, err - } - return deployedRelease, nil -} - -func (m manager) getCandidateRelease(namespace, name string, chart *cpb.Chart, - values map[string]interface{}) (*rpb.Release, error) { - upgrade := action.NewUpgrade(m.actionConfig) - upgrade.Namespace = namespace - upgrade.DryRun = true - return upgrade.Run(name, chart, values) -} - -// InstallRelease performs a Helm release install. -func (m manager) InstallRelease(ctx context.Context, opts ...InstallOption) (*rpb.Release, error) { - install := action.NewInstall(m.actionConfig) - install.ReleaseName = m.releaseName - install.Namespace = m.namespace - for _, o := range opts { - if err := o(install); err != nil { - return nil, fmt.Errorf("failed to apply install option: %w", err) - } - } - - installedRelease, err := install.Run(m.chart, m.values) - if err != nil { - // Workaround for helm/helm#3338 - if installedRelease != nil { - uninstall := action.NewUninstall(m.actionConfig) - _, uninstallErr := uninstall.Run(m.releaseName) - - // In certain cases, InstallRelease will return a partial release in - // the response even when it doesn't record the release in its release - // store (e.g. when there is an error rendering the release manifest). - // In that case the rollback will fail with a not found error because - // there was nothing to rollback. - // - // Only log a message about a rollback failure if the failure was caused - // by something other than the release not being found. - if uninstallErr != nil && !notFoundErr(uninstallErr) { - return nil, fmt.Errorf("failed installation (%s) and failed rollback: %w", err, uninstallErr) - } - } - return nil, fmt.Errorf("failed to install release: %w", err) - } - return installedRelease, nil -} - -func ForceUpgrade(force bool) UpgradeOption { - return func(u *action.Upgrade) error { - u.Force = force - return nil - } -} - -// UpgradeRelease performs a Helm release upgrade. -func (m manager) UpgradeRelease(ctx context.Context, opts ...UpgradeOption) (*rpb.Release, *rpb.Release, error) { - upgrade := action.NewUpgrade(m.actionConfig) - upgrade.Namespace = m.namespace - for _, o := range opts { - if err := o(upgrade); err != nil { - return nil, nil, fmt.Errorf("failed to apply upgrade option: %w", err) - } - } - - upgradedRelease, err := upgrade.Run(m.releaseName, m.chart, m.values) - if err != nil { - // Workaround for helm/helm#3338 - if upgradedRelease != nil { - rollback := action.NewRollback(m.actionConfig) - rollback.Force = true - - // As of Helm 2.13, if UpgradeRelease returns a non-nil release, that - // means the release was also recorded in the release store. - // Therefore, we should perform the rollback when we have a non-nil - // release. Any rollback error here would be unexpected, so always - // log both the upgrade and rollback errors. - rollbackErr := rollback.Run(m.releaseName) - if rollbackErr != nil { - return nil, nil, fmt.Errorf("failed upgrade (%s) and failed rollback: %w", err, rollbackErr) - } - } - return nil, nil, fmt.Errorf("failed to upgrade release: %w", err) - } - return m.deployedRelease, upgradedRelease, err -} - -// ReconcileRelease creates or patches resources as necessary to match the -// deployed release's manifest. -func (m manager) ReconcileRelease(ctx context.Context) (*rpb.Release, error) { - err := reconcileRelease(ctx, m.kubeClient, m.deployedRelease.Manifest) - return m.deployedRelease, err -} - -func reconcileRelease(_ context.Context, kubeClient kube.Interface, expectedManifest string) error { - expectedInfos, err := kubeClient.Build(bytes.NewBufferString(expectedManifest), false) - if err != nil { - return err - } - return expectedInfos.Visit(func(expected *resource.Info, err error) error { - if err != nil { - return fmt.Errorf("visit error: %w", err) - } - - helper := resource.NewHelper(expected.Client, expected.Mapping) - existing, err := helper.Get(expected.Namespace, expected.Name) - if apierrors.IsNotFound(err) { - if _, err := helper.Create(expected.Namespace, true, expected.Object); err != nil { - return fmt.Errorf("create error: %s", err) - } - return nil - } else if err != nil { - return fmt.Errorf("could not get object: %w", err) - } - - // Replicate helm's patch creation, which will create a Three-Way-Merge patch for - // native kubernetes Objects and fall back to a JSON merge patch for unstructured Objects such as CRDs - // We also extend the JSON merge patch by ignoring "remove" operations for fields added by kubernetes - // Reference in the helm source code: - // https://github.com/helm/helm/blob/1c9b54ad7f62a5ce12f87c3ae55136ca20f09c98/pkg/kube/client.go#L392 - patch, patchType, err := createPatch(existing, expected) - if err != nil { - return fmt.Errorf("error creating patch: %w", err) - } - - if patch == nil { - // nothing to do - return nil - } - - _, err = helper.Patch(expected.Namespace, expected.Name, patchType, patch, - &metav1.PatchOptions{}) - if err != nil { - return fmt.Errorf("patch error: %w", err) - } - return nil - }) -} - -func createPatch(existing runtime.Object, expected *resource.Info) ([]byte, apitypes.PatchType, error) { - existingJSON, err := json.Marshal(existing) - if err != nil { - return nil, apitypes.StrategicMergePatchType, err - } - expectedJSON, err := json.Marshal(expected.Object) - if err != nil { - return nil, apitypes.StrategicMergePatchType, err - } - - // Get a versioned object - versionedObject := kube.AsVersioned(expected) - - // Unstructured objects, such as CRDs, may not have an not registered error - // returned from ConvertToVersion. Anything that's unstructured should - // use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported - // on objects like CRDs. - _, isUnstructured := versionedObject.(runtime.Unstructured) - - // On newer K8s versions, CRDs aren't unstructured but have a dedicated type - _, isV1CRD := versionedObject.(*apiextv1.CustomResourceDefinition) - _, isV1beta1CRD := versionedObject.(*apiextv1beta1.CustomResourceDefinition) - isCRD := isV1CRD || isV1beta1CRD - - if isUnstructured || isCRD { - // fall back to generic JSON merge patch - patch, err := createJSONMergePatch(existingJSON, expectedJSON) - return patch, apitypes.JSONPatchType, err - } - - patchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject) - if err != nil { - return nil, apitypes.StrategicMergePatchType, err - } - - patch, err := strategicpatch.CreateThreeWayMergePatch(expectedJSON, expectedJSON, existingJSON, patchMeta, true) - if err != nil { - return nil, apitypes.StrategicMergePatchType, err - } - // An empty patch could be in the form of "{}" which represents an empty map out of the 3-way merge; - // filter them out here too to avoid sending the apiserver empty patch requests. - if len(patch) == 0 || bytes.Equal(patch, []byte("{}")) { - return nil, apitypes.StrategicMergePatchType, nil - } - return patch, apitypes.StrategicMergePatchType, nil -} - -func createJSONMergePatch(existingJSON, expectedJSON []byte) ([]byte, error) { - ops, err := jsonpatch.CreatePatch(existingJSON, expectedJSON) - if err != nil { - return nil, err - } - - // We ignore the "remove" operations from the full patch because they are - // fields added by Kubernetes or by the user after the existing release - // resource has been applied. The goal for this patch is to make sure that - // the fields managed by the Helm chart are applied. - // All "add" operations without a value (null) can be ignored - patchOps := make([]jsonpatch.JsonPatchOperation, 0) - for _, op := range ops { - if op.Operation != "remove" && !(op.Operation == "add" && op.Value == nil) { - patchOps = append(patchOps, op) - } - } - - // If there are no patch operations, return nil. Callers are expected - // to check for a nil response and skip the patch operation to avoid - // unnecessary chatter with the API server. - if len(patchOps) == 0 { - return nil, nil - } - - return json.Marshal(patchOps) -} - -// UninstallRelease performs a Helm release uninstall. -func (m manager) UninstallRelease(ctx context.Context, opts ...UninstallOption) (*rpb.Release, error) { - uninstall := action.NewUninstall(m.actionConfig) - for _, o := range opts { - if err := o(uninstall); err != nil { - return nil, fmt.Errorf("failed to apply uninstall option: %w", err) - } - } - uninstallResponse, err := uninstall.Run(m.releaseName) - if uninstallResponse == nil { - return nil, err - } - return uninstallResponse.Release, err -} - -// CleanupRelease deletes resources if they are not deleted already. -// Return true if all the resources are deleted, false otherwise. -func (m manager) CleanupRelease(ctx context.Context, manifest string) (bool, error) { - dc, err := m.actionConfig.RESTClientGetter.ToDiscoveryClient() - if err != nil { - return false, fmt.Errorf("failed to get Kubernetes discovery client: %w", err) - } - apiVersions, err := action.GetVersionSet(dc) - if err != nil && !discovery.IsGroupDiscoveryFailedError(err) { - return false, fmt.Errorf("failed to get apiVersions from Kubernetes: %w", err) - } - manifests := releaseutil.SplitManifests(manifest) - _, files, err := releaseutil.SortManifests(manifests, apiVersions, releaseutil.UninstallOrder) - if err != nil { - return false, fmt.Errorf("failed to sort manifests: %w", err) - } - // do not delete resources that are annotated with the Helm resource policy 'keep' - _, filesToDelete := manifestutil.FilterManifestsToKeep(files) - var builder strings.Builder - for _, file := range filesToDelete { - builder.WriteString("\n---\n" + file.Content) - } - resources, err := m.kubeClient.Build(strings.NewReader(builder.String()), false) - if err != nil { - return false, fmt.Errorf("failed to build resources from manifests: %w", err) - } - if resources == nil || len(resources) <= 0 { - return true, nil - } - for _, resource := range resources { - err = resource.Get() - if err != nil { - if apierrors.IsNotFound(err) { - continue // resource is already delete, check the next one. - } - return false, fmt.Errorf("failed to get resource: %w", err) - } - // found at least one resource that is not deleted so just delete everything again. - _, errs := m.kubeClient.Delete(resources) - if len(errs) > 0 { - return false, fmt.Errorf("failed to delete resources: %v", apiutilerrors.NewAggregate(errs)) - } - return false, nil - } - return true, nil -} diff --git a/internal/legacy/release/manager_factory.go b/internal/legacy/release/manager_factory.go deleted file mode 100644 index 75ec0492..00000000 --- a/internal/legacy/release/manager_factory.go +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package release - -import ( - "fmt" - - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/chart/loader" - "helm.sh/helm/v3/pkg/kube" - helmrelease "helm.sh/helm/v3/pkg/release" - "helm.sh/helm/v3/pkg/storage" - "helm.sh/helm/v3/pkg/storage/driver" - "helm.sh/helm/v3/pkg/strvals" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - v1 "k8s.io/client-go/kubernetes/typed/core/v1" - crmanager "sigs.k8s.io/controller-runtime/pkg/manager" - - "github.com/operator-framework/helm-operator-plugins/internal/legacy/helm/client" - "github.com/operator-framework/helm-operator-plugins/internal/legacy/helm/types" -) - -// ManagerFactory creates Managers that are specific to custom resources. It is -// used by the HelmOperatorReconciler during resource reconciliation, and it -// improves decoupling between reconciliation logic and the Helm backend -// components used to manage releases. -type ManagerFactory interface { - NewManager(r *unstructured.Unstructured, overrideValues map[string]string) (Manager, error) -} - -type managerFactory struct { - mgr crmanager.Manager - chartDir string -} - -// NewManagerFactory returns a new Helm manager factory capable of installing and uninstalling releases. -func NewManagerFactory(mgr crmanager.Manager, chartDir string) ManagerFactory { - return &managerFactory{mgr, chartDir} -} - -func (f managerFactory) NewManager(cr *unstructured.Unstructured, overrideValues map[string]string) (Manager, error) { - // Get both v2 and v3 storage backends - clientv1, err := v1.NewForConfig(f.mgr.GetConfig()) - if err != nil { - return nil, fmt.Errorf("failed to get core/v1 client: %w", err) - } - storageBackend := storage.Init(driver.NewSecrets(clientv1.Secrets(cr.GetNamespace()))) - - // Get the necessary clients and client getters. Use a client that injects the CR - // as an owner reference into all resources templated by the chart. - rcg, err := client.NewRESTClientGetter(f.mgr, cr.GetNamespace()) - if err != nil { - return nil, fmt.Errorf("failed to get REST client getter from manager: %w", err) - } - - kubeClient := kube.New(rcg) - restMapper := f.mgr.GetRESTMapper() - ownerRefClient, err := client.NewOwnerRefInjectingClient(*kubeClient, restMapper, cr) - if err != nil { - return nil, fmt.Errorf("failed to inject owner references: %w", err) - } - - crChart, err := loader.LoadDir(f.chartDir) - if err != nil { - return nil, fmt.Errorf("failed to load chart dir: %w", err) - } - - releaseName, err := getReleaseName(storageBackend, crChart.Name(), cr) - if err != nil { - return nil, fmt.Errorf("failed to get helm release name: %w", err) - } - - crValues, ok := cr.Object["spec"].(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("failed to get spec: expected map[string]interface{}") - } - - expOverrides, err := parseOverrides(overrideValues) - if err != nil { - return nil, fmt.Errorf("failed to parse override values: %w", err) - } - values := mergeMaps(crValues, expOverrides) - - actionConfig := &action.Configuration{ - RESTClientGetter: rcg, - Releases: storageBackend, - KubeClient: ownerRefClient, - Log: func(_ string, _ ...interface{}) {}, - } - - return &manager{ - actionConfig: actionConfig, - storageBackend: storageBackend, - kubeClient: ownerRefClient, - - releaseName: releaseName, - namespace: cr.GetNamespace(), - - chart: crChart, - values: values, - status: types.StatusFor(cr), - }, nil -} - -// getReleaseName returns a release name for the CR. -// -// getReleaseName searches for a release using the CR name. If a release -// cannot be found, or if it is found and was created by the chart managed -// by this manager, the CR name is returned. -// -// If a release is found but it was created by another chart, that means we -// have a release name collision, so return an error. This case is possible -// because Kubernetes allows instances of different types to have the same name -// in the same namespace. -// -// TODO(jlanford): As noted above, using the CR name as the release name raises -// the possibility of collision. We should move this logic to a validating -// admission webhook so that the CR owner receives immediate feedback of the -// collision. As is, the only indication of collision will be in the CR status -// and operator logs. -func getReleaseName(storageBackend *storage.Storage, crChartName string, - cr *unstructured.Unstructured) (string, error) { - // If a release with the CR name does not exist, return the CR name. - releaseName := cr.GetName() - history, exists, err := releaseHistory(storageBackend, releaseName) - if err != nil { - return "", err - } - if !exists { - return releaseName, nil - } - - // If a release name with the CR name exists, but the release's chart is - // different than the chart managed by this operator, return an error - // because something else created the existing release. - if history[0].Chart == nil { - return "", fmt.Errorf("could not find chart metadata in release with name %q", releaseName) - } - existingChartName := history[0].Chart.Name() - if existingChartName != crChartName { - return "", fmt.Errorf("duplicate release name: found existing release with name %q for chart %q", - releaseName, existingChartName) - } - - return releaseName, nil -} - -func releaseHistory(storageBackend *storage.Storage, releaseName string) ([]*helmrelease.Release, bool, error) { - releaseHistory, err := storageBackend.History(releaseName) - if err != nil { - if notFoundErr(err) { - return nil, false, nil - } - return nil, false, err - } - return releaseHistory, len(releaseHistory) > 0, nil -} - -func parseOverrides(in map[string]string) (map[string]interface{}, error) { - out := make(map[string]interface{}) - for k, v := range in { - val := fmt.Sprintf("%s=%s", k, v) - if err := strvals.ParseIntoString(val, out); err != nil { - return nil, err - } - } - return out, nil -} - -func mergeMaps(a, b map[string]interface{}) map[string]interface{} { - out := make(map[string]interface{}, len(a)) - for k, v := range a { - out[k] = v - } - for k, v := range b { - if v, ok := v.(map[string]interface{}); ok { - if bv, ok := out[k]; ok { - if bv, ok := bv.(map[string]interface{}); ok { - out[k] = mergeMaps(bv, v) - continue - } - } - } - out[k] = v - } - return out -} diff --git a/internal/legacy/release/manager_test.go b/internal/legacy/release/manager_test.go deleted file mode 100644 index 466bb693..00000000 --- a/internal/legacy/release/manager_test.go +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package release - -import ( - "testing" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - apitypes "k8s.io/apimachinery/pkg/types" - "k8s.io/cli-runtime/pkg/resource" - - appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/runtime" -) - -func newTestUnstructured(containers []interface{}) *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "kind": "MyResource", - "apiVersion": "myApi", - "metadata": map[string]interface{}{ - "name": "test", - "namespace": "ns", - }, - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ - "containers": containers, - }, - }, - }, - }, - } -} - -func newTestDeployment(containers []v1.Container) *appsv1.Deployment { - return &appsv1.Deployment{ - TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "apps/v1"}, - ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "ns"}, - Spec: appsv1.DeploymentSpec{ - Template: v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: containers, - }, - }, - }, - } -} - -func TestManagerGenerateStrategicMergePatch(t *testing.T) { - - tests := []struct { - o1 runtime.Object - o2 runtime.Object - patch string - patchType apitypes.PatchType - }{ - { - o1: newTestUnstructured([]interface{}{ - map[string]interface{}{ - "name": "test1", - }, - map[string]interface{}{ - "name": "test2", - }, - }), - o2: newTestUnstructured([]interface{}{ - map[string]interface{}{ - "name": "test1", - }, - }), - patch: ``, - patchType: apitypes.JSONPatchType, - }, - { - o1: newTestUnstructured([]interface{}{ - map[string]interface{}{ - "name": "test1", - }, - }), - o2: newTestUnstructured([]interface{}{ - map[string]interface{}{ - "name": "test1", - }, - map[string]interface{}{ - "name": "test2", - }, - }), - patch: `[{"op":"add","path":"/spec/template/spec/containers/1","value":{"name":"test2"}}]`, - patchType: apitypes.JSONPatchType, - }, - { - o1: newTestUnstructured([]interface{}{ - map[string]interface{}{ - "name": "test1", - }, - }), - o2: newTestUnstructured([]interface{}{ - map[string]interface{}{ - "name": "test1", - "test": nil, - }, - }), - patch: ``, - patchType: apitypes.JSONPatchType, - }, - { - o1: newTestUnstructured([]interface{}{ - map[string]interface{}{ - "name": "test1", - }, - }), - o2: newTestUnstructured([]interface{}{ - map[string]interface{}{ - "name": "test2", - }, - }), - patch: `[{"op":"replace","path":"/spec/template/spec/containers/0/name","value":"test2"}]`, - patchType: apitypes.JSONPatchType, - }, - { - o1: newTestDeployment([]v1.Container{ - {Name: "test1"}, - {Name: "test2"}, - }), - o2: newTestDeployment([]v1.Container{ - {Name: "test1"}, - }), - patch: `{"spec":{"template":{"spec":{"$setElementOrder/containers":[{"name":"test1"}]}}}}`, - patchType: apitypes.StrategicMergePatchType, - }, - { - o1: newTestDeployment([]v1.Container{ - {Name: "test1"}, - }), - o2: newTestDeployment([]v1.Container{ - {Name: "test1"}, - {Name: "test2"}, - }), - patch: `{"spec":{"template":{"spec":{"$setElementOrder/containers":[{"name":"test1"},{"name":"test2"}],"containers":[{"name":"test2","resources":{}}]}}}}`, - patchType: apitypes.StrategicMergePatchType, - }, - { - o1: newTestDeployment([]v1.Container{ - {Name: "test1"}, - }), - o2: newTestDeployment([]v1.Container{ - {Name: "test1", LivenessProbe: nil}, - }), - patch: ``, - patchType: apitypes.StrategicMergePatchType, - }, - { - o1: newTestDeployment([]v1.Container{ - {Name: "test1"}, - }), - o2: newTestDeployment([]v1.Container{ - {Name: "test2"}, - }), - patch: `{"spec":{"template":{"spec":{"$setElementOrder/containers":[{"name":"test2"}],"containers":[{"name":"test2","resources":{}}]}}}}`, - patchType: apitypes.StrategicMergePatchType, - }, - { - o1: &appsv1.Deployment{ - TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "apps/v1"}, - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "ns", - Annotations: map[string]string{ - "testannotation": "testvalue", - }, - }, - Spec: appsv1.DeploymentSpec{}, - }, - o2: &appsv1.Deployment{ - TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "apps/v1"}, - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "ns", - }, - Spec: appsv1.DeploymentSpec{}, - }, - patch: ``, - patchType: apitypes.StrategicMergePatchType, - }, - } - - for _, test := range tests { - - o2Info := &resource.Info{ - Object: test.o2, - } - - diff, patchType, err := createPatch(test.o1, o2Info) - assert.NoError(t, err) - assert.Equal(t, test.patchType, patchType) - assert.Equal(t, test.patch, string(diff)) - } -} diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index 533cfff5..92beb701 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "reflect" "strings" "sync" "time" @@ -42,6 +43,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" + ctrlpredicate "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/source" "github.com/operator-framework/helm-operator-plugins/pkg/annotation" @@ -69,6 +71,7 @@ type Reconciler struct { log logr.Logger gvk *schema.GroupVersionKind chrt *chart.Chart + selector metav1.LabelSelector overrideValues map[string]string skipDependentWatches bool maxConcurrentReconciles int @@ -384,6 +387,15 @@ func WithValueMapper(m values.Mapper) Option { } } +// WithSelector is an Option that configures the reconciler to creates a +// predicate that is used to filter resources based on the specified selector +func WithSelector(s metav1.LabelSelector) Option { + return func(r *Reconciler) error { + r.selector = s + return nil + } +} + // Reconcile reconciles a CR that defines a Helm v3 release. // // - If a release does not exist for this CR, a new release is installed. @@ -774,9 +786,21 @@ func (r *Reconciler) setupScheme(mgr ctrl.Manager) { func (r *Reconciler) setupWatches(mgr ctrl.Manager, c controller.Controller) error { obj := &unstructured.Unstructured{} obj.SetGroupVersionKind(*r.gvk) + + var preds []ctrlpredicate.Predicate + p, err := parsePredicateSelector(r.selector) + if err != nil { + return err + } + + if p != nil { + preds = append(preds, p) + } + if err := c.Watch( &source.Kind{Type: obj}, &sdkhandler.InstrumentedEnqueueRequestForObject{}, + preds..., ); err != nil { return err } @@ -804,6 +828,20 @@ func (r *Reconciler) setupWatches(mgr ctrl.Manager, c controller.Controller) err return nil } +// parsePredicateSelector parses the selector in the WatchOptions and creates a predicate +// that is used to filter resources based on the specified selector +func parsePredicateSelector(selector metav1.LabelSelector) (ctrlpredicate.Predicate, error) { + // If a selector has been specified in watches.yaml, add it to the watch's predicates. + if !reflect.ValueOf(selector).IsZero() { + p, err := ctrlpredicate.LabelSelectorPredicate(selector) + if err != nil { + return nil, fmt.Errorf("error constructing predicate from watches selector: %v", err) + } + return p, nil + } + return nil, nil +} + func ensureDeployedRelease(u *updater.Updater, rel *release.Release) { reason := conditions.ReasonInstallSuccessful message := "release was successfully installed" diff --git a/pkg/reconciler/reconciler_test.go b/pkg/reconciler/reconciler_test.go index 290a5f39..4b1f3ed1 100644 --- a/pkg/reconciler/reconciler_test.go +++ b/pkg/reconciler/reconciler_test.go @@ -1171,6 +1171,27 @@ var _ = Describe("Reconciler", func() { }) }) }) + + var _ = Describe("Test predicate selector", func() { + It("verifying when a valid selector is passed", func() { + selectorPass := metav1.LabelSelector{ + MatchLabels: map[string]string{ + "testKey": "testValue", + }, + } + + passPredicate, err := parsePredicateSelector(selectorPass) + Expect(err).NotTo(HaveOccurred()) + Expect(passPredicate).NotTo(BeNil()) + }) + + It("verifying there is no error when no predicate is passed", func() { + noSelector := metav1.LabelSelector{} + nilPredicate, err := parsePredicateSelector(noSelector) + Expect(err).NotTo(HaveOccurred()) + Expect(nilPredicate).To(BeNil()) + }) + }) }) func getManagerOrFail() manager.Manager {