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 @@ -835,7 +835,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
98 changes: 98 additions & 0 deletions pkg/reconciler/kubernetes/tektonpruner/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,105 @@ import (
"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
"github.com/tektoncd/operator/pkg/reconciler/common"
"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektoninstallerset/client"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
apimachineryRuntime "k8s.io/apimachinery/pkg/runtime"
)

// addDefaultResourceLimits injects pruner-specific resource limits based on benchmark data
// Overridable via TektonPruner.Spec.Options (options transformer runs last).
func addDefaultResourceLimits() mf.Transformer {
return func(u *unstructured.Unstructured) error {
kind := u.GetKind()
if kind != "Deployment" && kind != "StatefulSet" {
return nil
}

deploymentName := u.GetName()

// Only apply to pruner controller and webhook
if deploymentName != "tekton-pruner-controller" && deploymentName != "tekton-pruner-webhook" {
return nil
}

// Define default resources based on benchmark results
var defaultResources corev1.ResourceRequirements

if deploymentName == "tekton-pruner-controller" {
// Controller: handles PipelineRun/TaskRun informer cache
// Benchmark: 846-1126 MB for 26k-38k runs
// 2Gi limit provides safety margin for production scale
defaultResources = corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("256Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("2Gi"),
},
}
} else if deploymentName == "tekton-pruner-webhook" {
// Webhook: stateless, minimal memory footprint
defaultResources = corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("64Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("512Mi"),
},
}
}

// Both Deployment and StatefulSet have containers at spec.template.spec.containers
// Use unstructured path access to avoid duplication
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
}
}

func filterAndTransform(extension common.Extension) client.FilterAndTransform {
return func(ctx context.Context, manifest *mf.Manifest, comp v1alpha1.TektonComponent) (*mf.Manifest, error) {
prunerCR := comp.(*v1alpha1.TektonPruner)
Expand All @@ -36,6 +133,7 @@ func filterAndTransform(extension common.Extension) client.FilterAndTransform {
common.DeploymentImages(prunerImages),
common.AddDeploymentRestrictedPSA(),
common.AddConfigMapValues(PrunerConfigMapName, prunerCR.Spec.TektonPrunerConfig),
addDefaultResourceLimits(), // Add default resource limits (can be overridden via Options)
}
extra = append(extra, extension.Transformers(prunerCR)...)
err := common.Transform(ctx, manifest, prunerCR, extra...)
Expand Down
Loading