Skip to content

fix(bootstrap): unify scaleway into one 6-stage RollingSync graph - #24

Draft
nbrieussel wants to merge 4 commits into
mainfrom
bugfix/unify-argocd-sync-graphs
Draft

fix(bootstrap): unify scaleway into one 6-stage RollingSync graph#24
nbrieussel wants to merge 4 commits into
mainfrom
bugfix/unify-argocd-sync-graphs

Conversation

@nbrieussel

@nbrieussel nbrieussel commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Root cause of the original report (Velero and others failing to find credentials on a fresh cluster boot): services-vendor-scaleway (plain recurse Application, external Helm charts) and services-app-scaleway (ApplicationSet, in-house charts) were two completely uncoordinated ArgoCD sync graphs — no ordering was possible between a vendor chart and the in-house -init/-config app that fed it OpenBao secrets, no matter what sync-wave number either carried. Confirmed live multiple times over the course of this PR, including that a plain sync-wave annotation does nothing at all across separate Applications generated by an ApplicationSet (only real fix: ArgoCD's Progressive Syncs / RollingSync strategy, label-based grouping, requires --enable-progressive-syncs — see companion infra PR IntegratedDynamic/infrastructure#55).

Final design, collapsing everything into one services-scaleway ApplicationSet:

  • Every scaleway chart — vendor or in-house — now lives behind a thin local wrapper chart (Chart.yaml dependencies: on the real chart) + a small *.app.yaml param file, same pattern services/products/demo/chart already used for a local dependency.
  • Ordering is six coarse stages (a stage: field, not a per-app wave number) matching how this platform's own products depend on each other: openbao -> eso -> velero -> platform (wireguard/dex/external-dns/gateway) -> monitoring -> products. A product's chart and its -init/-config companion share a stage and sync concurrently as one unit; only cross-stage ordering is enforced (RollingSync waits for every Application in stage N to be Healthy before starting stage N+1).
  • Found a second, previously undetected instance of the same race while doing this: cert-manager's Scaleway DNS01 ACME webhook consumes its OpenBao-sourced Secret via secret.externalSecretName (not existingSecret/secretKeyRef), so it didn't match the grep pattern the original fix was scoped with. Now migrated and correctly placed in the eso stage.
  • CLAUDE.md updated to document the split: local keeps the original two-format *.vendor.yaml/*.app.yaml system, scaleway does not.

Verification

  • Every migrated chart (9 total: velero, external-dns, dex, kube-prometheus-stack, cert-manager, cert-manager's DNS01 webhook, OpenBao, ESO, Envoy Gateway) diffed byte-identical against its pre-migration render, aside from the expected subchart # Source: path change. One accepted cosmetic exception: kube-prometheus-stack's bundled Grafana dashboards reformat one embedded PromQL query's whitespace when nested one level deeper (same content, non-functional, confined to dashboard JSON).
  • helm template on the bootstrap chart itself succeeds for both env=scaleway and env=local.
  • Live-tested against a real destroy/apply cycle of the Scaleway cluster; iterated through two broken intermediate states (a Helm template syntax error, then confirming RollingSync — not sync-wave — was the only mechanism that actually works) before landing here.

Test plan

  • Fresh terraform apply of 10-cluster/scaleway pointed at this branch
  • Confirm kubectl get application -n argocd -l stage=<name> shows each stage's Applications only start syncing after the previous stage is fully Healthy
  • Confirm OpenBao itself, Velero, external-dns, Dex, kube-prometheus-stack, and the DNS01 webhook all come up clean with no CrashLoopBackOff/CreateContainerConfigError while OpenBao is still restoring

🤖 Generated with Claude Code

https://claude.ai/code/session_017ZMCuQ5t9hdrmUctLk7hEv

Nicolas Brieussel and others added 3 commits August 11, 2026 12:47
…edential race

Velero/external-dns/Dex/Grafana were deployed via the un-waved
services-vendor-scaleway recurse Application while the OpenBao-sourced
Secrets they need come from services-app-scaleway's wave-respecting
ApplicationSet — two independent sync graphs with no ordering guarantee
between them, so on a fresh cluster boot these pods raced OpenBao's
restore/unseal and started before their credentials existed (self-healing
via selfHeal/kubelet retries, but noisy and non-deterministic).

Migrate all four into small local wrapper charts (Chart.yaml `dependencies:`
on the real chart, same pattern services/products/demo/chart already uses)
driven through the app.yaml/ApplicationSet graph, with sync-waves placing
them after the app that produces their Secret (wave 4 after the wave-3
-init apps for velero/external-dns; wave 6 after the wave-5 dex-init/
grafana-config). Verified helm template output is byte-identical to the
old vendor charts aside from the expected subchart Source: path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZMCuQ5t9hdrmUctLk7hEv
…licationSet

The previous commit's per-file serverSideApply conditional broke `helm
template` on the bootstrap chart entirely (confirmed live: the bootstrap
Application never got past ComparisonError, so nothing downstream ever
synced). Two mistakes: a bare if/end directive mid-array isn't valid YAML
once Helm's own render pass parses its output, and Helm's template engine
scans every line for action delimiters regardless of '#' comments, so the
explanatory comment describing the broken syntax also broke rendering.

ApplicationSet's goTemplate mode substitutes values inside existing string
fields per generated app — it can't conditionally add/remove a syncOptions
array entry per file at all, so there's no per-file opt-in to build here.
Made ServerSideApply=true unconditional for every app.yaml-driven
Application instead (ArgoCD's own recommended default), and dropped the
now-dead serverSideApply field from dex/monitoring's chart.app.yaml.

Verified with `helm template` for both env=scaleway and env=local.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZMCuQ5t9hdrmUctLk7hEv
…ually orders apps

Confirmed live 2026-08-11 that moving Velero/external-dns/Dex/Grafana into
the app.yaml/ApplicationSet graph (previous commits) did not by itself fix
the OpenBao credential race: sync-wave annotations on Applications
generated by an ApplicationSet are inert — each is created and auto-synced
independently, with no ordering across them, unlike the classic app-of-apps
pattern (a single parent Application managing child Applications as its own
resources) that services-vendor-scaleway actually relies on and where
sync-wave genuinely works. velero's Application synced before velero-init's
own sync (which creates the Secret it needs) had even finished.

Requires argocd.argoproj.io/applicationset-controller's Progressive Syncs
beta feature (--enable-progressive-syncs, added to the ArgoCD Helm release
in the infra repo's 10-cluster/scaleway/argocd.tf). Adds a `wave` label
(same value as the existing syncWave field) to every generated Application
and a RollingSync strategy with one step per wave 0-6, so group N+1 only
starts syncing once every Application in group N is Healthy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZMCuQ5t9hdrmUctLk7hEv
Collapse services-vendor-scaleway (plain recurse Application) and
services-app-scaleway (ApplicationSet) into a single services-scaleway
ApplicationSet. The two-graph split meant a vendor chart (external Helm
repo) could never be ordered against the in-house -init/-config app that
fed it OpenBao secrets, no matter what sync-wave value either carried.

Every scaleway chart, vendor or not, now goes through the same wrapper-chart
+ *.app.yaml pattern (extends the previous commits' fix for
Velero/external-dns/Dex/Grafana to cert-manager, its Scaleway DNS01
webhook, OpenBao itself, the ESO operator, and Envoy Gateway). Found a
second, previously undetected instance of the exact same race in the
process: the DNS01 webhook consumes its OpenBao-sourced Secret via
`secret.externalSecretName` rather than `existingSecret`/`secretKeyRef`,
so it didn't match the grep pattern used to scope the original fix.

Ordering is six coarse stages instead of a per-app wave number:
openbao -> eso -> velero -> platform -> monitoring -> products, each
matching how this platform's own products actually depend on each other
(a product's chart and its -init/-config companion share a stage and sync
concurrently as one unit; only cross-stage ordering is enforced). Replaces
the syncWave field with `stage` throughout; the RollingSync strategy groups
on a `stage` label rather than the sync-wave annotation, which does nothing
across separate ApplicationSet-generated Applications regardless.

CLAUDE.md updated: local keeps the original two-format split, scaleway
does not.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZMCuQ5t9hdrmUctLk7hEv
@nbrieussel nbrieussel changed the title fix(bootstrap): unify the two ArgoCD sync graphs, kill the OpenBao credential race fix(bootstrap): unify scaleway into one 6-stage RollingSync graph Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant