-
-
Notifications
You must be signed in to change notification settings - Fork 14.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
init Modular Portable Service Layer, proof of concept #267111
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
|
||
# Module Based Service Abstraction Layer usage example | ||
|
||
This directory demonstrates | ||
- `configuration.nix`: How to use an abstract service module on NixOS | ||
- `generic-python-http-server.nix`: A simple abstract service | ||
- `nixos-test.nix`: Make it runnable | ||
- `nix build example/nixos-test.nix` | ||
|
||
Mess around with the repl | ||
|
||
nix repl -f example/nixos-test.nix | ||
nix-repl> nodes.machine.services.abstract.<TAB> | ||
args | ||
daemon | ||
... | ||
|
||
Enjoy! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# A NixOS configuration | ||
{ pkgs, ... }: { | ||
|
||
environment.systemPackages = [ pkgs.hello ]; # Truly NixOS | ||
|
||
services.abstract."serve-nix-manual" = { | ||
imports = [ ./generic-python-http-server.nix ]; | ||
# You could change the port, but the default is sweet | ||
# service.port = 8080; | ||
service.directory = pkgs.nix.doc; | ||
}; | ||
|
||
# A service that's distributed with NixOS may look like this. | ||
# Doesn't exists yet. Add to specialArgs in a submoduleWith. | ||
# | ||
# services.abstract."serve-nixpkgs-manual" = { serviceModules, ... }: { | ||
# imports = [ serviceModules.nginx ]; | ||
# service.virtualHosts."/" = { | ||
# root = pkgs.nix.doc; | ||
# }; | ||
# }; | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
{ lib, config, options, pkgs, ... }: | ||
let | ||
inherit (lib) mkOption types; | ||
cfg = config.service; | ||
in | ||
{ | ||
# I think `service` could be the place where the service itself is configured. | ||
# Alternatively, this could have been `pythonHttpServer`, but then over time | ||
# we'd endup with a service module namespace where we don't really know which | ||
# names will conflict, whenever we introduce a new generic top level option in | ||
# the future. | ||
options.service = { | ||
port = lib.mkOption { | ||
type = types.int; | ||
default = 8080; | ||
description = '' | ||
Port number where the web server should listen. | ||
''; | ||
}; | ||
directory = lib.mkOption { | ||
type = types.path; | ||
description = '' | ||
A path to serve over HTTP. | ||
''; | ||
}; | ||
}; | ||
|
||
config = { | ||
# Obligatory relaying of: Warning http.server is not recommended for production. It only implements basic security checks. | ||
process = "${pkgs.python3}/bin/python3"; | ||
args = [ | ||
"-m" "http.server" (toString cfg.port) | ||
"--directory" cfg.directory | ||
]; | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
let | ||
pkgs = import ../. { }; | ||
inherit (pkgs.testers) runNixOSTest; | ||
in | ||
|
||
runNixOSTest { | ||
name = "abstract-service"; | ||
|
||
nodes.machine = { lib, options, ... }: { | ||
imports = [ | ||
./configuration.nix | ||
]; | ||
|
||
# Ignore this. It makes nodes.machine.options appear in | ||
# nix repl -f example/nixos-test.nix | ||
options.options = lib.mkOption { default = options; }; | ||
}; | ||
|
||
testScript = '' | ||
machine.wait_for_open_port(8080); | ||
''; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# This module is valid for all implementations of the service abstraction layer, | ||
# and might even be vendored from another repo or moved outside of nixpkgs/nixos/ | ||
# into nixpkgs/something. | ||
|
||
{ lib, config, options, pkgs, ... }: | ||
let | ||
inherit (lib) mkOption types; | ||
in | ||
{ | ||
options = { | ||
services.abstract = mkOption { | ||
type = types.lazyAttrsOf (types.submoduleWith { | ||
modules = [ | ||
./instance/generic.nix | ||
{ _module.args.pkgs = pkgs; } | ||
]; }); | ||
default = { }; | ||
description = '' | ||
All services that are configured on the system. | ||
''; | ||
}; | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# This corresponds to the arguments of createManagedProcess in svanderburg's proposal | ||
{ lib, config, options, ... }: | ||
let | ||
inherit (lib) literalExpression mkOption types; | ||
pathOrStr = types.coercedTo types.path (x: "${x}") types.str; | ||
in | ||
{ | ||
options = { | ||
enable = mkOption { | ||
type = types.bool; | ||
default = true; | ||
description = '' | ||
Whether to enable the service. A service is enabled by default, because you took the effort to define its attribute. | ||
''; | ||
}; | ||
process = mkOption { | ||
type = pathOrStr; | ||
description = '' | ||
When this property is specified, it translates both to: {option}`foreground.process` and {option}`daemon.process`. | ||
''; | ||
}; | ||
foreground = { | ||
process = lib.mkOption { | ||
type = pathOrStr; | ||
description = '' | ||
Path to an executable that launches a process in foreground mode. | ||
''; | ||
default = config.process; | ||
defaultText = literalExpression "config.process"; | ||
}; | ||
args = lib.mkOption { | ||
type = types.listOf pathOrStr; | ||
description = '' | ||
Arguments to pass to the {option}`foreground.process`. | ||
''; | ||
default = config.args; | ||
defaultText = literalExpression "config.args"; | ||
}; | ||
}; | ||
daemon = { | ||
process = lib.mkOption { | ||
type = pathOrStr; | ||
description = '' | ||
Path to an executable that launches a process in daemon mode. | ||
''; | ||
default = config.process; | ||
defaultText = literalExpression "config.process"; | ||
}; | ||
args = lib.mkOption { | ||
type = types.listOf pathOrStr; | ||
description = '' | ||
Arguments to pass to the {option}`daemon.process`. | ||
''; | ||
default = config.args; | ||
defaultText = literalExpression "config.args"; | ||
}; | ||
}; | ||
Comment on lines
+40
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think allowing programs to daemonize themselves will be a trap for this. Foremost new programs don't implement this anymore and a generally discouraged to do so. Old programs implement this in completely varying degrees ranging from full signal handling, creating pid files and being their own process manager to basically doing IMO we should only take foreground programs and let the service manager handle all of the "daemon" parts. If the service manager doesn't support this, then IMO it is unsuitable. |
||
args = lib.mkOption { | ||
type = types.listOf pathOrStr; | ||
description = '' | ||
Arguments to pass to the `foreground.process` or `daemon.process`. In other words, these arguments are passed regardless of process manager choice. | ||
''; | ||
default = []; | ||
}; | ||
# TODO more from svanderburg's proposal. Look for the list of properties after defining `createManagedProcess` | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
{ lib, config, options, ... }: | ||
let | ||
inherit (lib) mkOption types; | ||
in | ||
{ | ||
options = { | ||
systemd.services = lib.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. | ||
''; | ||
# ^ if we don't want to support this, we can add the systemd options that | ||
# we care about here, and combine them into a host system definition | ||
# outside the control of this module - in services/abstract/systemd.nix. | ||
|
||
type = types.lazyAttrsOf (types.deferredModuleWith { staticModules = [ | ||
# TODO Add the modules from systemd? They'll be deduplicated, but | ||
# will generate docs in this part of the hierarchy. Is such duplication | ||
# desirable? Perhaps! | ||
]; }); | ||
}; | ||
}; | ||
config = { | ||
# Empty string "" is ok, because it will be prefixed by the abstraction layer | ||
# anyway. Actual names here are for when multiple systemd services are needed. | ||
# I might have overengineered this for a demonstration :thinking_face:. | ||
# | ||
# Note that this is the systemd.services option above, not the system one. | ||
# This pattern allows a module to do all sorts of systemd stuff if it needs to. | ||
systemd.services."" = { | ||
description = "TBD add an option for that"; | ||
wantedBy = [ "multi-user.target" ]; | ||
serviceConfig = { | ||
Type = "simple"; | ||
Restart = "always"; | ||
RestartSec = "5"; | ||
ExecStart = [ (lib.escapeShellArgs ([ config.foreground.process ] ++ config.foreground.args)) ]; | ||
}; | ||
}; | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# This merges systemd support into the generic instance module. | ||
{ lib, config, options, ... }: | ||
let | ||
inherit (lib) concatMapAttrs mkOption types; | ||
|
||
dash = before: after: | ||
# maybe add nice clever escaping? | ||
if after == "" | ||
then before | ||
else "${before}-${after}"; | ||
in | ||
{ | ||
# First half of the magic: mix systemd logic into the otherwise abstract services | ||
options = { | ||
services.abstract = mkOption { | ||
type = types.lazyAttrsOf (types.submoduleWith { modules = [ ./instance/systemd.nix ]; }); | ||
}; | ||
}; | ||
|
||
# Second half of the magic: siphon units that were defined in isolation to the system | ||
config = { | ||
systemd.services = | ||
concatMapAttrs | ||
(abstractServiceName: abstractServiceConfig: | ||
if abstractServiceConfig.enable | ||
then | ||
concatMapAttrs | ||
(subServiceName: unitModule: { | ||
"${dash abstractServiceName subServiceName}" = { ... }: { | ||
imports = [ unitModule ]; | ||
}; | ||
}) | ||
abstractServiceConfig.systemd.services | ||
else {}) | ||
config.services.abstract; | ||
|
||
# systemd.sockets = | ||
# ... same ... | ||
|
||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This hardcodes the generic options, but that's not actually necessary and creates some confusion about what the actually used interface really is, in a way that isn't forward compatible.
It should be up to the actual service to decide in which format it wants to be loaded, and process managers can decide which formats they support.
This could be negotiated through specialArgs and may look like:
This says that a postgresql service expects the process manager to support the
generic_1_0
interface and load that.The
systemd_1_0
interface is optional and is only loaded if the process manager supports it.imports
can of course only depend onspecialArgs
arguments, but that is sufficient for this purpose.Note that services can already introspect e.g.
options?systemd
to figure out in which kind of process manager they are loaded. So the addition of such a mechanism is primarily important for forward compatibility as we'll inevitably evolve the taxonomy of services.This also allows the creation of helper modules that for instance provide polyfills, emulate one process manager's features on top of another, etc. These could be distributed independently, or as part of the suite of modules and interfaces that are built in to the process manager integrations.
Even just within the NixOS context, such versions let use eat the cake and have it too:
Arguably there's a non-zero cost to having a compatibility layer or keeping old code around, but at least it becomes a solvable problem, and the old solution doesn't come with the expectation of parity with the new solution.