Skip to content
Open
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
14 changes: 14 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,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,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
767 changes: 397 additions & 370 deletions bundle/manifests/patterns-operator.clusterserviceversion.yaml

Large diffs are not rendered by default.

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
4 changes: 2 additions & 2 deletions config/manager/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
newName: quay.io/validatedpatterns/patterns-operator
newTag: 0.0.64
newName: quay.io/aeros/patterns-operator
newTag: 6.6.8
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ spec:
path: clusterPlatform
- displayName: Cluster Version
path: clusterVersion
- 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)
displayName: Deletion Phase
path: deletionPhase
- description: Last error encountered by the pattern
displayName: Last Error
path: lastError
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
}
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 true if a sync with prune and force is already in progress, false otherwise
func syncApplication(client argoclient.Interface, app *argoapi.Application, withPrune bool) (bool, error) {
if app.Operation != nil && app.Operation.Sync != nil && app.Operation.Sync.Prune == withPrune && slices.Contains(app.Operation.Sync.SyncOptions, "Force=true") {
return true, 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 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