Repository navigation
[LFX Mentorship] Legacy Config backward compatibility and migration to CRD-based Config Management #7410
Description
Sub-issue of #7105 · Follow-up to #7211
What would you like to be added?
A safe path for existing clusters from the legacy ConfigMap/Secret-based config storage to the config.oam.dev/v1alpha1 CRDs, in three parts:
- Unconditional backward compatibility. The controller and the
velaCLI always read, show and delete across both backends. All new writes go to CRs. No flag or gate is needed to keep legacy objects working. vela config-template migrate, a new command that converts one legacy template and its configs intoConfigTemplateandConfigCRs on the operator's schedule.AutoMigrateLegacyConfigs, one controller feature gate (Alpha, default off). When on, the controller runs the same migration for every legacy template at startup.
The work also brings the controller up to parity with the imperative path for the Nacos expanded writer, makes GitOps-driven configs behave the same as CLI-driven ones, and closes two API contract gaps, template scope and sensitive handling, before the API ships. After release they would be breaking changes.
# vela-core Helm values
featureGates:
AutoMigrateLegacyConfigs: false # Alpha, default offThe work is split into small PRs, tracked in the checklist at the end.
Why is this needed?
#7211 added the CRDs, controllers and webhooks, with migration and upgrade work planned as follow-up before release. On a cluster that already has legacy configs, upgrading today causes these problems:
-
Legacy objects are invisible to the default CLI. Once the CRDs are installed,
vela config[-template] listonly reads CRs, andvela config[-template] deletereports legacy objects as not found. Configs created by older CLIs, addons or VelaUX can no longer be managed. -
There's no way to adopt legacy resources into CRs. Existing
config-template-*ConfigMaps andconfig.oam.dev/catalog=velacore-configSecrets stay in legacy storage forever unless they are recreated by hand. -
Nacos only works through the CLI. The imperative factory calls
writer.Writefor templates with anacosblock, but theConfigreconciler doesn't. AConfigapplied through GitOps reachesAvailablewithout ever writing to Nacos. -
The workflow provider still writes legacy storage.
create-configcalls the imperative factory and creates a legacy Secret, not aConfigCR. -
The storage mode is a global CLI flag.
--config-mode auto|legacy|crdis a persistent flag on everyvelacommand, so a user can still create legacy templates on a cluster that has the CRDs, and every command has two write paths to test and document. -
Template scope isn't enforced.
ConfigTemplate.spec.scopeis stored, printed and copied onto the output Secret's labels, but nothing acts on it. Ascope: systemtemplate can live in any namespace. AConfigcan reference a template in any namespace throughspec.templateRef.namespace. The webhook and controller then read that template with the controller's own permissions, so anyone allowed to create aConfigcan render any template in the cluster. -
Sensitive data can leak through a non-sensitive template. The output Secret's
config.oam.dev/sensitiveannotation comes only from the template. AConfigthat reads credentials throughspec.propertiesFrom.secretRefagainst a non-sensitive template ends up with those credentials copied intoinput-propertiesand markedsensitive: "false". That makes them readable throughvela configread paths and theread-configworkflow step.sensitivecan also be flipped fromtruetofalseon a live template, which re-marks every dependent Secret as readable on the next reconcile.
Proposal
1. CLI behaviour: single write path, dual read
The storage choice is no longer a user decision. Each command behaves as follows once the CRDs are installed:
| Command | Behaviour |
|---|---|
vela config-template apply |
Always creates or updates a ConfigTemplate CR. Fails with a clear message if the CRD is not installed. If a legacy config-template-<name> ConfigMap with the same name exists in the namespace, prints a notice that the CR now shadows it and points at vela config-template migrate. |
vela config-template show |
Resolves the template from whichever backend holds it, CR first. |
vela config-template delete |
Deletes from whichever backend holds the template. Deleting a CR cascades through owner references as today; deleting a legacy ConfigMap keeps its current behaviour. |
vela config-template list |
Default output merges CRs and legacy ConfigMaps with a SOURCE column (crd / legacy). If both backends hold the same name, the CR row is shown and the legacy row is marked shadowed. --config-mode=legacy or --config-mode=crd shows one backend only. |
vela config create |
Resolves the template from either backend and always writes a Config CR. Interim: a template with a nacos block still writes a legacy Secret, with a notice, until the controller runs the expanded writer (section 4). |
vela config delete |
Deletes from whichever backend holds the config. Deleting a legacy config keeps its current behaviour: objects in objects-reference are removed and distribution is recalled. |
vela config list |
Same merge, SOURCE column and --config-mode filter as the template list. |
vela config distribute |
Unchanged; sets the Config as owner of the distribution Application when the config is CR-backed. |
--config-mode moves from the root command to the two list commands and loses its auto value. It is rejected on every other command.
The workflow provider's create-config writes a Config CR when the CRD is installed. read-config, list-config and delete-config read both backends. The factory path remains the fallback when the CRD is absent.
2. Controller behaviour
- Template resolution for a
Configreads theConfigTemplateCR first and falls back to the legacyconfig-template-*ConfigMap. This fallback is permanent; nothing switches it off. - The legacy ConfigMap watch stays on so that a
Configreferencing a legacy template reconciles when that ConfigMap changes. - Admission doesn't depend on apply order. A
Configapplied before or together with itsConfigTemplateis admitted and converges once the template exists; schema checks run only when the template isAvailable.
3. Migration
Migration is implemented once, in a shared library in pkg/config, and driven from two places: the vela config-template migrate command and the AutoMigrateLegacyConfigs gate.
Mapping rules
# Before: legacy config (Secret)
apiVersion: v1
kind: Secret
metadata:
name: my-registry
labels:
config.oam.dev/catalog: velacore-config
config.oam.dev/type: image-registry
---
# After: the same Secret, now owned by a Config
apiVersion: config.oam.dev/v1alpha1
kind: Config
metadata:
name: my-registry
labels:
config.oam.dev/created-by: migration
spec:
templateRef:
name: image-registry
propertiesFrom: # sensitive templates only
secretRef:
name: my-registry-properties- Templates:
config-template-<name>becomesConfigTemplate <name>in the same namespace.template,sensitive,alias,descriptionand theexpanded-writerdata carry over. Legacy scopeglobalandsystemmap tosystem;project,envand anything else map tonamespace. - Configs: each legacy Secret becomes a
Configwith the same name and namespace.templateRefcomes from theconfig.oam.dev/typelabel. Non-sensitive input goes intospec.properties. Sensitive input goes into a<name>-propertiesSecret owned by the Config and referenced throughspec.propertiesFrom, so it never enters the CR. - Adopt in place. The existing Secret gets the Config as its controller owner and keeps its name, UID and data. The first reconcile after adoption must produce an identical Secret, including the default type
/<template-name>. Objects listed inobjects-referenceand thedistribute-<name>Application also become owned by the Config. - Idempotent. Adopted sources are marked with
config.oam.dev/migrated-to: <uid>. If a CR with the same name already exists and wasn't created by migration, the object is skipped and aMigrationConflictEvent is recorded. - Opt-out per object with
config.oam.dev/migrate: "false". - Legacy objects are never deleted by migration. Downgrading to an older controller keeps working.
vela config-template migrate
vela config-template migrate <name> [-n <namespace>] [--all] [--dry-run]
Migrates the named legacy template and every legacy config that references it. --all migrates every legacy template in the namespace. --dry-run prints the CRs that would be created and the objects that would be adopted, and changes nothing. The command reports adopted, skipped and conflicting objects, and exits non-zero if any conflict.
AutoMigrateLegacyConfigs gate
Added to pkg/features/controller_features.go as Alpha, default off. When on, a leader-elected runnable runs the migration for every legacy template and config in the cluster once at controller startup, then exits. Each run emits Events on the source objects, a summary log line, and the metric kubevela_config_migration_total{result="adopted|skipped|conflict|failed"}. Turning the gate off stops future sweeps; already adopted objects stay adopted.
4. Nacos expanded writer in the controller
- The
Configreconciler reads thenacosblock fromConfigTemplate.spec.template(or from the legacy ConfigMap'sexpanded-writerkey), renders it withwriter.RenderForExpandedWriter, and writes it withwriter.Write. - The endpoint config (
nacos.endpoint.name) is resolved from either CR-backed or legacy storage. - The controller writes only when the rendered content changes, stores a content hash in status, and reports a
NacosSyncedcondition. Network and 5xx failures requeue with backoff; they aren't treated as terminal errors. - Deleting a Config removes nothing from Nacos, same as the CLI today.
- The ConfigTemplate webhook validates the
nacosblock. - Once this lands, the interim legacy fallback in
vela config createfor Nacos templates is removed.
5. GitOps compatibility
- Legacy objects managed by Argo CD or Flux are adopted like any other. Adoption adds only owner references and the
migrated-toannotation, doesn't change Secret data, and uses its own field manager, so the GitOps tool sees no diff. An informationalMigratedGitOpsManagedEvent tells owners to commit the matching CR to git. - Two risks are documented. If git later reapplies legacy data that differs from the rendered output, the controller and the GitOps tool will fight until the CR is committed to git. Deleting a migration-created
Configalso garbage-collects its Secret. kubectl delete configand GitOps pruning clean up the same objects asvela config delete: the output Secret,outputsobjects, and the distribution Application.
6. Template scope and cross-namespace access
The rules below are enforced in the Config and ConfigTemplate webhooks (failurePolicy: Fail). The reconciler repeats the structural checks as a second line of defence.
-
scope: systemis only valid invela-system. The ConfigTemplate webhook deniesscope: systemin any other namespace. CEL can't express this, because it can't readmetadata.namespace. -
Config.spec.templateRefaccess rules:Template Referenced from Allowed any scope the same namespace yes scope: system(invela-system)any namespace yes scope: namespaceanother namespace only if a SubjectAccessReview shows the requesting user can getthatConfigTemplate -
templateRefis already immutable, so an access check when the Config is created covers its whole lifetime. -
Legacy templates: legacy
config-template-*ConfigMaps invela-systemare treated assystemscope, both when the controller falls back to them and when migration adopts them. Today they are used from every namespace, so this keeps existing configs working. Legacy templates in other namespaces map tonamespacescope. -
Workflow steps:
create-configruns as the controller's service account, which would pass any access check. WhenAuthenticateApplicationis on, the provider runs the SubjectAccessReview for the Application's user (app.oam.dev/username). Otherwise it allows only same-namespace orsystemtemplates. -
The chart's RBAC stays as it is. The controller still needs to read templates cluster-wide, and the access decision is made per requesting user at admission.
Open question for review: the alternative reading is that
Configobjects built from asystemtemplate must themselves live invela-system. That would break every addon-shipped template, which today lives invela-systemand is used from application namespaces, so this proposal keepssystemtemplates usable cluster-wide and constrains only where the template itself may live. Please object here if the stricter rule is what's wanted.
7. Sensitive handling
-
sensitivecan't be turned off. On update, the ConfigTemplate webhook deniessensitive: true→false. A CEL transition rule onConfigTemplateSpecenforces the same thing where the webhook isn't installed:// +kubebuilder:validation:XValidation:rule="!has(oldSelf.sensitive) || !oldSelf.sensitive || (has(self.sensitive) && self.sensitive)",message="sensitive cannot be changed from true to false" -
Turning
sensitiveon (false→true) is allowed. Dependent Configs that still carry inlinespec.propertiesmove toErrorwith a message telling the user to switch topropertiesFrom. Their existing Secret is marked sensitive right away. -
propertiesFromalways means sensitive. AConfigthat usesspec.propertiesFromgets an output Secret markedconfig.oam.dev/sensitive: "true", whatever the template says. Read paths (vela config,read-config,list-config) treat it as sensitive and don't return the input properties. -
Migration keeps
sensitiveas it is on templates, and marks adopted configs as sensitive when they end up usingpropertiesFrom.
Verification plan
- Unit tests:
--config-modeaccepted onlistand rejected elsewhere; dual-read merge, shadowing andSOURCEcolumn;applyfails without the CRD and warns on a shadowed legacy template; dual-delete routing; legacy-to-CR mapping for templates and configs, sensitive and non-sensitive, including the scope mapping; the adoption parity check (identical Secret after the first reconcile); migrate--dry-runoutput and idempotency; Nacos render and change detection. - Webhook tests:
scope: systemoutsidevela-systemis denied; each row of thetemplateRefaccess table, with the SubjectAccessReview allowed and denied;sensitivetrue → false is denied; false → true is admitted; apropertiesFromConfig against a non-sensitive template produces a sensitive Secret. - envtest controller tests: startup sweep with the gate on; running the sweep twice changes nothing; conflict and opt-out handling; legacy fallback and watch always active; Nacos retry on transient errors; the CEL transition rule on
sensitive, with no webhook; dependent Configs going toErroraftersensitiveis turned on. - RBAC e2e: as a non-admin user bound to one namespace, check that a
Configreferencing asystemtemplate is admitted; one referencing anamespace-scoped template in another namespace is denied, then admitted once the user is grantedgeton it; and acreate-configworkflow step withAuthenticateApplicationon applies the same rule to the Application's user. - Upgrade e2e (kind, old image → new image): on the old controller, create a plain config, a sensitive config, a config with
outputs, a distributed config, a Nacos config, a config created by the workflow step, and an addon-shipped template. Upgrade, then check:vela configandvela config-templatelist, show and delete, with both the old and the new CLI, with the gate off.- Applications that consume these configs keep working, with no Secret churn (same UID and resourceVersion until something is edited).
vela config-template migrate --alladopts each object exactly once; a second run changes nothing.- With the gate on, a controller restart adopts whatever is left exactly once, and a further restart changes nothing.
- Nacos content stays the same, and a later edit through the
ConfigCR reaches Nacos. - Downgrading to the old controller works with the legacy objects intact.
- Nacos e2e: against a standalone
nacos/nacos-servercontainer, aConfigapplied withkubectlalone shows up under the expecteddataIdandgroup. - GitOps e2e: Argo CD (or Flux) syncs a
ConfigTemplateand aConfig, including a Nacos template. The sync is healthy, there's no diff loop after reconcile, pruning removes everything, and an adopted GitOps-managed legacy Secret shows no diff. - Docs: the gate in the Helm chart
featureGatesvalues, themigratecommand and thelist --config-modeflag in the CLI reference, along with the upgrade and rollback steps.
PR checklist
Each PR is small and links back to one box.
- CLI backward compatibility:
applyalways CR,--config-modescoped tolistwith merged default andSOURCEcolumn, dual-delete for configs and templates,showandcreateverified against both backends (section 1) - Shared migration library and
vela config-template migrate(section 3) -
AutoMigrateLegacyConfigsgate: startup sweep, Events, metric, opt-out (section 3) - Nacos expanded writer in the
Configreconciler, remove the create fallback (section 4) - Workflow provider:
create-configwrites aConfigCR, read/list/delete read both backends (section 1) - Scope enforcement in webhooks and reconciler (section 6)
-
sensitiverules (section 7) - GitOps behaviour, upgrade and downgrade e2e, docs (section 5, verification plan)
Out of scope (potential follow-ups)
- VelaUX changes.
- Event-driven adoption of legacy objects created after the startup sweep (for example by an older
velabinary). Rerunvela config-template migrateor restart the controller with the gate on. - Removing legacy storage, and a deprecation timeline.
Activity
config.oam.dev/v1alpha1) with server-side validation #7105[-][LFX Mentorship] Legacy Config migration and backward compatibility for CRD-based Config Management behind feature gates[/-][+][LFX Mentorship] Legacy Config backward compatibility and migration to CRD-based Config Management[/+]Refactor(config): Template carries the migrated-to UID instead of the…
Feat(config): review follow-ups from kubevela#7449 and MigrateAll
Feat(controller): AutoMigrateLegacyConfigs runs an adoption sweep at …
Chore(chart): expose featureGates.autoMigrateLegacyConfigs
Refactor(config): Template carries the migrated-to UID instead of the…
5 remaining items
Fix(config): Nacos sync follow-ups from #7457 (#7467)
Fix(test): the application controller's fake config factory implement…
Fix(config): review follow-ups from kubevela#7470 round 1
Fix(workflow): delete-config lets the application's own Config go for…
Chore(chart): the generated step definitions carry the new descriptions
Chore(definitions): say what the delete-config step deletes
Feat(workflow): the config steps write Config CRs and read both backe…
Fix(config): review Config updates, a concrete nacos endpoint namespa…
Fix(config): close the ways round the scope rules (review round 2)
Fix(webhook): a terminating ConfigTemplate may lose finalizers, not c…
Fix(webhook): the grandfathered list may shrink, not grow (cubic roun…
Test(config): standard-library imports in their own group (check-diff)
Feat(config): template scope and cross-namespace access (6/8) (#7481)
Fix(config): the ConfigTemplate writer keeps a stored sensitive
Test(e2e): retry the Config create until the webhook's cache has the …