Repository navigation
Description
Summary
The @immutable annotation on an app's values.yaml is translated by cozyvalues-gen into an x-kubernetes-validations transition rule (self == oldSelf) that reaches the ApplicationDefinition's openAPISchema, but the apps.cozystack.io aggregated apiserver never evaluates it on update. Immutability is therefore silently unenforced at admission for every Application kind, not just one app.
Mechanism
buildSpecSchema builds a structural schema from the embedded openAPISchema, but the only consumer of that schema is defaulting — pkg/registry/apps/application/rest_defaulting.go calls defaultLikeKubernetes(&m, r.specSchema). The update path in pkg/registry/apps/application/rest.go validates only the object name (ValidateApplicationName); there is no CEL / x-kubernetes-validations evaluation wired in, so self == oldSelf rules are carried in the schema and never checked. The unit test pkg/registry/apps/application/rest_spec_schema_test.go pins only that the schema builds with XValidations preserved through conversion — it does not assert that the rule is enforced on update.
Reproduce (verified live)
- Take any VMDisk (its
storageClassalready carriesself == oldSelf/ "storageClass is immutable" in the shippedApplicationDefinition). kubectl patch vmdisk <name> -n <tenant> --type=merge -p '{"spec":{"storageClass":"local"}}'- Expected: rejected by admission ("storageClass is immutable"). Actual:
vmdisk ... patched— the change is accepted.
Impact
Every field marked @immutable across all apps (e.g. MariaDB storageClass, and storageClass/source on vm-disk after #3969) is advisory only. Downstream helm templates that reuse an immutable resource's spec then silently no-op the edit; #3969 adds a fail() backstop in the vm-disk chart, but the proper fix is to enforce transition rules at the apiserver so the edit is rejected at kubectl apply time.
Suggested direction
Evaluate the embedded schema's x-kubernetes-validations (including transition rules that reference oldSelf) during update in the Application registry, and add a test that a self == oldSelf field actually rejects a changed value on update — not just that the schema builds.
Activity
dosubot commentedon Aug 26, 2026
scooby87 the analysis in the report is accurate — I can confirm the gap from the code.
buildSpecSchemais defaulting-only. Its comment explicitly says it returns "the structural-schema form used for defaulting" [1], and the only consumer (applySpecDefaults) callsdefaultLikeKuberneteswith no CEL evaluation path at all [2].The
Updatepath has no schema-based validation. AfterupdateValidation(the generic apiserver-level hook — not CEL fromx-kubernetes-validations), it runsvalidateNoInternalKeys, tenant quota checks, and legacy-field warnings, then goes straight toConvertApplicationToHelmReleaseand writes [3]. Nothing in that path evaluates transition rules.k8s.io/apiextensions-apiserver/pkg/apiserver/schema/celis not imported anywhere in the registry package. The only CEL import in the codebase iscelopenapi "k8s.io/apiserver/pkg/cel/openapi"inpkg/cmd/server/openapi_test.go— that's for VAP type-checking of the published OpenAPI schema, unrelated to admission [4].Existing tests cover name validation and schema construction — not enforcement. The test file rest_validation_test.go pins name-format validation, name-length limits, and tenant namespace overflow, but contains nothing about
x-kubernetes-validationsor transition rules being enforced on update. The line-1673 comment in rest.go about "kstatus readiness" refers to the HelmReleaseHealthCheckExprs, not to schema CEL rules [5].For the fix, the right upstream primitive is
k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel— specifically theValidatorit exposes, which handlesoldSelftransition rules (this is the same package CRD admission uses). ThespecSchema *structuralschema.Structuralis already built and stored onREST[6], so the wiring would be:NewRESTtime usingcel.NewValidator(specSchema, ...).REST.Update, afterapplySpecDefaults, callvalidator.Validate(...)with both old and new spec objects.field.ErrorListas aStatusReasonInvaliderror if non-empty.self == oldSelffield and asserts a422 Invalidresponse.To reply, just mention @dosu.
Share context across your team and agents. Try Dosu.
scooby87 commentedon Aug 27, 2026
Duplicate of #2657, which already tracks enforcing
x-kubernetes-validationsCEL rules (includingself == oldSelftransition rules) in theapps.cozystack.ioaggregated apiserver. Closing in favour of #2657.