@@ -31,7 +31,6 @@ import (
3131 "time"
3232
3333 "github.com/go-logr/logr"
34- "github.com/samber/lo"
3534
3635 kerrors "k8s.io/apimachinery/pkg/api/errors"
3736 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -524,6 +523,102 @@ func (r *PatternReconciler) applyDefaults(input *api.Pattern) (*api.Pattern, err
524523 return output , nil
525524}
526525
526+ func (r * PatternReconciler ) updateDeletionPhase (instance * api.Pattern , phase api.PatternDeletionPhase ) error {
527+ log .Printf ("Updating deletion phase to '%s'" , phase )
528+ instance .Status .DeletionPhase = phase
529+ if err := r .Client .Status ().Update (context .TODO (), instance ); err != nil {
530+ return fmt .Errorf ("failed to update deletion phase: %w" , err )
531+ }
532+
533+ // Re-fetch to get updated status
534+ if err := r .Get (context .TODO (), client .ObjectKeyFromObject (instance ), instance ); err != nil {
535+ return fmt .Errorf ("failed to re-fetch pattern after phase update: %w" , err )
536+ }
537+
538+ return nil
539+ }
540+
541+ func (r * PatternReconciler ) deleteSpokeApps (instance * api.Pattern , targetApp , app * argoapi.Application , namespace string ) error {
542+ log .Printf ("Deletion phase: %s - checking if all child applications are gone from spoke" , api .DeletingSpokeApps )
543+
544+ // Update application with deletePattern=2 to trigger spoke deletion
545+ if changed , _ := updateApplication (r .argoClient , targetApp , app , namespace ); changed {
546+ return fmt .Errorf ("updated application %q for spoke deletion" , app .Name )
547+ }
548+ if app .Status .Sync .Status == argoapi .SyncStatusCodeOutOfSync {
549+ inProgress , err := syncApplicationWithPrune (r .argoClient , app )
550+ if err != nil {
551+ return err
552+ }
553+ if inProgress {
554+ return fmt .Errorf ("sync with prune and force is already in progress for application %q" , app .Name )
555+ }
556+ }
557+
558+ childApps , err := getChildApplications (r .argoClient , app )
559+ if err != nil {
560+ return err
561+ } else {
562+ for _ , childApp := range childApps {
563+ if _ , err := syncApplicationWithPrune (r .argoClient , & childApp ); err != nil {
564+ return err
565+ }
566+ }
567+ }
568+
569+ // Check if all child applications are gone from spoke
570+ allGone , err := r .checkSpokeChildApplicationsGone (instance )
571+ if err != nil {
572+ return fmt .Errorf ("error checking child applications: %w" , err )
573+ }
574+
575+ if ! allGone {
576+ log .Printf ("Waiting for all child applications to be deleted from spoke clusters" )
577+ return fmt .Errorf ("waiting for child applications to be deleted from spoke clusters" )
578+ }
579+
580+ return nil
581+ }
582+
583+ func (r * PatternReconciler ) deleteHubApps (targetApp , app * argoapi.Application , namespace string ) error {
584+ log .Printf ("Deletion phase: %s - deleting from hub" , api .DeletingHubApps )
585+
586+ // Delete managed clusters (excluding local-cluster)
587+ // These must be removed before hub deletion can proceed because ACM won't delete properly if they exist
588+ if haveACMHub (r ) {
589+ deletedCount , err := r .deleteManagedClusters (context .TODO ())
590+ if err != nil {
591+ return fmt .Errorf ("failed to delete managed clusters: %w" , err )
592+ }
593+
594+ if deletedCount > 0 {
595+ log .Printf ("Deleted %d managed cluster(s), waiting for them to be fully removed" , deletedCount )
596+ return fmt .Errorf ("deleted %d managed cluster(s), waiting for removal to complete before proceeding with hub deletion" , deletedCount )
597+ }
598+
599+ // Update application with deletePattern=1 to trigger hub deletion
600+ if changed , _ := updateApplication (r .argoClient , targetApp , app , namespace ); changed {
601+ return fmt .Errorf ("updated application %q for hub deletion" , app .Name )
602+ }
603+
604+ inProgress , err := syncApplicationWithPrune (r .argoClient , app )
605+ if err != nil {
606+ return err
607+ }
608+ if inProgress {
609+ return fmt .Errorf ("sync with prune and force is already in progress for application %q" , app .Name )
610+ }
611+
612+ return fmt .Errorf ("waiting for removal of that acm hub" )
613+ }
614+
615+ log .Printf ("Removing the application, and cascading to anything instantiated by ArgoCD" )
616+ if err := removeApplication (r .argoClient , app .Name , namespace ); err != nil {
617+ return err
618+ }
619+ return fmt .Errorf ("waiting for application %q to be removed" , app .Name )
620+ }
621+
527622func (r * PatternReconciler ) finalizeObject (instance * api.Pattern ) error {
528623 // Add finalizer when object is created
529624 log .Printf ("Finalizing pattern object" )
@@ -553,94 +648,36 @@ func (r *PatternReconciler) finalizeObject(instance *api.Pattern) error {
553648 }
554649
555650 // Initialize deletion phase if not set
556- if qualifiedInstance .Status .DeletionPhase == "" {
557- log .Printf ("Initializing deletion phase: deletingSpokeApps" )
558- qualifiedInstance .Status .DeletionPhase = "deletingSpokeApps"
559- if err := r .Client .Status ().Update (context .TODO (), qualifiedInstance ); err != nil {
560- return fmt .Errorf ("failed to update deletion phase: %w" , err )
561- }
562- // Re-fetch to get updated status
563- if err := r .Get (context .TODO (), client .ObjectKeyFromObject (qualifiedInstance ), qualifiedInstance ); err != nil {
564- return fmt .Errorf ("failed to re-fetch pattern after phase update: %w" , err )
565- }
566- }
567-
568- // Phase 1: Delete applications from spoke clusters
569- if qualifiedInstance .Status .DeletionPhase == "deletingSpokeApps" {
570- log .Printf ("Deletion phase: deletingSpokeApps - checking if all child applications are gone from spoke" )
571-
572- // Update application with deletePattern=2 to trigger spoke deletion
573- if changed , _ := updateApplication (r .argoClient , targetApp , app , ns ); changed {
574- return fmt .Errorf ("updated application %q for spoke deletion" , app .Name )
575- }
576- if app .Status .Sync .Status == argoapi .SyncStatusCodeOutOfSync {
577- inProgress , err := syncApplicationWithPrune (r .argoClient , app , ns )
578- if err != nil {
651+ if qualifiedInstance .Status .DeletionPhase == api .InitializeDeletion {
652+ log .Printf ("Initializing deletion phase" )
653+ if haveACMHub (r ) {
654+ if err := r .updateDeletionPhase (qualifiedInstance , api .DeletingSpokeApps ); err != nil {
579655 return err
580656 }
581- if inProgress {
582- return fmt .Errorf ("sync with prune and force is already in progress for application %q" , app .Name )
657+ } else {
658+ if err := r .updateDeletionPhase (qualifiedInstance , api .DeletingHubApps ); err != nil {
659+ return err
583660 }
584661 }
662+ }
585663
586- // Check if all child applications are gone from spoke
587- allGone , err := r .checkSpokeChildApplicationsGone (qualifiedInstance )
588- if err != nil {
589- return fmt .Errorf ("error checking child applications: %w" , err )
590- }
591-
592- if ! allGone {
593- log .Printf ("Waiting for all child applications to be deleted from spoke clusters" )
594- return fmt .Errorf ("waiting for child applications to be deleted from spoke clusters" )
664+ // Phase 1: Delete applications from spoke clusters
665+ if qualifiedInstance .Status .DeletionPhase == api .DeletingSpokeApps {
666+ if err := r .deleteSpokeApps (qualifiedInstance , targetApp , app , ns ); err != nil {
667+ return err
595668 }
596669
597- // All child applications are gone, now transition to phase 2
598- // The app-of-apps removal from spoke is handled by the helm chart when deletePattern=2
599- log .Printf ("All child applications are gone, transitioning to deletingHubApps phase" )
600- qualifiedInstance .Status .DeletionPhase = "deletingHubApps"
601- if err := r .Client .Status ().Update (context .TODO (), qualifiedInstance ); err != nil {
602- return fmt .Errorf ("failed to update deletion phase to deletingHubApps: %w" , err )
670+ log .Printf ("All child applications are gone, transitioning to %s phase" , api .DeletingHubApps )
671+ if err := r .updateDeletionPhase (qualifiedInstance , api .DeletingHubApps ); err != nil {
672+ return err
603673 }
604674 }
605675
606676 // Phase 2: Delete applications from hub
607- if qualifiedInstance .Status .DeletionPhase == "deletingHubApps" {
608- log .Printf ("Deletion phase: deletingHubApps - deleting from hub" )
609-
610- // Delete managed clusters (excluding local-cluster)
611- // These must be removed before hub deletion can proceed because ACM won't delete properly if they exist
612- if haveACMHub (r ) {
613- deletedCount , err := r .deleteManagedClusters (context .TODO ())
614- if err != nil {
615- return fmt .Errorf ("failed to delete managed clusters: %w" , err )
616- }
617-
618- if deletedCount > 0 {
619- log .Printf ("Deleted %d managed cluster(s), waiting for them to be fully removed" , deletedCount )
620- return fmt .Errorf ("deleted %d managed cluster(s), waiting for removal to complete before proceeding with hub deletion" , deletedCount )
621- }
622-
623- // Update application with deletePattern=1 to trigger hub deletion
624- if changed , _ := updateApplication (r .argoClient , targetApp , app , ns ); changed {
625- return fmt .Errorf ("updated application %q for hub deletion" , app .Name )
626- }
627-
628- inProgress , err := syncApplicationWithPrune (r .argoClient , app , ns )
629- if err != nil {
630- return err
631- }
632- if inProgress {
633- return fmt .Errorf ("sync with prune and force is already in progress for application %q" , app .Name )
634- }
635-
636- return fmt .Errorf ("waiting for removal of that acm hub" )
637- }
638-
639- log .Printf ("Removing the application, and cascading to anything instantiated by ArgoCD" )
640- if err := removeApplication (r .argoClient , app .Name , ns ); err != nil {
677+ if qualifiedInstance .Status .DeletionPhase == api .DeletingHubApps {
678+ if err := r .deleteHubApps (targetApp , app , ns ); err != nil {
641679 return err
642680 }
643- return fmt .Errorf ("waiting for application %q to be removed" , app .Name )
644681 }
645682 }
646683
@@ -905,24 +942,31 @@ func (r *PatternReconciler) checkSpokeChildApplicationsGone(p *api.Pattern) (boo
905942 }
906943
907944 // Parse JSON response
908- var searchResponse map [string ]any
945+ type SearchAPIResponse struct {
946+ Data struct {
947+ SearchResult []struct {
948+ Items []struct {
949+ Name string `json:"name"`
950+ Namespace string `json:"namespace"`
951+ Cluster string `json:"cluster"`
952+ } `json:"items"`
953+ } `json:"searchResult"`
954+ } `json:"data"`
955+ }
956+ var searchResponse SearchAPIResponse
909957 if err := json .Unmarshal (body , & searchResponse ); err != nil {
910958 return false , fmt .Errorf ("failed to parse JSON response: %w" , err )
911959 }
912960
913- raw := searchResponse ["data" ].(map [string ]any )["searchResult" ].([]any )[0 ].(map [string ]any )["items" ].([]any )
914-
915- // Convert []any → []map[string]any
916- items := lo .Map (raw , func (i any , _ int ) map [string ]any {
917- return i .(map [string ]any )
918- })
919-
920- remote_app_names := lo .Map (items , func (item map [string ]any , _ int ) string {
921- return item ["name" ].(string )
922- })
961+ var remote_app_names []string
962+ if searchResult := searchResponse .Data .SearchResult ; len (searchResult ) > 0 {
963+ for _ , item := range searchResult [0 ].Items {
964+ remote_app_names = append (remote_app_names , fmt .Sprintf ("%s/%s in %s" , item .Namespace , item .Name , item .Cluster ))
965+ }
966+ }
923967
924968 if len (remote_app_names ) != 0 {
925- return false , fmt .Errorf ("Cluster apps still exists : %s" , remote_app_names )
969+ return false , fmt .Errorf ("spoke cluster apps still exist : %s" , remote_app_names )
926970 }
927971
928972 return true , nil
0 commit comments