Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/TektonConfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -975,7 +975,44 @@ spec:
- The event-based pruner responds to resource events in real-time, providing more efficient cleanup
- When `enforcedConfigLevel` is set to `namespace`, individual namespaces can override these settings using ConfigMaps

#### Resource Limits

The operator applies default resource limits to pruner deployments based on benchmark data:

**Controller:** `requests: 100m CPU, 256Mi memory` | `limits: 2Gi memory`
**Webhook:** `requests: 50m CPU, 64Mi memory` | `limits: 512Mi memory`

CPU limits are omitted to avoid CFS throttling during reconciliation bursts. Memory limits are based on linear growth (~13.6 MB per 1k resident PipelineRuns). The 2Gi controller limit supports up to ~50k runs with some safety margin.

**Benchmark data:**

| Scale | Resident PRs | Heap (MB) | Container (MB) |
|-------|--------------|-----------|----------------|
| Baseline | 0 | 23 | - |
| Low | 1,000 | 65 | 58 |
| Medium | 6,000 | 148 | 69 |
| High | 16,000 | 301 | 104 |
| Production | 41,000 | 579 | 170 |


**Override via options:**
```yaml
tektonpruner:
options:
deployments:
tekton-pruner-controller:
spec:
template:
spec:
containers:
- name: controller
resources:
limits:
memory: 4Gi
cpu: 1000m
```

For >50k resident runs, calculate required memory: `memory_mb = 23 + (13.6 × runs_in_thousands)` and apply 1.5x safety factor.

### Additional fields as `options`

Expand Down
93 changes: 93 additions & 0 deletions pkg/reconciler/common/transformer_resource_limits.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
Copyright 2026 The Tekton 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 common

import (
mf "github.com/manifestival/manifestival"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
apimachineryRuntime "k8s.io/apimachinery/pkg/runtime"
)

// AddDefaultResourceRequirements returns a transformer that injects default resource
// requests and limits into Deployments and StatefulSets based on their name.
//
// The resourceMap parameter maps deployment/statefulset names to their default
// ResourceRequirements. Only resources with empty or missing resource specifications
// are updated - existing resource configurations are preserved.
//
// This allows components to define resource defaults without overriding user-specified
// values or values set by the additional options transformer.
func AddDefaultResourceRequirements(resourceMap map[string]corev1.ResourceRequirements) mf.Transformer {
return func(u *unstructured.Unstructured) error {
kind := u.GetKind()
if kind != "Deployment" && kind != "StatefulSet" {
return nil
}

// Check if this resource has defaults defined
defaultResources, found := resourceMap[u.GetName()]
if !found {
return nil
}

// Both Deployment and StatefulSet have containers at spec.template.spec.containers
containers, found, err := unstructured.NestedSlice(u.Object, "spec", "template", "spec", "containers")
if !found || err != nil {
return err
}

modified := false
for i := range containers {
containerMap := containers[i].(map[string]interface{})

// Check if resources field exists
resources, hasResources := containerMap["resources"]
if !hasResources {
// No resources field, add defaults
resourcesMap, err := apimachineryRuntime.DefaultUnstructuredConverter.ToUnstructured(&defaultResources)
if err != nil {
return err
}
containerMap["resources"] = resourcesMap
modified = true
} else {
// Has resources field, check if both requests and limits are empty
resourcesMap := resources.(map[string]interface{})
_, hasRequests := resourcesMap["requests"]
_, hasLimits := resourcesMap["limits"]
if !hasRequests && !hasLimits {
// Both nil, replace with defaults
resourcesMap, err := apimachineryRuntime.DefaultUnstructuredConverter.ToUnstructured(&defaultResources)
if err != nil {
return err
}
containerMap["resources"] = resourcesMap
modified = true
}
}
}

if modified {
if err := unstructured.SetNestedSlice(u.Object, containers, "spec", "template", "spec", "containers"); err != nil {
return err
}
}

return nil
}
}
60 changes: 60 additions & 0 deletions pkg/reconciler/kubernetes/tektonpruner/defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
Copyright 2026 The Tekton 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 tektonpruner

import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)

// Default resource limits for pruner deployments based on empirical benchmark data.
const (
defaultControllerCPURequest = "100m"
defaultControllerMemoryRequest = "256Mi"
defaultControllerMemoryLimit = "2Gi"

defaultWebhookCPURequest = "50m"
defaultWebhookMemoryRequest = "64Mi"
defaultWebhookMemoryLimit = "512Mi"
)

var (
DefaultControllerResources = corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(defaultControllerCPURequest),
corev1.ResourceMemory: resource.MustParse(defaultControllerMemoryRequest),
},
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse(defaultControllerMemoryLimit),
},
}

DefaultWebhookResources = corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(defaultWebhookCPURequest),
corev1.ResourceMemory: resource.MustParse(defaultWebhookMemoryRequest),
},
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse(defaultWebhookMemoryLimit),
},
}

DefaultResourcesMap = map[string]corev1.ResourceRequirements{
"tekton-pruner-controller": DefaultControllerResources,
"tekton-pruner-webhook": DefaultWebhookResources,
}
)
1 change: 1 addition & 0 deletions pkg/reconciler/kubernetes/tektonpruner/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func filterAndTransform(extension common.Extension) client.FilterAndTransform {
common.DeploymentImages(prunerImages),
common.AddDeploymentRestrictedPSA(),
common.AddConfigMapValues(PrunerConfigMapName, prunerCR.Spec.TektonPrunerConfig),
common.AddDefaultResourceRequirements(DefaultResourcesMap),
}
extra = append(extra, extension.Transformers(prunerCR)...)
err := common.Transform(ctx, manifest, prunerCR, extra...)
Expand Down
Loading