docsOperate

The recorded cluster version in clusters.yaml

clusters.yaml carries a version key per cluster: the release that cluster is believed to be running. This page is the contract for that key, because the file is shared -- the VS Code extension and the MemQL Cockpit both read and write it, and a key one of them does not model is at risk on the other's next write.

Why the record exists at all

No installed cluster can currently state its release honestly.

  • ServerHello.version is the literal string "v1". It names the wire protocol, not the release, and always has.
  • The VERSION file read 0.15.0 at every tag from v0.16.1 to v0.21.25, and until memql#3998 the Dockerfile overwrote even that with a build stamp of the form 0.15.0-<epoch> before the binary shipped. (Since memql#5714 a release cut refuses when VERSION differs from the tag, but the binary still does not read the file.) That stamping step is gone: the release now goes into the binary via the linker and nowhere else, and neither runtime stage copies a VERSION file.

MemQL engine releases now stamp a real version into the binary and introduce it on an additive ServerHello.engine_version field. That fixes the problem going forward and cannot reach backwards: it cannot teach an already-installed v0.18.0 to introduce itself, and v0.18.0 is precisely the cluster an operator most needs to identify.

So the version is recorded rather than observed. The record is readable with the cluster switched off, unreachable, or never dialled -- which matters, because the failure that motivated this work was a session that had just been severed. A version only a live connection could report would be missing in exactly the moment it was needed.

The key

yaml
clusters:
- name: local
endpoint: api.memql.localhost:443
version: v0.18.0
YAML keyversion
Typestring
Extension modelClusterConfig.version?: string (clusters/model.ts)
Cockpit modelVersion string with yaml:"version,omitempty"
AbsentThe cluster's release is unknown. Not an error.

Every cluster in an operator's file predates this key, so absent is the normal case and must render as unknown rather than as anything else.

Write semantics: three states, and why

version is a plain string, so it takes the same three-state write semantics as every other string field in this file:

Value suppliedEffect on disk
undefinedLeave whatever is on disk alone
""Delete the key
a stringWrite it verbatim

The distinction between the first two is load-bearing here in a way it is not for a hand-edited field. The version is learned opportunistically from several sources of differing trustworthiness, and most refresh attempts learn nothing. "I learned nothing this time" has to be spelled differently from "this cluster has no version", or a single failed refresh would erase a version a better source had already established.

This is also why the key is a string rather than anything richer. The neighbouring local flag is a boolean, and booleans in this file follow a different rule -- false serialises as ABSENT, because the cockpit declares that field omitempty and drops the key whenever it is false, so a tool that wrote local: false back would make the two churn the file against each other on every save. A string has no such collapse: there is no third state to negotiate, and version: "" is simply not a thing either tool writes.

What may be recorded

Whatever the cluster said, unmangled. The record is a report, not a judgement. A release tag is the common case, but the value may equally be:

  • a release tag, with or without the v prefix (v0.18.0, 0.18.0)
  • a branch name
  • a commit sha
  • the 0.15.0-<epoch> build stamp older images (built before memql#3998) may still carry

Anything unparseable is reported as notComparable by the comparison module -- and never as "up to date", which is the failure direction that reproduces the original incident. That module can only make that call on a value that reached disk intact, so nothing on the write path may normalise, coerce, or reject what it is given.

Record quality: a write may not downgrade

The learners write through this key from five sources, and they do not agree in trustworthiness. The rule is that a write happens only when the value differs and does not downgrade record quality: a value that is not release-shaped never overwrites one that is.

Without that rule, an install receipt naming v0.18.0 would be replaced by a live connection reporting the build stamp 0.15.0-1737072000 -- a strictly worse record, written by a strictly more recent observation.

The five sources, most trustworthy first

SourceWhat it actually knows
handshakeServerHello.engine_version -- the release the engine binary was cut from, stamped in at link time
receiptwhat the install wizard checked out, on this machine, at one moment
argocdthe targetRevision the manifests ask for. Legitimately a branch name
deployControlthe resolved lockfile's version -- the deployment record. Absent for a local cluster
livememqlVersion() over a connection: the same fact as handshake, reached through the DSL

Quality still outranks trust: a branch name from the most trusted source loses to a real tag from the least. The order above only decides between values that each name a release.

handshake ranks first because it is the only one that cannot disagree with the binary serving the connection. Every other source describes something adjacent to the running engine and can drift from it -- the checkout can be moved after the install, the manifests can ask for something that has not reconciled, the deployment record can outlive what it deployed. So when the handshake and the install receipt both name a release and the two differ, the receipt is the stale one.

Two answers from this source are non-answers, and neither needs a rule of its own. A cluster whose engine predates the field states "", which is dropped before ranking; a binary not cut from a release states dev, which is not release-shaped and so can never land on a recorded release. Both were true of the entire fleet on the day the source was added, which is why it changed no cluster's record until one was upgraded.

Preservation guarantees: the two tools differ

They do not write this file the same way, and the difference is the whole reason a shared key needs coordinating.

The extension read-modify-writes the file as a YAML document, not by serialising a struct over it. Two properties follow, both enforced by tests in clustersFile.test.ts:

  • Comments survive an extension write. An operator's comments are part of their file.
  • Unmodelled keys survive an extension write. A key written by a newer cockpit is left untouched.

That matters more for this key than for a hand-edited one, because the version learners write far more often than any human edit does.

The cockpit does not. cli/config.SaveClusters is a yaml.Marshal of the whole ClustersFile struct, so its next write rewrites the file from the struct alone. Comments are lost and any key it does not model is dropped.

This is a known limitation rather than a bug to route around, and it is asserted so it cannot drift silently: TestClusterLocalFieldRoundTrip in the cockpit's cli/config/clusters_test.go ends by checking that a future_key written by another tool does not survive the round trip.

The consequence is the rule:

Every key in clusters.yaml must be modelled on BOTH sides. Adding one to a single tool means the other silently deletes it on its next write.

Coordination is a requirement, not a courtesy

This key goes through the same cross-tool coordination as its neighbours -- the token rename (which fixed a field advertising a credential class the bff structurally rejects) and the local flag.

Given the asymmetry above, that coordination is load-bearing. If the cockpit preserved unmodelled keys, the extension could ship version alone and nothing would need coordinating at all; because it does not, the cockpit's matching Version string has to land as part of the same change rather than after it. A recorded version that the operator's next cockpit command silently erased would be worse than no version at all -- it would be a field that works until it does not, for reasons nothing on screen could explain.

  • Upgrade barriers -- when moving between two recorded versions is not a retag, and the upgrade is refused with instructions instead.
  • Deploy-bundle runbook -- how a release is actually deployed once a version has been chosen.