@@ -21,6 +21,8 @@ import (
2121 "github.com/michelangelo-ai/michelangelo/go/api"
2222 apiHandler "github.com/michelangelo-ai/michelangelo/go/api/handler"
2323 "github.com/michelangelo-ai/michelangelo/go/base/env"
24+ "github.com/michelangelo-ai/michelangelo/go/components/pipelinerun"
25+ "github.com/michelangelo-ai/michelangelo/go/components/triggerrun"
2426 apipb "github.com/michelangelo-ai/michelangelo/proto-go/api"
2527 v2pb "github.com/michelangelo-ai/michelangelo/proto-go/api/v2"
2628 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -41,9 +43,11 @@ const (
4143// operations and maintains environment context and logging capabilities.
4244type Reconciler struct {
4345 api.Handler
44- env env.Context
45- logger * zap.Logger
46- apiHandlerFactory apiHandler.Factory
46+ env env.Context
47+ logger * zap.Logger
48+ apiHandlerFactory apiHandler.Factory
49+ triggerRunManager triggerrun.Manager
50+ pipelineRunManager pipelinerun.Manager
4751}
4852
4953// Reconcile is the main reconciliation loop entry point for Pipeline resources.
@@ -75,14 +79,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
7579 }
7680 }
7781 } else {
78- // Pipeline is being deleted — remove finalizer to allow deletion
79- // Cascade delete logic will be added in a subsequent PR
80- logger .Info ("Pipeline is being deleted, removing finalizer" )
81- controllerutil .RemoveFinalizer (pipeline , api .PipelineFinalizer )
82- if err := r .Update (ctx , pipeline , & metav1.UpdateOptions {}); err != nil {
83- return ctrl.Result {}, fmt .Errorf ("remove pipeline finalizer: %w" , err )
84- }
85- return ctrl.Result {}, nil
82+ return r .handleDeletion (ctx , pipeline , logger )
8683 }
8784
8885 originalPipeline := pipeline .DeepCopy ()
@@ -139,6 +136,55 @@ func (r *Reconciler) updatePipelineStatus(ctx context.Context, pipeline *v2pb.Pi
139136 return result , nil
140137}
141138
139+ func (r * Reconciler ) handleDeletion (ctx context.Context , pipeline * v2pb.Pipeline , logger * zap.Logger ) (ctrl.Result , error ) {
140+ // If the finalizer is not present we don't own this deletion; nothing to cascade.
141+ // Avoids wasted list/kill/delete work on pipelines that pre-date the finalizer rollout.
142+ if ! controllerutil .ContainsFinalizer (pipeline , api .PipelineFinalizer ) {
143+ return ctrl.Result {}, nil
144+ }
145+ logger .Info ("Pipeline is being deleted, starting cascade delete" )
146+
147+ triggerRuns , err := r .triggerRunManager .ListTriggerRunsForPipeline (ctx , pipeline .Namespace , pipeline .Name )
148+ if err != nil {
149+ logger .Error ("Failed to list trigger runs for cascade delete" ,
150+ zap .Error (err ),
151+ zap .String ("operation" , "list_trigger_runs" ),
152+ zap .String ("namespace" , pipeline .Namespace ),
153+ zap .String ("name" , pipeline .Name ))
154+ return ctrl.Result {}, fmt .Errorf ("list trigger runs for pipeline %s/%s: %w" , pipeline .Namespace , pipeline .Name , err )
155+ }
156+
157+ pipelineRuns , err := r .pipelineRunManager .ListPipelineRunsForPipeline (ctx , pipeline .Namespace , pipeline .Name )
158+ if err != nil {
159+ logger .Error ("Failed to list pipeline runs for cascade delete" ,
160+ zap .Error (err ),
161+ zap .String ("operation" , "list_pipeline_runs" ),
162+ zap .String ("namespace" , pipeline .Namespace ),
163+ zap .String ("name" , pipeline .Name ))
164+ return ctrl.Result {}, fmt .Errorf ("list pipeline runs for pipeline %s/%s: %w" , pipeline .Namespace , pipeline .Name , err )
165+ }
166+
167+ if len (triggerRuns ) == 0 && len (pipelineRuns ) == 0 {
168+ logger .Info ("No children found, removing finalizer" )
169+ controllerutil .RemoveFinalizer (pipeline , api .PipelineFinalizer )
170+ if updateErr := r .Update (ctx , pipeline , & metav1.UpdateOptions {}); updateErr != nil {
171+ logger .Error ("Failed to remove finalizer after cascade delete" ,
172+ zap .Error (updateErr ),
173+ zap .String ("operation" , "remove_finalizer" ),
174+ zap .String ("namespace" , pipeline .Namespace ),
175+ zap .String ("name" , pipeline .Name ))
176+ return ctrl.Result {}, fmt .Errorf ("remove finalizer on pipeline %s/%s: %w" , pipeline .Namespace , pipeline .Name , updateErr )
177+ }
178+ return ctrl.Result {}, nil
179+ }
180+
181+ // Kill and delete steps will be added in subsequent PRs
182+ logger .Info ("Children found, requeueing for cascade delete" ,
183+ zap .Int ("triggerRuns" , len (triggerRuns )),
184+ zap .Int ("pipelineRuns" , len (pipelineRuns )))
185+ return ctrl.Result {RequeueAfter : reconcileInterval }, nil
186+ }
187+
142188// formatRevisionName generates a standardized revision name for a pipeline.
143189//
144190// The name format is: "pipeline-{lowercase-pipeline-name}-{git-ref-prefix}"
@@ -176,6 +222,8 @@ func (r *Reconciler) Register(mgr ctrl.Manager) error {
176222 return err
177223 }
178224 r .Handler = handler
225+ r .triggerRunManager = triggerrun .NewManager (mgr .GetClient (), r .logger )
226+ r .pipelineRunManager = pipelinerun .NewManager (mgr .GetClient (), r .logger )
179227 return ctrl .NewControllerManagedBy (mgr ).
180228 For (& v2pb.Pipeline {}).
181229 Complete (r )
0 commit comments