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
5 changes: 5 additions & 0 deletions cmd/app/view_pane.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ func renderSyncStatusBody(details *model.SyncStatusDetails, width int, now time.
terminateHint = "[t] terminate"
}
field("Phase", details.Phase, lipgloss.NewStyle().Foreground(phaseColor), terminateHint)
// Argo CD parks the operation in Running with no message when a resource
// carries Prune=confirm, so the reason has to come from the resource list.
if details.AwaitingPruneConfirmation {
field("Waiting on", "prune confirmation", lipgloss.NewStyle().Foreground(currentPalette.Warning))
}
field("Started", humantime.AgoLong(details.StartedAt, now), text)
duration := details.FinishedAt.Sub(details.StartedAt)
if details.FinishedAt.IsZero() {
Expand Down
24 changes: 24 additions & 0 deletions cmd/app/view_pane_operation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,27 @@ func TestSyncStatusPane_PlainSyncCarriesNoQualifier(t *testing.T) {
t.Errorf("expected a bare Sync, got %q", line)
}
}

func TestSyncStatusPane_ExplainsASyncHeldOnPruneConfirmation(t *testing.T) {
lines := renderSyncStatusBody(&model.SyncStatusDetails{
Operation: "Sync",
Phase: "Running",
AwaitingPruneConfirmation: true,
}, 46, time.Now(), "")

out := stripANSI(strings.Join(lines, "\n"))
if !strings.Contains(out, "confirmation") {
t.Errorf("expected the pane to say the sync is waiting for confirmation, got:\n%s", out)
}
}

func TestSyncStatusPane_SaysNothingAboutConfirmationOnAnOrdinarySync(t *testing.T) {
lines := renderSyncStatusBody(&model.SyncStatusDetails{
Operation: "Sync",
Phase: "Running",
}, 46, time.Now(), "")

if strings.Contains(stripANSI(strings.Join(lines, "\n")), "confirmation") {
t.Error("expected no confirmation notice on an ordinary running sync")
}
}
59 changes: 42 additions & 17 deletions pkg/api/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ type SyncOperation struct {
// DryRun and Resources qualify what the operation actually did: a dry run
// applies nothing, and a non-empty Resources means only a subset was synced.
DryRun bool `json:"dryRun,omitempty"`
Prune bool `json:"prune,omitempty"`
Resources []SyncResourceTarget `json:"resources,omitempty"`
}

Expand Down Expand Up @@ -668,17 +669,37 @@ func ConvertOperationState(argoApp ArgoApplication) *model.SyncStatusDetails {
}
operation, note := describeOperation(argoApp)
return &model.SyncStatusDetails{
Operation: operation,
OperationNote: note,
Phase: opState.Phase,
Message: shortenShas(flattenWhitespace(opState.Message)),
StartedAt: opState.StartedAt,
FinishedAt: opState.FinishedAt,
Revision: opState.resolvedRevision(),
InitiatedBy: opState.Operation.InitiatedBy.Username,
Automated: opState.Operation.InitiatedBy.Automated,
Resources: resources,
AwaitingPruneConfirmation: awaitingPruneConfirmation(argoApp),
Operation: operation,
OperationNote: note,
Phase: opState.Phase,
Message: shortenShas(flattenWhitespace(opState.Message)),
StartedAt: opState.StartedAt,
FinishedAt: opState.FinishedAt,
Revision: opState.resolvedRevision(),
InitiatedBy: opState.Operation.InitiatedBy.Username,
Automated: opState.Operation.InitiatedBy.Automated,
Resources: resources,
}
}

// awaitingPruneConfirmation reports a sync that has stopped and is waiting on
// a human. Argo CD leaves such an operation in Running indefinitely with no
// message, so without this the only visible state is a sync that never ends.
func awaitingPruneConfirmation(argoApp ArgoApplication) bool {
if argoApp.Status.OperationState.Phase != "Running" {
return false
}
sync := argoApp.Status.OperationState.Operation.Sync
if sync == nil || !sync.Prune {
return false
}
for _, r := range argoApp.Status.Resources {
if r.RequiresPruning && r.RequiresDeletionConfirmation {
return true
}
}
return false
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// describeOperation names what the last operation actually was. Argo CD
Expand Down Expand Up @@ -781,13 +802,17 @@ type ResourceTree struct {

// ResourceStatus holds sync/health status for a managed resource (from Application.status.resources[])
type ResourceStatus struct {
Group string `json:"group"`
Kind string `json:"kind"`
Name string `json:"name"`
Namespace string `json:"namespace,omitempty"`
Status string `json:"status"` // Sync status: "Synced", "OutOfSync"
Version string `json:"version"`
Health *ResourceHealth `json:"health,omitempty"`
Group string `json:"group"`
Kind string `json:"kind"`
Name string `json:"name"`
Namespace string `json:"namespace,omitempty"`
Status string `json:"status"` // Sync status: "Synced", "OutOfSync"
Version string `json:"version"`
Health *ResourceHealth `json:"health,omitempty"`
RequiresPruning bool `json:"requiresPruning,omitempty"`
// RequiresDeletionConfirmation is set by Argo CD when the resource carries
// Prune=confirm or Delete=confirm.
RequiresDeletionConfirmation bool `json:"requiresDeletionConfirmation,omitempty"`
}

// GetResourceTree retrieves the resource tree for an application
Expand Down
82 changes: 82 additions & 0 deletions pkg/api/applications_prune_confirmation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package api

import "testing"

func runningSyncOf(resources []ResourceStatus) ArgoApplication {
return ArgoApplication{
Metadata: ApplicationMetadata{Name: "demo-app"},
Status: ApplicationStatus{
Resources: resources,
OperationState: OperationState{
Phase: "Running",
Operation: Operation{Sync: &SyncOperation{Prune: true}},
},
},
}
}

func TestConvertOperationState_SyncWaitingOnPruneConfirmation_SaysWhyItIsStuck(t *testing.T) {
app := runningSyncOf([]ResourceStatus{
{Kind: "ConfigMap", Name: "settings"},
{Kind: "Namespace", Name: "legacy", RequiresPruning: true, RequiresDeletionConfirmation: true},
})

details := ConvertOperationState(app)

if !details.AwaitingPruneConfirmation {
t.Error("expected a sync held on prune confirmation to be reported as such")
}
}

func TestConvertOperationState_RunningSyncWithNothingToConfirm_IsJustRunning(t *testing.T) {
app := runningSyncOf([]ResourceStatus{{Kind: "ConfigMap", Name: "settings"}})

details := ConvertOperationState(app)

if details.AwaitingPruneConfirmation {
t.Error("expected no confirmation notice when no resource requires one")
}
}

func TestConvertOperationState_NonSyncOperationIsNotWaitingOnPruneConfirmation(t *testing.T) {
app := runningSyncOf([]ResourceStatus{{Kind: "Namespace", Name: "legacy", RequiresPruning: true, RequiresDeletionConfirmation: true}})
app.Status.OperationState.Operation.Sync = nil

details := ConvertOperationState(app)

if details.AwaitingPruneConfirmation {
t.Error("expected a non-sync operation not to report waiting on prune confirmation")
}
}

func TestConvertOperationState_SyncWithoutPruneIsNotWaitingOnPruneConfirmation(t *testing.T) {
app := runningSyncOf([]ResourceStatus{{Kind: "Namespace", Name: "legacy", RequiresPruning: true, RequiresDeletionConfirmation: true}})
app.Status.OperationState.Operation.Sync.Prune = false

details := ConvertOperationState(app)

if details.AwaitingPruneConfirmation {
t.Error("expected a sync with pruning disabled not to report waiting on prune confirmation")
}
}

func TestConvertOperationState_DeletionConfirmationWithoutPruningIsNotPruneConfirmation(t *testing.T) {
app := runningSyncOf([]ResourceStatus{{Kind: "Namespace", Name: "legacy", RequiresDeletionConfirmation: true}})

details := ConvertOperationState(app)

if details.AwaitingPruneConfirmation {
t.Error("expected deletion confirmation without pruning not to report waiting on prune confirmation")
}
}

func TestConvertOperationState_FinishedSync_IsNotWaitingOnAnything(t *testing.T) {
app := runningSyncOf([]ResourceStatus{{Kind: "Namespace", Name: "legacy", RequiresPruning: true, RequiresDeletionConfirmation: true}})
app.Status.OperationState.Phase = "Succeeded"

details := ConvertOperationState(app)

if details.AwaitingPruneConfirmation {
t.Error("expected a finished sync not to report waiting on confirmation")
}
}
23 changes: 13 additions & 10 deletions pkg/model/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,19 @@ type SyncResourceResult struct {
// SyncStatusDetails is the full last-operation state shown in the
// sync-status pane, including per-resource results.
type SyncStatusDetails struct {
Phase string `json:"phase"`
Message string `json:"message"`
StartedAt time.Time `json:"startedAt"`
FinishedAt time.Time `json:"finishedAt"` // zero while the operation is running
Revision string `json:"revision"`
InitiatedBy string `json:"initiatedBy"`
Automated bool `json:"automated"`
Operation string `json:"operation"`
OperationNote string `json:"operationNote,omitempty"`
Resources []SyncResourceResult `json:"resources"`
Phase string `json:"phase"`
Message string `json:"message"`
StartedAt time.Time `json:"startedAt"`
FinishedAt time.Time `json:"finishedAt"` // zero while the operation is running
Revision string `json:"revision"`
InitiatedBy string `json:"initiatedBy"`
Automated bool `json:"automated"`
Operation string `json:"operation"`
OperationNote string `json:"operationNote,omitempty"`
// AwaitingPruneConfirmation explains an operation that would otherwise sit
// in Running forever with no visible reason.
AwaitingPruneConfirmation bool `json:"awaitingPruneConfirmation,omitempty"`
Resources []SyncResourceResult `json:"resources"`
}

// App represents an ArgoCD application
Expand Down
Loading