Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Set operation state to nil if new operation has been initiated by a direct object update (#20875) #20915

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ const (
// can be disregarded.
AnnotationIgnoreHealthCheck = "argocd.argoproj.io/ignore-healthcheck"

// AnnotationAllowPatchingOperationTestOnly when set the patch operations also can update the operation field.
// Intended for use in tests only. Keep in sync with tests using argocd.argoproj.io~1allow-patching-operation-test-only or
// argocd.argoproj.io/allow-patching-operation-test-only directly.
AnnotationAllowPatchingOperationTestOnly = "argocd.argoproj.io/allow-patching-operation-test-only"

// AnnotationKeyManagedBy is annotation name which indicates that k8s resource is managed by an application.
AnnotationKeyManagedBy = "managed-by"
// AnnotationValueManagedByArgoCD is a 'managed-by' annotation value for resources managed by Argo CD
Expand Down
26 changes: 26 additions & 0 deletions controller/appcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -1927,6 +1927,27 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new
return patchMs
}

// resetAppStatusOperationState updates the app status operation state to be nil. If no changes were made, it is a no-op
func (ctrl *ApplicationController) resetAppStatusOperationState(orig *appv1.Application) {
logCtx := getAppLog(orig).WithField("method", "resetAppStatusOperationState")
if orig.Status.OperationState == nil {
return
}
newStatus := orig.Status.DeepCopy()
newStatus.OperationState = nil
patch, _, err := createMergePatch(
&appv1.Application{Status: orig.Status},
&appv1.Application{Status: *newStatus})
if err != nil {
logCtx.Errorf("Error constructing app status patch: %v", err)
return
}
_, err = ctrl.PatchAppWithWriteBack(context.Background(), orig.Name, orig.Namespace, types.MergePatchType, patch, metav1.PatchOptions{})
if err != nil {
logCtx.Warnf("Error updating application: %v", err)
}
}

// autoSync will initiate a sync operation for an application configured with automated sync
func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *appv1.SyncStatus, resources []appv1.ResourceStatus, revisionUpdated bool) (*appv1.ApplicationCondition, time.Duration) {
logCtx := getAppLog(app)
Expand Down Expand Up @@ -2318,6 +2339,11 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar
jitter := time.Duration(float64(ctrl.statusRefreshJitter) * rand.Float64())
delay = &jitter
}
// Something updated application operation state bypassing the controller and
// operation state in status still reflects the old sync. Reset it.
if oldApp.Operation == nil && newApp.Operation != nil && newApp.Status.OperationState != nil {
ctrl.resetAppStatusOperationState(newApp)
}
}

ctrl.requestAppRefresh(newApp.QualifiedName(), compareWith, delay)
Expand Down
6 changes: 4 additions & 2 deletions server/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -937,12 +937,14 @@ func (s *Server) updateApp(app *appv1.Application, newApp *appv1.Application, ct
app.Labels = newApp.Labels
app.Annotations = newApp.Annotations
}

if app.Annotations[argocommon.AnnotationAllowPatchingOperationTestOnly] == "true" {
app.Operation = newApp.Operation
}
app.Finalizers = newApp.Finalizers

res, err := s.appclientset.ArgoprojV1alpha1().Applications(app.Namespace).Update(ctx, app, metav1.UpdateOptions{})
if err == nil {
s.logAppEvent(app, ctx, argo.EventReasonResourceUpdated, "updated application spec")
s.logAppEvent(app, ctx, argo.EventReasonResourceUpdated, "updated application")
s.waitSync(res)
return res, nil
}
Expand Down
33 changes: 33 additions & 0 deletions server/application/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1972,6 +1972,39 @@ func TestAppJsonPatch(t *testing.T) {
app, err = appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: ptr.To(`[{"op": "remove", "path": "/metadata/annotations/test.annotation"}]`)})
require.NoError(t, err)
assert.NotContains(t, app.Annotations, "test.annotation")

app, err = appServer.Patch(
ctx,
&application.ApplicationPatchRequest{
Name: &testApp.Name,
Patch: ptr.To(fmt.Sprintf(
`[
{
"op": "add",
"path": "/metadata/annotations",
"value": {
"%s": "true"
}
},
{
"op": "add",
"path": "/operation",
"value": {
"sync": {
"revisions": ["test"]
},
"initiatedBy": {
"username": "test"
}
}
}
]`,
common.AnnotationAllowPatchingOperationTestOnly,
)),
},
)
require.NoError(t, err)
assert.NotNil(t, app.Operation)
}

func TestAppMergePatch(t *testing.T) {
Expand Down
40 changes: 40 additions & 0 deletions test/e2e/app_management_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3001,3 +3001,43 @@ func TestDeletionConfirmation(t *testing.T) {
When().ConfirmDeletion().
Then().Expect(DoesNotExist())
}

func TestNewOperationTriggeredByUpdatingAppObject(t *testing.T) {
Given(t).
Path(guestbookPath).
When().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy)).
When().
// Keep annotation value in sync with common.AnnotationAllowPatchingOperationTestOnly
PatchApp(fmt.Sprintf(
`[
{
"op": "add",
"path": "/metadata/annotations",
"value": {
"%s": "true"
}
},
{
"op": "add",
"path": "/operation",
"value": {
"sync": {
"revisions": ["test"]
},
"initiatedBy": {
"username": "test"
}
}
}
]`,
common.AnnotationAllowPatchingOperationTestOnly,
)).
Then().
Expect(OperationStateIsNil())
}
7 changes: 7 additions & 0 deletions test/e2e/fixture/app/expectation.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ func OperationMessageContains(text string) Expectation {
}
}

func OperationStateIsNil() Expectation {
return func(c *Consequences) (state, string) {
actual := c.app().Status.OperationState
return simple(actual == nil, fmt.Sprintf("operation state should be nil, is %s", actual))
}
}

func simple(success bool, message string) (state, string) {
if success {
return succeeded, message
Expand Down