feat: nix: match NixOS module to the config schema

This commit is contained in:
Igor Ramazanov
2025-07-27 02:20:09 +00:00
committed by Aarnav Tale
parent 89e38e1356
commit c59632a0b7
+157 -134
View File
@@ -6,63 +6,61 @@
}: let
inherit
(lib)
attrsToList
listToAttrs
map
filterAttrs
filterAttrsRecursive
mkEnableOption
mkIf
mkOption
mkPackageOption
typeOf
recursiveUpdate
types
updateManyAttrsByPath
;
cfg = config.services.headplane;
settingsFormat = pkgs.formats.yaml {};
settingsFile = settingsFormat.generate "headplane-config.yaml" cfg.settings;
agentEnv = listToAttrs (map (n: {
name = n.name;
value =
if ((typeOf n.value) == "bool")
then
(
if (n.value)
then "true"
else "false"
)
else n.value;
}) (attrsToList cfg.agent.settings));
settingsWithAgentExecutablePath = recursiveUpdate cfg.settings {
integration.agent.executable_path = "${cfg.settings.integration.agent.package}/bin/hp_agent";
};
settingsWithoutAgentPackage =
updateManyAttrsByPath [
{
path = ["integration" "agent"];
update = old: filterAttrs (key: value: key != "package") old;
}
]
settingsWithAgentExecutablePath;
settingsWithoutNulls = filterAttrsRecursive (key: value: value != null) settingsWithoutAgentPackage;
settingsFile = settingsFormat.generate "headplane-config.yaml" settingsWithoutNulls;
in {
options.services.headplane = {
enable = mkEnableOption "headplane";
package = mkPackageOption pkgs "headplane" {};
settings = lib.mkOption {
settings = mkOption {
description = ''
Headplane configuration options. Generates a YAML config file.
See: https://github.com/tale/headplane/blob/main/config.example.yaml
'';
type = lib.types.submodule {
freeformType = settingsFormat.type;
type = types.submodule {
options = {
server = lib.mkOption {
type = lib.types.submodule {
server = mkOption {
type = types.submodule {
options = {
host = lib.mkOption {
type = lib.types.str;
host = mkOption {
type = types.str;
default = "127.0.0.1";
description = "The host address to bind to.";
example = "0.0.0.0";
};
port = lib.mkOption {
type = lib.types.port;
port = mkOption {
type = types.port;
default = 3000;
description = "The port to listen on.";
};
cookie_secret_path = lib.mkOption {
type = lib.types.nullOr lib.types.path;
cookie_secret_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a file containing the cookie secret.
@@ -71,8 +69,8 @@ in {
example = "config.sops.secrets.headplane_cookie.path";
};
cookie_secure = lib.mkOption {
type = lib.types.bool;
cookie_secure = mkOption {
type = types.bool;
default = true;
description = ''
Should the cookies only work over HTTPS?
@@ -81,33 +79,15 @@ in {
'';
};
agent = lib.mkOption {
type = lib.types.submodule {
options = {
authkey_path = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to a file containing the agent auth key.
'';
example = "config.sops.secrets.agent_authkey.path";
};
ttl = lib.mkOption {
type = lib.types.int;
default = 180000;
description = "How long to cache agent information (in milliseconds).";
};
cache_path = lib.mkOption {
type = lib.types.str;
default = "/var/lib/headplane/agent_cache.json";
description = "Where to store the agent cache.";
};
};
};
default = {};
description = "Agent configuration for the Headplane agent.";
data_path = mkOption {
type = types.path;
default = "/var/lib/headplane";
description = ''
The path to persist Headplane specific data.
All data going forward is stored in this directory, including the internal database and any cache related files.
Data formats prior to 0.6.1 will automatically be migrated.
'';
example = "/var/lib/headplane";
};
};
};
@@ -115,11 +95,11 @@ in {
description = "Server configuration for Headplane web application.";
};
headscale = lib.mkOption {
type = lib.types.submodule {
headscale = mkOption {
type = types.submodule {
options = {
url = lib.mkOption {
type = lib.types.str;
url = mkOption {
type = types.str;
default = "http://127.0.0.1:8080";
description = ''
The URL to your Headscale instance.
@@ -130,8 +110,8 @@ in {
example = "https://headscale.example.com";
};
tls_cert_path = lib.mkOption {
type = lib.types.nullOr lib.types.path;
tls_cert_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a file containing the TLS certificate.
@@ -139,17 +119,15 @@ in {
example = "config.sops.secrets.tls_cert.path";
};
tls_key_path = lib.mkOption {
type = lib.types.nullOr lib.types.path;
public_url = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Path to a file containing the TLS private key.
'';
example = "config.sops.secrets.tls_key.path";
description = "Public URL if differrent. This affects certain parts of the web UI.";
example = "https://headscale.example.com";
};
config_path = lib.mkOption {
type = lib.types.nullOr lib.types.path;
config_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to the Headscale configuration file.
@@ -160,8 +138,8 @@ in {
example = "/etc/headscale/config.yaml";
};
config_strict = lib.mkOption {
type = lib.types.bool;
config_strict = mkOption {
type = types.bool;
default = true;
description = ''
Headplane internally validates the Headscale configuration
@@ -169,20 +147,93 @@ in {
If you want to disable this validation, set this to false.
'';
};
dns_records_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
If you are using `dns.extra_records_path` in your Headscale configuration, you need to set this to the path for Headplane to be able to read the DNS records.
Ensure that the file is both readable and writable by the Headplane process.
When using this, Headplane will no longer need to automatically restart Headscale for DNS record changes.
'';
example = "/var/lib/headplane/extra_records.json";
};
};
};
default = {};
description = "Headscale specific settings for Headplane integration.";
};
integration = lib.mkOption {
type = lib.types.submodule {
integration = mkOption {
type = types.submodule {
options = {
proc = lib.mkOption {
type = lib.types.submodule {
agent = mkOption {
type = types.submodule {
options = {
enabled = lib.mkOption {
type = lib.types.bool;
enabled = mkOption {
type = types.bool;
default = false;
description = ''
The Headplane agent allows retrieving information about nodes.
This allows the UI to display version, OS, and connectivity data.
You will see the Headplane agent in your Tailnet as a node when it connects.
'';
};
pre_authkey_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a file containing the agent preauth key.
To connect to your Tailnet, you need to generate a pre-auth key.
This can be done via the web UI or through the `headscale` CLI.
'';
example = "config.sops.secrets.agent_pre_authkey.path";
};
host_name = mkOption {
type = types.str;
default = "headplane-agent";
description = "Optionally change the name of the agent in the Tailnet";
};
cache_ttl = mkOption {
type = types.int;
default = 180000;
description = ''
How long to cache agent information (in milliseconds).
If you want data to update faster, reduce the TTL, but this will increase the frequency of requests to Headscale.
'';
};
cache_path = mkOption {
type = types.path;
default = "/var/lib/headplane/agent_cache.json";
description = "Where to store the agent cache.";
};
work_dir = mkOption {
type = types.path;
default = "/var/lib/headplane/agent";
description = ''
Do not change this unless you are running a custom deployment.
The work_dir represents where the agent will store its data to be able to automatically reauthenticate with your Tailnet.
It needs to be writable by the user running the Headplane process.
'';
};
package = mkPackageOption pkgs "headplane-agent" {};
};
};
default = {};
description = "Agent configuration for the Headplane agent.";
};
proc = mkOption {
type = types.submodule {
options = {
enabled = mkOption {
type = types.bool;
default = true;
description = ''
Enable "Native" integration that works when Headscale and
@@ -202,25 +253,25 @@ in {
description = "Integration configurations for Headplane to interact with Headscale.";
};
oidc = lib.mkOption {
type = lib.types.submodule {
oidc = mkOption {
type = types.submodule {
options = {
issuer = lib.mkOption {
type = lib.types.str;
issuer = mkOption {
type = types.str;
default = "";
description = "URL to OpenID issuer.";
example = "https://provider.example.com/issuer-url";
};
client_id = lib.mkOption {
type = lib.types.str;
client_id = mkOption {
type = types.str;
default = "";
description = "The client ID for the OIDC client.";
example = "your-client-id";
};
client_secret_path = lib.mkOption {
type = lib.types.nullOr lib.types.path;
client_secret_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a file containing the OIDC client secret.
@@ -228,23 +279,24 @@ in {
example = "config.sops.secrets.oidc_client_secret.path";
};
disable_api_key_login = lib.mkOption {
type = lib.types.bool;
disable_api_key_login = mkOption {
type = types.bool;
default = false;
description = "Whether to disable API key login.";
};
token_endpoint_auth_method = lib.mkOption {
type = lib.types.enum [
token_endpoint_auth_method = mkOption {
type = types.enum [
"client_secret_post"
"client_secret_basic"
"client_secret_jwt"
];
default = "client_secret_post";
description = "The token endpoint authentication method.";
};
headscale_api_key_path = lib.mkOption {
type = lib.types.nullOr lib.types.path;
headscale_api_key_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a file containing the Headscale API key.
@@ -252,8 +304,8 @@ in {
example = "config.sops.secrets.headscale_api_key.path";
};
redirect_uri = lib.mkOption {
type = lib.types.str;
redirect_uri = mkOption {
type = types.str;
default = "";
description = ''
This should point to your publicly accessible URL
@@ -261,6 +313,15 @@ in {
'';
example = "https://headscale.example.com/admin/oidc/callback";
};
user_storage_file = mkOption {
type = types.path;
default = "/var/lib/headplane/users.json";
description = ''
Path to a file containing the users and their permissions for Headplane.
'';
example = "/var/lib/headplane/users.json";
};
};
};
default = {};
@@ -270,20 +331,6 @@ in {
};
default = {};
};
agent = mkOption {
type = types.submodule {
options = {
enable = mkEnableOption "headplane-agent";
package = mkPackageOption pkgs "headplane-agent" {};
settings = mkOption {
type = types.attrsOf [types.str types.bool];
description = "Headplane agent env vars config. See: https://github.com/tale/headplane/blob/main/docs/Headplane-Agent.md";
default = {};
};
};
};
};
};
config = mkIf cfg.enable {
@@ -292,30 +339,6 @@ in {
etc."headplane/config.yaml".source = "${settingsFile}";
};
systemd.services.headplane-agent =
mkIf cfg.agent.enable
{
description = "Headplane side-running agent";
wantedBy = ["multi-user.target"];
after = ["headplane.service"];
requires = ["headplane.service"];
environment = agentEnv;
serviceConfig = {
User = config.services.headscale.user;
Group = config.services.headscale.group;
ExecStart = "${pkgs.headplane-agent}/bin/hp_agent";
Restart = "always";
RestartSec = 5;
# TODO: Harden `systemd` security according to the "The Principle of Least Power".
# See: `$ systemd-analyze security headplane-agent`.
};
};
systemd.services.headplane = {
description = "Headscale Web UI";