NativeLink

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.

CasConfig::try_from_json5_file

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}",
    },
  },
}
convert_string_with_shellexpand

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.json5

No --config flag, no search path, no default location, no environment variable naming the file. The single positional argument is the whole interface.

Args

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:

  1. Start the binary against the config.

    nativelink ./my-config.json5
  2. Read the first error, not the last. A parse failure names the offending key and its position. A misspelled evicton_policy produces an unknown field error naming evicton_policy and listing the fields that were expected, not a silent default and a cache that never evicts.

  3. 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-config

That 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

NextStores

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 reference

Every field and default, generated from the same Rust types described here.

On this page