Skip to content

Commit ce76df7

Browse files
committed
fix: force sync to child apps and remove dependency on samber/lo
1 parent 4b62934 commit ce76df7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+166
-26782
lines changed

api/v1alpha1/pattern_types.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ type PatternStatus struct {
209209
// +operator-sdk:csv:customresourcedefinitions:type=status
210210
// DeletionPhase tracks the current phase of pattern deletion
211211
// Values: "" (not deleting), "deletingSpokeApps" (phase 1: delete apps from spoke), "deletingHubApps" (phase 2: delete apps from hub)
212-
DeletionPhase string `json:"deletionPhase,omitempty"`
212+
DeletionPhase PatternDeletionPhase `json:"deletionPhase,omitempty"`
213213
}
214214

215215
// See: https://book.kubebuilder.io/reference/markers/crd.html
@@ -266,6 +266,14 @@ const (
266266
Suspended PatternConditionType = "Suspended"
267267
)
268268

269+
type PatternDeletionPhase string
270+
271+
const (
272+
InitializeDeletion PatternDeletionPhase = ""
273+
DeletingSpokeApps PatternDeletionPhase = "DeletingSpokeApps"
274+
DeletingHubApps PatternDeletionPhase = "DeletingHubApps"
275+
)
276+
269277
func init() {
270278
SchemeBuilder.Register(&Pattern{}, &PatternList{})
271279
}

config/rbac/role.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ rules:
4444
- list
4545
- patch
4646
- update
47+
- apiGroups:
48+
- cluster.open-cluster-management.io
49+
resources:
50+
- managedclusters
51+
verbs:
52+
- delete
53+
- list
4754
- apiGroups:
4855
- config.openshift.io
4956
resources:

go.mod

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ require (
2727
sigs.k8s.io/controller-runtime v0.21.0
2828
)
2929

30-
require (
31-
github.com/argoproj/argo-cd/v3 v3.0.19
32-
github.com/samber/lo v1.52.0
33-
)
30+
require github.com/argoproj/argo-cd/v3 v3.0.19
3431

3532
require (
3633
cloud.google.com/go/compute/metadata v0.9.0 // indirect

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -410,8 +410,6 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
410410
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
411411
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
412412
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
413-
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
414-
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
415413
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
416414
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
417415
github.com/segmentio/analytics-go/v3 v3.3.0 h1:8VOMaVGBW03pdBrj1CMFfY9o/rnjJC+1wyQHlVxjw5o=

internal/controller/argo.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ func newApplicationParameters(p *api.Pattern) []argoapi.HelmParameter {
430430
// Phase 1 (deletingSpokeApps): deletePattern = "2" (delete apps from spoke)
431431
// Phase 2 (deletingHubApps): deletePattern = "1" (delete apps from hub)
432432
deletePatternValue := "2" // default to spoke deletion
433-
if p.Status.DeletionPhase == "deletingHubApps" {
433+
if p.Status.DeletionPhase == api.DeletingHubApps {
434434
deletePatternValue = "1"
435435
}
436436
parameters = append(parameters, argoapi.HelmParameter{
@@ -972,7 +972,7 @@ func updateHelmParameter(goal api.PatternParameter, actual []argoapi.HelmParamet
972972

973973
// syncApplicationWithPrune syncs the application with prune and force options if such a sync is not already in progress.
974974
// Returns true if a sync with prune and force is already in progress, false otherwise
975-
func syncApplicationWithPrune(client argoclient.Interface, app *argoapi.Application, namespace string) (bool, error) {
975+
func syncApplicationWithPrune(client argoclient.Interface, app *argoapi.Application) (bool, error) {
976976
if app.Operation != nil && app.Operation.Sync != nil && app.Operation.Sync.Prune && slices.Contains(app.Operation.Sync.SyncOptions, "Force=true") {
977977
return true, nil
978978
}
@@ -984,10 +984,24 @@ func syncApplicationWithPrune(client argoclient.Interface, app *argoapi.Applicat
984984
},
985985
}
986986

987-
_, err := client.ArgoprojV1alpha1().Applications(namespace).Update(context.Background(), app, metav1.UpdateOptions{})
987+
_, err := client.ArgoprojV1alpha1().Applications(app.Namespace).Update(context.Background(), app, metav1.UpdateOptions{})
988988
if err != nil {
989989
return false, fmt.Errorf("failed to sync application %q with prune: %w", app.Name, err)
990990
}
991991

992992
return true, nil
993993
}
994+
995+
// returns the child applications owned by the app-of-apps parentApp
996+
func getChildApplications(client argoclient.Interface, parentApp *argoapi.Application) ([]argoapi.Application, error) {
997+
listOptions := metav1.ListOptions{
998+
LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", parentApp.Name),
999+
}
1000+
1001+
appList, err := client.ArgoprojV1alpha1().Applications("").List(context.Background(), listOptions)
1002+
if err != nil {
1003+
return nil, fmt.Errorf("failed to list child applications of %s: %w", parentApp.Name, err)
1004+
}
1005+
1006+
return appList.Items, nil
1007+
}

internal/controller/pattern_controller.go

Lines changed: 132 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
527622
func (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

Comments
 (0)