Skip to content
kix /docs
Install the CLI

Reference Package schema

options

The typed configuration interface a package exposes to its instances.

options = {
replicas = lib.mkOption {
type = lib.types.ints.positive;
default = 1;
description = "Number of application replicas.";
};
};

options is an attribute set of Nix module option declarations. It defines the values accepted under instances.<namespace>.<name>.config and the evaluated config passed to the package’s build entries.

The attribute is optional. Its default is { }.

Declare options with lib.mkOption. The commonly used fields are:

FieldMeaning
typeValidation and merge behavior for the value.
defaultValue used when no module sets the option.
descriptionExplanation shown in generated option reference material.
exampleExample value for reference material.

Nested attribute paths create nested configuration:

options.metrics.enabled = lib.mkOption {
type = lib.types.bool;
default = true;
};

Kix also provides reusable declarations such as kix.options.image, kix.options.ingress, kix.options.monitor, and kix.options.healthCheck.

Kix evaluates four sources together:

  1. defaults declared in options;
  2. defaults or fixed values in the package’s top-level config module;
  3. configuration injected by dependent packages; and
  4. the instance’s config modules.

Each of these modules can take clusterConfig as a module argument, for example config = { clusterConfig, ... }: { zone = lib.mkDefault clusterConfig.clusterDomain; };.

A lib.mkDefault in the package’s own config takes precedence over an option declaration’s default, while an instance value or dependency injection takes precedence over that package default. Standard Nix module types, merge rules, lib.mkForce, and validation still apply.

The resulting values are passed to each build entry as config. Setting an undeclared option is an evaluation error.

A package option’s default must survive an instance that sets only part of the option. The module system replaces an attribute-set default as a whole as soon as any definition exists, so the option contract refuses three declarations whose defaults an instance would partly discard:

RuleDeclarationWhat an instance loses
1An untyped option (anything, unspecified, raw, attrs, or attrsOf of one of those) with a non-empty attribute-set defaultEvery default key once it sets any key, and misspelled keys are accepted
2An attrsOf or lazyAttrsOf of any other type with a non-empty defaultEvery package entry once it sets one key
3A submodule whose own default sets a key that the field does not default to the same valueThat key once it sets any field

nullOr, uniq, and unique are looked through, and each branch of an either or oneOf is checked. A coercedTo whose source type rejects the default is checked as its final type. An option declared without type merges as unspecified, so rule 1 applies to it. A default wrapped in lib.mkDefault, lib.mkOverride, lib.mkOrder, lib.mkMerge, or lib.mkIf is checked as the value it wraps. The default of an option whose type takes no attribute set, such as str, int, or listOf, is not evaluated, so it may read a sibling field that has no default. List defaults are not checked, because every Nix merge replaces a list.

A declaration the check cannot read also breaks the contract: a default that throws when evaluated, a default with any other _type, a submodule whose fields throw when evaluated with { }, a non-empty attribute-set default on a type none of the rules covers (such as attrTag, or a coercedTo whose source type accepts the default), an entry in options that is not an option, and an option nested more than twelve levels deep. A recursive submodule type stops at the first option that repeats a declaration on its path, so the depth limit applies only to options without a declaration position.

The check fails with one bullet per violating option, each naming the file and line of the declaration:

the options of kix/packages/<package> break the option contract (docs: reference/package-schema/options, "Option contract"):
- <file>:<line>: option `hpa` has type `anything` and a non-empty default (keys: maxReplicas, minReplicas). ...

The every-package-evaluates check of kixpkgs checks every package under kix/packages/ before it evaluates any package in a cluster. Evaluating a cluster does not check the contract, so a package outside kixpkgs is checked only by its own flake or its author.

kix.checkPackageOptions runs the check on one package. It calls the package the way instance evaluation does, at each availability level up to the package’s meta.availability.max, so a default that depends on availability is checked at every level the package can run at. A violation that occurs at some levels only names them. It returns null when the options pass and throws otherwise:

kix.checkPackageOptions {
package = import ./my-package;
# Names the package in the error message.
who = "./my-package";
# Optional; the phase-1 scope the package is called with.
scope = { instanceName = "my-package"; namespaceName = "my-package"; };
}

From the directory that holds a package, with the kixpkgs flake reference your flake uses:

Terminal window
nix eval --impure --extra-experimental-features 'nix-command flakes pipe-operators' --expr '
let
kixpkgs = builtins.getFlake "<kixpkgs flake reference>";
inherit (kixpkgs.inputs.nixpkgs) lib;
pkgs = kixpkgs.inputs.nixpkgs.legacyPackages.x86_64-linux;
inherit (kixpkgs.lib.forPkgs { inherit pkgs lib; }) kix;
in
kix.checkPackageOptions { package = import ./my-package; who = "./my-package"; }'

A flake built with mkFlake gets one check per package listed in its packages argument, named option-contract-<name>, so nix flake check runs the contract:

flake.nix
outputs = inputs: inputs.kixpkgs.lib.mkFlake {
inherit inputs;
clusters.production = ./clusters/production.nix;
packages.my-package = ./packages/my-package;
};

To check one kixpkgs package from the repository root, run the following. It prints null when the options pass:

Terminal window
nix eval --impure --extra-experimental-features 'nix-command flakes pipe-operators' --expr '
let
nixpkgs = (builtins.getFlake (toString ./.)).inputs.nixpkgs;
in
(import ./kix/package-eval.nix {
inherit (nixpkgs) lib;
kix = import ./kix/lib { pkgs = nixpkgs.legacyPackages.x86_64-linux; inherit (nixpkgs) lib; };
}).checkOptionContract "<package>"'

Each rule has a remedy that keeps the package’s values:

  • Declare the option as a submodule whose fields carry their own defaults. For container resources, use kix.options.resources. For a submodule (rule 3), put each value on its field’s default and give the option the default { }.
  • For a typed collection (rule 2) whose entries should stay unless overridden, default the option to { } and set each entry in the package’s config with lib.mkDefault.
  • When an instance should replace the value as a whole, such as a selector or a program’s configuration file, default the option to null and apply the package’s value in the build when it is null.

With the second and third remedies, give the option a defaultText that names the package’s value, so the generated reference shows it.