github.com/oam-dev/kubevela@v1.9.11/pkg/resourcekeeper/gc_rev.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package resourcekeeper
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"sort"
    23  	"time"
    24  
    25  	"github.com/pkg/errors"
    26  	appsv1 "k8s.io/api/apps/v1"
    27  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	ktypes "k8s.io/apimachinery/pkg/types"
    30  	"k8s.io/klog/v2"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  
    33  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
    34  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    35  	"github.com/oam-dev/kubevela/pkg/auth"
    36  	"github.com/oam-dev/kubevela/pkg/cache"
    37  	"github.com/oam-dev/kubevela/pkg/monitor/metrics"
    38  	"github.com/oam-dev/kubevela/pkg/multicluster"
    39  	"github.com/oam-dev/kubevela/pkg/oam"
    40  	"github.com/oam-dev/kubevela/pkg/oam/util"
    41  	"github.com/oam-dev/kubevela/pkg/utils"
    42  )
    43  
    44  // GarbageCollectApplicationRevision execute garbage collection functions, including:
    45  // - clean up app revisions
    46  // - clean up legacy component revisions
    47  func (h *gcHandler) GarbageCollectApplicationRevision(ctx context.Context) error {
    48  	t := time.Now()
    49  	defer func() {
    50  		metrics.AppReconcileStageDurationHistogram.WithLabelValues("gc-app-rev").Observe(time.Since(t).Seconds())
    51  	}()
    52  	if err := cleanUpComponentRevision(ctx, h); err != nil {
    53  		return err
    54  	}
    55  	return cleanUpApplicationRevision(ctx, h)
    56  }
    57  
    58  // cleanUpApplicationRevision check all appRevisions of the application, remove them if the number of them exceed the limit
    59  func cleanUpApplicationRevision(ctx context.Context, h *gcHandler) error {
    60  	if h.cfg.disableApplicationRevisionGC {
    61  		return nil
    62  	}
    63  	t := time.Now()
    64  	defer func() {
    65  		metrics.AppReconcileStageDurationHistogram.WithLabelValues("gc-rev.apprev").Observe(time.Since(t).Seconds())
    66  	}()
    67  	sortedRevision, err := getSortedAppRevisions(ctx, h.Client, h.app.Name, h.app.Namespace)
    68  	if err != nil {
    69  		return err
    70  	}
    71  	appRevisionInUse := gatherUsingAppRevision(h.app)
    72  	appRevisionLimit := getApplicationRevisionLimitForApp(h.app, h.cfg.appRevisionLimit)
    73  	needKill := len(sortedRevision) - appRevisionLimit - len(appRevisionInUse)
    74  	if h._rootRT == nil && h._currentRT == nil && len(h._historyRTs) == 0 && h._crRT == nil && h.app.DeletionTimestamp != nil {
    75  		needKill = len(sortedRevision)
    76  		appRevisionInUse = nil
    77  	}
    78  	if needKill <= 0 {
    79  		return nil
    80  	}
    81  	klog.InfoS("Going to garbage collect app revisions", "limit", h.cfg.appRevisionLimit,
    82  		"total", len(sortedRevision), "using", len(appRevisionInUse), "kill", needKill)
    83  
    84  	for _, rev := range sortedRevision {
    85  		if needKill <= 0 {
    86  			break
    87  		}
    88  		// don't delete app revision in use
    89  		if appRevisionInUse[rev.Name] {
    90  			continue
    91  		}
    92  		if err := h.Client.Delete(ctx, rev.DeepCopy()); err != nil && !kerrors.IsNotFound(err) {
    93  			return err
    94  		}
    95  		needKill--
    96  	}
    97  	return nil
    98  }
    99  
   100  func cleanUpComponentRevision(ctx context.Context, h *gcHandler) error {
   101  	if h.cfg.disableComponentRevisionGC {
   102  		return nil
   103  	}
   104  	t := time.Now()
   105  	defer func() {
   106  		metrics.AppReconcileStageDurationHistogram.WithLabelValues("gc-rev.comprev").Observe(time.Since(t).Seconds())
   107  	}()
   108  	// collect component revision in use
   109  	compRevisionInUse := map[string]map[string]struct{}{}
   110  	ctx = auth.ContextWithUserInfo(ctx, h.app)
   111  	for i, resource := range h.app.Status.AppliedResources {
   112  		compName := resource.Name
   113  		ns := resource.Namespace
   114  		r := &unstructured.Unstructured{}
   115  		r.GetObjectKind().SetGroupVersionKind(resource.GroupVersionKind())
   116  		_ctx := multicluster.ContextWithClusterName(ctx, resource.Cluster)
   117  		err := h.Client.Get(_ctx, ktypes.NamespacedName{Name: compName, Namespace: ns}, r)
   118  		notFound := kerrors.IsNotFound(err)
   119  		if err != nil && !notFound {
   120  			return errors.WithMessagef(err, "get applied resource index=%d", i)
   121  		}
   122  		if compRevisionInUse[compName] == nil {
   123  			compRevisionInUse[compName] = map[string]struct{}{}
   124  		}
   125  		if notFound {
   126  			continue
   127  		}
   128  		compRevision, ok := r.GetLabels()[oam.LabelAppComponentRevision]
   129  		if ok {
   130  			compRevisionInUse[compName][compRevision] = struct{}{}
   131  		}
   132  	}
   133  
   134  	for _, curComp := range h.app.Status.AppliedResources {
   135  		crList := &appsv1.ControllerRevisionList{}
   136  		listOpts := []client.ListOption{client.MatchingLabels{
   137  			oam.LabelControllerRevisionComponent: utils.EscapeResourceNameToLabelValue(curComp.Name),
   138  		}, client.InNamespace(h.getComponentRevisionNamespace(ctx))}
   139  		_ctx := multicluster.ContextWithClusterName(ctx, curComp.Cluster)
   140  		if err := h.Client.List(_ctx, crList, listOpts...); err != nil {
   141  			return err
   142  		}
   143  		needKill := len(crList.Items) - h.cfg.appRevisionLimit - len(compRevisionInUse[curComp.Name])
   144  		if needKill < 1 {
   145  			continue
   146  		}
   147  		sortedRevision := crList.Items
   148  		sort.Sort(historiesByComponentRevision(sortedRevision))
   149  		for _, rev := range sortedRevision {
   150  			if needKill <= 0 {
   151  				break
   152  			}
   153  			if _, inUse := compRevisionInUse[curComp.Name][rev.Name]; inUse {
   154  				continue
   155  			}
   156  			_rev := rev.DeepCopy()
   157  			oam.SetCluster(_rev, curComp.Cluster)
   158  			if err := h.resourceKeeper.DeleteComponentRevision(_ctx, _rev); err != nil {
   159  				return err
   160  			}
   161  			needKill--
   162  		}
   163  	}
   164  	return nil
   165  }
   166  
   167  // gatherUsingAppRevision get all using appRevisions include app's status pointing to
   168  func gatherUsingAppRevision(app *v1beta1.Application) map[string]bool {
   169  	usingRevision := map[string]bool{}
   170  	if app.Status.LatestRevision != nil && len(app.Status.LatestRevision.Name) != 0 {
   171  		usingRevision[app.Status.LatestRevision.Name] = true
   172  	}
   173  	return usingRevision
   174  }
   175  
   176  func getApplicationRevisionLimitForApp(app *v1beta1.Application, fallback int) int {
   177  	for _, p := range app.Spec.Policies {
   178  		if p.Type == v1alpha1.GarbageCollectPolicyType && p.Properties != nil && p.Properties.Raw != nil {
   179  			prop := &v1alpha1.GarbageCollectPolicySpec{}
   180  			if err := json.Unmarshal(p.Properties.Raw, prop); err == nil && prop.ApplicationRevisionLimit != nil && *prop.ApplicationRevisionLimit >= 0 {
   181  				return *prop.ApplicationRevisionLimit
   182  			}
   183  		}
   184  	}
   185  	return fallback
   186  }
   187  
   188  // getSortedAppRevisions get application revisions by revision number
   189  func getSortedAppRevisions(ctx context.Context, cli client.Client, appName string, appNs string) ([]v1beta1.ApplicationRevision, error) {
   190  	revs, err := ListApplicationRevisions(ctx, cli, appName, appNs)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	sort.Slice(revs, func(i, j int) bool {
   195  		ir, _ := util.ExtractRevisionNum(revs[i].Name, "-")
   196  		ij, _ := util.ExtractRevisionNum(revs[j].Name, "-")
   197  		return ir < ij
   198  	})
   199  	return revs, nil
   200  }
   201  
   202  // ListApplicationRevisions get application revisions by label
   203  func ListApplicationRevisions(ctx context.Context, cli client.Client, appName string, appNs string) ([]v1beta1.ApplicationRevision, error) {
   204  	appRevisionList := new(v1beta1.ApplicationRevisionList)
   205  	var err error
   206  	if cache.OptimizeListOp {
   207  		err = cli.List(ctx, appRevisionList, client.MatchingFields{cache.AppIndex: appNs + "/" + appName})
   208  	} else {
   209  		err = cli.List(ctx, appRevisionList, client.InNamespace(appNs), client.MatchingLabels{oam.LabelAppName: appName})
   210  	}
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  	return appRevisionList.Items, nil
   215  }
   216  
   217  func (h *gcHandler) getComponentRevisionNamespace(ctx context.Context) string {
   218  	if ns, ok := ctx.Value(0).(string); ok && ns != "" {
   219  		return ns
   220  	}
   221  	return h.app.Namespace
   222  }
   223  
   224  type historiesByComponentRevision []appsv1.ControllerRevision
   225  
   226  func (h historiesByComponentRevision) Len() int      { return len(h) }
   227  func (h historiesByComponentRevision) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
   228  func (h historiesByComponentRevision) Less(i, j int) bool {
   229  	ir, _ := util.ExtractRevisionNum(h[i].Name, "-")
   230  	ij, _ := util.ExtractRevisionNum(h[j].Name, "-")
   231  	return ir < ij
   232  }