github.com/jenkins-x/jx/v2@v2.1.155/pkg/cmd/gc/gc_activities.go (about)

     1  package gc
     2  
     3  import (
     4  	"sort"
     5  	"strings"
     6  	"time"
     7  
     8  	gojenkins "github.com/jenkins-x/golang-jenkins"
     9  	v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1"
    10  	"github.com/jenkins-x/jx/v2/pkg/cmd/helper"
    11  	"github.com/pkg/errors"
    12  	prowjobv1 "k8s.io/test-infra/prow/apis/prowjobs/v1"
    13  
    14  	jv1 "github.com/jenkins-x/jx-api/pkg/client/clientset/versioned/typed/jenkins.io/v1"
    15  	"github.com/jenkins-x/jx/v2/pkg/util"
    16  	"github.com/spf13/cobra"
    17  	tektonv1alpha1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
    18  	tektonclient "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/typed/pipeline/v1alpha1"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	prowjobclient "k8s.io/test-infra/prow/client/clientset/versioned/typed/prowjobs/v1"
    21  
    22  	"github.com/jenkins-x/jx-logging/pkg/log"
    23  	"github.com/jenkins-x/jx/v2/pkg/cmd/opts"
    24  	"github.com/jenkins-x/jx/v2/pkg/cmd/templates"
    25  )
    26  
    27  // GetOptions is the start of the data required to perform the operation.  As new fields are added, add them here instead of
    28  // referencing the cmd.Flags()
    29  type GCActivitiesOptions struct {
    30  	*opts.CommonOptions
    31  
    32  	DryRun                  bool
    33  	ReleaseHistoryLimit     int
    34  	PullRequestHistoryLimit int
    35  	ReleaseAgeLimit         time.Duration
    36  	PullRequestAgeLimit     time.Duration
    37  	PipelineRunAgeLimit     time.Duration
    38  	ProwJobAgeLimit         time.Duration
    39  	jclient                 gojenkins.JenkinsClient
    40  }
    41  
    42  var (
    43  	GCActivitiesLong = templates.LongDesc(`
    44  		Garbage collect the Jenkins X PipelineActivity and PipelineRun resources
    45  
    46  `)
    47  
    48  	GCActivitiesExample = templates.Examples(`
    49  		# garbage collect PipelineActivity and PipelineRun resources
    50  		jx gc activities
    51  
    52  		# dry run mode
    53  		jx gc pa --dry-run
    54  `)
    55  )
    56  
    57  type buildCounter struct {
    58  	ReleaseCount int
    59  	PRCount      int
    60  }
    61  
    62  type buildsCount struct {
    63  	cache map[string]*buildCounter
    64  }
    65  
    66  // AddBuild adds the build and returns the number of builds for this repo and branch
    67  func (c *buildsCount) AddBuild(repoAndBranch string, isPR bool) int {
    68  	if c.cache == nil {
    69  		c.cache = map[string]*buildCounter{}
    70  	}
    71  	bc := c.cache[repoAndBranch]
    72  	if bc == nil {
    73  		bc = &buildCounter{}
    74  		c.cache[repoAndBranch] = bc
    75  	}
    76  	if isPR {
    77  		bc.PRCount++
    78  		return bc.PRCount
    79  	}
    80  	bc.ReleaseCount++
    81  	return bc.ReleaseCount
    82  }
    83  
    84  // NewCmd s a command object for the "step" command
    85  func NewCmdGCActivities(commonOpts *opts.CommonOptions) *cobra.Command {
    86  	options := &GCActivitiesOptions{
    87  		CommonOptions: commonOpts,
    88  	}
    89  
    90  	cmd := &cobra.Command{
    91  		Use:     "activities",
    92  		Aliases: []string{"pa", "act", "pr"},
    93  		Short:   "garbage collection for PipelineActivities and PipelineRun resources",
    94  		Long:    GCActivitiesLong,
    95  		Example: GCActivitiesExample,
    96  		Run: func(cmd *cobra.Command, args []string) {
    97  			options.Cmd = cmd
    98  			options.Args = args
    99  			err := options.Run()
   100  			helper.CheckErr(err)
   101  		},
   102  	}
   103  	cmd.Flags().BoolVarP(&options.DryRun, "dry-run", "d", false, "Dry run mode. If enabled just list the resources that would be removed")
   104  	cmd.Flags().IntVarP(&options.ReleaseHistoryLimit, "release-history-limit", "l", 5, "Maximum number of PipelineActivities to keep around per repository release")
   105  	cmd.Flags().IntVarP(&options.PullRequestHistoryLimit, "pr-history-limit", "", 2, "Minimum number of PipelineActivities to keep around per repository Pull Request")
   106  	cmd.Flags().DurationVarP(&options.PullRequestAgeLimit, "pull-request-age", "p", time.Hour*48, "Maximum age to keep PipelineActivities for Pull Requests")
   107  	cmd.Flags().DurationVarP(&options.ReleaseAgeLimit, "release-age", "r", time.Hour*24*30, "Maximum age to keep PipelineActivities for Releases")
   108  	cmd.Flags().DurationVarP(&options.PipelineRunAgeLimit, "pipelinerun-age", "", time.Hour*12, "Maximum age to keep completed PipelineRuns for all pipelines")
   109  	cmd.Flags().DurationVarP(&options.ProwJobAgeLimit, "prowjob-age", "", time.Hour*24*7, "Maximum age to keep completed ProwJobs for all pipelines")
   110  	return cmd
   111  }
   112  
   113  // Run implements this command
   114  func (o *GCActivitiesOptions) Run() error {
   115  	client, currentNs, err := o.JXClientAndDevNamespace()
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	prowEnabled, err := o.IsProw()
   121  	if err != nil {
   122  		return err
   123  	}
   124  
   125  	// cannot use field selectors like `spec.kind=Preview` on CRDs so list all environments
   126  	activityInterface := client.JenkinsV1().PipelineActivities(currentNs)
   127  	activities, err := activityInterface.List(metav1.ListOptions{})
   128  	if err != nil {
   129  		return err
   130  	}
   131  	if len(activities.Items) == 0 {
   132  		// no preview environments found so lets return gracefully
   133  		log.Logger().Debug("no activities found")
   134  		return nil
   135  	}
   136  
   137  	var jobNames []string
   138  	if !prowEnabled {
   139  		o.jclient, err = o.JenkinsClient()
   140  		if err != nil {
   141  			return err
   142  		}
   143  
   144  		jobs, err := o.jclient.GetJobs()
   145  		if err != nil {
   146  			return err
   147  		}
   148  		for _, j := range jobs {
   149  			err = o.GetAllPipelineJobNames(o.jclient, &jobNames, j.Name)
   150  			if err != nil {
   151  				return err
   152  			}
   153  		}
   154  	}
   155  
   156  	now := time.Now()
   157  	counters := &buildsCount{}
   158  
   159  	var completedActivities []v1.PipelineActivity
   160  
   161  	// Filter out running activities
   162  	for _, a := range activities.Items {
   163  		if a.Spec.CompletedTimestamp != nil {
   164  			completedActivities = append(completedActivities, a)
   165  		}
   166  	}
   167  
   168  	// Sort with newest created activities first
   169  	sort.Slice(completedActivities, func(i, j int) bool {
   170  		return !completedActivities[i].Spec.CompletedTimestamp.Before(completedActivities[j].Spec.CompletedTimestamp)
   171  	})
   172  
   173  	//
   174  	for _, a := range completedActivities {
   175  		activity := a
   176  		branchName := a.BranchName()
   177  		isPR, isBatch := o.isPullRequestOrBatchBranch(branchName)
   178  		maxAge, revisionHistory := o.ageAndHistoryLimits(isPR, isBatch)
   179  		// lets remove activities that are too old
   180  		if activity.Spec.CompletedTimestamp != nil && activity.Spec.CompletedTimestamp.Add(maxAge).Before(now) {
   181  
   182  			err = o.deleteActivity(activityInterface, &activity)
   183  			if err != nil {
   184  				return err
   185  			}
   186  			continue
   187  		}
   188  
   189  		repoBranchAndContext := activity.RepositoryOwner() + "/" + activity.RepositoryName() + "/" + activity.BranchName() + "/" + activity.Spec.Context
   190  		c := counters.AddBuild(repoBranchAndContext, isPR)
   191  		if c > revisionHistory && a.Spec.CompletedTimestamp != nil {
   192  			err = o.deleteActivity(activityInterface, &activity)
   193  			if err != nil {
   194  				return err
   195  			}
   196  			continue
   197  		}
   198  
   199  		if !prowEnabled {
   200  			// if activity has no job in Jenkins delete it
   201  			matched := false
   202  			for _, j := range jobNames {
   203  				if a.Spec.Pipeline == j {
   204  					matched = true
   205  					break
   206  				}
   207  			}
   208  			if !matched {
   209  				err = o.deleteActivity(activityInterface, &activity)
   210  				if err != nil {
   211  					return err
   212  				}
   213  			}
   214  		}
   215  	}
   216  
   217  	// Clean up completed PipelineRuns
   218  	err = o.gcPipelineRuns(currentNs)
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	// Clean up completed ProwJobs
   224  	err = o.gcProwJobs(currentNs)
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	return nil
   230  }
   231  
   232  func (o *GCActivitiesOptions) deleteActivity(activityInterface jv1.PipelineActivityInterface, a *v1.PipelineActivity) error {
   233  	prefix := ""
   234  	if o.DryRun {
   235  		prefix = "not "
   236  	}
   237  	log.Logger().Infof("%sdeleting PipelineActivity %s", prefix, util.ColorInfo(a.Name))
   238  	if o.DryRun {
   239  		return nil
   240  	}
   241  	return activityInterface.Delete(a.Name, metav1.NewDeleteOptions(0))
   242  }
   243  
   244  func (o *GCActivitiesOptions) gcPipelineRuns(ns string) error {
   245  	tektonClient, _, err := o.TektonClient()
   246  	if err != nil {
   247  		return err
   248  	}
   249  	pipelineRunInterface := tektonClient.TektonV1alpha1().PipelineRuns(ns)
   250  	runList, err := pipelineRunInterface.List(metav1.ListOptions{})
   251  	if err != nil {
   252  		log.Logger().Warnf("no PipelineRun instances found: %s", err.Error())
   253  		return nil
   254  	}
   255  
   256  	now := time.Now()
   257  
   258  	for _, pr := range runList.Items {
   259  		pipelineRun := pr
   260  		completionTime := pipelineRun.Status.CompletionTime
   261  		if pipelineRun.IsDone() && completionTime != nil && completionTime.Add(o.PipelineRunAgeLimit).Before(now) {
   262  			err = o.deletePipelineRun(pipelineRunInterface, &pipelineRun)
   263  			if err != nil {
   264  				return err
   265  			}
   266  			continue
   267  		}
   268  	}
   269  	return nil
   270  }
   271  
   272  func (o *GCActivitiesOptions) deletePipelineRun(pipelineRunInterface tektonclient.PipelineRunInterface, pr *tektonv1alpha1.PipelineRun) error {
   273  	prefix := ""
   274  	if o.DryRun {
   275  		prefix = "not "
   276  	}
   277  	log.Logger().Infof("%sdeleting PipelineRun %s", prefix, util.ColorInfo(pr.Name))
   278  	if o.DryRun {
   279  		return nil
   280  	}
   281  	return pipelineRunInterface.Delete(pr.Name, metav1.NewDeleteOptions(0))
   282  }
   283  
   284  func (o *GCActivitiesOptions) gcProwJobs(ns string) error {
   285  	prowJobClient, _, err := o.ProwJobClient()
   286  	if err != nil {
   287  		return err
   288  	}
   289  	pjInterface := prowJobClient.ProwV1().ProwJobs(ns)
   290  	pjList, err := pjInterface.List(metav1.ListOptions{})
   291  	if err != nil {
   292  		log.Logger().Warnf("no ProwJob instances found: %s", err.Error())
   293  		return nil
   294  	}
   295  
   296  	now := time.Now()
   297  
   298  	for _, pj := range pjList.Items {
   299  		prowJob := pj
   300  		completionTime := prowJob.Status.CompletionTime
   301  		if completionTime != nil && completionTime.Add(o.ProwJobAgeLimit).Before(now) {
   302  			err = o.deleteProwJob(pjInterface, &prowJob)
   303  			if err != nil {
   304  				return errors.Wrapf(err, "error deleting ProwJob %s", prowJob.Name)
   305  			}
   306  		}
   307  	}
   308  	return nil
   309  }
   310  
   311  func (o *GCActivitiesOptions) deleteProwJob(pjInterface prowjobclient.ProwJobInterface, pj *prowjobv1.ProwJob) error {
   312  	prefix := ""
   313  	if o.DryRun {
   314  		prefix = "not "
   315  	}
   316  	log.Logger().Infof("%sdeleting ProwJob %s", prefix, util.ColorInfo(pj.Name))
   317  	if o.DryRun {
   318  		return nil
   319  	}
   320  	return pjInterface.Delete(pj.Name, metav1.NewDeleteOptions(0))
   321  }
   322  
   323  func (o *GCActivitiesOptions) ageAndHistoryLimits(isPR, isBatch bool) (time.Duration, int) {
   324  	maxAge := o.ReleaseAgeLimit
   325  	revisionLimit := o.ReleaseHistoryLimit
   326  	if isPR || isBatch {
   327  		maxAge = o.PullRequestAgeLimit
   328  		revisionLimit = o.PullRequestHistoryLimit
   329  	}
   330  	return maxAge, revisionLimit
   331  }
   332  
   333  func (o *GCActivitiesOptions) isPullRequestOrBatchBranch(branchName string) (bool, bool) {
   334  	return strings.HasPrefix(branchName, "PR-"), branchName == "batch"
   335  }