mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 03:56:36 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user