I put a ClusterSecretStore manifest alongside the External Secrets Operator Helm Application in the same ArgoCD parent. Sync-wave annotations set: ESO at wave 0, ClusterSecretStore at wave 1. Should work — ESO installs its CRDs in wave 0, ClusterSecretStore applies in wave 1 after they exist.
It didn’t work. The error came before either wave fired:
The Kubernetes API could not find external-secrets.io/ClusterSecretStore
for requested resource argocd/vault-backend.
Make sure the "ClusterSecretStore" CRD is installed on the destination cluster.
Why Sync-Waves Didn’t Help
Sync-waves control the order resources are applied. They don’t control when resources are validated.
ArgoCD runs a dry-run validation against the Kubernetes API for every resource in a sync — before wave 0 starts, before anything is applied. If any resource references a CRD that doesn’t exist yet, the entire sync is rejected upfront.
The sequence:
1. ArgoCD starts sync
2. Dry-run validation: checks all resources against the API
3. ClusterSecretStore CRD not registered → validation fails
4. Sync aborted — wave 0 never fires
5. ESO never installs → CRD never registers
Sync-waves are irrelevant here because the sync never reaches wave 0.
What Doesn’t Work
Adding SkipDryRunOnMissingResource=true to the manifest skips the validation. That fixes the immediate error but removes the safety net for all future syncs, not just this one. It’s the wrong tradeoff.
Putting the CRD manifest in the same Application as the ClusterSecretStore also doesn’t work — you’d need the operator running to serve the API endpoint, not just the CRD object existing.
What Works: Separate Applications
The key insight: ArgoCD only validates a child Application’s contents when that Application actually syncs, not when the parent validates. If the ClusterSecretStore lives in its own Application with a sync-wave annotation, the parent Application only validates that an Application object exists — which is always valid. The inner contents are validated later, after the wave fires and ESO is already running.
Structure:
cluster/infra/eso/
app.yaml ← ESO Helm Application, no wave annotation
clustersecretstore-app.yaml ← wave 1: Application pointing at the ClusterSecretStore manifest
# clustersecretstore-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: eso-config
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
source:
path: cluster/base/eso # contains the actual ClusterSecretStore manifest
destination:
namespace: external-secrets
The parent Application sees two Application objects. Both are valid — ArgoCD can validate Application objects without knowing what’s inside them. Wave 0 fires, ESO deploys and becomes Healthy. Wave 1 fires, eso-config Application syncs, ClusterSecretStore is validated and applied — by this point the CRDs exist.
The Pattern
Any time you have a CRD-dependent resource (ClusterIssuer, ClusterSecretStore, PodMonitor, anything) in the same Application as the operator that installs the CRD, put the resource in a separate child Application at a higher sync-wave. The wrapper Application object is always valid. The contents are only validated when the wave fires.
Takeaway
ArgoCD sync-waves order application. They don’t defer validation. If you need a resource to be validated after a CRD is installed, it needs to be in a separate Application — not just a separately-annotated manifest in the same sync.