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
8 changes: 5 additions & 3 deletions pkg/watcher/reconciler/pipelinerun/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,11 @@ func NewControllerWithConfig(ctx context.Context, resultsClient pb.ResultsClient
impl := pipelinerunreconciler.NewImpl(ctx, c, func(_ *controller.Impl) controller.Options {
return controller.Options{
// This results pipelinerun reconciler shouldn't mutate the pipelinerun's status.
SkipStatusUpdates: true,
ConfigStore: configStore,
FinalizerName: "results.tekton.dev/pipelinerun",
SkipStatusUpdates: true,
ConfigStore: configStore,
FinalizerName: "results.tekton.dev/pipelinerun",
UseServerSideApplyForFinalizers: true,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a feature flag or is it safe/stable enough to hardcode as enabled?

Copy link
Copy Markdown
Contributor Author

@enarha enarha May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I contributed that functionality to Knative, but it's an opt-in feature, so not sure really how widely it's used. IMO that's too low level stuff and not really related to Tekton or the Results functionality, to expose to users to configure and only makes the configuration even more complex. With that said, on the project level we can take more cautious approach and only enable it to a single component before enabling it to all. The real benefit though will come when all components/controllers acting on the same objects enable it.
The only risk IMO is the transition from merge patch to SSA for the in-flight objects. I was sure with SSA this works (SSA will update the object unless there is a clash of ownership - another SSA manager updated the same field), but I'm testing this scenario now and it looks like it does not. I'm looking into that now, thus put the PR on hold.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aThorp96 to add just one more thing, we already use SSA for managing the annotations. That was done almost a year ago. Now we switch the finalizers management. So in terms of "technology", we already use SSA for some time now.

FinalizerFieldManager: "tekton-results-watcher/finalizers",
}
})

Expand Down
79 changes: 79 additions & 0 deletions pkg/watcher/reconciler/pipelinerun/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
package pipelinerun

import (
"bytes"
"context"
"encoding/json"
"fmt"
"time"

Expand All @@ -35,6 +37,8 @@ import (
pb "github.com/tektoncd/results/proto/v1alpha2/results_go_proto"
"go.uber.org/zap"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"knative.dev/pkg/controller"
"knative.dev/pkg/logging"
Expand Down Expand Up @@ -189,6 +193,25 @@ func (r *Reconciler) finalize(ctx context.Context, pr *pipelinev1.PipelineRun, r
return nil
}

// MIGRATION: Check if finalizer was added by merge patch (Update operation)
// If so, manually remove it before the SSA framework tries.
// This code path handles the upgrade scenario where in-flight PipelineRuns have
// finalizers set via merge patch, but the new controller uses SSA.
// This is a temporary migration feature and can be removed once all in-flight
// resources from the old version are deleted.

if r.isFinalizerOwnedByMergePatch(pr) {
logging.FromContext(ctx).Infof("Finalizer on %s/%s was set via merge patch, manually removing for migration",
pr.Namespace, pr.Name)
if err := r.removeFinalizerViaMergePatch(ctx, pr); err != nil {
logging.FromContext(ctx).Warnw("Failed to remove finalizer via merge patch during migration",
zap.Error(err))
// Don't fail - let SSA framework try anyway
}
// Successfully removed via merge patch, we're done
return nil
}

var storeDeadline, now time.Time

// Check if the store deadline is configured
Expand Down Expand Up @@ -239,3 +262,59 @@ func (r *Reconciler) finalize(ctx context.Context, pr *pipelinev1.PipelineRun, r

return nil
}

// isFinalizerOwnedByMergePatch checks if the finalizer was added via merge patch (Update operation).
// MIGRATION: This is a temporary migration feature to handle the upgrade scenario where
// in-flight PipelineRuns have finalizers set via merge patch by the old controller version.
// Kubernetes SSA treats (manager, Update) and (manager, Apply) as different owners, so we need
// to detect and handle the old ownership pattern.
// This function can be removed once all resources from the pre-SSA version are deleted.
func (r *Reconciler) isFinalizerOwnedByMergePatch(pr *pipelinev1.PipelineRun) bool {
for _, mf := range pr.ManagedFields {
// Check if this is from the old merge patch operation
if mf.Operation == metav1.ManagedFieldsOperationUpdate {
// Parse FieldsV1 to check if it owns the finalizers field
// FieldsV1 is a JSON structure, we need to check if it contains f:metadata.f:finalizers
if mf.FieldsV1 != nil && mf.FieldsV1.Raw != nil {
// Check if this managed field entry owns finalizers AND specifically our finalizer
if bytes.Contains(mf.FieldsV1.Raw, []byte(`"f:finalizers"`)) &&
bytes.Contains(mf.FieldsV1.Raw, []byte(`"results.tekton.dev/pipelinerun"`)) {
return true
}
}
}
}
return false
}

// removeFinalizerViaMergePatch removes the finalizer using merge patch.
// MIGRATION: This is a temporary migration feature to handle the upgrade scenario where
// in-flight PipelineRuns have finalizers set via merge patch by the old controller version.
// This uses merge patch to remove finalizers that cannot be removed via SSA due to different
// ownership (manager, Update) vs (manager, Apply).
// This function can be removed once all resources from the pre-SSA version are deleted.
func (r *Reconciler) removeFinalizerViaMergePatch(ctx context.Context, pr *pipelinev1.PipelineRun) error {
// Remove our finalizer from the list
var newFinalizers []string
for _, f := range pr.Finalizers {
if f != "results.tekton.dev/pipelinerun" {
newFinalizers = append(newFinalizers, f)
}
}

mergePatch := map[string]interface{}{
"metadata": map[string]interface{}{
"finalizers": newFinalizers,
"resourceVersion": pr.ResourceVersion,
},
}

patch, err := json.Marshal(mergePatch)
if err != nil {
return err
}

_, err = r.pipelineClient.TektonV1().PipelineRuns(pr.Namespace).Patch(
ctx, pr.Name, types.MergePatchType, patch, metav1.PatchOptions{})
return err
}
8 changes: 5 additions & 3 deletions pkg/watcher/reconciler/taskrun/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,11 @@ func NewControllerWithConfig(ctx context.Context, resultsClient pb.ResultsClient
impl := taskrunreconciler.NewImpl(ctx, c, func(_ *controller.Impl) controller.Options {
return controller.Options{
// This results taskrun reconciler shouldn't mutate the taskrun's status.
SkipStatusUpdates: true,
ConfigStore: configStore,
FinalizerName: "results.tekton.dev/taskrun",
SkipStatusUpdates: true,
ConfigStore: configStore,
FinalizerName: "results.tekton.dev/taskrun",
UseServerSideApplyForFinalizers: true,
FinalizerFieldManager: "tekton-results-watcher/finalizers",
}
})

Expand Down
78 changes: 78 additions & 0 deletions pkg/watcher/reconciler/taskrun/reconciler.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package taskrun

import (
"bytes"
"context"
"encoding/json"
"fmt"
"time"

Expand All @@ -21,6 +23,8 @@ import (
"github.com/tektoncd/results/pkg/watcher/results"
pb "github.com/tektoncd/results/proto/v1alpha2/results_go_proto"
"go.uber.org/zap"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"knative.dev/pkg/controller"
"knative.dev/pkg/logging"
Expand Down Expand Up @@ -125,6 +129,24 @@ func (r *Reconciler) finalize(ctx context.Context, tr *pipelinev1.TaskRun, rerr
return nil
}

// MIGRATION: Check if finalizer was added by merge patch (Update operation)
// If so, manually remove it before the SSA framework tries.
// This code path handles the upgrade scenario where in-flight TaskRuns have
// finalizers set via merge patch, but the new controller uses SSA.
// This is a temporary migration feature and can be removed once all in-flight
// resources from the old version are deleted.
if r.isFinalizerOwnedByMergePatch(tr) {
logging.FromContext(ctx).Infof("Finalizer on %s/%s was set via merge patch, manually removing for migration",
tr.Namespace, tr.Name)
if err := r.removeFinalizerViaMergePatch(ctx, tr); err != nil {
logging.FromContext(ctx).Warnw("Failed to remove finalizer via merge patch during migration",
zap.Error(err))
// Don't fail - let SSA framework try anyway
}
// Successfully removed via merge patch, we're done
return nil
}

now := time.Now().UTC()

// Check if the forwarding buffer is configured and passed
Expand Down Expand Up @@ -190,3 +212,59 @@ func (r *Reconciler) finalize(ctx context.Context, tr *pipelinev1.TaskRun, rerr

return nil
}

// isFinalizerOwnedByMergePatch checks if the finalizer was added via merge patch (Update operation).
// MIGRATION: This is a temporary migration feature to handle the upgrade scenario where
// in-flight TaskRuns have finalizers set via merge patch by the old controller version.
// Kubernetes SSA treats (manager, Update) and (manager, Apply) as different owners, so we need
// to detect and handle the old ownership pattern.
// This function can be removed once all resources from the pre-SSA version are deleted.
func (r *Reconciler) isFinalizerOwnedByMergePatch(tr *pipelinev1.TaskRun) bool {
for _, mf := range tr.ManagedFields {
// Check if this is from the old merge patch operation
if mf.Operation == metav1.ManagedFieldsOperationUpdate {
// Parse FieldsV1 to check if it owns the finalizers field
// FieldsV1 is a JSON structure, we need to check if it contains f:metadata.f:finalizers
if mf.FieldsV1 != nil && mf.FieldsV1.Raw != nil {
// Check if this managed field entry owns finalizers AND specifically our finalizer
if bytes.Contains(mf.FieldsV1.Raw, []byte(`"f:finalizers"`)) &&
bytes.Contains(mf.FieldsV1.Raw, []byte(`"results.tekton.dev/taskrun"`)) {
return true
}
}
}
}
return false
}

// removeFinalizerViaMergePatch removes the finalizer using merge patch.
// MIGRATION: This is a temporary migration feature to handle the upgrade scenario where
// in-flight TaskRuns have finalizers set via merge patch by the old controller version.
// This uses merge patch to remove finalizers that cannot be removed via SSA due to different
// ownership (manager, Update) vs (manager, Apply).
// This function can be removed once all resources from the pre-SSA version are deleted.
func (r *Reconciler) removeFinalizerViaMergePatch(ctx context.Context, tr *pipelinev1.TaskRun) error {
// Remove our finalizer from the list
var newFinalizers []string
for _, f := range tr.Finalizers {
if f != "results.tekton.dev/taskrun" {
newFinalizers = append(newFinalizers, f)
}
}

mergePatch := map[string]interface{}{
"metadata": map[string]interface{}{
"finalizers": newFinalizers,
"resourceVersion": tr.ResourceVersion,
},
}

patch, err := json.Marshal(mergePatch)
if err != nil {
return err
}

_, err = r.pipelineClient.TektonV1().TaskRuns(tr.Namespace).Patch(
ctx, tr.Name, types.MergePatchType, patch, metav1.PatchOptions{})
return err
}
Loading