From 91bda4e0f6b18d87f4987b4096c67ec838ddec7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabien=20Castar=C3=A8de?= Date: Thu, 18 Dec 2025 12:42:30 +0100 Subject: [PATCH 1/3] Fix duplicate resource creation after pod restart for async resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a provider pod restarts, the ephemeral Terraform workspace state stored in /tmp// is lost. For resources with UseAsync=true, this causes the Refresh operation to fail to detect existing resources, triggering duplicate resource creation. This fix detects async resources that were previously created by checking for the crossplane.io/external-create-succeeded annotation and uses Import instead of Refresh. Import reconstructs state directly from the cloud provider API, avoiding the duplicate creation issue. Signed-off-by: Fabien Castarède --- pkg/controller/external.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/controller/external.go b/pkg/controller/external.go index e2af1520..eb5047cc 100644 --- a/pkg/controller/external.go +++ b/pkg/controller/external.go @@ -215,6 +215,19 @@ func (e *external) Observe(ctx context.Context, mg xpresource.Managed) (managed. return e.Import(ctx, tr) } + // For async resources that were previously created, use Import instead + // of Refresh if the resource has been successfully created before. + // This prevents duplicate resource creation after provider pod restarts + // when the ephemeral workspace state in /tmp is lost. + // The external-create-succeeded annotation persists in Kubernetes and + // indicates the resource was successfully created or imported previously. + if e.config.UseAsync && meta.GetExternalName(tr) != "" { + annotations := tr.GetAnnotations() + if _, hasCreateSucceeded := annotations["crossplane.io/external-create-succeeded"]; hasCreateSucceeded { + return e.Import(ctx, tr) + } + } + res, err := e.workspace.Refresh(ctx) if err != nil { return managed.ExternalObservation{}, errors.Wrap(err, errRefresh) From d14aeba0d41b611ac55a7198d22a8338208901fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabien=20Castar=C3=A8de?= Date: Thu, 18 Dec 2025 15:49:23 +0100 Subject: [PATCH 2/3] Add debug logging to async resource Import fallback logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fabien Castarède --- pkg/controller/external.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/controller/external.go b/pkg/controller/external.go index eb5047cc..1266b1c3 100644 --- a/pkg/controller/external.go +++ b/pkg/controller/external.go @@ -224,8 +224,12 @@ func (e *external) Observe(ctx context.Context, mg xpresource.Managed) (managed. if e.config.UseAsync && meta.GetExternalName(tr) != "" { annotations := tr.GetAnnotations() if _, hasCreateSucceeded := annotations["crossplane.io/external-create-succeeded"]; hasCreateSucceeded { + e.logger.Debug("Using Import instead of Refresh for async resource with external-create-succeeded annotation", "external-name", meta.GetExternalName(tr)) return e.Import(ctx, tr) } + e.logger.Debug("Async resource missing external-create-succeeded annotation, using Refresh", "external-name", meta.GetExternalName(tr), "annotations", annotations) + } else { + e.logger.Debug("Not using Import fallback", "useAsync", e.config.UseAsync, "externalName", meta.GetExternalName(tr)) } res, err := e.workspace.Refresh(ctx) From 6797ec85f30b07274e951e298b8405fcb730427b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabien=20Castar=C3=A8de?= Date: Thu, 18 Dec 2025 15:55:52 +0100 Subject: [PATCH 3/3] Add missing meta package import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fabien Castarède --- pkg/controller/external.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/controller/external.go b/pkg/controller/external.go index 1266b1c3..50f86b39 100644 --- a/pkg/controller/external.go +++ b/pkg/controller/external.go @@ -10,6 +10,7 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + "github.com/crossplane/crossplane-runtime/v2/pkg/meta" "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" xpresource "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/pkg/errors"