Skip to content

Fix/serialize volume policy - #267

Merged
vsilent merged 3 commits into
devfrom
fix/serialize-volume-policy
Sep 23, 2026
Merged

vsilent merged 3 commits into
devfrom
fix/serialize-volume-policy

Conversation

@vsilent

@vsilent vsilent commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

robotizeit and others added 3 commits September 23, 2026 15:07
9686684 added `volumes` to the contract's parser and to the struct, but
`TargetConfigContract` has a hand-written `Serialize` that builds an
intermediate struct field by field — and it did not know about the new
one. So a declaration parsed correctly, then vanished on the way out.

That path is `stacker submit`: the contract is serialized into the submit
body. The declaration never reached the registry, and the bake would have
reset the volume it was meant to keep. Nothing surfaced — the submit
succeeded, the stored contract merely had an empty service block where
the volumes should have been. Caught by querying the database after a
real resubmit of stackpilot.

Every existing test read a contract and asserted on the parsed result;
none serialized one back. The new test round-trips through JSON and
checks both directions, which is what the submit path actually does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The serialization defect fixed in the previous commit reached production
because every existing test parsed a contract and asserted on the result.
None serialized one back, so a kind missing from the hand-written
`Serialize` looked fine everywhere.

Two paths serialize it, and both were losing the declaration:

- `submit.rs:130` — the marketplace registry, covered by the round-trip
  test added with the fix
- `stacker_client.rs:4102` — `stacker sync`, writing the contract onto
  every app of a project. Covered here

A third reads it: `compose_env_keys` in `deploy.rs`. It takes fields and
must ignore the rest — a volume name is not an environment variable, and
parameterizing one would leave the compose asking for a value nothing
supplies. Covered here too.

Both new serialization tests were verified against the bug: with the fix
reverted they fail, with it applied they pass.

Two boundaries the audit surfaced also get tests. A legacy
`required`/`optional`/`secret` contract must round-trip untouched and must
not grow an empty `volumes:` key. And a service declaring only volumes and
no fields must survive serialization rather than collapsing to `{}` —
which is precisely the shape the production database showed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The worst of these would have published an image in a worse state than
before any of this work started, so they are grouped rather than split.

**The sanitize step could run against nothing and report success.**
Moving `FinalizeContext` to a parsed contract meant the flat protected-key
set was re-derived by serializing it back — and `Serialize` moves a
default-shaped generated secret out of `fields` into the legacy `secret:`
list, which `protected_keys_from_contract` did not read. For the most
ordinary contract there is, the set came out empty. Empty is worse than
useless: `resolve_non_contract_references` then treats every `${VAR}` as
unmanaged and restores its literal value, undoing the deploy-time
parameterization and writing the author's passwords back into the baked
compose — while the bake prints "Sanitized". Fixed twice over: the set is
now passed in rather than re-derived, and the function reads both shapes.

**An unparseable contract silently became an empty one.** `bake.rs` did
`from_value(...).ok().unwrap_or_default()`, while the gate that is meant
to catch this was evaluated against the raw JSON — so the gate passed and
finalize sanitized against nothing. Reachable: every contract type denies
unknown fields, so a template using a newer kind fails wholesale on an
older binary. It now aborts.

**The keep-list matched more than it claimed.** The comment promised that
keeping `ollama` would not also keep `not-ollama-backup`; the generated
pattern `*[-_]ollama[-_]*` matched exactly that. The test asserted only
that the literal `*ollama*` was absent, so it passed against the bug.
Names now come from the contract and name compose volumes directly, so
the fuzzy patterns are gone — exact match, and the test checks the name
it names.

Also: two services declaring one volume with different policies is
refused rather than silently resolved toward `fixed`; `display` no longer
vanishes when a field collapses into the legacy `secret:` shorthand,
which was dropping the password-input hint on every `stacker sync`; and
`VolumePolicy`'s deserializer matches `Mutability` exhaustively, so a
fifth variant is a compile error instead of a runtime panic on user input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vsilent
vsilent merged commit b707514 into dev Sep 23, 2026
6 of 10 checks passed
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.

2 participants