Unverified Commit d222d01c authored by Robert Hensing's avatar Robert Hensing Committed by GitHub
Browse files

nixos/modular-services: add systemd.mainExecStart option (#469450)

parents 1329c527 b1caac95
Loading
Loading
Loading
Loading
+60 −1
Original line number Diff line number Diff line
@@ -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
        ];
      };
    };
+51 −0
Original line number Diff line number Diff line
@@ -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 ]]
+1 −5
Original line number Diff line number Diff line
@@ -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
  };
+51 −57
Original line number Diff line number Diff line
# Non-module dependencies (`importApply`)
{ writeScript, runtimeShell }:
{ }:

# Service module
{
@@ -185,29 +185,7 @@ 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"
                )
              }
            ''
          )
        ++ [
      argv = [
        (getExe cfg.package)
        "server"
        "--listen"
@@ -225,8 +203,23 @@ in
      ++ cfg.extraArguments;
    };
  }
  // lib.optionalAttrs (options ? systemd) {
    # refine the service
  # 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" ];
@@ -242,5 +235,6 @@ in
            ++ optional (cfg.cacert != null) "cacert:${cfg.cacert}";
        };
      };
  };
    }
  );
}