Feat: application_controller should be responsible for deleting application revision (#5739)

* [Feature] application_controller should be responsible for deleting application revision

Signed-off-by: dhanu <andreasdhanu@gmail.com>

* test

Signed-off-by: dhanu <andreasdhanu@gmail.com>

* delete-app-revision: fix reviews, add tests

Signed-off-by: dhanu <andreasdhanu@gmail.com>

* delete-app-revision: rebase master

Signed-off-by: dhanu <andreasdhanu@gmail.com>

* delete-app-revision: make func private

Signed-off-by: dhanu <andreasdhanu@gmail.com>

* delete-app-revision clean

Signed-off-by: dhanu <andreasdhanu@gmail.com>

* delete-app-revision: fix staticcheck

Signed-off-by: dhanu <andreasdhanu@gmail.com>

* delete-app-revision: fix review

Signed-off-by: dhanu <andreasdhanu@gmail.com>

---------

Signed-off-by: dhanu <andreasdhanu@gmail.com>
This commit is contained in:
Dhanu Saputra
2023-05-17 17:41:01 +08:00
committed by GitHub
parent 530d7c5bd6
commit 208a4aed2d
7 changed files with 771 additions and 170 deletions
@@ -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")
@@ -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 {
@@ -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 {
+19 -6
View File
@@ -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
}
+236
View File
@@ -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
}
+471
View File
@@ -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)
}
})
}
}
+16
View File
@@ -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