Skip to content
12 changes: 12 additions & 0 deletions api/v1alpha1/pattern_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ type PatternStatus struct {
AnalyticsUUID string `json:"analyticsUUID,omitempty"`
// +operator-sdk:csv:customresourcedefinitions:type=status
LocalCheckoutPath string `json:"path,omitempty"`
// +operator-sdk:csv:customresourcedefinitions:type=status
// DeletionPhase tracks the current phase of pattern deletion
// Values: "" (not deleting), "deletingSpokeApps" (phase 1: delete apps from spoke), "deletingHubApps" (phase 2: delete apps from hub)
DeletionPhase PatternDeletionPhase `json:"deletionPhase,omitempty"`
}

// See: https://book.kubebuilder.io/reference/markers/crd.html
Expand Down Expand Up @@ -262,6 +266,14 @@ const (
Suspended PatternConditionType = "Suspended"
)

type PatternDeletionPhase string

const (
InitializeDeletion PatternDeletionPhase = ""
DeletingSpokeApps PatternDeletionPhase = "DeletingSpokeApps"
DeletingHubApps PatternDeletionPhase = "DeletingHubApps"
)

func init() {
SchemeBuilder.Register(&Pattern{}, &PatternList{})
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ spec:
- type
type: object
type: array
deletionPhase:
description: |-
DeletionPhase tracks the current phase of pattern deletion
Values: "" (not deleting), "deletingSpokeApps" (phase 1: delete apps from spoke), "deletingHubApps" (phase 2: delete apps from hub)
type: string
lastError:
description: Last error encountered by the pattern
type: string
Expand Down
13 changes: 13 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ rules:
- list
- patch
- update
- apiGroups:
- cluster.open-cluster-management.io
resources:
- managedclusters
verbs:
- delete
- list
- apiGroups:
- config.openshift.io
resources:
Expand Down Expand Up @@ -104,6 +111,12 @@ rules:
- list
- patch
- update
- apiGroups:
- view.open-cluster-management.io
resources:
- managedclusterviews
verbs:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
Expand Down
67 changes: 67 additions & 0 deletions internal/controller/acm.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"fmt"
"log"

kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
Expand Down Expand Up @@ -64,3 +65,69 @@
}
return true
}

// listManagedClusters lists all ManagedCluster resources (excluding local-cluster)
// Returns a list of cluster names and an error
func (r *PatternReconciler) listManagedClusters(ctx context.Context) ([]string, error) {

Check failure on line 71 in internal/controller/acm.go

View workflow job for this annotation

GitHub Actions / Run golangci-lint on PR

func (*PatternReconciler).listManagedClusters is unused (unused)
gvrMC := schema.GroupVersionResource{
Group: "cluster.open-cluster-management.io",
Version: "v1",
Resource: "managedclusters",
}

// ManagedCluster is a cluster-scoped resource, so no namespace needed
mcList, err := r.dynamicClient.Resource(gvrMC).List(ctx, metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to list ManagedClusters: %w", err)
}

var clusterNames []string
for _, item := range mcList.Items {
name := item.GetName()
// Exclude local-cluster (hub cluster)
if name != "local-cluster" {
clusterNames = append(clusterNames, name)
}
}

return clusterNames, nil
}

// deleteManagedClusters deletes all ManagedCluster resources (excluding local-cluster)
// Returns the number of clusters deleted and an error
func (r *PatternReconciler) deleteManagedClusters(ctx context.Context) (int, error) {
gvrMC := schema.GroupVersionResource{
Group: "cluster.open-cluster-management.io",
Version: "v1",
Resource: "managedclusters",
}

// ManagedCluster is a cluster-scoped resource, so no namespace needed
mcList, err := r.dynamicClient.Resource(gvrMC).List(ctx, metav1.ListOptions{})
if err != nil {
return 0, fmt.Errorf("failed to list ManagedClusters: %w", err)
}

deletedCount := 0
for _, item := range mcList.Items {
name := item.GetName()
// Exclude local-cluster (hub cluster)
if name == "local-cluster" {
continue
}

// Delete the managed cluster
err := r.dynamicClient.Resource(gvrMC).Delete(ctx, name, metav1.DeleteOptions{})
if err != nil {
// If already deleted, that's fine
if kerrors.IsNotFound(err) {
continue
}
return deletedCount, fmt.Errorf("failed to delete ManagedCluster %q: %w", name, err)
}
log.Printf("Deleted ManagedCluster: %q", name)
deletedCount++
}

return deletedCount, nil
}
46 changes: 45 additions & 1 deletion internal/controller/argo.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"log"
"os"
"slices"
"strconv"
"strings"

Expand Down Expand Up @@ -425,9 +426,16 @@ func newApplicationParameters(p *api.Pattern) []argoapi.HelmParameter {
}
}
if !p.DeletionTimestamp.IsZero() {
// Determine deletePattern value based on deletion phase
// Phase 1 (deletingSpokeApps): deletePattern = "2" (delete apps from spoke)
// Phase 2 (deletingHubApps): deletePattern = "1" (delete apps from hub)
deletePatternValue := "2" // default to spoke deletion
if p.Status.DeletionPhase == api.DeletingHubApps {
deletePatternValue = "1"
}
parameters = append(parameters, argoapi.HelmParameter{
Name: "global.deletePattern",
Value: "1",
Value: deletePatternValue,
ForceString: true,
})
}
Expand Down Expand Up @@ -961,3 +969,39 @@ func updateHelmParameter(goal api.PatternParameter, actual []argoapi.HelmParamet
}
return false
}

// syncApplicationWithPrune syncs the application with prune and force options if such a sync is not already in progress.
// Returns true if a sync with prune and force is already in progress, false otherwise
func syncApplicationWithPrune(client argoclient.Interface, app *argoapi.Application) (bool, error) {
if app.Operation != nil && app.Operation.Sync != nil && app.Operation.Sync.Prune && slices.Contains(app.Operation.Sync.SyncOptions, "Force=true") {
return true, nil
}

app.Operation = &argoapi.Operation{
Sync: &argoapi.SyncOperation{
Prune: true,
SyncOptions: []string{"Force=true"},
},
}

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

return true, nil
}

// returns the child applications owned by the app-of-apps parentApp
func getChildApplications(client argoclient.Interface, parentApp *argoapi.Application) ([]argoapi.Application, error) {
listOptions := metav1.ListOptions{
LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", parentApp.Name),
}

appList, err := client.ArgoprojV1alpha1().Applications("").List(context.Background(), listOptions)
if err != nil {
return nil, fmt.Errorf("failed to list child applications of %s: %w", parentApp.Name, err)
}

return appList.Items, nil
}
Loading
Loading