feat: add support for importing nixlets into each other

This commit is contained in:
technofab 2026-02-09 17:37:02 +01:00
parent c44a5fc995
commit 2c21317f45
Signed by: technofab
SSH key fingerprint: SHA256:bV4h88OqS/AxjbPn66uUdvK9JsgIW4tv3vwJQ8tpMqQ
6 changed files with 345 additions and 151 deletions

3
docs/options.md Normal file
View file

@ -0,0 +1,3 @@
# Options
{{ include_raw("options.md") }}

View file

@ -2,155 +2,227 @@
lib,
kubenix,
...
} @ attrs:
with lib; rec {
evalValues = file: {rawValues, ...} @ args: (lib.evalModules {
specialArgs = {
utils = import ./utils.nix attrs;
};
modules = [
file
(_: {
# pass through all args to the values.nix module
config =
rawValues
// {
_module.args = args;
} @ attrs: let
inherit (lib) mkOption types evalModules concatMapStringsSep assertMsg;
nixlet-lib = rec {
nixletModule = ./nixletModule.nix;
evalValues = file: {
rawValues,
dependencies,
args,
check ? true,
...
}: let
moduleArgs =
args
// {
utils = import ./utils.nix attrs;
};
# get the values from the dependencies, then import them nested
# (so you can set postgres.replicaCount in values.nix for example when adding "postgres" as dependency)
extraModules = map (depName: {
options.${depName} = mkOption {
type = types.submodule {
imports = ["${dependencies.${depName}.path}/values.nix"];
_module.args =
moduleArgs
// {
# make sure that dependencies see their own name and version etc.
nixlet = {
inherit (dependencies.${depName}) name version description;
inherit (moduleArgs.nixlet) project;
};
};
};
})
];
});
mkValues = file: args: (evalValues file args).config;
default = {};
description = let
n = dependencies.${depName};
in ''
Imported Nixlet as a dependency:
# wraps mkNixletInner to allow passing either a path or an attrset
mkNixlet = arg:
mkNixletInner (
if (builtins.typeOf arg) == "set"
then arg
else
{path = arg;}
// (
if builtins.pathExists "${arg}/nixlet.nix"
then (import "${arg}/nixlet.nix")
else throw "Nixlet at '${arg}' does not contain nixlet.nix and mkNixlet was called with just a path"
)
);
|Name|Version|Description|
|----|-------|-----------|
|${n.name}|${n.version}|${n.description}|
'';
};
}) (builtins.attrNames dependencies);
in
builtins.addErrorContext "[nixlets] while evaluating values" (
evalModules {
modules =
[
file
{
_module = {
args = moduleArgs;
inherit check;
};
}
{config = rawValues;}
]
++ extraModules;
}
);
mkNixletInner = {
path,
name,
version ? null,
description ? "",
defaultProject ? null,
...
}: let
# every nixlet gets "nixlet" as arg with some useful data about itself
baseNixletArg = {
inherit name version description;
project = defaultProject;
};
nixlet = {
inherit name version description path;
values = evalValues "${path}/values.nix" {
rawValues = {};
nixlet = baseNixletArg;
# wraps mkNixletInner to allow passing either a path or an attrset
mkNixlet = arg:
mkNixletInner (
if (builtins.typeOf arg) == "set"
then arg
else
{path = arg;}
// (
if builtins.pathExists "${arg}/nixlet.nix"
then (import "${arg}/nixlet.nix")
else throw "Nixlet at '${arg}' does not contain nixlet.nix and mkNixlet was called with just a path"
)
);
mkNixletInner = {
path,
name,
version ? null,
description ? "",
defaultProject ? null,
...
}: let
# every nixlet gets "nixlet" as arg with some useful data about itself
baseNixletArg = {
inherit name version description;
project = defaultProject;
};
mkDocs = opts: mkDocs (opts // {inherit nixlet;});
eval = {
system,
project ? defaultProject,
overrides ? (_: {}),
values ? {},
}:
assert lib.assertMsg (project != null) "No default project set, please pass a project to the render method"; let
nixletArg = baseNixletArg // {inherit project;};
nixlet = {
_type = "nixlet";
inherit name version description path;
# just values of the current nixlet (lighweight)
values = evalValues "${path}/values.nix" {
rawValues = {};
dependencies = {};
# no checking since this doesn't include dependencies
check = false;
args.nixlet = baseNixletArg;
};
# full values, including dependencies etc. (complex)
fullValues = args: let
evaled = nixlet.eval args;
in
kubenix.evalModules.${system} {
module = {kubenix, ...}: {
imports = with kubenix.modules; [
k8s
helm
docker
files
./secretsModule.nix
(_: let
finalValues = mkValues "${path}/values.nix" {
rawValues = values;
nixlet = nixletArg;
};
in {
imports = [path];
_module.args.nixlet =
{
values = finalValues;
}
// nixletArg;
})
overrides
];
kubenix.project = project;
};
evalValues "${path}/values.nix" {
rawValues = {};
inherit (evaled.config.nixlet) dependencies;
args.nixlet = baseNixletArg;
};
render = {
system,
project ? defaultProject,
overrides ? (_: {}),
values ? {},
}:
(nixlet.eval {
inherit system project overrides values;
})
.config
.kubernetes
.resultYAML;
# combines all secrets files in a single directory
secrets = args: (nixlet.eval args).config.kubernetes.secretsCombined;
};
in
nixlet;
mkDocs = opts: mkDocs (opts // {inherit nixlet;});
eval = {
system,
project ? defaultProject,
overrides ? (_: {}),
values ? {},
}:
assert assertMsg (project != null) "No default project set, please pass a project to the eval/render method"; let
nixletArg = baseNixletArg // {inherit project;};
in
builtins.addErrorContext "[nixlets] while evaluating nixlet ${name}" (
kubenix.evalModules.${system} {
module = {
config,
kubenix,
...
}: {
imports = with kubenix.modules; [
k8s
helm
docker
files
./secretsModule.nix
./nixletModule.nix
(let
finalValues =
(evalValues "${path}/values.nix" {
rawValues = values;
inherit (config.nixlet) dependencies;
args.nixlet = nixletArg;
}).config;
in {
imports = [path];
_module.args = {
nixlet =
{
values = finalValues;
}
// nixletArg;
inherit nixlet-lib system;
};
})
overrides
];
kubenix.project = project;
};
}
);
render = {
system,
project ? defaultProject,
overrides ? (_: {}),
values ? {},
}:
(nixlet.eval {
inherit system project overrides values;
})
.config
.kubernetes
.resultYAML;
# combines all secrets files in a single directory
secrets = args: (nixlet.eval args).config.kubernetes.secretsCombined;
fetchNixlet = url: sha256: mkNixlet (builtins.fetchTarball {inherit url sha256;});
fetchNixletFromGitlab = {
project,
name,
version,
sha256,
}: let
projectEscaped = builtins.replaceStrings ["/"] ["%2F"] project;
in
fetchNixlet "https://gitlab.com/api/v4/projects/${projectEscaped}/packages/generic/${name}/${version}/${name}.tar.gz" sha256;
};
in
nixlet;
uploadNixletsToGitlab = {
pkgs,
projectId,
nixlets,
...
}:
pkgs.writeShellScriptBin "nixlets-upload" (
''
if [[ -z "$AUTH_HEADER" ]]; then
echo "Must provide AUTH_HEADER environment variable!" 1>&2
exit 1
fi
''
+ lib.concatStringsSep "\n" (
builtins.map (nixlet:
with nixlet; ''
URL="https://gitlab.com/api/v4/projects/${projectId}/packages/generic/${name}/${version}/${name}.tar.gz"
if ${pkgs.curl}/bin/curl --output /dev/null --silent --head --fail --header "$AUTH_HEADER" $URL; then
echo "> Skipped ${name}@${version} because it already exists in the Package Registry"
else
echo "> Uploading new version ${name}@${version}"
${pkgs.gnutar}/bin/tar -czf /tmp/${name}.tar.gz --mode='u+rwX' -C ${path} --transform 's/^\./\/${name}/' .
${pkgs.curl}/bin/curl --header "$AUTH_HEADER" --upload-file "/tmp/${name}.tar.gz" "$URL"; echo;
${pkgs.coreutils}/bin/rm -f /tmp/${nixlet.name}.tar.gz
echo "> Finished ${name}@${version}, see above"
fi
'')
nixlets
)
);
fetchNixlet = url: sha256: mkNixlet (builtins.fetchTarball {inherit url sha256;});
fetchNixletFromGitlab = {
project,
name,
version,
sha256,
}: let
projectEscaped = builtins.replaceStrings ["/"] ["%2F"] project;
in
fetchNixlet "https://gitlab.com/api/v4/projects/${projectEscaped}/packages/generic/${name}/${version}/${name}.tar.gz" sha256;
mkDocs = opts:
import ./valuesDocs.nix (opts // {inherit lib;});
}
uploadNixletsToGitlab = {
pkgs,
projectId,
nixlets,
...
}:
pkgs.writeShellScriptBin "nixlets-upload" (
''
if [[ -z "$AUTH_HEADER" ]]; then
echo "Must provide AUTH_HEADER environment variable!" 1>&2
exit 1
fi
''
+ concatMapStringsSep "\n" (
(nixlet:
with nixlet; ''
URL="https://gitlab.com/api/v4/projects/${projectId}/packages/generic/${name}/${version}/${name}.tar.gz"
if ${pkgs.curl}/bin/curl --output /dev/null --silent --head --fail --header "$AUTH_HEADER" $URL; then
echo "> Skipped ${name}@${version} because it already exists in the Package Registry"
else
echo "> Uploading new version ${name}@${version}"
${pkgs.gnutar}/bin/tar -czf /tmp/${name}.tar.gz --mode='u+rwX' -C ${path} --transform 's/^\./\/${name}/' .
${pkgs.curl}/bin/curl --header "$AUTH_HEADER" --upload-file "/tmp/${name}.tar.gz" "$URL"; echo;
${pkgs.coreutils}/bin/rm -f /tmp/${nixlet.name}.tar.gz
echo "> Finished ${name}@${version}, see above"
fi
'')
nixlets
)
);
mkDocs = opts:
import ./valuesDocs.nix (opts // {inherit lib;});
};
in
nixlet-lib

75
lib/nixletModule.nix Normal file
View file

@ -0,0 +1,75 @@
{
lib,
config,
nixlet,
system,
...
}: let
inherit (lib) mkOption types mkOptionType isType mkMerge mapAttrs mkIf literalExpression;
cfg = config.nixlet;
nixletType = mkOptionType {
name = "nixlet";
description = "reference";
descriptionClass = "noun";
check = isType "nixlet";
};
in {
imports = [
{
# shortcut, allows accessing deps a bit shorter/more easily
_module.args.deps = cfg.deps;
}
];
options.nixlet = {
dependencies = mkOption {
type = types.attrsOf nixletType;
default = {};
description = ''
Import other nixlets as dependencies. Works similar to Helm, specify values for these
Nixlets by using their name as a prefix. Like `postgres.replicaCount` in `values.nix` for example.
'';
example = literalExpression ''
{
"postgres" = nixlet-lib.mkNixlet <path>;
"mongodb" = nixlet-lib.fetchNixlet ...; # etc.
}
'';
};
deps = mkOption {
readOnly = true;
type = types.attrsOf types.attrs;
default = mapAttrs (name: val:
builtins.addErrorContext "[nixlets] while evaluating dependency ${name}"
(val.eval {
inherit system;
inherit (config.kubenix) project;
values = nixlet.values.${name};
}).config)
cfg.dependencies;
description = ''
Evaluated dependency nixlets. Allows accessing their resources like for example:
```nix
config.nixlet.deps."<name>".kubernetes.resources
```
'';
};
depAutoMerge = mkOption {
type = types.bool;
default = true;
description = ''
Whether to automatically merge dependency nixlets' configs
with the current nixlet. If disabled, you can access dependency outputs via:
```nix
config.nixlet.deps."<name>".kubernetes.resources
```
'';
};
};
config = mkIf cfg.depAutoMerge {
kubernetes.resources = mkMerge (map (dep: dep.kubernetes.resources) (builtins.attrValues cfg.deps));
};
}

View file

@ -1,6 +1,8 @@
{
lib,
nixlet,
# whether to generate docs for the full values, including dependencies
fullValues ? false,
transformOptions ? opt: opt,
filter ? _: true,
headingDepth ? 3,
@ -13,7 +15,12 @@
mapAttrsToList
concatStrings
replicate
optionalString
optionAttrSetToDocList
attrByPath
generators
;
inherit (generators) toPretty;
_transformOptions = opt:
transformOptions (opt
@ -25,7 +32,12 @@
name = lib.removePrefix "config." opt.name;
});
rawOpts = lib.optionAttrSetToDocList nixlet.values.options;
valueSource =
if fullValues
# TODO: get rid of system, just here cuz of kubenix
then (nixlet.fullValues {system = "x86_64-linux";})
else nixlet.values;
rawOpts = optionAttrSetToDocList valueSource.options;
transformedOpts = map _transformOptions rawOpts;
filteredOpts = lib.filter (opt: opt.visible && !opt.internal) transformedOpts;
@ -58,7 +70,20 @@
${opt.type}
```
''
+ (lib.optionalString (opt ? default && opt.default != null) ''
# used to show what changes a nixlet did to values of dependencies
+ (let
val = toPretty {} (attrByPath opt.loc "_not found_" valueSource.config);
default = removeSuffix "\n" opt.default.text;
in
optionalString (opt.type != "submodule" && val != default)
''
**Overridden value**:
```nix
${val}
```
'')
+ (optionalString (opt ? default && opt.default != null) ''
**Default value**:
@ -66,7 +91,7 @@
${removeSuffix "\n" opt.default.text}
```
'')
+ (lib.optionalString (opt ? example) ''
+ (optionalString (opt ? example) ''
**Example value**:

View file

@ -3,8 +3,22 @@
cell,
...
}: let
inherit (inputs) doclib;
inherit (inputs) pkgs doclib nixlet-lib;
inherit (cell) nixlets;
optionsDoc = doclib.mkOptionDocs {
module = nixlet-lib.nixletModule;
roots = [
{
url = "https://gitlab.com/TECHNOFAB/nixlets/-/blob/main/lib";
path = "${inputs.self}/lib";
}
];
};
optionsDocs = pkgs.runCommand "options-docs" {} ''
mkdir -p $out
ln -s ${optionsDoc} $out/options.md
'';
in
(doclib.mkDocs {
docs."default" = {
@ -23,9 +37,13 @@ in
domains = ["nixlets.projects.tf"];
};
};
macros = {
enable = true;
includeDir = toString optionsDocs;
};
dynamic-nav = {
enable = true;
files."Nixlets Values" = builtins.map (val: {${val.name} = val.mkDocs {};}) (builtins.attrValues nixlets);
files."Nixlets Values" = builtins.map (val: {${val.name} = val.mkDocs {fullValues = true;};}) (builtins.attrValues nixlets);
};
config = {
site_name = "Nixlets";
@ -45,6 +63,7 @@ in
{"Usage" = "usage.md";}
{"Generating Docs" = "generating_docs.md";}
{"Secrets" = "secrets.md";}
{"Options" = "options.md";}
];
markdown_extensions = [
{

6
nix/repo/flake.lock generated
View file

@ -38,11 +38,11 @@
"nixmkdocs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1766062227,
"narHash": "sha256-jhr5CUi9eDeMIAJn7ayXP8Wr+Y2loV5EhdDIKDkRIdw=",
"lastModified": 1767549915,
"narHash": "sha256-by3r2qddlyzylup5fzSaDwtoy3eFHNKb65IuIq6bsAs=",
"owner": "TECHNOFAB",
"repo": "nixmkdocs",
"rev": "cb0bb5dc3382e8ba5d81324a2f1fd94ccd5a5df4",
"rev": "f3b2f4b19178e97c5580367be0f97e61a085db6d",
"type": "gitlab"
},
"original": {