-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cleanup policy for builds #13788
Cleanup policy for builds #13788
Conversation
[test] |
[testextended][extended:core(builds)] |
@openshift/devex ptal |
pkg/build/controller/common/util.go
Outdated
sortedBuilds := make([]buildapi.Build, len(builds)) | ||
copy(sortedBuilds, builds) | ||
|
||
for i := len(sortedBuilds) - 1; i >= 0; i-- { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a good reason not to use sort.Sort here?
pkg/build/controller/common/util.go
Outdated
return nil | ||
} | ||
|
||
if bcList.Items[0].Spec.FailedBuildsHistoryLimit != nil || bcList.Items[0].Spec.SuccessfulBuildsHistoryLimit != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: would be tempted to do:
if bcList.Items[0].Spec.FailedBuildsHistoryLimit == nil && bcList.Items[0].Spec.SuccessfulBuildsHistoryLimit == nil {
return nil
}
pkg/build/controller/common/util.go
Outdated
// HandleBuildPruning handles the deletion of old successful and failed builds | ||
// based on settings in the BuildConfig. | ||
func HandleBuildPruning(build *buildapi.Build, buildLister buildclient.BuildLister, buildConfigLister buildclient.BuildConfigLister, buildDeleter pruner.BuildDeleter) error { | ||
if build.Status.Config == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should do the following: use util.ConfigNameForBuild to get the bc name from the build. Then use a .Get() to get the single bc without listing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1! didn't know that existed, that should make things much more simple
pkg/build/controller/common/util.go
Outdated
} | ||
|
||
if len(bcList.Items) == 0 { | ||
glog.V(0).Infof("There are no buildconfigs associated with this build: %v/%v", build.Namespace, build.Name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think V(0) logging is warranted in this routine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in next push
pkg/build/controller/common/util.go
Outdated
|
||
if bcList.Items[0].Spec.FailedBuildsHistoryLimit != nil || bcList.Items[0].Spec.SuccessfulBuildsHistoryLimit != nil { | ||
|
||
bList, err := buildLister.List(build.Namespace, kapi.ListOptions{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd consider using util.BuildConfigBuilds here, then you may not need your SortBuildsByPhase routine.
pkg/build/controller/common/util.go
Outdated
if bcList.Items[0].Spec.FailedBuildsHistoryLimit != nil { | ||
failedBuildsHistoryLimit := int(*bcList.Items[0].Spec.FailedBuildsHistoryLimit) | ||
glog.V(4).Infof("Preparing to prune %v old failed builds.", (len(failedBuilds) - failedBuildsHistoryLimit)) | ||
if failedBuildsHistoryLimit == 0 && len(failedBuilds) > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you negate your sorting predicate, you should be able to simpify this to something like successfulBuilds[successfulBuildsHistoryLimit:]
here.
Thanks @jim-minter , your suggestions cleaned up the code quite a bit, and I'll have those "tricks" in my arsenal for next time! |
@openshift/api-review ptal |
pkg/build/client/clients.go
Outdated
@@ -11,6 +11,11 @@ type BuildConfigGetter interface { | |||
Get(namespace, name string) (*buildapi.BuildConfig, error) | |||
} | |||
|
|||
// BuildConfigLister provides methods for listing the BuildConfigs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes to this file may now be superfluous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in next push
pkg/build/controller/common/util.go
Outdated
return nil | ||
} | ||
|
||
successfulBuilds, err := buildutil.BuildConfigBuilds(buildLister, build.Namespace, bcName, isCompletedBuild) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you move these lines into if buildConfig.Spec.SuccessfulBuildsHistoryLimit != nil {
(x2), you can remove the if clause above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
pkg/build/controller/common/util.go
Outdated
if buildConfig.Spec.SuccessfulBuildsHistoryLimit != nil { | ||
successfulBuildsHistoryLimit := int(*buildConfig.Spec.SuccessfulBuildsHistoryLimit) | ||
glog.V(4).Infof("Preparing to prune %v old successful builds.", (len(successfulBuilds.Items) - successfulBuildsHistoryLimit)) | ||
if successfulBuildsHistoryLimit == 0 && len(successfulBuilds.Items) > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the if ... else is necessary, I think just the following will do:
if len(successfulBuilds.Items) > successfulBuildsHistoryLimit {
 buildsToDelete = append(buildsToDelete, successfulBuilds.Items[successfulBuildsHistoryLimit:]...)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that's a leftover from the way it was being done before, fixed in next push
pkg/build/api/v1/types.go
Outdated
|
||
// SuccessfulBuildsHistoryLimit is the number of old successful builds to retain. | ||
// This field is a pointer to allow for differentiation between an explicit zero and not specified. | ||
SuccessfulBuildsHistoryLimit *int32 `json:"successfulBuildsHistoryLimit,omitempty" protobuf:"varint,4,opt,name=successfulBuildsHistoryLimit"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need to validate that these values, if they exist, are >= 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
implemented in next push
re[test] |
1 similar comment
re[test] |
@@ -148,6 +148,18 @@ func ValidateBuildConfig(config *buildapi.BuildConfig) field.ErrorList { | |||
"run policy must Parallel, Serial, or SerialLatestOnly")) | |||
} | |||
|
|||
if config.Spec.SuccessfulBuildsHistoryLimit != nil { | |||
if *config.Spec.SuccessfulBuildsHistoryLimit < 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a ValidateNonnegativeField helper
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
@@ -236,7 +246,7 @@ func (bc *BuildPodController) HandlePod(pod *kapi.Pod) error { | |||
case buildapi.BuildPhaseError, buildapi.BuildPhaseFailed: | |||
bc.recorder.Eventf(build, kapi.EventTypeNormal, buildapi.BuildFailedEventReason, fmt.Sprintf(buildapi.BuildFailedEventMessage, build.Namespace, build.Name)) | |||
} | |||
common.HandleBuildCompletion(build, bc.runPolicies) | |||
common.HandleBuildCompletion(build, bc.buildLister, bc.buildConfigGetter, bc.buildDeleter, bc.runPolicies) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handling this only when a running build transitions to a new phase is not optimal. For example, if I set the limits to a BC without running any build, I would expect any prunable builds to be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you are just setting the limits on a BC, what would trigger the pruning then? saving the BC?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
pkg/build/controller/common/util.go
Outdated
successfulBuildsHistoryLimit := int(*buildConfig.Spec.SuccessfulBuildsHistoryLimit) | ||
glog.V(4).Infof("Preparing to prune %v old successful builds.", (len(successfulBuilds.Items) - successfulBuildsHistoryLimit)) | ||
if len(successfulBuilds.Items) > successfulBuildsHistoryLimit { | ||
buildsToDelete = append(buildsToDelete, successfulBuilds.Items...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't append all of the builds?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push (delete the wrong line!)
pkg/build/controller/common/util.go
Outdated
for i, b := range buildsToDelete { | ||
glog.V(4).Infof("Pruning old build: %s/%s", b.Namespace, b.Name) | ||
if err := buildDeleter.DeleteBuild(&buildsToDelete[i]); err != nil { | ||
return err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can continue deleting other builds and return an aggregate error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just concat all the err strings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a NewAggregate helper which takes a slice of errors
@@ -148,6 +148,16 @@ func ValidateBuildConfig(config *buildapi.BuildConfig) field.ErrorList { | |||
"run policy must Parallel, Serial, or SerialLatestOnly")) | |||
} | |||
|
|||
if config.Spec.SuccessfulBuildsHistoryLimit != nil { | |||
allErrs = append(allErrs, validation.ValidateNonnegativeField(int64(*config.Spec.SuccessfulBuildsHistoryLimit), specPath.Child("successfulBuildsHistoryLimit"))...) | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
delete newline
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated in next push
|
||
if config.Spec.FailedBuildsHistoryLimit != nil { | ||
allErrs = append(allErrs, validation.ValidateNonnegativeField(int64(*config.Spec.FailedBuildsHistoryLimit), specPath.Child("failedBuildsHistoryLimit"))...) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
delete newline
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated in next push
@@ -64,13 +68,19 @@ func NewBuildPodController(buildInformer cache.SharedIndexInformer, podInformer | |||
eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: v1core.New(extkc.Core().RESTClient()).Events("")}) | |||
|
|||
buildListerUpdater := buildclient.NewOSClientBuildClient(oc) | |||
buildConfigGetter := buildclient.NewOSClientBuildConfigClient(oc) | |||
buildDeleter := pruner.NewBuildDeleter(oc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
refactor BuildDeleter out of the prune backage and into where the other build clients live.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
pkg/build/controller/common/util.go
Outdated
|
||
func isFailedBuild(build buildapi.Build) bool { | ||
return build.Status.Phase == buildapi.BuildPhaseFailed | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't think it's worth introducing these helpers unless you're going to check multiple phases, but if you are set on keeping them, please move them into pkg/build/util.go where we already have some helpers for checking build phases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that there is an existing IsBuildComplete helper which checks if the build is in any terminal state. you might want to rename it to IsBuildTerminated or change it to IsBuildActive and reverse all the boolean checks that were calling the old function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
pkg/build/controller/common/util.go
Outdated
} | ||
|
||
func (b ByStartTimestamp) Less(i, j int) bool { | ||
return !b[j].Status.StartTimestamp.Time.After(b[i].Status.StartTimestamp.Time) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'd expect to sort by creation time, not start time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
pkg/build/controller/common/util.go
Outdated
// HandleBuildPruning handles the deletion of old successful and failed builds | ||
// based on settings in the BuildConfig. | ||
func HandleBuildPruning(build *buildapi.Build, buildLister buildclient.BuildLister, buildConfigGetter buildclient.BuildConfigGetter, buildDeleter pruner.BuildDeleter) error { | ||
bcName := buildutil.ConfigNameForBuild(build) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you get back nil/emptystring here, just bail, it means the build isn't associated with a buildconfig so there's nothing to do.
t.Errorf("should have set the CompletionTimestamp, but instead it was nil") | ||
} | ||
if build.Status.Duration > 0 { | ||
t.Errorf("should have set the Durationi to 0s, but instead it was %v", build.Status.Duration) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/Durationi/Duration/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
}, | ||
Spec: buildapi.BuildConfigSpec{ | ||
SuccessfulBuildsHistoryLimit: &buildsToKeep, | ||
FailedBuildsHistoryLimit: &buildsToKeep, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use different numbers for the failed count and successful count. by using the same number you have no way of knowing if your code is using the right field for the right build type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
pkg/build/controller/common/util.go
Outdated
func HandleBuildPruning(build *buildapi.Build, buildLister buildclient.BuildLister, buildConfigGetter buildclient.BuildConfigGetter, buildDeleter pruner.BuildDeleter) error { | ||
bcName := buildutil.ConfigNameForBuild(build) | ||
|
||
buildConfig, err := buildConfigGetter.Get(build.Namespace, bcName, metav1.GetOptions{}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of going to the api to the get the buildconfig (and the builds), we should be going to the sharedinformer cache. @csrwng might be able to give you some pointers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a handful of minor nits, a follow up for using caches, and also i'd like to see at least one extended test case that also validates the pruning behavior for both completed and failed builds.
// recorder is used to record events. | ||
Recorder record.EventRecorder | ||
} | ||
|
||
func (c *BuildConfigController) HandleBuildConfig(bc *buildapi.BuildConfig) error { | ||
glog.V(4).Infof("Handling BuildConfig %s/%s", bc.Namespace, bc.Name) | ||
|
||
glog.V(4).Infof("Handling Build Pruning: %v/%v", bc.Namespace, bc.Name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this logging is redundant, it doesn't add any information to what was logged above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
glog.V(4).Infof("Handling Build Pruning: %v/%v", bc.Namespace, bc.Name) | ||
if err := buildutil.HandleBuildPruning(bc.Name, bc.Namespace, c.BuildLister, c.BuildConfigGetter, c.BuildDeleter); err != nil { | ||
return err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
why isn't this returning the same wrapper error message as HandleBuildCompletion does when it has an error pruning?
-
i think you should proceed through the rest of the HandleBuildConfig logic and return an aggregated error object at the end, rather than quitting here if something goes wrong. Even if pruning fails, we still want to kick off the new build.
(Unless the HandleBuildConfig event gets retried when an error is encountered?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
pkg/build/controller/common/util.go
Outdated
sort.Sort(ByCreationTimestamp(successfulBuilds.Items)) | ||
|
||
successfulBuildsHistoryLimit := int(*buildConfig.Spec.SuccessfulBuildsHistoryLimit) | ||
glog.V(4).Infof("Preparing to prune %v old successful builds.", (len(successfulBuilds.Items) - successfulBuildsHistoryLimit)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move this log into the if block, and/or log more details: number of builds found, number of builds configured to keep, number that will be pruned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
pkg/build/controller/common/util.go
Outdated
sort.Sort(ByCreationTimestamp(failedBuilds.Items)) | ||
|
||
failedBuildsHistoryLimit := int(*buildConfig.Spec.FailedBuildsHistoryLimit) | ||
glog.V(4).Infof("Preparing to prune %v old failed builds.", (len(failedBuilds.Items) - failedBuildsHistoryLimit)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same for this log message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
} | ||
|
||
if !reflect.DeepEqual(failedStartingBuilds.Items[:3], failedRemainingBuilds.Items) { | ||
t.Errorf("the two most recent failed builds should be left, but they were not") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/two/three/
and for both this and the case above, it's generally useful to print the actual and expected values (in this case the lists of builds) to help debug if it does fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
|
||
// HandleBuildPruning handles the deletion of old successful and failed builds | ||
// based on settings in the BuildConfig. | ||
func HandleBuildPruning(buildConfigName string, namespace string, buildLister buildclient.BuildLister, buildConfigGetter buildclient.BuildConfigGetter, buildDeleter buildclient.BuildDeleter) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm still a little bothered this function isn't using the shared informer caches to get the buildconfig+builds, but we can fix it in a follow up. Please open an issue to track the need to deal with it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opened #14049
fmt.Fprintf(g.GinkgoWriter, "%v", err) | ||
} | ||
|
||
o.ExpectWithOffset(1, int32(len(builds.Items))).To(o.Equal(*buildConfig.Spec.SuccessfulBuildsHistoryLimit), "there should be %v completed builds left after pruning, but instead there were %v", *buildConfig.Spec.SuccessfulBuildsHistoryLimit, len(builds.Items)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this an expect with offset?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
fmt.Fprintf(g.GinkgoWriter, "%v", err) | ||
} | ||
|
||
o.ExpectWithOffset(1, int32(len(builds.Items))).To(o.Equal(*buildConfig.Spec.FailedBuildsHistoryLimit), "there should be %v failed builds left after pruning, but instead there were %v", *buildConfig.Spec.FailedBuildsHistoryLimit, len(builds.Items)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this one too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in next push
API LGTM (matches successfulJobsHistoryLimit/failedJobsHistoryLimit naming and types) |
flake #14197 [merge] |
@coreydaley you pushed another commit, what did you change? that cancels my merge tag, fyi. |
@bparees I just rebased, didn't see your merge tag, was trying to rerun the tests. |
@coreydaley you appear to have picked up a compile error w/ your rebase due to a signature change:
|
Adds ability to set successfulBuildsHistoryLimit and failedBuildsHistoryLimit on buildConfigs which will prune old builds. Closes #13640 Completes https://trello.com/c/048p7YRO/1044-5-cleanup-policy-for-builds-builds
@bparees Fixed in latest push |
Evaluated for origin test up to 2afdad0 |
continuous-integration/openshift-jenkins/test Running (https://ci.openshift.redhat.com/jenkins/job/test_pull_request_origin/1496/) (Base Commit: b96ef9a) |
Evaluated for origin testextended up to 2afdad0 |
continuous-integration/openshift-jenkins/test SUCCESS |
continuous-integration/openshift-jenkins/testextended SUCCESS (https://ci.openshift.redhat.com/jenkins/job/test_pull_request_origin_extended/407/) (Base Commit: b96ef9a) (Extended Tests: core(builds)) |
@bparees ptal and hit me with another merge tag |
[merge] |
flake #14236 |
flake #14241 |
flake #14129 |
flake #13980 |
flake #14259 |
Evaluated for origin merge up to 2afdad0 |
continuous-integration/openshift-jenkins/merge SUCCESS (https://ci.openshift.redhat.com/jenkins/job/merge_pull_request_origin/712/) (Base Commit: 938d40f) (Image: devenv-rhel7_6247) |
|
||
builds, err := oc.Client().Builds(oc.Namespace()).List(metav1.ListOptions{}) | ||
if err != nil { | ||
fmt.Fprintf(g.GinkgoWriter, "%v", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and above: was it intentionally to not print a newline at the end of the error?
Adds ability to set successfulBuildsHistoryLimit and failedBuildsHistoryLimit on
buildConfigs which will prune old builds.
Closes #13640
Completes https://trello.com/c/048p7YRO/1044-5-cleanup-policy-for-builds-builds