The config file
JSON5 mechanics, environment substitution, how the binary is pointed at a config, and how to find out a config is wrong before it takes a cluster down with it.
Who this is for: anyone about to edit a NativeLink config for the first time. What you'll know at the end: what dialect the file is written in, how to keep secrets and addresses out of it, how the binary finds it, and how to check one is valid without waiting for a production rollout to tell you. Time: twenty minutes.
Before you start
A config file you got from an example, for instance from Examples and templates.
It's JSON5, not JSON
NativeLink parses its config with serde_json5, not serde_json. The
difference matters every time you edit a file:
{
// Comments are legal. Use them: a config is the only place some
// operational decisions get written down at all.
stores: [
{
name: "CAS_MAIN_STORE", // unquoted keys are legal too
filesystem: {
content_path: "/tmp/nativelink/data/content_path-cas",
temp_path: "/tmp/nativelink/tmp_path-cas",
eviction_policy: {
max_bytes: 10000000000, // trailing comma is legal
},
},
},
],
}Three things are legal here that plain JSON rejects: // and /* */
comments, unquoted object keys, and trailing commas. Nearly every shipped
example in nativelink-config/examples/ uses all three, so a strict-JSON tool
pointed at one of them will report syntax errors that aren't real.
Tell your editor it's JSON5
Most editors infer the dialect from the extension. .json5 is the extension
the repo uses, and it's the one that gets you comment support instead of a
wall of red squiggles. If you rename a config to .json to make a tool
happy, the tool will be happy and wrong.
Environment substitution
Any string field that is deserialized through the config crate's shellexpand helpers accepts shell-style variable references, including the default-if-unset form:
{
worker_api_endpoint: {
uri: "grpc://${SCHEDULER_ENDPOINT:-127.0.0.1}:50061",
},
}${SCHEDULER_ENDPOINT:-127.0.0.1} means "the value of SCHEDULER_ENDPOINT,
or 127.0.0.1 if it isn't set". That single feature is what lets one config
file serve a laptop and a cluster: the file names a default that works on your
machine, and the deployment overrides it with an environment variable.
The docker-compose deployment in this repo is built entirely on that pattern.
Its worker and scheduler configs point at
grpc://${CAS_ENDPOINT:-127.0.0.1}:50051 and
grpc://${SCHEDULER_ENDPOINT:-127.0.0.1}:50061, so the same files run
unmodified whether the pieces are on one host or several.
Substitution also works for numeric-looking fields that take a size, because those are parsed from strings:
{
memory: {
eviction_policy: {
max_bytes: "${NATIVELINK_CAS_MEMORY_CONTENT_LIMIT:-100mb}",
},
},
}Not every field expands
Substitution is opt-in per field, applied by the deserializer the field
declares. The endpoint, path, and size fields you're most likely to want to
parameterise do expand; an arbitrary field may not. If a ${VAR} shows up
verbatim in an error message or a log line, that field isn't one of them.
How the binary finds the config
One way only, and it is positional:
nativelink /path/to/config.json5No --config flag, no search path, no default location, no environment
variable naming the file. The single positional argument is the whole
interface.
That has one practical consequence worth internalising: a container image does not carry its config. Every deployment recipe in this repo mounts a config file in and passes its path as the argument. If you're writing a Kubernetes manifest, the config is a ConfigMap you mount, not something baked into the image.
Checking a config before you trust it
Here is the honest answer, because the alternative wastes an afternoon:
there is no --validate flag and no dry-run mode. The binary takes a
config path and starts.
What saves you is the order in which it starts. main() reads and parses the
entire config, and only afterwards raises the open-file limit, constructs the
stores, and binds any socket, so a config that fails to parse is rejected
before the process has touched a port, which makes "run it and see" a
genuinely safe validation step even on a machine where the real ports are
already in use by something else.
Combined with #[serde(deny_unknown_fields)], which is on nearly every
struct in the config crate, that gives you a fast, precise loop:
Start the binary against the config.
nativelink ./my-config.json5Read the first error, not the last. A parse failure names the offending key and its position. A misspelled
evicton_policyproduces anunknown fielderror namingevicton_policyand listing the fields that were expected, not a silent default and a cache that never evicts.Stop it once it's listening. Once the process reports its listeners are up, the config parsed. Everything after that point is runtime behaviour, not config validity.
You did it right if
- A deliberately misspelled key makes the process exit before it prints anything about listeners.
- Fixing the spelling makes it start.
- The failing run did not need a free port, because it never reached the bind.
Editor validation, if you want it
The config crate can emit a JSON Schema for the whole CasConfig type, which
editors will use for completion and inline errors:
cargo run --bin build-schema --features dev-schema --package nativelink-configThat writes nativelink_config.schema.json into the working directory. The
file is not committed to the repo; it's generated on demand, and it's
generated from the same Rust types the binary parses with, so it can't drift
from them. Point your editor's JSON5 schema mapping at the file you generated
and misspellings show up as you type rather than at startup.
Generated, not maintained
Because the schema comes from schema_for!(CasConfig), regenerating it after
you change NativeLink versions is the whole upgrade procedure. There's no
hand-written schema to fall out of date.
FAQ
Declaring stores, naming them, and the idea that unlocks the rest of the config: stores compose, and one store can refer to another by name.
SidewaysConfiguration referenceEvery field and default, generated from the same Rust types described here.