diff --git a/applicationset/controllers/applicationset_controller.go b/applicationset/controllers/applicationset_controller.go index 2f30c5da6d..729963340d 100644 --- a/applicationset/controllers/applicationset_controller.go +++ b/applicationset/controllers/applicationset_controller.go @@ -377,6 +377,11 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque if err != nil { return ctrl.Result{}, fmt.Errorf("failed to get current applications for application set: %w", err) } + + if err := r.setOrphanedLabels(ctx, logCtx, applicationSetInfo, currentApplications, generatedApplications); err != nil { + logCtx.WithError(err).Error("failed to update orphaned application labels") + } + err = r.updateResourcesStatus(ctx, logCtx, &applicationSetInfo, currentApplications) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to update resources status for application set: %w", err) @@ -860,6 +865,43 @@ func (r *ApplicationSetReconciler) getCurrentApplications(ctx context.Context, a return current.Items, nil } +// setOrphanedLabels adds the orphaned-by-applicationset label to Applications that exist in the +// cluster but are no longer produced by the generator (and therefore kept only by a create-only +// policy). It also removes the label from Applications that have come back into the desired set. +func (r *ApplicationSetReconciler) setOrphanedLabels(ctx context.Context, logCtx *log.Entry, appSet argov1alpha1.ApplicationSet, current []argov1alpha1.Application, desired []argov1alpha1.Application) error { + desiredMap := make(map[string]bool, len(desired)) + for _, app := range desired { + desiredMap[app.Name] = true + } + + for i := range current { + app := ¤t[i] + isOrphaned := !desiredMap[app.Name] + _, hasLabel := app.Labels[common.LabelKeyOrphanedByApplicationSet] + + if isOrphaned == hasLabel { + continue + } + + updated := app.DeepCopy() + if isOrphaned { + if updated.Labels == nil { + updated.Labels = map[string]string{} + } + updated.Labels[common.LabelKeyOrphanedByApplicationSet] = appSet.Name + logCtx.Infof("Marking application %q as orphaned by ApplicationSet %q", app.Name, appSet.Name) + } else { + delete(updated.Labels, common.LabelKeyOrphanedByApplicationSet) + logCtx.Infof("Removing orphan label from application %q", app.Name) + } + + if err := r.Patch(ctx, updated, client.MergeFrom(app)); err != nil { + return fmt.Errorf("failed to patch orphan label on application %q: %w", app.Name, err) + } + } + return nil +} + // deleteInCluster will delete Applications that are currently on the cluster, but not in appList. // The function must be called after all generators had been called and generated applications func (r *ApplicationSetReconciler) deleteInCluster(ctx context.Context, logCtx *log.Entry, applicationSet argov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error { diff --git a/common/common.go b/common/common.go index 4406da6ef9..4e1032778e 100644 --- a/common/common.go +++ b/common/common.go @@ -192,6 +192,10 @@ const ( LabelKeyAppName = "app.kubernetes.io/name" // LabelKeyAutoLabelClusterInfo if set to true will automatically add extra labels from the cluster info (currently it only adds a k8s version label) LabelKeyAutoLabelClusterInfo = "argocd.argoproj.io/auto-label-cluster-info" + // LabelKeyOrphanedByApplicationSet is set on Application objects that are no longer produced by + // an ApplicationSet generator but are preserved by a create-only sync policy. The label value + // is the name of the owning ApplicationSet. + LabelKeyOrphanedByApplicationSet = "argocd.argoproj.io/orphaned-by-applicationset" // LabelKeyLegacyApplicationName is the legacy label (v0.10 and below) and is superseded by 'app.kubernetes.io/instance' LabelKeyLegacyApplicationName = "applications.argoproj.io/app-name" // LabelKeySecretType contains the type of argocd secret (currently: 'cluster', 'repository', 'repo-config' or 'repo-creds') diff --git a/ui/src/app/applications/components/application-details/application-details.tsx b/ui/src/app/applications/components/application-details/application-details.tsx index f7f5f2c435..a118e55d27 100644 --- a/ui/src/app/applications/components/application-details/application-details.tsx +++ b/ui/src/app/applications/components/application-details/application-details.tsx @@ -115,6 +115,15 @@ export const ApplicationDetails: FC>(new Set()); + + useEffect(() => { + services.applications + .list([], 'application', {fields: ['items.metadata.name'], selector: 'argocd.argoproj.io/orphaned-by-applicationset'}) + .then(apps => setOrphanedAppNames(new Set(apps.items.map(a => a.metadata.name)))) + .catch(() => {/* non-critical, ignore */}); + }, []); + const getAppNamespace = useCallback(() => { if (typeof props.match.params.appnamespace === 'undefined') { return ''; @@ -734,6 +743,7 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat showCompactNodes: pref.groupNodes, userMsgs: pref.userHelpTipMsgs, tree, + orphanedAppNames, onClearFilter: clearFilter, onGroupdNodeClick: (nodeIds: string[]) => openGroupNodeDetails(nodeIds), zoom: pref.zoom, diff --git a/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx b/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx index 8f775f01c7..8e9940b42a 100644 --- a/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx +++ b/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx @@ -76,6 +76,7 @@ export interface ApplicationResourceTreeProps { nameWrap: boolean; setNodeExpansion: (node: string, isExpanded: boolean) => any; getNodeExpansion: (node: string) => boolean; + orphanedAppNames?: Set; } interface Line { @@ -821,6 +822,13 @@ function renderResourceNode(props: ApplicationResourceTreeProps, id: string, nod {node.hook && } {healthState != null && } {comparisonStatus != null && } + {appNode && !rootNode && props.orphanedAppNames?.has(node.name) && ( + + )} {appNode && !rootNode && ( {ctx => { diff --git a/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx b/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx index 7a83359667..d12ed65667 100644 --- a/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx +++ b/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx @@ -229,6 +229,7 @@ export const ApplicationStatusPanel = ({application, showDiff, showOperation, sh const infos = cntByCategory.get('info'); const warnings = cntByCategory.get('warning'); const errors = cntByCategory.get('error'); + const orphanedByAppSet = application.metadata.labels?.['argocd.argoproj.io/orphaned-by-applicationset']; const source = getAppDefaultSource(application); const hasMultipleSources = application.spec.sources?.length > 0; const revisionType = source?.repoURL?.startsWith('oci://') ? 'oci' : source?.chart ? 'helm' : 'git'; @@ -381,6 +382,19 @@ export const ApplicationStatusPanel = ({application, showDiff, showOperation, sh )} + {orphanedByAppSet && ( +
+ {sectionHeader({title: 'ORPHANED'})} + +
+ No longer generated by {orphanedByAppSet}. Kept by create-only policy. +
+
+ )} {app.status.sync.status} + {app.metadata.labels?.['argocd.argoproj.io/orphaned-by-applicationset'] && ( + <> +
+ + Orphaned + + + )} (