Skip to content

Commit 2575c6b

Browse files
authored
Merge pull request #469 from dminnear-rh/pattern-delete
Update pattern deletion logic
2 parents 2bff99d + c7e481c commit 2575c6b

8 files changed

Lines changed: 511 additions & 55 deletions

File tree

api/v1alpha1/pattern_types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,11 @@ type PatternStatus struct {
206206
AnalyticsUUID string `json:"analyticsUUID,omitempty"`
207207
// +operator-sdk:csv:customresourcedefinitions:type=status
208208
LocalCheckoutPath string `json:"path,omitempty"`
209+
// +operator-sdk:csv:customresourcedefinitions:type=status
210+
// DeletionPhase tracks the current phase of pattern deletion
211+
// Values: "" (not deleting), "DeleteSpokeChildApps" (Phase 1: Delete child applications from spoke clusters), "DeleteSpoke" (Phase 2: Delete app of apps from spoke),
212+
// "DeleteHubChildApps" (Phase 3: Delete applications from hub), "DeleteHub" (Phase 4: Delete app of apps from hub)
213+
DeletionPhase PatternDeletionPhase `json:"deletionPhase,omitempty"`
209214
}
210215

211216
// See: https://book.kubebuilder.io/reference/markers/crd.html
@@ -262,6 +267,16 @@ const (
262267
Suspended PatternConditionType = "Suspended"
263268
)
264269

270+
type PatternDeletionPhase string
271+
272+
const (
273+
InitializeDeletion PatternDeletionPhase = ""
274+
DeleteSpokeChildApps PatternDeletionPhase = "DeleteSpokeChildApps"
275+
DeleteSpoke PatternDeletionPhase = "DeleteSpoke"
276+
DeleteHubChildApps PatternDeletionPhase = "DeleteHubChildApps"
277+
DeleteHub PatternDeletionPhase = "DeleteHub"
278+
)
279+
265280
func init() {
266281
SchemeBuilder.Register(&Pattern{}, &PatternList{})
267282
}

config/crd/bases/gitops.hybrid-cloud-patterns.io_patterns.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,14 @@ spec:
224224
- type
225225
type: object
226226
type: array
227+
deletionPhase:
228+
description: "DeletionPhase tracks the current phase of pattern deletion\nValues:
229+
\"\" (not deleting), \"DeleteSpokeChildApps\" (Phase 1: Delete child
230+
applications from spoke clusters), \"DeleteSpoke\" (Phase 2: Delete
231+
app of apps from spoke),\n\t\t\t\t \"DeleteHubChildApps\" (Phase
232+
3: Delete applications from hub), \"DeleteHub\" (Phase 4: Delete
233+
app of apps from hub)"
234+
type: string
227235
lastError:
228236
description: Last error encountered by the pattern
229237
type: string

config/rbac/role.yaml

Lines changed: 13 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:
@@ -104,6 +111,12 @@ rules:
104111
- list
105112
- patch
106113
- update
114+
- apiGroups:
115+
- view.open-cluster-management.io
116+
resources:
117+
- managedclusterviews
118+
verbs:
119+
- create
107120
---
108121
apiVersion: rbac.authorization.k8s.io/v1
109122
kind: Role

internal/controller/acm.go

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,45 +22,84 @@ import (
2222
"fmt"
2323
"log"
2424

25+
kerrors "k8s.io/apimachinery/pkg/api/errors"
2526
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2627
"k8s.io/apimachinery/pkg/runtime/schema"
2728
)
2829

2930
func haveACMHub(r *PatternReconciler) bool {
3031
gvrMCH := schema.GroupVersionResource{Group: "operator.open-cluster-management.io", Version: "v1", Resource: "multiclusterhubs"}
3132

32-
serverNamespace := ""
33-
34-
cms, err := r.fullClient.CoreV1().ConfigMaps("").List(context.TODO(), metav1.ListOptions{
35-
LabelSelector: fmt.Sprintf("%v = %v", "ocm-configmap-type", "image-manifest"),
36-
})
37-
if (err != nil || len(cms.Items) == 0) && serverNamespace != "" {
38-
cms, err = r.fullClient.CoreV1().ConfigMaps(serverNamespace).List(context.TODO(), metav1.ListOptions{
39-
LabelSelector: fmt.Sprintf("%v = %v", "ocm-configmap-type", "image-manifest"),
40-
})
33+
_, err := r.dynamicClient.Resource(gvrMCH).Namespace("open-cluster-management").Get(context.Background(), "multiclusterhub", metav1.GetOptions{})
34+
if err != nil {
35+
log.Printf("Error obtaining hub: %s\n", err)
36+
return false
4137
}
42-
if err != nil || len(cms.Items) == 0 {
43-
cms, err = r.fullClient.CoreV1().ConfigMaps("open-cluster-management").List(context.TODO(), metav1.ListOptions{
44-
LabelSelector: fmt.Sprintf("%v = %v", "ocm-configmap-type", "image-manifest"),
45-
})
38+
return true
39+
}
40+
41+
// listManagedClusters lists all ManagedCluster resources (excluding local-cluster)
42+
// Returns a list of cluster names and an error
43+
func (r *PatternReconciler) listManagedClusters(ctx context.Context) ([]string, error) {
44+
gvrMC := schema.GroupVersionResource{
45+
Group: "cluster.open-cluster-management.io",
46+
Version: "v1",
47+
Resource: "managedclusters",
4648
}
49+
50+
// ManagedCluster is a cluster-scoped resource, so no namespace needed
51+
mcList, err := r.dynamicClient.Resource(gvrMC).List(ctx, metav1.ListOptions{})
4752
if err != nil {
48-
log.Printf("config map error: %s\n", err.Error())
49-
return false
53+
return nil, fmt.Errorf("failed to list ManagedClusters: %w", err)
5054
}
51-
if len(cms.Items) == 0 {
52-
log.Printf("No config map\n")
53-
return false
55+
56+
var clusterNames []string
57+
for _, item := range mcList.Items {
58+
name := item.GetName()
59+
// Exclude local-cluster (hub cluster)
60+
if name != "local-cluster" {
61+
clusterNames = append(clusterNames, name)
62+
}
5463
}
55-
ns := cms.Items[0].Namespace
5664

57-
umch, err := r.dynamicClient.Resource(gvrMCH).Namespace(ns).List(context.TODO(), metav1.ListOptions{})
65+
return clusterNames, nil
66+
}
67+
68+
// deleteManagedClusters deletes all ManagedCluster resources (excluding local-cluster)
69+
// Returns the number of clusters deleted and an error
70+
func (r *PatternReconciler) deleteManagedClusters(ctx context.Context) (int, error) {
71+
gvrMC := schema.GroupVersionResource{
72+
Group: "cluster.open-cluster-management.io",
73+
Version: "v1",
74+
Resource: "managedclusters",
75+
}
76+
77+
// ManagedCluster is a cluster-scoped resource, so no namespace needed
78+
mcList, err := r.dynamicClient.Resource(gvrMC).List(ctx, metav1.ListOptions{})
5879
if err != nil {
59-
log.Printf("Error obtaining hub: %s\n", err)
60-
return false
61-
} else if len(umch.Items) == 0 {
62-
log.Printf("No hub in %s\n", ns)
63-
return false
80+
return 0, fmt.Errorf("failed to list ManagedClusters: %w", err)
6481
}
65-
return true
82+
83+
deletedCount := 0
84+
for _, item := range mcList.Items {
85+
name := item.GetName()
86+
// Exclude local-cluster (hub cluster)
87+
if name == "local-cluster" {
88+
continue
89+
}
90+
91+
// Delete the managed cluster
92+
err := r.dynamicClient.Resource(gvrMC).Delete(ctx, name, metav1.DeleteOptions{})
93+
if err != nil {
94+
// If already deleted, that's fine
95+
if kerrors.IsNotFound(err) {
96+
continue
97+
}
98+
return deletedCount, fmt.Errorf("failed to delete ManagedCluster %q: %w", name, err)
99+
}
100+
log.Printf("Deleted ManagedCluster: %q", name)
101+
deletedCount++
102+
}
103+
104+
return deletedCount, nil
66105
}

internal/controller/acm_test.go

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,29 +43,17 @@ var _ = Describe("HaveACMHub", func() {
4343

4444
Context("when the ACM Hub exists", func() {
4545
BeforeEach(func() {
46-
configMap := &v1.ConfigMap{
47-
ObjectMeta: metav1.ObjectMeta{
48-
Name: "test-configmap",
49-
Namespace: "default",
50-
Labels: map[string]string{
51-
"ocm-configmap-type": "image-manifest",
52-
},
53-
},
54-
}
55-
_, err := kubeClient.CoreV1().ConfigMaps("default").Create(context.Background(), configMap, metav1.CreateOptions{})
56-
Expect(err).ToNot(HaveOccurred())
57-
5846
hub := &unstructured.Unstructured{
5947
Object: map[string]any{
6048
"apiVersion": "operator.open-cluster-management.io/v1",
6149
"kind": "MultiClusterHub",
6250
"metadata": map[string]any{
63-
"name": "test-hub",
64-
"namespace": "default",
51+
"name": "multiclusterhub",
52+
"namespace": "open-cluster-management",
6553
},
6654
},
6755
}
68-
_, err = dynamicClient.Resource(gvrMCH).Namespace("default").Create(context.Background(), hub, metav1.CreateOptions{})
56+
_, err := dynamicClient.Resource(gvrMCH).Namespace("open-cluster-management").Create(context.Background(), hub, metav1.CreateOptions{})
6957
Expect(err).ToNot(HaveOccurred())
7058
})
7159

internal/controller/argo.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"log"
2323
"os"
24+
"slices"
2425
"strconv"
2526
"strings"
2627

@@ -512,9 +513,22 @@ func newApplicationParameters(p *api.Pattern) []argoapi.HelmParameter {
512513
}
513514
}
514515
if !p.DeletionTimestamp.IsZero() {
516+
// Determine deletePattern value based on deletion phase
517+
518+
// Phase 1: Delete child applications from spoke clusters: DeleteSpokeChildApps
519+
// Phase 2: Delete app of apps from spoke: DeleteSpoke
520+
// Phase 3: Delete applications from hub: DeleteHubChildApps
521+
// Phase 4: Delete app of apps from hub: DeleteHub
522+
523+
deletePatternValue := p.Status.DeletionPhase // default to the phase on the pattern object
524+
525+
// If we need to clean up child apps from the hub, we change it (clustergroup chart app creation logic)
526+
if p.Status.DeletionPhase == api.DeleteHubChildApps {
527+
deletePatternValue = "DeleteChildApps"
528+
}
515529
parameters = append(parameters, argoapi.HelmParameter{
516530
Name: "global.deletePattern",
517-
Value: "1",
531+
Value: string(deletePatternValue),
518532
ForceString: true,
519533
})
520534
}
@@ -1048,3 +1062,39 @@ func updateHelmParameter(goal api.PatternParameter, actual []argoapi.HelmParamet
10481062
}
10491063
return false
10501064
}
1065+
1066+
// syncApplication syncs the application with prune and force options if such a sync is not already in progress.
1067+
// Returns nil if a sync is already in progress, error otherwise
1068+
func syncApplication(client argoclient.Interface, app *argoapi.Application, withPrune bool) error {
1069+
if app.Operation != nil && app.Operation.Sync != nil && app.Operation.Sync.Prune == withPrune && slices.Contains(app.Operation.Sync.SyncOptions, "Force=true") {
1070+
return nil
1071+
}
1072+
1073+
app.Operation = &argoapi.Operation{
1074+
Sync: &argoapi.SyncOperation{
1075+
Prune: withPrune,
1076+
SyncOptions: []string{"Force=true"},
1077+
},
1078+
}
1079+
1080+
_, err := client.ArgoprojV1alpha1().Applications(app.Namespace).Update(context.Background(), app, metav1.UpdateOptions{})
1081+
if err != nil {
1082+
return fmt.Errorf("failed to sync application %q with 'prune: %t': %w", app.Name, withPrune, err)
1083+
}
1084+
1085+
return nil
1086+
}
1087+
1088+
// returns the child applications owned by the app-of-apps parentApp
1089+
func getChildApplications(client argoclient.Interface, parentApp *argoapi.Application) ([]argoapi.Application, error) {
1090+
listOptions := metav1.ListOptions{
1091+
LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", parentApp.Name),
1092+
}
1093+
1094+
appList, err := client.ArgoprojV1alpha1().Applications("").List(context.Background(), listOptions)
1095+
if err != nil {
1096+
return nil, fmt.Errorf("failed to list child applications of %s: %w", parentApp.Name, err)
1097+
}
1098+
1099+
return appList.Items, nil
1100+
}

0 commit comments

Comments
 (0)