Commit 950a5f33 authored by Robert Hensing's avatar Robert Hensing
Browse files

modular services: Add configData option for etc-like files

parent 96111a65
Loading
Loading
Loading
Loading
+27 −0
Original line number Diff line number Diff line
@@ -8,6 +8,8 @@ See the [Modular Services chapter] in the manual [[source]](../../doc/manual/dev

# Design decision log

## Initial design

- `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.
@@ -26,3 +28,28 @@ See the [Modular Services chapter] in the manual [[source]](../../doc/manual/dev
    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

## Configuration Data (`configData`) Design

Without a mechanism for adding files, all configuration had to go through `process.*`, requiring process restarts even when those would have been avoidable.
Many services implement automatic reloading or reloading on e.g. `SIGUSR1`, but those mechanisms need files to read. `configData` provides such files.

### Naming and Terminology

- **`configData` instead of `environment.etc`**: The name `configData` is service manager agnostic. While systemd system services can use `/etc`, other service managers may expose configuration data differently (e.g., different directory, relative paths).

- **`path` attribute**: Each `configData` entry automatically gets a `path` attribute set by the service manager implementation, allowing services to reference the location of their configuration files. These paths themselves are not subject to change from generation to generation; only their contents are.

- **`name` attribute**: In `environment.etc` this would be `target` but that's confusing, especially for symlinks, as it's not the symlink's target.

### Service Manager Integration

- **Portable base**: The `configData` interface is declared in `portable/config-data.nix`, making it available to all service manager implementations.

- **Systemd integration**: The systemd implementation (`systemd/system.nix`) maps `configData` entries to `environment.etc` entries under `/etc/system-services/`.

- **Path computation**: `systemd/config-data-path.nix` recursively computes unique paths for services and sub-services (e.g., `/etc/system-services/webserver/` vs `/etc/system-services/webserver-api/`).
  Fun fact: for the module system it is a completely normal module, despite its recursive definition.
  If we parameterize `/etc/system-services`, it will have to become an `importApply` style module nonetheless (function returning module).

- **Simple attribute structure**: Unlike `environment.etc`, `configData` uses a simpler structure with just `enable`, `name`, `text`, `source`, and `path` attributes. Complex ownership options were omitted for simplicity and portability.
  Per-service user creation is still TBD.
+65 −0
Original line number Diff line number Diff line
# Tests in: ../../../../tests/modular-service-etc/test.nix
# This file is a function that returns a module.
pkgs:
{
  lib,
  name,
  config,
  options,
  ...
}:
let
  inherit (lib) mkOption types;
in
{
  options = {
    enable = mkOption {
      type = types.bool;
      default = true;
      description = ''
        Whether this configuration file should be generated.
        This option allows specific configuration files to be disabled.
      '';
    };

    name = mkOption {
      type = types.str;
      description = ''
        Name of the configuration file (relative to the service's configuration directory). Defaults to the attribute name.
      '';
    };

    path = mkOption {
      type = types.str;
      readOnly = true;
      description = ''
        The actual path where this configuration file will be available.
        This is determined by the service manager implementation.

        On NixOS it is an absolute path.
        Other service managers may provide a relative path, in order to be unprivileged and/or relocatable.
      '';
    };

    text = mkOption {
      default = null;
      type = types.nullOr types.lines;
      description = "Text content of the configuration file.";
    };

    source = mkOption {
      type = types.path;
      description = "Path of the source file.";
    };
  };

  config = {
    name = lib.mkDefault name;
    source = lib.mkIf (config.text != null) (
      let
        name' = "service-configdata-" + lib.replaceStrings [ "/" ] [ "-" ] name;
      in
      lib.mkDerivedConfig options.text (pkgs.writeText name')
    );
  };
}
+44 −0
Original line number Diff line number Diff line
# Tests in: ../../../../tests/modular-service-etc/test.nix
# Configuration data support for portable services
# This module provides configData for services, enabling configuration reloading
# without terminating and restarting the service process.
{
  lib,
  pkgs,
  ...
}:
let
  inherit (lib) mkOption types;
  inherit (lib.modules) importApply;
in
{
  options = {
    configData = mkOption {
      default = { };
      example = lib.literalExpression ''
        {
          "server.conf" = {
            text = '''
              port = 8080
              workers = 4
            ''';
          };
          "ssl/cert.pem" = {
            source = ./cert.pem;
          };
        }
      '';
      description = ''
        Configuration data files for the service

        These files are made available to the service and can be updated without restarting the service process, enabling configuration reloading.
        The service manager implementation determines how these files are exposed to the service (e.g., via a specific directory path).
        This path is available in the `path` sub-option for each `configData.<name>` entry.

        This is particularly useful for services that support configuration reloading via signals (e.g., SIGHUP) or which pick up changes automatically, so that no downtime is required in order to reload the service.
      '';

      type = types.lazyAttrsOf (types.submodule (importApply ./config-data-item.nix pkgs));
    };
  };
}
+1 −0
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@ in
  _class = "service";
  imports = [
    ../../../misc/assertions.nix
    ./config-data.nix
  ];
  options = {
    services = mkOption {
+39 −0
Original line number Diff line number Diff line
# Tests in: ../../tests/modular-service-etc/test.nix
# This module sets the path for configData entries in systemd services
let
  setPathsModule =
    prefix:
    { lib, name, ... }:
    let
      inherit (lib) mkOption types;
      servicePrefix = "${prefix}${name}";
    in
    {
      _class = "service";
      options = {
        # Extend portable configData option
        configData = mkOption {
          type = types.lazyAttrsOf (
            types.submodule (
              { config, ... }:
              {
                config = {
                  path = lib.mkDefault "/etc/system-services/${servicePrefix}/${config.name}";
                };
              }
            )
          );
        };
        services = mkOption {
          type = types.attrsOf (
            types.submoduleWith {
              modules = [
                (setPathsModule "${servicePrefix}-")
              ];
            }
          );
        };
      };
    };
in
setPathsModule ""
Loading