Skip to content

feat(settings): importable settings model and validator, with in-memory validation #834

Closed

Description

@EricAndrechek

Problem

A program that writes settings directories for a WaveHouse server has no way to check a folder against the server's own rules before handing it over. Examples: a control plane, a provisioning tool, a GitOps generator.

  • Not importable. The only checking path is settings.ValidateDir, which lives under internal/. No other module can import it.
  • The CLI works only on disk. wavehouse validate runs the same check, but only over a directory on disk, and reports human-readable text plus an exit code. A program has to write a temporary tree, spawn the binary at the right version, and scrape the output.
  • No HTTP dry run. The policy HTTP surface was removed in 0.2.0 (fix(policy): reject no-op rules, drop the policy HTTP surface #541). POST /v1/ops/settings/reload validates and adopts.
  • A writer must get it right first time. In a nested directory, a rejected folder fails closed with no fallback to its previous settings. One bad write takes that tenant out of service until the next good one.

A writer that re-implements the rules drifts from the server the first time they change. The only safe validator is the one the server runs, at the same version.

Proposed solution

  1. Make the settings model and its validator importable. Move the parts that are pure data and pure checks out of internal/ into public packages in the same module. They are then versioned by the server's own release tags: a consumer pinned to github.com/Wave-RF/WaveHouse@v0.2.x validates with exactly the rules a v0.2.x server applies.

    • Public, from internal/settings:
      • the document types (Document, TenantConfig and its blocks, RolesFile, PipesFile);
      • Files();
      • Finding, Severity and HasErrors;
      • the validator;
      • Seed().
    • Their dependencies: internal/policy, internal/pipes, internal/tenant (the tenant id grammar) and internal/chsql. Today these import only the standard library, go-humanize and yaml.v3.
    • Stays internal:
      • the runtime half: Registry, Store, Watch (and with it fsnotify), and WriteSeed;
      • the server's runtime dependencies (NATS, Pebble, Redis, the AWS SDK, the ClickHouse driver, OpenTelemetry), which the public packages must not import.
    • Package paths are the maintainers' call (settings/, policy/ and so on at the module root, or under pkg/).
  2. Validate in memory. ValidateDir already reads the four files into bytes (checkDir) and from then on checks only bytes. Split it at that seam and export the second half, for example:

    func ValidateFiles(files map[string][]byte) (*Document, []Finding)

    An fs.FS signature works too. ValidateDir then becomes "read the directory, then ValidateFiles".

    • Disk-only checks stay in ValidateDir: the directory exists, no unexpected entries, no non-regular files, and config.json's permission bits.
    • Export the tenant id check, so a writer can validate a folder name before it creates the folder.
    • One path. wavehouse validate, boot and reload keep going through this single path; nothing gets a second validator.
  3. Write with the same types.

    • Export Seed(), so a writer starts from the server's defaults instead of copying them.
    • Make sure marshaling the public types yields a document the validator accepts: every required key present, nothing the strict decoder rejects. A writer that builds the struct and marshals it then cannot emit a shape the server refuses.
  4. Keep findings stable. Keep Finding JSON-tagged and identical to the findings the reload endpoint returns. The same list can then be shown to whoever edited the settings, whichever path produced it.

  5. Document it. Add a short "Validating from Go" section to the settings-directory page, with the version rule: validate with the package from the same tag as the server.

Acceptance criteria

  • External import works. A module outside this repository can import the settings, policy and pipes packages at a release tag without importing anything under internal/.
  • No runtime dependencies. go list -deps on those packages shows no server-runtime dependency.
  • Same findings in memory as on disk. For every existing validator fixture, ValidateFiles on the fixture's bytes returns the same findings as ValidateDir on the fixture directory, apart from the disk-only checks.
  • Seed round-trips cleanly. Seed() → unmarshal into the public types → marshal → ValidateFiles returns no errors.
  • No behaviour change. The CLI, boot and reload still share a single validation path.

Alternatives considered

Out of scope (possible follow-up)

Some checks need the table schema, such as a policy check that names a column the table lacks. Today they can't run without ClickHouse. A follow-up could expose them as a pure function over a column list, so a writer that already knows the schema can run them too.

Related: #583 (multi-tenant settings directory), #506.

Activity

  1. moved this from Backlog to In progress in WaveHouse Task Boardon Oct 13, 2026
  2. added a commit that references this issue on Oct 14, 2026
    e999b6e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/configConfig file, config knobs, hot-reloadenhancementNew feature or request

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions