Modular services (#372170)
This commit is contained in:
@@ -15,6 +15,7 @@ let
|
||||
inherit (pkgs) buildPackages runCommand docbook_xsl_ns;
|
||||
|
||||
inherit (pkgs.lib)
|
||||
evalModules
|
||||
hasPrefix
|
||||
removePrefix
|
||||
flip
|
||||
@@ -117,8 +118,41 @@ let
|
||||
${testOptionsDoc.optionsJSON}/${common.outputPath}/options.json
|
||||
sed -e '/@PYTHON_MACHINE_METHODS@/ {' -e 'r ${testDriverMachineDocstrings}/machine-methods.md' -e 'd' -e '}' \
|
||||
-i ./development/writing-nixos-tests.section.md
|
||||
substituteInPlace ./development/modular-services.md \
|
||||
--replace-fail \
|
||||
'@PORTABLE_SERVICE_OPTIONS@' \
|
||||
${portableServiceOptions.optionsJSON}/${common.outputPath}/options.json
|
||||
substituteInPlace ./development/modular-services.md \
|
||||
--replace-fail \
|
||||
'@SYSTEMD_SERVICE_OPTIONS@' \
|
||||
${systemdServiceOptions.optionsJSON}/${common.outputPath}/options.json
|
||||
'';
|
||||
|
||||
portableServiceOptions = buildPackages.nixosOptionsDoc {
|
||||
inherit (evalModules { modules = [ ../../modules/system/service/portable/service.nix ]; }) options;
|
||||
inherit revision warningsAreErrors;
|
||||
transformOptions =
|
||||
opt:
|
||||
opt
|
||||
// {
|
||||
# Clean up declaration sites to not refer to the NixOS source tree.
|
||||
declarations = map stripAnyPrefixes opt.declarations;
|
||||
};
|
||||
};
|
||||
|
||||
systemdServiceOptions = buildPackages.nixosOptionsDoc {
|
||||
inherit (evalModules { modules = [ ../../modules/system/service/systemd/service.nix ]; }) options;
|
||||
# TODO: filter out options that are not systemd-specific, maybe also change option prefix to just `service-opt-`?
|
||||
inherit revision warningsAreErrors;
|
||||
transformOptions =
|
||||
opt:
|
||||
opt
|
||||
// {
|
||||
# Clean up declaration sites to not refer to the NixOS source tree.
|
||||
declarations = map stripAnyPrefixes opt.declarations;
|
||||
};
|
||||
};
|
||||
|
||||
in
|
||||
rec {
|
||||
inherit (optionsDoc) optionsJSON optionsNix optionsDocBook;
|
||||
|
||||
@@ -12,4 +12,5 @@ writing-documentation.chapter.md
|
||||
nixos-tests.chapter.md
|
||||
developing-the-test-driver.chapter.md
|
||||
testing-installer.chapter.md
|
||||
modular-services.md
|
||||
```
|
||||
|
||||
98
nixos/doc/manual/development/modular-services.md
Normal file
98
nixos/doc/manual/development/modular-services.md
Normal file
@@ -0,0 +1,98 @@
|
||||
|
||||
# Modular Services {#modular-services}
|
||||
|
||||
Status: in development. This functionality is new in NixOS 25.11, and significant changes should be expected. We'd love to hear your feedback in <https://github.com/NixOS/nixpkgs/pull/372170>
|
||||
|
||||
Traditionally, NixOS services were defined using sets of options *in* modules, not *as* modules. This made them non-modular, resulting in problems with composability, reuse, and portability.
|
||||
|
||||
A configuration management framework is an application of `evalModules` with the `class` and `specialArgs` input attribute set to particular values.
|
||||
NixOS is such a configuration management framework, and so are [Home Manager](https://github.com/nix-community/home-manager) and [`nix-darwin`](https://github.com/lnl7/nix-darwin).
|
||||
|
||||
The service management component of a configuration management framework is the set of module options that connects Nix expressions with the underlying service (or process) manager.
|
||||
For NixOS this is the module wrapping [`systemd`](https://systemd.io/), on `nix-darwin` this is the module wrapping [`launchd`](https://en.wikipedia.org/wiki/Launchd).
|
||||
|
||||
A *modular service* is a [module] that defines values for a core set of options declared in the service management component of a configuration management framework, including which program to run.
|
||||
Since it's a module, it can be composed with other modules via `imports` to extend its functionality.
|
||||
|
||||
NixOS provides two options into which such modules can be plugged:
|
||||
|
||||
- `system.services.<name>`
|
||||
- an option for user services (TBD)
|
||||
|
||||
Crucially, these options have the type [`attrsOf`] [`submodule`].
|
||||
The name of the service is the attribute name corresponding to `attrsOf`.
|
||||
<!-- ^ This is how composition is *always* provided, instead of a difficult thing (but this is reference docs, not a changelog) -->
|
||||
The `submodule` is pre-loaded with two modules:
|
||||
- a generic module that is intended to be portable
|
||||
- a module with systemd-specific options, whose values or defaults derive from the generic module's option values.
|
||||
|
||||
So note that the default value of `system.services.<name>` is not a complete service. It requires that the user provide a value, and this is typically done by importing a module. For example:
|
||||
|
||||
<!-- Not using typical example syntax, because reading this is *not* optional, and should it should not be folded closed. -->
|
||||
```nix
|
||||
{
|
||||
system.services.my-service-instance = {
|
||||
imports = [ pkgs.some-application.services.some-service-module ];
|
||||
foo.settings = {
|
||||
# ...
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Portability {#modular-service-portability}
|
||||
|
||||
It is possible to write service modules that are portable. This is done by either avoiding the `systemd` option tree, or by defining process-manager-specific definitions in an optional way:
|
||||
|
||||
```nix
|
||||
{ config, options, lib, ... }: {
|
||||
_class = "service";
|
||||
config = {
|
||||
process.argv = [ (lib.getExe config.foo.program) ];
|
||||
} // lib.optionalAttrs (options?systemd) {
|
||||
# ... systemd-specific definitions ...
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This way, the module can be loaded into a configuration manager that does not use systemd, and the `systemd` definitions will be ignored.
|
||||
Similarly, other configuration managers can declare their own options for services to customize.
|
||||
|
||||
## Composition and Ownership {#modular-service-composition}
|
||||
|
||||
Compared to traditional services, modular services are inherently more composable, by virtue of being modules and receiving a user-provided name when imported.
|
||||
However, composition can not end there, because services need to be able to interact with each other.
|
||||
This can be achieved in two ways:
|
||||
1. Users can link services together by providing the necessary NixOS configuration.
|
||||
2. Services can be compositions of other services.
|
||||
|
||||
These aren't mutually exclusive. In fact, it is a good practice when developing services to first write them as individual services, and then compose them into a higher-level composition. Each of these services is a valid modular service, including their composition.
|
||||
|
||||
## Migration {#modular-service-migration}
|
||||
|
||||
Many services could be migrated to the modular service system, but even when the modular service system is mature, it is not necessary to migrate all services.
|
||||
For instance, many system-wide services are a mandatory part of a desktop system, and it doesn't make sense to have multiple instances of them.
|
||||
Moving their logic into separate Nix files may still be beneficial for the efficient evaluation of configurations that don't use those services, but that is a rather minor benefit, unless modular services potentially become the standard way to define services.
|
||||
|
||||
<!-- TODO example of a single-instance service -->
|
||||
|
||||
## Portable Service Options {#modular-service-options-portable}
|
||||
|
||||
```{=include=} options
|
||||
id-prefix: service-opt-
|
||||
list-id: service-options
|
||||
source: @PORTABLE_SERVICE_OPTIONS@
|
||||
```
|
||||
|
||||
## Systemd-specific Service Options {#modular-service-options-systemd}
|
||||
|
||||
```{=include=} options
|
||||
id-prefix: systemd-service-opt-
|
||||
list-id: systemd-service-options
|
||||
source: @SYSTEMD_SERVICE_OPTIONS@
|
||||
```
|
||||
|
||||
[module]: https://nixos.org/manual/nixpkgs/stable/index.html#module-system
|
||||
<!-- TODO: more anchors -->
|
||||
[`attrsOf`]: #sec-option-types-composed
|
||||
[`submodule`]: #sec-option-types-submodule
|
||||
@@ -11,6 +11,24 @@
|
||||
"book-nixos-manual": [
|
||||
"index.html#book-nixos-manual"
|
||||
],
|
||||
"modular-service-composition": [
|
||||
"index.html#modular-service-composition"
|
||||
],
|
||||
"modular-service-migration": [
|
||||
"index.html#modular-service-migration"
|
||||
],
|
||||
"modular-service-options-portable": [
|
||||
"index.html#modular-service-options-portable"
|
||||
],
|
||||
"modular-service-options-systemd": [
|
||||
"index.html#modular-service-options-systemd"
|
||||
],
|
||||
"modular-service-portability": [
|
||||
"index.html#modular-service-portability"
|
||||
],
|
||||
"modular-services": [
|
||||
"index.html#modular-services"
|
||||
],
|
||||
"module-services-anubis": [
|
||||
"index.html#module-services-anubis"
|
||||
],
|
||||
|
||||
@@ -32,5 +32,7 @@
|
||||
};
|
||||
|
||||
};
|
||||
# impl of assertions is in <nixpkgs/nixos/modules/system/activation/top-level.nix>
|
||||
# impl of assertions is in
|
||||
# - <nixpkgs/nixos/modules/system/activation/top-level.nix>
|
||||
# - <nixpkgs/nixos/modules/system/service/portable/lib.nix>
|
||||
}
|
||||
|
||||
@@ -1843,6 +1843,8 @@
|
||||
./system/boot/uvesafb.nix
|
||||
./system/boot/zram-as-tmp.nix
|
||||
./system/etc/etc-activation.nix
|
||||
./system/service/systemd/system.nix
|
||||
./system/service/systemd/user.nix
|
||||
./tasks/auto-upgrade.nix
|
||||
./tasks/bcache.nix
|
||||
./tasks/cpu-freq.nix
|
||||
|
||||
28
nixos/modules/system/service/README.md
Normal file
28
nixos/modules/system/service/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
# Modular Services
|
||||
|
||||
This directory defines a modular service infrastructure for NixOS.
|
||||
See the [Modular Services chapter] in the manual [[source]](../../doc/manual/development/modular-services.md).
|
||||
|
||||
[Modular Services chapter]: https://nixos.org/manual/nixos/unstable/#modular-services
|
||||
|
||||
# Design decision log
|
||||
|
||||
- `system.services.<name>`. Alternatives considered
|
||||
- `systemServices`: similar to does not allow importing a composition of services into `system`. Not sure if that's a good idea in the first place, but I've kept the possibility open.
|
||||
- `services.abstract`: used in https://github.com/NixOS/nixpkgs/pull/267111, but too weird. Service modules should fit naturally into the configuration system.
|
||||
Also "abstract" is wrong, because it has submodules - in other words, evalModules results, concrete services - not abstract at all.
|
||||
- `services.modular`: only slightly better than `services.abstract`, but still weird
|
||||
|
||||
- No `daemon.*` options. https://github.com/NixOS/nixpkgs/pull/267111/files#r1723206521
|
||||
|
||||
- For now, do not add an `enable` option, because it's ambiguous. Does it disable at the Nix level (not generate anything) or at the systemd level (generate a service that is disabled)?
|
||||
|
||||
- Move all process options into a `process` option tree. Putting this at the root is messy, because we also have sub-services at that level. Those are rather distinct. Grouping them "by kind" should raise fewer questions.
|
||||
|
||||
- `modules/system/service/systemd/system.nix` has `system` twice. Not great, but
|
||||
- they have different meanings
|
||||
1. These are system-provided modules, provided by the configuration manager
|
||||
2. `systemd/system` configures SystemD _system units_.
|
||||
- This reserves `modules/service` for actual service modules, at least until those are lifted out of NixOS, potentially
|
||||
|
||||
33
nixos/modules/system/service/portable/lib.nix
Normal file
33
nixos/modules/system/service/portable/lib.nix
Normal file
@@ -0,0 +1,33 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
inherit (lib) concatLists mapAttrsToList showOption;
|
||||
in
|
||||
rec {
|
||||
flattenMapServicesConfigToList =
|
||||
f: loc: config:
|
||||
f loc config
|
||||
++ concatLists (
|
||||
mapAttrsToList (
|
||||
k: v:
|
||||
flattenMapServicesConfigToList f (
|
||||
loc
|
||||
++ [
|
||||
"services"
|
||||
k
|
||||
]
|
||||
) v
|
||||
) config.services
|
||||
);
|
||||
|
||||
getWarnings = flattenMapServicesConfigToList (
|
||||
loc: config: map (msg: "in ${showOption loc}: ${msg}") config.warnings
|
||||
);
|
||||
|
||||
getAssertions = flattenMapServicesConfigToList (
|
||||
loc: config:
|
||||
map (ass: {
|
||||
message = "in ${showOption loc}: ${ass.message}";
|
||||
assertion = ass.assertion;
|
||||
}) config.assertions
|
||||
);
|
||||
}
|
||||
48
nixos/modules/system/service/portable/service.nix
Normal file
48
nixos/modules/system/service/portable/service.nix
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib) mkOption types;
|
||||
pathOrStr = types.coercedTo types.path (x: "${x}") types.str;
|
||||
in
|
||||
{
|
||||
# https://nixos.org/manual/nixos/unstable/#modular-services
|
||||
_class = "service";
|
||||
imports = [
|
||||
../../../misc/assertions.nix
|
||||
];
|
||||
options = {
|
||||
services = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submoduleWith {
|
||||
modules = [
|
||||
./service.nix
|
||||
];
|
||||
}
|
||||
);
|
||||
description = ''
|
||||
A collection of [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured in one go.
|
||||
|
||||
You could consider the sub-service relationship to be an ownership relation.
|
||||
It **does not** automatically create any other relationship between services (e.g. systemd slices), unless perhaps such a behavior is explicitly defined and enabled in another option.
|
||||
'';
|
||||
default = { };
|
||||
visible = "shallow";
|
||||
};
|
||||
process = {
|
||||
argv = lib.mkOption {
|
||||
type = types.listOf pathOrStr;
|
||||
example = lib.literalExpression ''[ (lib.getExe config.package) "--nobackground" ]'';
|
||||
description = ''
|
||||
Command filename and arguments for starting this service.
|
||||
This is a raw command-line that should not contain any shell escaping.
|
||||
If expansion of environmental variables is required then use
|
||||
a shell script or `importas` from `pkgs.execline`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
183
nixos/modules/system/service/portable/test.nix
Normal file
183
nixos/modules/system/service/portable/test.nix
Normal file
@@ -0,0 +1,183 @@
|
||||
# Run:
|
||||
# nix-instantiate --eval nixos/modules/system/service/portable/test.nix
|
||||
let
|
||||
lib = import ../../../../../lib;
|
||||
|
||||
inherit (lib) mkOption types;
|
||||
|
||||
portable-lib = import ./lib.nix { inherit lib; };
|
||||
|
||||
dummyPkg =
|
||||
name:
|
||||
derivation {
|
||||
system = "dummy";
|
||||
name = name;
|
||||
builder = "/bin/false";
|
||||
};
|
||||
|
||||
exampleConfig = {
|
||||
_file = "${__curPos.file}:${toString __curPos.line}";
|
||||
services = {
|
||||
service1 = {
|
||||
process = {
|
||||
argv = [
|
||||
"/usr/bin/echo" # *giggles*
|
||||
"hello"
|
||||
];
|
||||
};
|
||||
assertions = [
|
||||
{
|
||||
assertion = false;
|
||||
message = "you can't enable this for that reason";
|
||||
}
|
||||
];
|
||||
warnings = [
|
||||
"The `foo' service is deprecated and will go away soon!"
|
||||
];
|
||||
};
|
||||
service2 = {
|
||||
process = {
|
||||
# No meta.mainProgram, because it's supposedly an executable script _file_,
|
||||
# not a directory with a bin directory containing the main program.
|
||||
argv = [
|
||||
(dummyPkg "cowsay.sh")
|
||||
"world"
|
||||
];
|
||||
};
|
||||
};
|
||||
service3 = {
|
||||
process = {
|
||||
argv = [ "/bin/false" ];
|
||||
};
|
||||
services.exclacow = {
|
||||
process = {
|
||||
argv = [
|
||||
(lib.getExe (
|
||||
dummyPkg "cowsay-ng"
|
||||
// {
|
||||
meta.mainProgram = "cowsay";
|
||||
}
|
||||
))
|
||||
"!"
|
||||
];
|
||||
};
|
||||
assertions = [
|
||||
{
|
||||
assertion = false;
|
||||
message = "you can't enable this for such reason";
|
||||
}
|
||||
];
|
||||
warnings = [
|
||||
"The `bar' service is deprecated and will go away soon!"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
exampleEval = lib.evalModules {
|
||||
modules = [
|
||||
{
|
||||
options.services = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submoduleWith {
|
||||
class = "service";
|
||||
modules = [
|
||||
./service.nix
|
||||
];
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
exampleConfig
|
||||
];
|
||||
};
|
||||
|
||||
test =
|
||||
assert
|
||||
exampleEval.config == {
|
||||
services = {
|
||||
service1 = {
|
||||
process = {
|
||||
argv = [
|
||||
"/usr/bin/echo"
|
||||
"hello"
|
||||
];
|
||||
};
|
||||
services = { };
|
||||
assertions = [
|
||||
{
|
||||
assertion = false;
|
||||
message = "you can't enable this for that reason";
|
||||
}
|
||||
];
|
||||
warnings = [
|
||||
"The `foo' service is deprecated and will go away soon!"
|
||||
];
|
||||
};
|
||||
service2 = {
|
||||
process = {
|
||||
argv = [
|
||||
"${dummyPkg "cowsay.sh"}"
|
||||
"world"
|
||||
];
|
||||
};
|
||||
services = { };
|
||||
assertions = [ ];
|
||||
warnings = [ ];
|
||||
};
|
||||
service3 = {
|
||||
process = {
|
||||
argv = [ "/bin/false" ];
|
||||
};
|
||||
services.exclacow = {
|
||||
process = {
|
||||
argv = [
|
||||
"${dummyPkg "cowsay-ng"}/bin/cowsay"
|
||||
"!"
|
||||
];
|
||||
};
|
||||
services = { };
|
||||
assertions = [
|
||||
{
|
||||
assertion = false;
|
||||
message = "you can't enable this for such reason";
|
||||
}
|
||||
];
|
||||
warnings = [ "The `bar' service is deprecated and will go away soon!" ];
|
||||
};
|
||||
assertions = [ ];
|
||||
warnings = [ ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
assert
|
||||
portable-lib.getWarnings [ "service1" ] exampleEval.config.services.service1 == [
|
||||
"in service1: The `foo' service is deprecated and will go away soon!"
|
||||
];
|
||||
|
||||
assert
|
||||
portable-lib.getAssertions [ "service1" ] exampleEval.config.services.service1 == [
|
||||
{
|
||||
message = "in service1: you can't enable this for that reason";
|
||||
assertion = false;
|
||||
}
|
||||
];
|
||||
|
||||
assert
|
||||
portable-lib.getWarnings [ "service3" ] exampleEval.config.services.service3 == [
|
||||
"in service3.services.exclacow: The `bar' service is deprecated and will go away soon!"
|
||||
];
|
||||
assert
|
||||
portable-lib.getAssertions [ "service3" ] exampleEval.config.services.service3 == [
|
||||
{
|
||||
message = "in service3.services.exclacow: you can't enable this for such reason";
|
||||
assertion = false;
|
||||
}
|
||||
];
|
||||
|
||||
"ok";
|
||||
|
||||
in
|
||||
test
|
||||
121
nixos/modules/system/service/systemd/service.nix
Normal file
121
nixos/modules/system/service/systemd/service.nix
Normal file
@@ -0,0 +1,121 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
systemdPackage,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
concatMapStringsSep
|
||||
isDerivation
|
||||
isInt
|
||||
isFloat
|
||||
isPath
|
||||
isString
|
||||
mkOption
|
||||
replaceStrings
|
||||
types
|
||||
;
|
||||
inherit (builtins) toJSON;
|
||||
|
||||
# Local copy of systemd exec argument escaping function.
|
||||
# TODO: This could perhaps be deduplicated, but it is unclear where it should go.
|
||||
# Preferably, we don't create a hard dependency on NixOS here, so that this
|
||||
# module can be reused in a non-NixOS context, such as mutaable services
|
||||
# in /run/systemd/system.
|
||||
|
||||
# Quotes an argument for use in Exec* service lines.
|
||||
# systemd accepts "-quoted strings with escape sequences, toJSON produces
|
||||
# a subset of these.
|
||||
# Additionally we escape % to disallow expansion of % specifiers. Any lone ;
|
||||
# in the input will be turned it ";" and thus lose its special meaning.
|
||||
# Every $ is escaped to $$, this makes it unnecessary to disable environment
|
||||
# substitution for the directive.
|
||||
escapeSystemdExecArg =
|
||||
arg:
|
||||
let
|
||||
s =
|
||||
if isPath arg then
|
||||
"${arg}"
|
||||
else if isString arg then
|
||||
arg
|
||||
else if isInt arg || isFloat arg || isDerivation arg then
|
||||
toString arg
|
||||
else
|
||||
throw "escapeSystemdExecArg only allows strings, paths, numbers and derivations";
|
||||
in
|
||||
replaceStrings [ "%" "$" ] [ "%%" "$$" ] (toJSON s);
|
||||
|
||||
# Quotes a list of arguments into a single string for use in a Exec*
|
||||
# line.
|
||||
escapeSystemdExecArgs = concatMapStringsSep " " escapeSystemdExecArg;
|
||||
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../portable/service.nix
|
||||
(lib.mkAliasOptionModule [ "systemd" "service" ] [ "systemd" "services" "" ])
|
||||
(lib.mkAliasOptionModule [ "systemd" "socket" ] [ "systemd" "sockets" "" ])
|
||||
];
|
||||
options = {
|
||||
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.
|
||||
|
||||
This option's value is not suitable for reading, but you can define a module here that interacts with just the unit configuration in the host system configuration.
|
||||
|
||||
Note that this option contains _deferred_ modules.
|
||||
This means that the module has not been combined with the system configuration yet, no values can be read from this option.
|
||||
What you can do instead is define a module that reads from the module arguments (such as `config`) that are available when the module is merged into the system configuration.
|
||||
'';
|
||||
type = types.lazyAttrsOf (
|
||||
types.deferredModuleWith {
|
||||
staticModules = [
|
||||
# TODO: Add modules for the purpose of generating documentation?
|
||||
];
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
};
|
||||
systemd.sockets = mkOption {
|
||||
description = ''
|
||||
Declares systemd socket units. Names will be prefixed by the service name / path.
|
||||
|
||||
See {option}`systemd.services`.
|
||||
'';
|
||||
type = types.lazyAttrsOf types.deferredModule;
|
||||
default = { };
|
||||
};
|
||||
|
||||
# Also import systemd logic into sub-services
|
||||
# extends the portable `services` option
|
||||
services = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submoduleWith {
|
||||
class = "service";
|
||||
modules = [
|
||||
./service.nix
|
||||
];
|
||||
specialArgs = {
|
||||
inherit systemdPackage;
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
config = {
|
||||
# Note that this is the systemd.services option above, not the system one.
|
||||
systemd.services."" = {
|
||||
# TODO description;
|
||||
wantedBy = lib.mkDefault [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = lib.mkDefault "simple";
|
||||
Restart = lib.mkDefault "always";
|
||||
RestartSec = lib.mkDefault "5";
|
||||
ExecStart = [
|
||||
(escapeSystemdExecArgs config.process.argv)
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
90
nixos/modules/system/service/systemd/system.nix
Normal file
90
nixos/modules/system/service/systemd/system.nix
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
options,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
inherit (lib)
|
||||
concatMapAttrs
|
||||
mkOption
|
||||
types
|
||||
concatLists
|
||||
mapAttrsToList
|
||||
;
|
||||
|
||||
portable-lib = import ../portable/lib.nix { inherit lib; };
|
||||
|
||||
dash =
|
||||
before: after:
|
||||
if after == "" then
|
||||
before
|
||||
else if before == "" then
|
||||
after
|
||||
else
|
||||
"${before}-${after}";
|
||||
|
||||
makeUnits =
|
||||
unitType: prefix: service:
|
||||
concatMapAttrs (unitName: unitModule: {
|
||||
"${dash prefix unitName}" =
|
||||
{ ... }:
|
||||
{
|
||||
imports = [ unitModule ];
|
||||
};
|
||||
}) service.systemd.${unitType}
|
||||
// concatMapAttrs (
|
||||
subServiceName: subService: makeUnits unitType (dash prefix subServiceName) subService
|
||||
) service.services;
|
||||
in
|
||||
{
|
||||
# First half of the magic: mix systemd logic into the otherwise abstract services
|
||||
options = {
|
||||
system.services = mkOption {
|
||||
description = ''
|
||||
A collection of NixOS [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured as systemd services.
|
||||
'';
|
||||
type = types.attrsOf (
|
||||
types.submoduleWith {
|
||||
class = "service";
|
||||
modules = [
|
||||
./service.nix
|
||||
];
|
||||
specialArgs = {
|
||||
# perhaps: features."systemd" = { };
|
||||
inherit pkgs;
|
||||
systemdPackage = config.systemd.package;
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
visible = "shallow";
|
||||
};
|
||||
};
|
||||
|
||||
# Second half of the magic: siphon units that were defined in isolation to the system
|
||||
config = {
|
||||
|
||||
assertions = concatLists (
|
||||
mapAttrsToList (
|
||||
name: cfg: portable-lib.getAssertions (options.system.services.loc ++ [ name ]) cfg
|
||||
) config.system.services
|
||||
);
|
||||
|
||||
warnings = concatLists (
|
||||
mapAttrsToList (
|
||||
name: cfg: portable-lib.getWarnings (options.system.services.loc ++ [ name ]) cfg
|
||||
) config.system.services
|
||||
);
|
||||
|
||||
systemd.services = concatMapAttrs (
|
||||
serviceName: topLevelService: makeUnits "services" serviceName topLevelService
|
||||
) config.system.services;
|
||||
|
||||
systemd.sockets = concatMapAttrs (
|
||||
serviceName: topLevelService: makeUnits "sockets" serviceName topLevelService
|
||||
) config.system.services;
|
||||
};
|
||||
}
|
||||
92
nixos/modules/system/service/systemd/test.nix
Normal file
92
nixos/modules/system/service/systemd/test.nix
Normal file
@@ -0,0 +1,92 @@
|
||||
# Run:
|
||||
# nix-build -A nixosTests.modularService
|
||||
|
||||
{
|
||||
evalSystem,
|
||||
runCommand,
|
||||
hello,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
machine = evalSystem (
|
||||
{ lib, ... }:
|
||||
let
|
||||
hello' = lib.getExe hello;
|
||||
in
|
||||
{
|
||||
|
||||
# Test input
|
||||
|
||||
system.services.foo = {
|
||||
process = {
|
||||
argv = [
|
||||
hello'
|
||||
"--greeting"
|
||||
"hoi"
|
||||
];
|
||||
};
|
||||
};
|
||||
system.services.bar = {
|
||||
process = {
|
||||
argv = [
|
||||
hello'
|
||||
"--greeting"
|
||||
"hoi"
|
||||
];
|
||||
};
|
||||
systemd.service = {
|
||||
serviceConfig.X-Bar = "lol crossbar whatever";
|
||||
};
|
||||
services.db = {
|
||||
process = {
|
||||
argv = [
|
||||
hello'
|
||||
"--greeting"
|
||||
"Hi, I'm a database, would you believe it"
|
||||
];
|
||||
};
|
||||
systemd.service = {
|
||||
serviceConfig.RestartSec = "42";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# irrelevant stuff
|
||||
system.stateVersion = "25.05";
|
||||
fileSystems."/".device = "/test/dummy";
|
||||
boot.loader.grub.enable = false;
|
||||
}
|
||||
);
|
||||
|
||||
inherit (machine.config.system.build) toplevel;
|
||||
in
|
||||
runCommand "test-modular-service-systemd-units"
|
||||
{
|
||||
passthru = {
|
||||
inherit
|
||||
machine
|
||||
toplevel
|
||||
;
|
||||
};
|
||||
}
|
||||
''
|
||||
echo ${toplevel}/etc/systemd/system/foo.service:
|
||||
cat -n ${toplevel}/etc/systemd/system/foo.service
|
||||
(
|
||||
set -x
|
||||
grep -F 'ExecStart="${hello}/bin/hello" "--greeting" "hoi"' ${toplevel}/etc/systemd/system/foo.service >/dev/null
|
||||
|
||||
grep -F 'ExecStart="${hello}/bin/hello" "--greeting" "hoi"' ${toplevel}/etc/systemd/system/bar.service >/dev/null
|
||||
grep -F 'X-Bar=lol crossbar whatever' ${toplevel}/etc/systemd/system/bar.service >/dev/null
|
||||
|
||||
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
|
||||
|
||||
[[ ! -e ${toplevel}/etc/systemd/system/foo.socket ]]
|
||||
[[ ! -e ${toplevel}/etc/systemd/system/bar.socket ]]
|
||||
[[ ! -e ${toplevel}/etc/systemd/system/bar-db.socket ]]
|
||||
)
|
||||
echo 🐬👍
|
||||
touch $out
|
||||
''
|
||||
3
nixos/modules/system/service/systemd/user.nix
Normal file
3
nixos/modules/system/service/systemd/user.nix
Normal file
@@ -0,0 +1,3 @@
|
||||
# TBD, analogous to system.nix but for user units
|
||||
{
|
||||
}
|
||||
@@ -89,6 +89,16 @@ let
|
||||
featureFlags.minimalModules = { };
|
||||
};
|
||||
evalMinimalConfig = module: nixosLib.evalModules { modules = [ module ]; };
|
||||
evalSystem =
|
||||
module:
|
||||
import ../lib/eval-config.nix {
|
||||
system = null;
|
||||
modules = [
|
||||
../modules/misc/nixpkgs/read-only.nix
|
||||
{ nixpkgs.pkgs = pkgs; }
|
||||
module
|
||||
];
|
||||
};
|
||||
|
||||
inherit
|
||||
(rec {
|
||||
@@ -589,6 +599,7 @@ in
|
||||
gerrit = runTest ./gerrit.nix;
|
||||
geth = runTest ./geth.nix;
|
||||
ghostunnel = runTest ./ghostunnel.nix;
|
||||
ghostunnel-modular = runTest ./ghostunnel-modular.nix;
|
||||
gitdaemon = runTest ./gitdaemon.nix;
|
||||
gitea = handleTest ./gitea.nix { giteaPackage = pkgs.gitea; };
|
||||
github-runner = runTest ./github-runner.nix;
|
||||
@@ -894,6 +905,9 @@ in
|
||||
mjolnir = runTest ./matrix/mjolnir.nix;
|
||||
mobilizon = runTest ./mobilizon.nix;
|
||||
mod_perl = runTest ./mod_perl.nix;
|
||||
modularService = pkgs.callPackage ../modules/system/service/systemd/test.nix {
|
||||
inherit evalSystem;
|
||||
};
|
||||
molly-brown = runTest ./molly-brown.nix;
|
||||
mollysocket = runTest ./mollysocket.nix;
|
||||
monado = runTest ./monado.nix;
|
||||
|
||||
120
nixos/tests/ghostunnel-modular.nix
Normal file
120
nixos/tests/ghostunnel-modular.nix
Normal file
@@ -0,0 +1,120 @@
|
||||
{ hostPkgs, lib, ... }:
|
||||
{
|
||||
_class = "nixosTest";
|
||||
name = "ghostunnel";
|
||||
nodes = {
|
||||
backend =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
services.nginx.enable = true;
|
||||
services.nginx.virtualHosts."backend".root = pkgs.runCommand "webroot" { } ''
|
||||
mkdir $out
|
||||
echo hi >$out/hi.txt
|
||||
'';
|
||||
networking.firewall.allowedTCPPorts = [ 80 ];
|
||||
};
|
||||
service =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
system.services."ghostunnel-plain-old" = {
|
||||
imports = [ pkgs.ghostunnel.services.default ];
|
||||
ghostunnel = {
|
||||
listen = "0.0.0.0:443";
|
||||
cert = "/root/service-cert.pem";
|
||||
key = "/root/service-key.pem";
|
||||
disableAuthentication = true;
|
||||
target = "backend:80";
|
||||
unsafeTarget = true;
|
||||
};
|
||||
};
|
||||
system.services."ghostunnel-client-cert" = {
|
||||
imports = [ pkgs.ghostunnel.services.default ];
|
||||
ghostunnel = {
|
||||
listen = "0.0.0.0:1443";
|
||||
cert = "/root/service-cert.pem";
|
||||
key = "/root/service-key.pem";
|
||||
cacert = "/root/ca.pem";
|
||||
target = "backend:80";
|
||||
allowCN = [ "client" ];
|
||||
unsafeTarget = true;
|
||||
};
|
||||
};
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
443
|
||||
1443
|
||||
];
|
||||
};
|
||||
client =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [
|
||||
pkgs.curl
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
|
||||
# prepare certificates
|
||||
|
||||
def cmd(command):
|
||||
print(f"+{command}")
|
||||
r = os.system(command)
|
||||
if r != 0:
|
||||
raise Exception(f"Command {command} failed with exit code {r}")
|
||||
|
||||
# Create CA
|
||||
cmd("${hostPkgs.openssl}/bin/openssl genrsa -out ca-key.pem 4096")
|
||||
cmd("${hostPkgs.openssl}/bin/openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -subj '/C=NL/ST=Zuid-Holland/L=The Hague/O=Stevige Balken en Planken B.V./OU=OpSec/CN=Certificate Authority' -out ca.pem")
|
||||
|
||||
# Create service
|
||||
cmd("${hostPkgs.openssl}/bin/openssl genrsa -out service-key.pem 4096")
|
||||
cmd("${hostPkgs.openssl}/bin/openssl req -subj '/CN=service' -sha256 -new -key service-key.pem -out service.csr")
|
||||
cmd("echo subjectAltName = DNS:service,IP:127.0.0.1 >> extfile.cnf")
|
||||
cmd("echo extendedKeyUsage = serverAuth >> extfile.cnf")
|
||||
cmd("${hostPkgs.openssl}/bin/openssl x509 -req -days 365 -sha256 -in service.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out service-cert.pem -extfile extfile.cnf")
|
||||
|
||||
# Create client
|
||||
cmd("${hostPkgs.openssl}/bin/openssl genrsa -out client-key.pem 4096")
|
||||
cmd("${hostPkgs.openssl}/bin/openssl req -subj '/CN=client' -new -key client-key.pem -out client.csr")
|
||||
cmd("echo extendedKeyUsage = clientAuth > extfile-client.cnf")
|
||||
cmd("${hostPkgs.openssl}/bin/openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile extfile-client.cnf")
|
||||
|
||||
cmd("ls -al")
|
||||
|
||||
start_all()
|
||||
|
||||
# Configuration
|
||||
service.copy_from_host("ca.pem", "/root/ca.pem")
|
||||
service.copy_from_host("service-cert.pem", "/root/service-cert.pem")
|
||||
service.copy_from_host("service-key.pem", "/root/service-key.pem")
|
||||
client.copy_from_host("ca.pem", "/root/ca.pem")
|
||||
client.copy_from_host("service-cert.pem", "/root/service-cert.pem")
|
||||
client.copy_from_host("client-cert.pem", "/root/client-cert.pem")
|
||||
client.copy_from_host("client-key.pem", "/root/client-key.pem")
|
||||
|
||||
backend.wait_for_unit("nginx.service")
|
||||
service.wait_for_unit("multi-user.target")
|
||||
service.wait_for_unit("multi-user.target")
|
||||
client.wait_for_unit("multi-user.target")
|
||||
|
||||
# Check assumptions before the real test
|
||||
client.succeed("bash -c 'diff <(curl -v --no-progress-meter http://backend/hi.txt) <(echo hi)'")
|
||||
|
||||
# Plain old simple TLS can connect, ignoring cert
|
||||
client.succeed("bash -c 'diff <(curl -v --no-progress-meter --insecure https://service/hi.txt) <(echo hi)'")
|
||||
|
||||
# Plain old simple TLS provides correct signature with its cert
|
||||
client.succeed("bash -c 'diff <(curl -v --no-progress-meter --cacert /root/ca.pem https://service/hi.txt) <(echo hi)'")
|
||||
|
||||
# Client can authenticate with certificate
|
||||
client.succeed("bash -c 'diff <(curl -v --no-progress-meter --cert /root/client-cert.pem --key /root/client-key.pem --cacert /root/ca.pem https://service:1443/hi.txt) <(echo hi)'")
|
||||
|
||||
# Client must authenticate with certificate
|
||||
client.fail("bash -c 'diff <(curl -v --no-progress-meter --cacert /root/ca.pem https://service:1443/hi.txt) <(echo hi)'")
|
||||
'';
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
roberth
|
||||
];
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
fetchFromGitHub,
|
||||
lib,
|
||||
nixosTests,
|
||||
ghostunnel,
|
||||
apple-sdk_12,
|
||||
darwinMinVersionHook,
|
||||
}:
|
||||
@@ -39,6 +40,11 @@ buildGoModule rec {
|
||||
podman = nixosTests.podman-tls-ghostunnel;
|
||||
};
|
||||
|
||||
passthru.services.default = {
|
||||
imports = [ ./service.nix ];
|
||||
ghostunnel.package = ghostunnel; # FIXME: finalAttrs.finalPackage
|
||||
};
|
||||
|
||||
meta = {
|
||||
description = "TLS proxy with mutual authentication support for securing non-TLS backend applications";
|
||||
homepage = "https://github.com/ghostunnel/ghostunnel#readme";
|
||||
|
||||
241
pkgs/by-name/gh/ghostunnel/service.nix
Normal file
241
pkgs/by-name/gh/ghostunnel/service.nix
Normal file
@@ -0,0 +1,241 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
options,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
concatStringsSep
|
||||
getExe
|
||||
mkDefault
|
||||
mkIf
|
||||
mkOption
|
||||
optional
|
||||
types
|
||||
;
|
||||
cfg = config.ghostunnel;
|
||||
|
||||
in
|
||||
{
|
||||
# https://nixos.org/manual/nixos/unstable/#modular-services
|
||||
_class = "service";
|
||||
options = {
|
||||
ghostunnel = {
|
||||
package = mkOption {
|
||||
description = "Package to use for ghostunnel";
|
||||
type = types.package;
|
||||
};
|
||||
|
||||
listen = mkOption {
|
||||
description = ''
|
||||
Address and port to listen on (can be HOST:PORT, unix:PATH).
|
||||
'';
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
target = mkOption {
|
||||
description = ''
|
||||
Address to forward connections to (can be HOST:PORT or unix:PATH).
|
||||
'';
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
keystore = mkOption {
|
||||
description = ''
|
||||
Path to keystore (combined PEM with cert/key, or PKCS12 keystore).
|
||||
|
||||
NB: storepass is not supported because it would expose credentials via `/proc/*/cmdline`.
|
||||
|
||||
Specify this or `cert` and `key`.
|
||||
'';
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
};
|
||||
|
||||
cert = mkOption {
|
||||
description = ''
|
||||
Path to certificate (PEM with certificate chain).
|
||||
|
||||
Not required if `keystore` is set.
|
||||
'';
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
};
|
||||
|
||||
key = mkOption {
|
||||
description = ''
|
||||
Path to certificate private key (PEM with private key).
|
||||
|
||||
Not required if `keystore` is set.
|
||||
'';
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
};
|
||||
|
||||
cacert = mkOption {
|
||||
description = ''
|
||||
Path to CA bundle file (PEM/X509). Uses system trust store if `null`.
|
||||
'';
|
||||
type = types.nullOr types.str;
|
||||
};
|
||||
|
||||
disableAuthentication = mkOption {
|
||||
description = ''
|
||||
Disable client authentication, no client certificate will be required.
|
||||
'';
|
||||
type = types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
allowAll = mkOption {
|
||||
description = ''
|
||||
If true, allow all clients, do not check client cert subject.
|
||||
'';
|
||||
type = types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
allowCN = mkOption {
|
||||
description = ''
|
||||
Allow client if common name appears in the list.
|
||||
'';
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
allowOU = mkOption {
|
||||
description = ''
|
||||
Allow client if organizational unit name appears in the list.
|
||||
'';
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
allowDNS = mkOption {
|
||||
description = ''
|
||||
Allow client if DNS subject alternative name appears in the list.
|
||||
'';
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
allowURI = mkOption {
|
||||
description = ''
|
||||
Allow client if URI subject alternative name appears in the list.
|
||||
'';
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
extraArguments = mkOption {
|
||||
description = "Extra arguments to pass to `ghostunnel server`";
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
unsafeTarget = mkOption {
|
||||
description = ''
|
||||
If set, does not limit target to localhost, 127.0.0.1, [::1], or UNIX sockets.
|
||||
|
||||
This is meant to protect against accidental unencrypted traffic on
|
||||
untrusted networks.
|
||||
'';
|
||||
type = types.bool;
|
||||
default = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
assertions = [
|
||||
{
|
||||
message = ''
|
||||
At least one access control flag is required.
|
||||
Set at least one of:
|
||||
- ${options.ghostunnel.disableAuthentication}
|
||||
- ${options.ghostunnel.allowAll}
|
||||
- ${options.ghostunnel.allowCN}
|
||||
- ${options.ghostunnel.allowOU}
|
||||
- ${options.ghostunnel.allowDNS}
|
||||
- ${options.ghostunnel.allowURI}
|
||||
'';
|
||||
assertion =
|
||||
cfg.disableAuthentication
|
||||
|| cfg.allowAll
|
||||
|| cfg.allowCN != [ ]
|
||||
|| cfg.allowOU != [ ]
|
||||
|| cfg.allowDNS != [ ]
|
||||
|| cfg.allowURI != [ ];
|
||||
}
|
||||
];
|
||||
|
||||
ghostunnel = {
|
||||
# Clients should not be authenticated with the public root certificates
|
||||
# (afaict, it doesn't make sense), so we only provide that default when
|
||||
# client cert auth is disabled.
|
||||
cacert = mkIf cfg.disableAuthentication (mkDefault null);
|
||||
};
|
||||
|
||||
# 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
|
||||
])
|
||||
(
|
||||
pkgs.writeScript "load-credentials" ''
|
||||
#!${pkgs.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;
|
||||
};
|
||||
|
||||
# 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}";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -94,7 +94,7 @@ class Redirects:
|
||||
- The first element of an identifier's redirects list must denote its current location.
|
||||
"""
|
||||
xref_targets = {}
|
||||
ignored_identifier_patterns = ("opt-", "auto-generated-", "function-library-")
|
||||
ignored_identifier_patterns = ("opt-", "auto-generated-", "function-library-", "service-opt-", "systemd-service-opt")
|
||||
for id, target in initial_xref_targets.items():
|
||||
# filter out automatically generated identifiers from module options and library documentation
|
||||
if id.startswith(ignored_identifier_patterns):
|
||||
|
||||
Reference in New Issue
Block a user