Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions api/v1alpha1/pattern_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ 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), "DeleteSpokeChildApps" (Phase 1: Delete child applications from spoke clusters), "DeleteSpoke" (Phase 2: Delete app of apps from spoke),
// "DeleteHubChildApps" (Phase 3: Delete applications from hub), "DeleteHub" (Phase 4: Delete app of apps from hub)
DeletionPhase PatternDeletionPhase `json:"deletionPhase,omitempty"`
}

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

type PatternDeletionPhase string

const (
InitializeDeletion PatternDeletionPhase = ""
DeleteSpokeChildApps PatternDeletionPhase = "DeleteSpokeChildApps"
DeleteSpoke PatternDeletionPhase = "DeleteSpoke"
DeleteHubChildApps PatternDeletionPhase = "DeleteHubChildApps"
DeleteHub PatternDeletionPhase = "DeleteHub"
)

func init() {
SchemeBuilder.Register(&Pattern{}, &PatternList{})
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,14 @@ spec:
- type
type: object
type: array
deletionPhase:
description: "DeletionPhase tracks the current phase of pattern deletion\nValues:
\"\" (not deleting), \"DeleteSpokeChildApps\" (Phase 1: Delete child
applications from spoke clusters), \"DeleteSpoke\" (Phase 2: Delete
app of apps from spoke),\n\t\t\t\t \"DeleteHubChildApps\" (Phase
3: Delete applications from hub), \"DeleteHub\" (Phase 4: Delete
app of 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
91 changes: 65 additions & 26 deletions internal/controller/acm.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,45 +22,84 @@ import (
"fmt"
"log"

kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)

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

serverNamespace := ""

cms, err := r.fullClient.CoreV1().ConfigMaps("").List(context.TODO(), metav1.ListOptions{
LabelSelector: fmt.Sprintf("%v = %v", "ocm-configmap-type", "image-manifest"),
})
if (err != nil || len(cms.Items) == 0) && serverNamespace != "" {
cms, err = r.fullClient.CoreV1().ConfigMaps(serverNamespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: fmt.Sprintf("%v = %v", "ocm-configmap-type", "image-manifest"),
})
_, err := r.dynamicClient.Resource(gvrMCH).Namespace("open-cluster-management").Get(context.Background(), "multiclusterhub", metav1.GetOptions{})
if err != nil {
log.Printf("Error obtaining hub: %s\n", err)
return false
}
if err != nil || len(cms.Items) == 0 {
cms, err = r.fullClient.CoreV1().ConfigMaps("open-cluster-management").List(context.TODO(), metav1.ListOptions{
LabelSelector: fmt.Sprintf("%v = %v", "ocm-configmap-type", "image-manifest"),
})
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) {
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 {
log.Printf("config map error: %s\n", err.Error())
return false
return nil, fmt.Errorf("failed to list ManagedClusters: %w", err)
}
if len(cms.Items) == 0 {
log.Printf("No config map\n")
return false

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

umch, err := r.dynamicClient.Resource(gvrMCH).Namespace(ns).List(context.TODO(), metav1.ListOptions{})
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 {
log.Printf("Error obtaining hub: %s\n", err)
return false
} else if len(umch.Items) == 0 {
log.Printf("No hub in %s\n", ns)
return false
return 0, fmt.Errorf("failed to list ManagedClusters: %w", err)
}
return true

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
}
18 changes: 3 additions & 15 deletions internal/controller/acm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,29 +43,17 @@ var _ = Describe("HaveACMHub", func() {

Context("when the ACM Hub exists", func() {
BeforeEach(func() {
configMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-configmap",
Namespace: "default",
Labels: map[string]string{
"ocm-configmap-type": "image-manifest",
},
},
}
_, err := kubeClient.CoreV1().ConfigMaps("default").Create(context.Background(), configMap, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())

hub := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "operator.open-cluster-management.io/v1",
"kind": "MultiClusterHub",
"metadata": map[string]any{
"name": "test-hub",
"namespace": "default",
"name": "multiclusterhub",
"namespace": "open-cluster-management",
},
},
}
_, err = dynamicClient.Resource(gvrMCH).Namespace("default").Create(context.Background(), hub, metav1.CreateOptions{})
_, err := dynamicClient.Resource(gvrMCH).Namespace("open-cluster-management").Create(context.Background(), hub, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
})

Expand Down
52 changes: 51 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 @@ -512,9 +513,22 @@ func newApplicationParameters(p *api.Pattern) []argoapi.HelmParameter {
}
}
if !p.DeletionTimestamp.IsZero() {
// Determine deletePattern value based on deletion phase

// Phase 1: Delete child applications from spoke clusters: DeleteSpokeChildApps
// Phase 2: Delete app of apps from spoke: DeleteSpoke
// Phase 3: Delete applications from hub: DeleteHubChildApps
// Phase 4: Delete app of apps from hub: DeleteHub

deletePatternValue := p.Status.DeletionPhase // default to the phase on the pattern object

// If we need to clean up child apps from the hub, we change it (clustergroup chart app creation logic)
if p.Status.DeletionPhase == api.DeleteHubChildApps {
deletePatternValue = "DeleteChildApps"
}
parameters = append(parameters, argoapi.HelmParameter{
Name: "global.deletePattern",
Value: "1",
Value: string(deletePatternValue),
ForceString: true,
})
}
Expand Down Expand Up @@ -1048,3 +1062,39 @@ func updateHelmParameter(goal api.PatternParameter, actual []argoapi.HelmParamet
}
return false
}

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

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

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

return 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