Describe the bug
The cozystack aggregated apiserver (cozystack-api) ignores UpdateOptions on the update path. As a result, kubectl patch/apply --dry-run=server against any apps.cozystack.io resource (Postgres, MariaDB, Kafka, Kubernetes, VMDisk, …) is silently treated as a real write and persists the change. --dry-run=server is supposed to validate and admit a request without persisting it.
Environment
- Cozystack version: v1.5.0 (code path also present on
main)
- Component:
cozystack-api aggregated apiserver — pkg/registry/apps/application/rest.go
To Reproduce
- Create any
apps.cozystack.io resource in a tenant namespace:
kubectl apply -f - <<'EOF'
apiVersion: apps.cozystack.io/v1alpha1
kind: Postgres
metadata:
name: dryrun-demo
namespace: tenant-example
spec:
size: 1Gi
storageClass: local
replicas: 1
EOF
- Issue a server-side dry-run patch that changes a field:
kubectl patch postgres.apps.cozystack.io dryrun-demo -n tenant-example \
--type merge -p '{"spec":{"storageClass":"replicated"}}' --dry-run=server
- Re-read the object:
kubectl get postgres.apps.cozystack.io dryrun-demo -n tenant-example \
-o jsonpath='{.spec.storageClass}{"\n"}'
Expected behaviour
Dry-run does not persist. Step 3 prints local. (Standard Kubernetes --dry-run=server contract: run admission/validation, return the would-be result, write nothing.)
Actual behaviour
Step 2 reports postgres.apps.cozystack.io/dryrun-demo patched and step 3 prints replicated — the dry-run request mutated the live resource and its backing HelmRelease.
Root cause
REST.Update() (pkg/registry/apps/application/rest.go, func at ~L481) receives options *metav1.UpdateOptions but discards it. At ~L592:
updateErr := r.c.Update(ctx, helmRelease, &client.UpdateOptions{Raw: &metav1.UpdateOptions{}})
A fresh empty &metav1.UpdateOptions{} is constructed, dropping DryRun (and also FieldManager / FieldValidation). The sibling methods forward the incoming options correctly:
Create (~L238): r.c.Create(ctx, helmRelease, &client.CreateOptions{Raw: options})
Delete (~L674): r.c.Delete(ctx, helmRelease, &client.DeleteOptions{Raw: options})
Additionally, the create-on-update (upsert) path at ~L495 calls r.Create(ctx, obj, createValidation, &metav1.CreateOptions{}), which likewise drops dry-run for the create case.
Impact
- Server-side dry-run is ignored for every
apps.cozystack.io kind on update/patch/apply — a correctness/safety bug for anyone using --dry-run=server to preview changes (GitOps diff tooling, CI policy checks, manual pre-flight review). The preview silently applies.
FieldManager/FieldValidation are also dropped, which can affect server-side-apply field ownership and request validation on updates.
- Not specific to v1.5.0 (the code path predates it), so not a release blocker — but it is latent in v1.5.0.
Suggested fix
Forward the incoming options, mirroring Create/Delete:
updateErr := r.c.Update(ctx, helmRelease, &client.UpdateOptions{Raw: options})
and carry options.DryRun into the CreateOptions for the upsert path at ~L495. Add a regression test asserting that a --dry-run=server update leaves the backing HelmRelease unchanged.
Describe the bug
The cozystack aggregated apiserver (
cozystack-api) ignoresUpdateOptionson the update path. As a result,kubectl patch/apply --dry-run=serveragainst anyapps.cozystack.ioresource (Postgres, MariaDB, Kafka, Kubernetes, VMDisk, …) is silently treated as a real write and persists the change.--dry-run=serveris supposed to validate and admit a request without persisting it.Environment
main)cozystack-apiaggregated apiserver —pkg/registry/apps/application/rest.goTo Reproduce
apps.cozystack.ioresource in a tenant namespace:kubectl patch postgres.apps.cozystack.io dryrun-demo -n tenant-example \ --type merge -p '{"spec":{"storageClass":"replicated"}}' --dry-run=serverkubectl get postgres.apps.cozystack.io dryrun-demo -n tenant-example \ -o jsonpath='{.spec.storageClass}{"\n"}'Expected behaviour
Dry-run does not persist. Step 3 prints
local. (Standard Kubernetes--dry-run=servercontract: run admission/validation, return the would-be result, write nothing.)Actual behaviour
Step 2 reports
postgres.apps.cozystack.io/dryrun-demo patchedand step 3 printsreplicated— the dry-run request mutated the live resource and its backing HelmRelease.Root cause
REST.Update()(pkg/registry/apps/application/rest.go, func at ~L481) receivesoptions *metav1.UpdateOptionsbut discards it. At ~L592:A fresh empty
&metav1.UpdateOptions{}is constructed, droppingDryRun(and alsoFieldManager/FieldValidation). The sibling methods forward the incoming options correctly:Create(~L238):r.c.Create(ctx, helmRelease, &client.CreateOptions{Raw: options})Delete(~L674):r.c.Delete(ctx, helmRelease, &client.DeleteOptions{Raw: options})Additionally, the create-on-update (upsert) path at ~L495 calls
r.Create(ctx, obj, createValidation, &metav1.CreateOptions{}), which likewise drops dry-run for the create case.Impact
apps.cozystack.iokind on update/patch/apply — a correctness/safety bug for anyone using--dry-run=serverto preview changes (GitOps diff tooling, CI policy checks, manual pre-flight review). The preview silently applies.FieldManager/FieldValidationare also dropped, which can affect server-side-apply field ownership and request validation on updates.Suggested fix
Forward the incoming options, mirroring
Create/Delete:and carry
options.DryRuninto theCreateOptionsfor the upsert path at ~L495. Add a regression test asserting that a--dry-run=serverupdate leaves the backing HelmRelease unchanged.