nixos/modular-services: add systemd.mainExecStart option (#469450)
This commit is contained in:
@@ -58,6 +58,61 @@ in
|
||||
(lib.mkAliasOptionModule [ "systemd" "socket" ] [ "systemd" "sockets" "" ])
|
||||
];
|
||||
options = {
|
||||
systemd.lib = mkOption {
|
||||
description = ''
|
||||
Library functions for working with systemd services.
|
||||
|
||||
Available functions:
|
||||
|
||||
- `escapeSystemdExecArgs`: Escapes a list of arguments for use in ExecStart.
|
||||
Prevents systemd's specifier (%) and variable ($) substitution by escaping
|
||||
them to %% and $$ respectively.
|
||||
|
||||
Example: `escapeSystemdExecArgs [ "/bin/echo" "Unit %n" ]`
|
||||
produces `"/bin/echo" "Unit %%n"`
|
||||
'';
|
||||
type = types.lazyAttrsOf types.raw;
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
systemd.mainExecStart = mkOption {
|
||||
description = ''
|
||||
Main command line for systemd's ExecStart with systemd's specifier and
|
||||
environment variable substitution enabled.
|
||||
|
||||
This option sets the primary ExecStart entry. Additional ExecStart entries
|
||||
can be added via `systemd.service.serviceConfig.ExecStart` with `lib.mkBefore`
|
||||
or `lib.mkAfter`.
|
||||
|
||||
This option allows you to use systemd specifiers like `%n` (unit name),
|
||||
`%i` (instance), `%t` (runtime directory), and environment variables using
|
||||
`''${VAR}` syntax in your command line.
|
||||
|
||||
By default, this is set to the escaped version of {option}`process.argv`
|
||||
to prevent systemd substitution. Set this option explicitly to enable
|
||||
systemd's substitution features.
|
||||
|
||||
To extend {option}`process.argv` with systemd specifiers, you can append
|
||||
to the escaped arguments:
|
||||
|
||||
```nix
|
||||
systemd.mainExecStart =
|
||||
config.systemd.lib.escapeSystemdExecArgs config.process.argv + " --systemd-unit %n";
|
||||
```
|
||||
|
||||
This pattern allows you to pass the unit name (or other systemd specifiers)
|
||||
as additional arguments while keeping the base command from {option}`process.argv`
|
||||
properly escaped.
|
||||
|
||||
See {manpage}`systemd.service(5)` (section "COMMAND LINES") for details on
|
||||
variable substitution and {manpage}`systemd.unit(5)` (section "SPECIFIERS")
|
||||
for available specifiers like `%n`, `%i`, `%t`.
|
||||
'';
|
||||
type = types.str;
|
||||
default = config.systemd.lib.escapeSystemdExecArgs config.process.argv;
|
||||
defaultText = lib.literalExpression "config.systemd.lib.escapeSystemdExecArgs config.process.argv";
|
||||
};
|
||||
|
||||
systemd.services = mkOption {
|
||||
description = ''
|
||||
This module configures systemd services, with the notable difference that their unit names will be prefixed with the abstract service name.
|
||||
@@ -106,6 +161,10 @@ in
|
||||
};
|
||||
};
|
||||
config = {
|
||||
systemd.lib = {
|
||||
inherit escapeSystemdExecArgs;
|
||||
};
|
||||
|
||||
# Note that this is the systemd.services option above, not the system one.
|
||||
systemd.services."" = {
|
||||
# TODO description;
|
||||
@@ -115,7 +174,7 @@ in
|
||||
Restart = lib.mkDefault "always";
|
||||
RestartSec = lib.mkDefault "5";
|
||||
ExecStart = [
|
||||
(escapeSystemdExecArgs config.process.argv)
|
||||
config.systemd.mainExecStart
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -52,6 +52,45 @@ let
|
||||
};
|
||||
};
|
||||
|
||||
# Test that systemd.mainExecStart overrides process.argv
|
||||
# and allows systemd's specifier and variable substitution
|
||||
system.services.argv-with-subst = {
|
||||
process = {
|
||||
argv = [
|
||||
hello'
|
||||
"--greeting"
|
||||
"This should be ignored"
|
||||
];
|
||||
};
|
||||
systemd.mainExecStart = ''/bin/sh -c "echo %n and ''${HOME}"'';
|
||||
};
|
||||
|
||||
# Test that process.argv escapes % and $ by default
|
||||
system.services.argv-escaped = {
|
||||
process = {
|
||||
argv = [
|
||||
"/bin/sh"
|
||||
"-c"
|
||||
"echo %n and \${HOME}"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# Test extending process.argv with systemd specifiers
|
||||
system.services.argv-extended =
|
||||
{ config, ... }:
|
||||
{
|
||||
process = {
|
||||
argv = [
|
||||
hello'
|
||||
"--greeting"
|
||||
"Fun $1 fact, remainder is often expressed as m%n"
|
||||
];
|
||||
};
|
||||
systemd.mainExecStart =
|
||||
config.systemd.lib.escapeSystemdExecArgs config.process.argv + " --systemd-unit %n";
|
||||
};
|
||||
|
||||
# irrelevant stuff
|
||||
system.stateVersion = "25.05";
|
||||
fileSystems."/".device = "/test/dummy";
|
||||
@@ -83,6 +122,18 @@ runCommand "test-modular-service-systemd-units"
|
||||
grep 'ExecStart="${hello}/bin/hello" "--greeting" ".*database.*"' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null
|
||||
grep -F 'RestartSec=42' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null
|
||||
|
||||
# Test that systemd.mainExecStart overrides process.argv
|
||||
# Note: %n and $HOME are NOT escaped, allowing systemd to substitute them
|
||||
grep -F 'ExecStart=/bin/sh -c "echo %n and ''${HOME}"' ${toplevel}/etc/systemd/system/argv-with-subst.service >/dev/null
|
||||
|
||||
# Test that process.argv escapes % as %% and $ as $$
|
||||
# This prevents systemd from performing specifier/variable substitution
|
||||
grep -F 'ExecStart="/bin/sh" "-c" "echo %%n and $${HOME}"' ${toplevel}/etc/systemd/system/argv-escaped.service >/dev/null
|
||||
|
||||
# Test extending process.argv with systemd specifiers
|
||||
# The base command should be escaped ($1 -> $$1, m%n -> m%%n), but the appended --systemd-unit %n should not be
|
||||
grep -F 'ExecStart="${hello}/bin/hello" "--greeting" "Fun $$1 fact, remainder is often expressed as m%%n" --systemd-unit %n' ${toplevel}/etc/systemd/system/argv-extended.service >/dev/null
|
||||
|
||||
[[ ! -e ${toplevel}/etc/systemd/system/foo.socket ]]
|
||||
[[ ! -e ${toplevel}/etc/systemd/system/bar.socket ]]
|
||||
[[ ! -e ${toplevel}/etc/systemd/system/bar-db.socket ]]
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
lib,
|
||||
nixosTests,
|
||||
ghostunnel,
|
||||
writeScript,
|
||||
runtimeShell,
|
||||
}:
|
||||
|
||||
buildGoModule rec {
|
||||
@@ -37,9 +35,7 @@ buildGoModule rec {
|
||||
|
||||
passthru.services.default = {
|
||||
imports = [
|
||||
(lib.modules.importApply ./service.nix {
|
||||
inherit writeScript runtimeShell;
|
||||
})
|
||||
(lib.modules.importApply ./service.nix { })
|
||||
];
|
||||
ghostunnel.package = ghostunnel; # FIXME: finalAttrs.finalPackage
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Non-module dependencies (`importApply`)
|
||||
{ writeScript, runtimeShell }:
|
||||
{ }:
|
||||
|
||||
# Service module
|
||||
{
|
||||
@@ -185,62 +185,56 @@ in
|
||||
# TODO assertions
|
||||
|
||||
process = {
|
||||
argv =
|
||||
# Use a shell if credentials need to be pulled from the environment.
|
||||
optional
|
||||
(builtins.any (v: v != null) [
|
||||
cfg.keystore
|
||||
cfg.cert
|
||||
cfg.key
|
||||
cfg.cacert
|
||||
])
|
||||
(
|
||||
writeScript "load-credentials" ''
|
||||
#!${runtimeShell}
|
||||
exec $@ ${
|
||||
concatStringsSep " " (
|
||||
optional (cfg.keystore != null) "--keystore=$CREDENTIALS_DIRECTORY/keystore"
|
||||
++ optional (cfg.cert != null) "--cert=$CREDENTIALS_DIRECTORY/cert"
|
||||
++ optional (cfg.key != null) "--key=$CREDENTIALS_DIRECTORY/key"
|
||||
++ optional (cfg.cacert != null) "--cacert=$CREDENTIALS_DIRECTORY/cacert"
|
||||
)
|
||||
}
|
||||
''
|
||||
)
|
||||
++ [
|
||||
(getExe cfg.package)
|
||||
"server"
|
||||
"--listen"
|
||||
cfg.listen
|
||||
"--target"
|
||||
cfg.target
|
||||
]
|
||||
++ optional cfg.allowAll "--allow-all"
|
||||
++ map (v: "--allow-cn=${v}") cfg.allowCN
|
||||
++ map (v: "--allow-ou=${v}") cfg.allowOU
|
||||
++ map (v: "--allow-dns=${v}") cfg.allowDNS
|
||||
++ map (v: "--allow-uri=${v}") cfg.allowURI
|
||||
++ optional cfg.disableAuthentication "--disable-authentication"
|
||||
++ optional cfg.unsafeTarget "--unsafe-target"
|
||||
++ cfg.extraArguments;
|
||||
argv = [
|
||||
(getExe cfg.package)
|
||||
"server"
|
||||
"--listen"
|
||||
cfg.listen
|
||||
"--target"
|
||||
cfg.target
|
||||
]
|
||||
++ optional cfg.allowAll "--allow-all"
|
||||
++ map (v: "--allow-cn=${v}") cfg.allowCN
|
||||
++ map (v: "--allow-ou=${v}") cfg.allowOU
|
||||
++ map (v: "--allow-dns=${v}") cfg.allowDNS
|
||||
++ map (v: "--allow-uri=${v}") cfg.allowURI
|
||||
++ optional cfg.disableAuthentication "--disable-authentication"
|
||||
++ optional cfg.unsafeTarget "--unsafe-target"
|
||||
++ cfg.extraArguments;
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (options ? systemd) {
|
||||
# refine the service
|
||||
systemd.service = {
|
||||
after = [ "network.target" ];
|
||||
wants = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Restart = "always";
|
||||
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
|
||||
DynamicUser = true;
|
||||
LoadCredential =
|
||||
optional (cfg.keystore != null) "keystore:${cfg.keystore}"
|
||||
++ optional (cfg.cert != null) "cert:${cfg.cert}"
|
||||
++ optional (cfg.key != null) "key:${cfg.key}"
|
||||
++ optional (cfg.cacert != null) "cacert:${cfg.cacert}";
|
||||
# Refine the service for systemd
|
||||
// lib.optionalAttrs (options ? systemd) (
|
||||
let
|
||||
# Build credential flags with systemd variable substitution
|
||||
credentialFlags = concatStringsSep " " (
|
||||
optional (cfg.keystore != null) "--keystore=\${CREDENTIALS_DIRECTORY}/keystore"
|
||||
++ optional (cfg.cert != null) "--cert=\${CREDENTIALS_DIRECTORY}/cert"
|
||||
++ optional (cfg.key != null) "--key=\${CREDENTIALS_DIRECTORY}/key"
|
||||
++ optional (cfg.cacert != null) "--cacert=\${CREDENTIALS_DIRECTORY}/cacert"
|
||||
);
|
||||
in
|
||||
{
|
||||
# Use mainExecStart to add credential flags with systemd variable substitution
|
||||
systemd.mainExecStart =
|
||||
config.systemd.lib.escapeSystemdExecArgs config.process.argv
|
||||
+ lib.optionalString (credentialFlags != "") " ${credentialFlags}";
|
||||
|
||||
systemd.service = {
|
||||
after = [ "network.target" ];
|
||||
wants = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Restart = "always";
|
||||
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
|
||||
DynamicUser = true;
|
||||
LoadCredential =
|
||||
optional (cfg.keystore != null) "keystore:${cfg.keystore}"
|
||||
++ optional (cfg.cert != null) "cert:${cfg.cert}"
|
||||
++ optional (cfg.key != null) "key:${cfg.key}"
|
||||
++ optional (cfg.cacert != null) "cacert:${cfg.cacert}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user