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 }