Skip to content

Commit 5455bd2

Browse files
committed
Add AsyncOperationInProgress to ExternalObservation
When AsyncOperationInProgress is true, the managed reconciler sets Synced=False with reason ReconcilePending instead of ReconcileSuccess. This prevents a false Synced=True signal during long-running async operations (e.g. MSK cluster instance type changes). Note: go.mod contains a temporary replace directive pointing at ajnye/crossplane for the ReconcilePending condition constructor. This will be updated to the upstream commit once crossplane/crossplane#7352 merges. Fixes #941 Signed-off-by: Aj Nye <aj.nye@appian.com>
1 parent c416af5 commit 5455bd2

5 files changed

Lines changed: 101 additions & 4 deletions

File tree

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,5 @@ require (
200200
sigs.k8s.io/randfill v1.0.0 // indirect
201201
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect
202202
)
203+
204+
replace github.com/crossplane/crossplane/apis/v2 => github.com/ajnye/crossplane/apis/v2 v2.0.0-20260429153315-46c3b9a56f05

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgv
6666
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
6767
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
6868
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
69+
github.com/ajnye/crossplane/apis/v2 v2.0.0-20260429153315-46c3b9a56f05 h1:oLTNu18VEUJ4MIwUDu7R6Mogw63TiwCgLiAYrcQLctM=
70+
github.com/ajnye/crossplane/apis/v2 v2.0.0-20260429153315-46c3b9a56f05/go.mod h1:h7KE74Z4TFs1L/FFv3RdsiG9Uax7L56oHpcggSZnONg=
6971
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
7072
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
7173
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
@@ -136,8 +138,6 @@ github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSw
136138
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
137139
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
138140
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
139-
github.com/crossplane/crossplane/apis/v2 v2.0.0-20260424160951-8f231230ebb6 h1:9ki6AJQgBJIcLNjK+scUZp2ZDenuAo18d0JSNOlkY2Y=
140-
github.com/crossplane/crossplane/apis/v2 v2.0.0-20260424160951-8f231230ebb6/go.mod h1:h7KE74Z4TFs1L/FFv3RdsiG9Uax7L56oHpcggSZnONg=
141141
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q=
142142
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
143143
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=

pkg/reconciler/managed/reconciler.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,13 @@ type ExternalObservation struct {
536536
// finding where the observed diverges from the desired state.
537537
// The string should be a cmp.Diff that details the difference.
538538
Diff string
539+
540+
// AsyncOperationInProgress indicates that an asynchronous operation
541+
// (e.g. a long-running cloud API call) is currently in progress for
542+
// this resource. When true, the managed reconciler will set
543+
// Synced=False with reason ReconcilePending instead of
544+
// ReconcileSuccess, and will not call Update().
545+
AsyncOperationInProgress bool
539546
}
540547

541548
// An ExternalCreation is the result of the creation of an external resource.
@@ -1496,8 +1503,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
14961503
// https://github.com/crossplane/crossplane/issues/289
14971504
reconcileAfter := r.pollIntervalHook(managed, r.effectivePollInterval(managed))
14981505
log.Debug("External resource is up to date", "requeue-after", time.Now().Add(reconcileAfter))
1499-
status.MarkConditions(xpv2.ReconcileSuccess())
1500-
r.metricRecorder.recordFirstTimeReady(managed)
1506+
1507+
if observation.AsyncOperationInProgress {
1508+
log.Debug("Async operation in progress, setting ReconcilePending")
1509+
status.MarkConditions(xpv2.ReconcilePending("Async operation in progress"))
1510+
} else {
1511+
status.MarkConditions(xpv2.ReconcileSuccess())
1512+
r.metricRecorder.recordFirstTimeReady(managed)
1513+
}
15011514

15021515
// record that we intentionally did not update the managed resource
15031516
// because no drift was detected. We call this so late in the reconcile

pkg/reconciler/managed/reconciler_legacy_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,47 @@ func TestReconciler(t *testing.T) {
11211121
},
11221122
want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}},
11231123
},
1124+
"ExternalResourceUpToDateAsyncOperationInProgress": {
1125+
reason: "When an async operation is in progress, Synced should be set to ReconcilePending instead of ReconcileSuccess.",
1126+
args: args{
1127+
m: &fake.Manager{
1128+
Client: &test.MockClient{
1129+
MockGet: legacyManagedMockGetFn(nil, 42),
1130+
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
1131+
want := newLegacyManaged(42)
1132+
want.SetConditions(xpv2.ReconcilePending("Async operation in progress").WithObservedGeneration(42))
1133+
1134+
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
1135+
reason := "An async-in-progress reconcile should set ReconcilePending, not ReconcileSuccess."
1136+
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
1137+
}
1138+
1139+
return nil
1140+
}),
1141+
},
1142+
Scheme: fake.SchemeWith(&fake.LegacyManaged{}),
1143+
},
1144+
mg: resource.ManagedKind(fake.GVK(&fake.LegacyManaged{})),
1145+
o: []ReconcilerOption{
1146+
WithInitializers(),
1147+
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
1148+
WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) {
1149+
return &ExternalClientFns{
1150+
ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
1151+
return ExternalObservation{
1152+
ResourceExists: true,
1153+
ResourceUpToDate: true,
1154+
AsyncOperationInProgress: true,
1155+
}, nil
1156+
},
1157+
DisconnectFn: func(_ context.Context) error { return nil },
1158+
}, nil
1159+
})),
1160+
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
1161+
},
1162+
},
1163+
want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}},
1164+
},
11241165
"ExternalResourceUpToDateWithJitter": {
11251166
reason: "When the external resource exists and is up to date a requeue should be triggered after a long wait with jitter added.",
11261167
args: args{

pkg/reconciler/managed/reconciler_modern_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,47 @@ func TestModernReconciler(t *testing.T) {
11271127
},
11281128
want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}},
11291129
},
1130+
"ExternalResourceUpToDateAsyncOperationInProgress": {
1131+
reason: "When an async operation is in progress, Synced should be set to ReconcilePending instead of ReconcileSuccess.",
1132+
args: args{
1133+
m: &fake.Manager{
1134+
Client: &test.MockClient{
1135+
MockGet: modernManagedMockGetFn(nil, 42),
1136+
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
1137+
want := newModernManaged(42)
1138+
want.SetConditions(xpv2.ReconcilePending("Async operation in progress").WithObservedGeneration(42))
1139+
1140+
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
1141+
reason := "An async-in-progress reconcile should set ReconcilePending, not ReconcileSuccess."
1142+
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
1143+
}
1144+
1145+
return nil
1146+
}),
1147+
},
1148+
Scheme: fake.SchemeWith(&fake.ModernManaged{}),
1149+
},
1150+
mg: resource.ManagedKind(fake.GVK(&fake.ModernManaged{})),
1151+
o: []ReconcilerOption{
1152+
WithInitializers(),
1153+
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
1154+
WithExternalConnector(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) {
1155+
return &ExternalClientFns{
1156+
ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
1157+
return ExternalObservation{
1158+
ResourceExists: true,
1159+
ResourceUpToDate: true,
1160+
AsyncOperationInProgress: true,
1161+
}, nil
1162+
},
1163+
DisconnectFn: func(_ context.Context) error { return nil },
1164+
}, nil
1165+
})),
1166+
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
1167+
},
1168+
},
1169+
want: want{result: reconcile.Result{RequeueAfter: defaultPollInterval}},
1170+
},
11301171
"ExternalResourceUpToDateWithJitter": {
11311172
reason: "When the external resource exists and is up to date a requeue should be triggered after a long wait with jitter added.",
11321173
args: args{

0 commit comments

Comments
 (0)