diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go index b582be6f3..e39ea908f 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go @@ -269,7 +269,18 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } r.stateKeep(logCtx, handler, app) - if err := garbageCollection(logCtx, handler); err != nil { + + opts := []resourcekeeper.GCOption{ + resourcekeeper.AppRevisionLimitGCOption(r.appRevisionLimit), + } + if DisableAllApplicationRevision { + opts = append(opts, resourcekeeper.DisableApplicationRevisionGCOption{}) + } + if DisableAllComponentRevision { + opts = append(opts, resourcekeeper.DisableGCComponentRevisionOption{}) + } + + if _, _, err := handler.resourceKeeper.GarbageCollect(logCtx, opts...); err != nil { logCtx.Error(err, "Failed to run garbage collection") r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedGC, err)) return r.endWithNegativeCondition(logCtx, app, condition.ReconcileError(err), phase) @@ -311,10 +322,24 @@ func (r *Reconciler) gcResourceTrackers(logCtx monitorContext.Context, handler * statusUpdater = r.updateStatus } - var options []resourcekeeper.GCOption - if !gcOutdated { - options = append(options, resourcekeeper.DisableMarkStageGCOption{}, resourcekeeper.DisableGCComponentRevisionOption{}, resourcekeeper.DisableLegacyGCOption{}) + options := []resourcekeeper.GCOption{ + resourcekeeper.AppRevisionLimitGCOption(r.appRevisionLimit), } + if DisableAllApplicationRevision { + options = append(options, resourcekeeper.DisableApplicationRevisionGCOption{}) + } + if DisableAllComponentRevision { + options = append(options, resourcekeeper.DisableGCComponentRevisionOption{}) + } + if !gcOutdated { + options = append(options, + resourcekeeper.DisableMarkStageGCOption{}, + resourcekeeper.DisableGCComponentRevisionOption{}, + resourcekeeper.DisableLegacyGCOption{}, + resourcekeeper.DisableApplicationRevisionGCOption{}, + ) + } + finished, waiting, err := handler.resourceKeeper.GarbageCollect(resourcekeeper.WithPhase(logCtx, phase), options...) if err != nil { logCtx.Error(err, "Failed to gc resourcetrackers") diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go index 3f939ca77..8b3c19193 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go @@ -19,7 +19,6 @@ package application import ( "context" "sync" - "time" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -406,28 +405,6 @@ func generateScopeReference(scopes []appfile.Scope) []corev1.ObjectReference { return references } -type garbageCollectFunc func(ctx context.Context, h *AppHandler) error - -// execute garbage collection functions, including: -// - clean up legacy app revisions -// - clean up legacy component revisions -func garbageCollection(ctx context.Context, h *AppHandler) error { - t := time.Now() - defer func() { - metrics.AppReconcileStageDurationHistogram.WithLabelValues("gc-rev").Observe(time.Since(t).Seconds()) - }() - collectFuncs := []garbageCollectFunc{ - garbageCollectFunc(cleanUpApplicationRevision), - garbageCollectFunc(cleanUpWorkflowComponentRevision), - } - for _, collectFunc := range collectFuncs { - if err := collectFunc(ctx, h); err != nil { - return err - } - } - return nil -} - // ApplyPolicies will render policies into manifests from appfile and dispatch them func (h *AppHandler) ApplyPolicies(ctx context.Context, af *appfile.Appfile) error { if ctx, ok := ctx.(monitorContext.Context); ok { diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/revision.go b/pkg/controller/core.oam.dev/v1alpha2/application/revision.go index 2f978d075..b428b5ab3 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/revision.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/revision.go @@ -22,7 +22,6 @@ import ( "reflect" "sort" "strings" - "time" "github.com/hashicorp/go-version" "github.com/kubevela/pkg/util/k8s" @@ -901,65 +900,6 @@ func (h *AppHandler) UpdateAppLatestRevisionStatus(ctx context.Context) error { return nil } -func getApplicationRevisionLimitForApp(app *v1beta1.Application, fallback int) int { - for _, p := range app.Spec.Policies { - if p.Type == v1alpha1.GarbageCollectPolicyType && p.Properties != nil && p.Properties.Raw != nil { - prop := &v1alpha1.GarbageCollectPolicySpec{} - if err := json.Unmarshal(p.Properties.Raw, prop); err == nil && prop.ApplicationRevisionLimit != nil && *prop.ApplicationRevisionLimit >= 0 { - return *prop.ApplicationRevisionLimit - } - } - } - return fallback -} - -// cleanUpApplicationRevision check all appRevisions of the application, remove them if the number of them exceed the limit -func cleanUpApplicationRevision(ctx context.Context, h *AppHandler) error { - if DisableAllApplicationRevision { - return nil - } - t := time.Now() - defer func() { - metrics.AppReconcileStageDurationHistogram.WithLabelValues("gc-rev.apprev").Observe(time.Since(t).Seconds()) - }() - sortedRevision, err := GetSortedAppRevisions(ctx, h.r.Client, h.app.Name, h.app.Namespace) - if err != nil { - return err - } - appRevisionInUse := gatherUsingAppRevision(h) - appRevisionLimit := getApplicationRevisionLimitForApp(h.app, h.r.appRevisionLimit) - needKill := len(sortedRevision) - appRevisionLimit - len(appRevisionInUse) - if needKill <= 0 { - return nil - } - klog.InfoS("Going to garbage collect app revisions", "limit", appRevisionLimit, - "total", len(sortedRevision), "using", len(appRevisionInUse), "kill", needKill) - - for _, rev := range sortedRevision { - if needKill <= 0 { - break - } - // don't delete app revision in use - if appRevisionInUse[rev.Name] { - continue - } - if err := h.r.Delete(ctx, rev.DeepCopy()); err != nil && !apierrors.IsNotFound(err) { - return err - } - needKill-- - } - return nil -} - -// gatherUsingAppRevision get all using appRevisions include app's status pointing to -func gatherUsingAppRevision(h *AppHandler) map[string]bool { - usingRevision := map[string]bool{} - if h.app.Status.LatestRevision != nil && len(h.app.Status.LatestRevision.Name) != 0 { - usingRevision[h.app.Status.LatestRevision.Name] = true - } - return usingRevision -} - func replaceComponentRevisionContext(u *unstructured.Unstructured, compRevName string) error { str := string(util.JSONMarshal(u)) if strings.Contains(str, process.ComponentRevisionPlaceHolder) { @@ -971,83 +911,6 @@ func replaceComponentRevisionContext(u *unstructured.Unstructured, compRevName s return nil } -func cleanUpWorkflowComponentRevision(ctx context.Context, h *AppHandler) error { - if DisableAllComponentRevision { - return nil - } - t := time.Now() - defer func() { - metrics.AppReconcileStageDurationHistogram.WithLabelValues("gc-rev.comprev").Observe(time.Since(t).Seconds()) - }() - // collect component revision in use - compRevisionInUse := map[string]map[string]struct{}{} - ctx = auth.ContextWithUserInfo(ctx, h.app) - for i, resource := range h.app.Status.AppliedResources { - compName := resource.Name - ns := resource.Namespace - r := &unstructured.Unstructured{} - r.GetObjectKind().SetGroupVersionKind(resource.GroupVersionKind()) - _ctx := multicluster.ContextWithClusterName(ctx, resource.Cluster) - err := h.r.Get(_ctx, ktypes.NamespacedName{Name: compName, Namespace: ns}, r) - notFound := apierrors.IsNotFound(err) - if err != nil && !notFound { - return errors.WithMessagef(err, "get applied resource index=%d", i) - } - if compRevisionInUse[compName] == nil { - compRevisionInUse[compName] = map[string]struct{}{} - } - if notFound { - continue - } - compRevision, ok := r.GetLabels()[oam.LabelAppComponentRevision] - if ok { - compRevisionInUse[compName][compRevision] = struct{}{} - } - } - - for _, curComp := range h.app.Status.AppliedResources { - crList := &appsv1.ControllerRevisionList{} - listOpts := []client.ListOption{client.MatchingLabels{ - oam.LabelControllerRevisionComponent: pkgutils.EscapeResourceNameToLabelValue(curComp.Name), - }, client.InNamespace(h.getComponentRevisionNamespace(ctx))} - _ctx := multicluster.ContextWithClusterName(ctx, curComp.Cluster) - if err := h.r.List(_ctx, crList, listOpts...); err != nil { - return err - } - needKill := len(crList.Items) - h.r.appRevisionLimit - len(compRevisionInUse[curComp.Name]) - if needKill < 1 { - continue - } - sortedRevision := crList.Items - sort.Sort(historiesByComponentRevision(sortedRevision)) - for _, rev := range sortedRevision { - if needKill <= 0 { - break - } - if _, inUse := compRevisionInUse[curComp.Name][rev.Name]; inUse { - continue - } - _rev := rev.DeepCopy() - oam.SetCluster(_rev, curComp.Cluster) - if err := h.resourceKeeper.DeleteComponentRevision(_ctx, _rev); err != nil { - return err - } - needKill-- - } - } - return nil -} - -type historiesByComponentRevision []appsv1.ControllerRevision - -func (h historiesByComponentRevision) Len() int { return len(h) } -func (h historiesByComponentRevision) Swap(i, j int) { h[i], h[j] = h[j], h[i] } -func (h historiesByComponentRevision) Less(i, j int) bool { - ir, _ := util.ExtractRevisionNum(h[i].Name, "-") - ij, _ := util.ExtractRevisionNum(h[j].Name, "-") - return ir < ij -} - // UpdateApplicationRevisionStatus update application revision status func (h *AppHandler) UpdateApplicationRevisionStatus(ctx context.Context, appRev *v1beta1.ApplicationRevision, wfStatus *common.WorkflowStatus) { if appRev == nil || DisableAllApplicationRevision { diff --git a/pkg/resourcekeeper/gc.go b/pkg/resourcekeeper/gc.go index d11853a4a..05dd25e7c 100644 --- a/pkg/resourcekeeper/gc.go +++ b/pkg/resourcekeeper/gc.go @@ -62,13 +62,16 @@ type GCOption interface { type gcConfig struct { passive bool - disableMark bool - disableSweep bool - disableFinalize bool - disableComponentRevisionGC bool - disableLegacyGC bool + disableMark bool + disableSweep bool + disableFinalize bool + disableComponentRevisionGC bool + disableLegacyGC bool + disableApplicationRevisionGC bool order v1alpha1.GarbageCollectOrder + + appRevisionLimit int } func newGCConfig(options ...GCOption) *gcConfig { @@ -130,7 +133,10 @@ func (h *resourceKeeper) buildGCConfig(ctx context.Context, options ...GCOption) } func (h *resourceKeeper) garbageCollect(ctx context.Context, cfg *gcConfig) (finished bool, waiting []v1beta1.ManagedResource, err error) { - gc := gcHandler{resourceKeeper: h, cfg: cfg} + gc := gcHandler{ + resourceKeeper: h, + cfg: cfg, + } gc.Init() // Mark Stage if !cfg.disableMark { @@ -162,6 +168,13 @@ func (h *resourceKeeper) garbageCollect(ctx context.Context, cfg *gcConfig) (fin return false, waiting, errors.Wrapf(err, "failed to garbage collect legacy resource trackers") } } + + if !cfg.disableApplicationRevisionGC { + if err = gc.GarbageCollectApplicationRevision(ctx); err != nil { + return false, waiting, errors.Wrapf(err, "failed to garbage collect application revision") + } + } + return finished, waiting, nil } diff --git a/pkg/resourcekeeper/gc_rev.go b/pkg/resourcekeeper/gc_rev.go new file mode 100644 index 000000000..b69e3f3c2 --- /dev/null +++ b/pkg/resourcekeeper/gc_rev.go @@ -0,0 +1,236 @@ +/* +Copyright 2021 The KubeVela 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 resourcekeeper + +import ( + "context" + "encoding/json" + "sort" + "time" + + "github.com/pkg/errors" + appsv1 "k8s.io/api/apps/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + ktypes "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/auth" + "github.com/oam-dev/kubevela/pkg/cache" + "github.com/oam-dev/kubevela/pkg/monitor/metrics" + "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/utils" +) + +// execute garbage collection functions, including: +// - clean up legacy app revisions +// - clean up legacy component revisions +func (h *gcHandler) GarbageCollectApplicationRevision(ctx context.Context) error { + t := time.Now() + defer func() { + metrics.AppReconcileStageDurationHistogram.WithLabelValues("gc-app-rev").Observe(time.Since(t).Seconds()) + }() + collectFuncs := []garbageCollectFunc{ + garbageCollectFunc(cleanUpApplicationRevision), + garbageCollectFunc(cleanUpWorkflowComponentRevision), + } + for _, collectFunc := range collectFuncs { + if err := collectFunc(ctx, h); err != nil { + return err + } + } + return nil +} + +type garbageCollectFunc func(ctx context.Context, h *gcHandler) error + +// cleanUpApplicationRevision check all appRevisions of the application, remove them if the number of them exceed the limit +func cleanUpApplicationRevision(ctx context.Context, h *gcHandler) error { + if h.cfg.disableApplicationRevisionGC { + return nil + } + t := time.Now() + defer func() { + metrics.AppReconcileStageDurationHistogram.WithLabelValues("gc-rev.apprev").Observe(time.Since(t).Seconds()) + }() + sortedRevision, err := getSortedAppRevisions(ctx, h.Client, h.app.Name, h.app.Namespace) + if err != nil { + return err + } + appRevisionInUse := gatherUsingAppRevision(h.app) + appRevisionLimit := getApplicationRevisionLimitForApp(h.app, h.cfg.appRevisionLimit) + needKill := len(sortedRevision) - appRevisionLimit - len(appRevisionInUse) + if needKill <= 0 { + return nil + } + klog.InfoS("Going to garbage collect app revisions", "limit", h.cfg.appRevisionLimit, + "total", len(sortedRevision), "using", len(appRevisionInUse), "kill", needKill) + + for _, rev := range sortedRevision { + if needKill <= 0 { + break + } + // don't delete app revision in use + if appRevisionInUse[rev.Name] { + continue + } + if err := h.Client.Delete(ctx, rev.DeepCopy()); err != nil && !kerrors.IsNotFound(err) { + return err + } + needKill-- + } + return nil +} + +func cleanUpWorkflowComponentRevision(ctx context.Context, h *gcHandler) error { + if h.cfg.disableComponentRevisionGC { + return nil + } + t := time.Now() + defer func() { + metrics.AppReconcileStageDurationHistogram.WithLabelValues("gc-rev.comprev").Observe(time.Since(t).Seconds()) + }() + // collect component revision in use + compRevisionInUse := map[string]map[string]struct{}{} + ctx = auth.ContextWithUserInfo(ctx, h.app) + for i, resource := range h.app.Status.AppliedResources { + compName := resource.Name + ns := resource.Namespace + r := &unstructured.Unstructured{} + r.GetObjectKind().SetGroupVersionKind(resource.GroupVersionKind()) + _ctx := multicluster.ContextWithClusterName(ctx, resource.Cluster) + err := h.Client.Get(_ctx, ktypes.NamespacedName{Name: compName, Namespace: ns}, r) + notFound := kerrors.IsNotFound(err) + if err != nil && !notFound { + return errors.WithMessagef(err, "get applied resource index=%d", i) + } + if compRevisionInUse[compName] == nil { + compRevisionInUse[compName] = map[string]struct{}{} + } + if notFound { + continue + } + compRevision, ok := r.GetLabels()[oam.LabelAppComponentRevision] + if ok { + compRevisionInUse[compName][compRevision] = struct{}{} + } + } + + for _, curComp := range h.app.Status.AppliedResources { + crList := &appsv1.ControllerRevisionList{} + listOpts := []client.ListOption{client.MatchingLabels{ + oam.LabelControllerRevisionComponent: utils.EscapeResourceNameToLabelValue(curComp.Name), + }, client.InNamespace(h.getComponentRevisionNamespace(ctx))} + _ctx := multicluster.ContextWithClusterName(ctx, curComp.Cluster) + if err := h.Client.List(_ctx, crList, listOpts...); err != nil { + return err + } + needKill := len(crList.Items) - h.cfg.appRevisionLimit - len(compRevisionInUse[curComp.Name]) + if needKill < 1 { + continue + } + sortedRevision := crList.Items + sort.Sort(historiesByComponentRevision(sortedRevision)) + for _, rev := range sortedRevision { + if needKill <= 0 { + break + } + if _, inUse := compRevisionInUse[curComp.Name][rev.Name]; inUse { + continue + } + _rev := rev.DeepCopy() + oam.SetCluster(_rev, curComp.Cluster) + if err := h.resourceKeeper.DeleteComponentRevision(_ctx, _rev); err != nil { + return err + } + needKill-- + } + } + return nil +} + +// gatherUsingAppRevision get all using appRevisions include app's status pointing to +func gatherUsingAppRevision(app *v1beta1.Application) map[string]bool { + usingRevision := map[string]bool{} + if app.Status.LatestRevision != nil && len(app.Status.LatestRevision.Name) != 0 { + usingRevision[app.Status.LatestRevision.Name] = true + } + return usingRevision +} + +func getApplicationRevisionLimitForApp(app *v1beta1.Application, fallback int) int { + for _, p := range app.Spec.Policies { + if p.Type == v1alpha1.GarbageCollectPolicyType && p.Properties != nil && p.Properties.Raw != nil { + prop := &v1alpha1.GarbageCollectPolicySpec{} + if err := json.Unmarshal(p.Properties.Raw, prop); err == nil && prop.ApplicationRevisionLimit != nil && *prop.ApplicationRevisionLimit >= 0 { + return *prop.ApplicationRevisionLimit + } + } + } + return fallback +} + +// getSortedAppRevisions get application revisions by revision number +func getSortedAppRevisions(ctx context.Context, cli client.Client, appName string, appNs string) ([]v1beta1.ApplicationRevision, error) { + revs, err := getAppRevisions(ctx, cli, appName, appNs) + if err != nil { + return nil, err + } + sort.Slice(revs, func(i, j int) bool { + ir, _ := util.ExtractRevisionNum(revs[i].Name, "-") + ij, _ := util.ExtractRevisionNum(revs[j].Name, "-") + return ir < ij + }) + return revs, nil +} + +// getAppRevisions get application revisions by label +func getAppRevisions(ctx context.Context, cli client.Client, appName string, appNs string) ([]v1beta1.ApplicationRevision, error) { + appRevisionList := new(v1beta1.ApplicationRevisionList) + var err error + if cache.OptimizeListOp { + err = cli.List(ctx, appRevisionList, client.MatchingFields{cache.AppIndex: appNs + "/" + appName}) + } else { + err = cli.List(ctx, appRevisionList, client.InNamespace(appNs), client.MatchingLabels{oam.LabelAppName: appName}) + } + if err != nil { + return nil, err + } + return appRevisionList.Items, nil +} + +func (h *gcHandler) getComponentRevisionNamespace(ctx context.Context) string { + if ns, ok := ctx.Value(0).(string); ok && ns != "" { + return ns + } + return h.app.Namespace +} + +type historiesByComponentRevision []appsv1.ControllerRevision + +func (h historiesByComponentRevision) Len() int { return len(h) } +func (h historiesByComponentRevision) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +func (h historiesByComponentRevision) Less(i, j int) bool { + ir, _ := util.ExtractRevisionNum(h[i].Name, "-") + ij, _ := util.ExtractRevisionNum(h[j].Name, "-") + return ir < ij +} diff --git a/pkg/resourcekeeper/gc_rev_test.go b/pkg/resourcekeeper/gc_rev_test.go new file mode 100644 index 000000000..5c9c99c51 --- /dev/null +++ b/pkg/resourcekeeper/gc_rev_test.go @@ -0,0 +1,471 @@ +/* +Copyright 2021 The KubeVela 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 resourcekeeper + +import ( + "context" + "testing" + + "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/pkg/errors" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + + apicommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/oam" +) + +func Test_gcHandler_GarbageCollectApplicationRevision(t *testing.T) { + type fields struct { + resourceKeeper *resourceKeeper + cfg *gcConfig + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "cleanUpApplicationRevision and cleanUpWorkflowComponentRevision success", + fields: fields{ + resourceKeeper: &resourceKeeper{ + Client: test.NewMockClient(), + app: &v1beta1.Application{}, + }, + cfg: &gcConfig{ + disableApplicationRevisionGC: false, + disableComponentRevisionGC: false, + }, + }, + }, + { + name: "failed", + fields: fields{ + resourceKeeper: &resourceKeeper{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(errors.New("mock")), + MockList: test.NewMockListFn(errors.New("mock")), + MockCreate: test.NewMockCreateFn(errors.New("mock")), + MockDelete: test.NewMockDeleteFn(errors.New("mock")), + MockDeleteAllOf: test.NewMockDeleteAllOfFn(errors.New("mock")), + MockUpdate: test.NewMockUpdateFn(errors.New("mock")), + MockPatch: test.NewMockPatchFn(errors.New("mock")), + }, + app: &v1beta1.Application{}, + }, + cfg: &gcConfig{ + disableApplicationRevisionGC: false, + disableComponentRevisionGC: false, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := &gcHandler{ + resourceKeeper: tt.fields.resourceKeeper, + cfg: tt.fields.cfg, + } + if err := h.GarbageCollectApplicationRevision(context.Background()); (err != nil) != tt.wantErr { + t.Errorf("gcHandler.GarbageCollectApplicationRevision() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_cleanUpApplicationRevision(t *testing.T) { + type args struct { + h *gcHandler + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "clean up app-v2", + args: args{ + h: &gcHandler{ + resourceKeeper: &resourceKeeper{ + Client: &test.MockClient{ + MockList: func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + l, _ := list.(*v1beta1.ApplicationRevisionList) + l.Items = []v1beta1.ApplicationRevision{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-v1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-v2", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-v3", + }, + }, + } + return nil + }, + MockDelete: test.NewMockDeleteFn(nil), + }, + app: &v1beta1.Application{ + Status: apicommon.AppStatus{ + LatestRevision: &apicommon.Revision{ + Name: "app-v1", + }, + }, + }, + }, + cfg: &gcConfig{ + disableApplicationRevisionGC: false, + appRevisionLimit: 1, + }, + }, + }, + }, + { + name: "disabled", + args: args{ + h: &gcHandler{ + cfg: &gcConfig{ + disableApplicationRevisionGC: true, + }, + }, + }, + }, + { + name: "list failed", + args: args{ + h: &gcHandler{ + resourceKeeper: &resourceKeeper{ + Client: &test.MockClient{ + MockList: test.NewMockListFn(errors.New("mock")), + }, + app: &v1beta1.Application{}, + }, + cfg: &gcConfig{ + disableApplicationRevisionGC: false, + appRevisionLimit: 1, + }, + }, + }, + wantErr: true, + }, + { + name: "delete failed", + args: args{ + h: &gcHandler{ + resourceKeeper: &resourceKeeper{ + Client: &test.MockClient{ + MockList: func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + l, _ := list.(*v1beta1.ApplicationRevisionList) + l.Items = []v1beta1.ApplicationRevision{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-v1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-v2", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-v3", + }, + }, + } + return nil + }, + MockDelete: test.NewMockDeleteFn(errors.New("mock")), + }, + app: &v1beta1.Application{ + Status: apicommon.AppStatus{ + LatestRevision: &apicommon.Revision{ + Name: "app-v1", + }, + }, + }, + }, + cfg: &gcConfig{ + disableApplicationRevisionGC: false, + appRevisionLimit: 1, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := cleanUpApplicationRevision(context.Background(), tt.args.h); (err != nil) != tt.wantErr { + t.Errorf("cleanUpApplicationRevision() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_cleanUpWorkflowComponentRevision(t *testing.T) { + type args struct { + h *gcHandler + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "clean up found revisions", + args: args{ + h: &gcHandler{ + resourceKeeper: &resourceKeeper{ + _crRT: &v1beta1.ResourceTracker{}, + Client: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + if key.Name == "revision3" { + return kerrors.NewNotFound(schema.GroupResource{}, "") + } + o, _ := obj.(*unstructured.Unstructured) + o.SetLabels(map[string]string{ + oam.LabelAppComponentRevision: "revision1", + }) + return nil + }, + MockDelete: test.NewMockDeleteFn(nil), + MockUpdate: test.NewMockUpdateFn(nil), + MockList: func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + l, _ := list.(*appsv1.ControllerRevisionList) + l.Items = []appsv1.ControllerRevision{ + { + ObjectMeta: metav1.ObjectMeta{Name: "revision1", Namespace: "default"}, + Revision: 1, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "revision2", Namespace: "default"}, + Revision: 2, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "revision3", Namespace: "default"}, + Revision: 3, + }, + } + return nil + }, + }, + app: &v1beta1.Application{ + Status: apicommon.AppStatus{ + AppliedResources: []apicommon.ClusterObjectReference{ + { + ObjectReference: corev1.ObjectReference{ + Namespace: "default", + Name: "revision1", + APIVersion: appsv1.SchemeGroupVersion.String(), + Kind: "Deployment", + }, + }, + { + ObjectReference: corev1.ObjectReference{ + Namespace: "default", + Name: "revision3", + APIVersion: appsv1.SchemeGroupVersion.String(), + Kind: "Deployment", + }, + }, + }, + }, + ObjectMeta: metav1.ObjectMeta{}}}, + cfg: &gcConfig{ + disableComponentRevisionGC: false, + appRevisionLimit: 1, + }, + }, + }, + }, + { + name: "no need clean up", + args: args{ + h: &gcHandler{ + resourceKeeper: &resourceKeeper{ + _crRT: &v1beta1.ResourceTracker{}, + Client: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + o, _ := obj.(*unstructured.Unstructured) + o.SetLabels(map[string]string{ + oam.LabelAppComponentRevision: "revision1", + }) + return nil + }, + MockDelete: test.NewMockDeleteFn(nil), + MockUpdate: test.NewMockUpdateFn(nil), + MockList: func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + l, _ := list.(*appsv1.ControllerRevisionList) + l.Items = []appsv1.ControllerRevision{ + { + ObjectMeta: metav1.ObjectMeta{Name: "revision1", Namespace: "default"}, + Revision: 1, + }, + } + return nil + }, + }, + app: &v1beta1.Application{ + Status: apicommon.AppStatus{ + AppliedResources: []apicommon.ClusterObjectReference{ + {}, + }, + }, + ObjectMeta: metav1.ObjectMeta{}}}, + cfg: &gcConfig{ + disableComponentRevisionGC: false, + appRevisionLimit: 1, + }, + }, + }, + }, + { + name: "disabled", + args: args{ + h: &gcHandler{ + cfg: &gcConfig{ + disableComponentRevisionGC: true, + }, + }, + }, + }, + { + name: "get failed", + args: args{ + h: &gcHandler{ + resourceKeeper: &resourceKeeper{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(errors.New("mock")), + }, + app: &v1beta1.Application{ + Status: apicommon.AppStatus{ + AppliedResources: []apicommon.ClusterObjectReference{ + {}, + }, + }, + ObjectMeta: metav1.ObjectMeta{}}}, + cfg: &gcConfig{ + disableComponentRevisionGC: false, + appRevisionLimit: 1, + }, + }, + }, + wantErr: true, + }, + { + name: "list failed", + args: args{ + h: &gcHandler{ + resourceKeeper: &resourceKeeper{ + Client: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + o, _ := obj.(*unstructured.Unstructured) + o.SetLabels(map[string]string{ + oam.LabelAppComponentRevision: "revision1", + }) + return nil + }, + MockList: test.NewMockListFn(errors.New("mock")), + }, + app: &v1beta1.Application{ + Status: apicommon.AppStatus{ + AppliedResources: []apicommon.ClusterObjectReference{ + {}, + }, + }, + ObjectMeta: metav1.ObjectMeta{}}}, + cfg: &gcConfig{ + disableComponentRevisionGC: false, + appRevisionLimit: 1, + }, + }, + }, + wantErr: true, + }, + { + name: "deleteComponentRevision failed", + args: args{ + h: &gcHandler{ + resourceKeeper: &resourceKeeper{ + _crRT: &v1beta1.ResourceTracker{}, + Client: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + o, _ := obj.(*unstructured.Unstructured) + o.SetLabels(map[string]string{ + oam.LabelAppComponentRevision: "revision1", + }) + return nil + }, + MockDelete: test.NewMockDeleteFn(errors.New("mock")), + MockUpdate: test.NewMockUpdateFn(nil), + MockList: func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + l, _ := list.(*appsv1.ControllerRevisionList) + l.Items = []appsv1.ControllerRevision{ + { + ObjectMeta: metav1.ObjectMeta{Name: "revision1", Namespace: "default"}, + Revision: 1, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "revision2", Namespace: "default"}, + Revision: 2, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "revisio3", Namespace: "default"}, + Revision: 3, + }, + } + return nil + }, + }, + app: &v1beta1.Application{ + Status: apicommon.AppStatus{ + AppliedResources: []apicommon.ClusterObjectReference{ + {}, + }, + }, + ObjectMeta: metav1.ObjectMeta{}}}, + cfg: &gcConfig{ + disableComponentRevisionGC: false, + appRevisionLimit: 1, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := cleanUpWorkflowComponentRevision(context.Background(), tt.args.h); (err != nil) != tt.wantErr { + t.Errorf("cleanUpWorkflowComponentRevision() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/resourcekeeper/options.go b/pkg/resourcekeeper/options.go index 3381106ba..ccba2ea72 100644 --- a/pkg/resourcekeeper/options.go +++ b/pkg/resourcekeeper/options.go @@ -98,6 +98,22 @@ func (option DisableLegacyGCOption) ApplyToGCConfig(cfg *gcConfig) { cfg.disableLegacyGC = true } +// DisableApplicationRevisionGCOption disable garbage collect application revision resourcetrackers +type DisableApplicationRevisionGCOption struct{} + +// ApplyToGCConfig apply change to gc config +func (option DisableApplicationRevisionGCOption) ApplyToGCConfig(cfg *gcConfig) { + cfg.disableApplicationRevisionGC = true +} + +// AppRevisionLimitGCOption is the maximum number of application revisions that will be maintained +type AppRevisionLimitGCOption int + +// ApplyToGCConfig apply change to gc config +func (option AppRevisionLimitGCOption) ApplyToGCConfig(cfg *gcConfig) { + cfg.appRevisionLimit = int(option) +} + // GarbageCollectStrategyOption apply garbage collect strategy to resourcetracker recording type GarbageCollectStrategyOption v1alpha1.GarbageCollectStrategy