How to refer to other part of the config?

I want to simplify my config, what I had before was:

{ config, ... }:
let mosquittoUsername = "10_24_0_1";
in {
  sops.secrets = {
    mosquittoPassword = { };
    postgresqlGoskPassword = { };
  };

  services.gosk = { enable = true; };

  services.gosk.processors = rec {
    readMqtt = {
      command = "read mqtt";
      publishTo = 6300;
      configFile = config.sops.templates.readMqtt.path;
    };
    writeDatabaseMapped = {
      command = "write database mapped";
      subscribeTo = [ readMqtt.publishTo ];
      configFile = config.sops.templates.writeDatabaseMapped.path;
      after = [ "container@postgresql-15-test.service" ];
    };
  };
  sops.templates = {
    readMqtt = {
      content = ''
        ---
        username: "${mosquittoUsername}"
        password: "${config.sops.placeholder.mosquittoPassword}"
        url: "mqtt://10.24.0.1:1883"
        interval: "10s"
        buffer_size: 1000
        example_required_file: "${./extraFiles/test.bin}"
      '';
    };
    writeDatabaseMapped = {
      content = ''
        ---
        url: "postgres://gosk:${config.sops.placeholder.postgresqlGoskPassword}@192.168.100.2:5432/gosk?sslmode=disable&application_name=gosk_writer_postgresql"
        batch_flush_length: 100
        batch_flush_interval: 1m
        buffer_size: 100
        number_of_workers: 1
        timeout: 10s
      '';
    };
  };
}

and the definition of the gosk service:

{ config, lib, pkgs, utils, ... }:

with lib;

let
  cfg = config.services.gosk;

  # Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
  escapeUnitName = name:
    lib.concatMapStrings (s: if lib.isList s then "-" else s)
    (builtins.split "[^a-zA-Z0-9_.\\-]+" name);
in {
  options.services.gosk = {
    enable = mkEnableOption "Go Signal K service";

    package = lib.mkPackageOption pkgs "gosk" { };

    processors = mkOption {
      type = types.attrsOf (types.submodule {
        options = {
          enable = mkOption {
            type = types.bool;
            default = true;
            description = "Whether or not to enable this command.";
          };
          command = mkOption {
            type = types.separatedString " ";
            default = "";
            example = "write database mapped";
            description = "Command to add to this gosk command.";
          };
          subscribeTo = mkOption {
            type = with types; listOf port;
            default = [ ];
            example = [ 6000 6001 ];
            description = "Ports to subscribe to.";
          };
          publishTo = mkOption {
            type = with types; nullOr port;
            default = null;
            example = 6000;
            description =
              "Port to publish to, if null then this process will not publish.";
          };
          configFile = mkOption {
            type = with types; nullOr path;
            default = null;
            example = ./gosk.yaml;
            description =
              "Configuration file in YAML format for this gosk command.";
          };
          after = mkOption {
            type = types.listOf utils.systemdUtils.lib.unitNameType;
            default = [ ];
            example = [ "postgresql" ];
            description = "Systemd services to wait for";
          };
          extraArgs = mkOption {
            type = with types; listOf str;
            default = [ ];
            example = ''
              [ "--pmport" "127.0.0.1:9203" ]
            '';
            description = "Extra arguments for the gosk processor.";
          };
        };
      });
      default = { };
      example = literalExpression ''
        {
          "map ais" = {
            command = "map";
            subscribeTo = [ 6001 ];
            publishTo = 6101;
            configFile = ./gosk.yaml;
          };
        }
      '';
      description = "gosk commands to run.";
    };
  };

  config = mkIf cfg.enable {
    systemd.services = mapAttrs' (name: c:
      nameValuePair "gosk-${escapeUnitName name}" (mkMerge [{
        inherit (c) enable;
        description = "gosk ${name}";
        wantedBy = [ "multi-user.target" ];
        inherit (c) after;

        serviceConfig = {
          ExecStart = toString ([ "${pkgs.gosk}/bin/gosk" ]
            ++ lib.optional (c.command != "") c.command ++ [
              (concatMapStringsSep " " (url: "-s tcp://127.0.0.1:${url}")
                c.subscribeTo)
            ] ++ lib.optional (isString c.publishURL)
            "-p tcp://127.0.0.1:${c.publishTo}"
            ++ lib.optional (c.configFile != null)
            "--config \${CREDENTIALS_DIRECTORY}/gosk.yaml" ++ c.extraArgs);
          Restart = "always";
          LoadCredential =
            if c.configFile != null then "gosk.yaml:${c.configFile}" else "";
          DynamicUser = "yes";
        };
      }])) cfg.processors;
    # sops.templates = mapAttrs' (name: c:
    # ) cfg.processors;
  };

  meta.maintainers = with maintainers; [ munnik ];
}

I want to join the content of the config file in the service definition so that it becomes more readable. And I don’t have to specifiy a separte sops.templates section. Something like this:

{ config, ... }:
let mosquittoUsername = "10_24_0_1";
in {
  sops.secrets = {
    mosquittoPassword = { };
    postgresqlGoskPassword = { };
  };

  services.gosk = { enable = true; };

  services.gosk.processors = rec {
    readMqtt = {
      arguments = "read mqtt";
      publishTo = 6300;
      config = ''
        ---
        username: "${mosquittoUsername}"
        password: "${config.sops.placeholder.mosquittoPassword}"
        url: "mqtt://10.24.0.1:1883"
        interval: "10s"
        buffer_size: 1000
        example_required_file: "${./extraFiles/test.bin}"
      '';
    };
    writeDatabaseMapped = {
      arguments = "write database mapped";
      subscribeTo = [ readMqtt.publishTo ];
      config = ''
        ---
        url: "postgres://gosk:${config.sops.placeholder.postgresqlGoskPassword}@192.168.100.2:5432/gosk?sslmode=disable&application_name=gosk_writer_postgresql"
        batch_flush_length: 100
        batch_flush_interval: 1m
        buffer_size: 100
        number_of_workers: 1
        timeout: 10s
      '';
      after = [ "container@postgresql-15-test.service" ];
    };
  };
}

Therefore, I modified gosk service definition to:

{ config, lib, pkgs, utils, ... }:

with lib;

let
  cfg = config.services.gosk;

  # Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
  goskUnitName = name:
    "gosk-" + lib.concatMapStrings (s: if lib.isList s then "-" else s)
    (builtins.split "[^a-zA-Z0-9_.\\-]+" name);
in {
  options.services.gosk = {
    enable = mkEnableOption "Go Signal K service";

    package = lib.mkPackageOption pkgs "gosk" { };

    processors = mkOption {
      type = types.attrsOf (types.submodule {
        options = {
          enable = mkOption {
            type = types.bool;
            default = true;
            description = "Whether or not to enable this service.";
          };
          arguments = mkOption {
            type = types.separatedString " ";
            default = "";
            example = "write database mapped";
            description = "Arguments to add to this gosk service.";
          };
          subscribeTo = mkOption {
            type = with types; listOf port;
            default = [ ];
            example = [ 6000 6001 ];
            description = "Ports to subscribe to.";
          };
          publishTo = mkOption {
            type = with types; nullOr port;
            default = null;
            example = 6000;
            description =
              "Port to publish to, if null then this process will not publish.";
          };
          config = mkOption {
            type = with types; nullOr str;
            default = null;
            example = ''
              ---
              name: "AIS"
              protocol: "nmea0183"
              url: "file:///dev/ttyUSBCom0" # verified
              baudRate: 38400
              dataBits: 8
              stopBits: 1
              parity: "N"
            '';
            description = "Configuration in YAML format for this gosk service.";
          };
          after = mkOption {
            type = types.listOf utils.systemdUtils.lib.unitNameType;
            default = [ ];
            example = [ "postgresql" ];
            description = "Systemd services to wait for";
          };
          extraArgs = mkOption {
            type = with types; listOf str;
            default = [ ];
            example = ''
              [ "--pmport" "127.0.0.1:9203" ]
            '';
            description = "Extra arguments for the gosk processor.";
          };
        };
      });
      default = { };
      example = literalExpression ''
        {
          "map ais" = {
            arguments = "map";
            subscribeTo = [ 6001 ];
            publishTo = 6101;
            config = \'\'
              ---
              name: "AIS"
              protocol: "nmea0183"
              url: "file:///dev/ttyUSBCom0" # verified
              baudRate: 38400
              dataBits: 8
              stopBits: 1
              parity: "N"
            \'\';
          };
        }
      '';
      description = "gosk services to run.";
    };
  };

  config = mkIf cfg.enable rec {
    sops.templates = mapAttrs' (name: c:
      nameValuePair (goskUnitName name) (mkMerge [{ content = c.config; }]))
      cfg.processors;
    systemd.services = mapAttrs' (name: c:
      nameValuePair (goskUnitName name) (mkMerge [{
        inherit (c) enable;
        inherit (c) after;
        description = "gosk ${name}";
        wantedBy = [ "multi-user.target" ];

        serviceConfig = {
          ExecStart = toString ([ "${pkgs.gosk}/bin/gosk" ]
            ++ lib.optional (c.arguments != "") c.arguments ++ [
              (concatMapStringsSep " "
                (port: "-s tcp://127.0.0.1:${toString port}") c.subscribeTo)
            ] ++ lib.optional (isInt c.publishTo)
            "-p tcp://127.0.0.1:${toString c.publishTo}"
            ++ lib.optional (c.config != null)
            "--config \${CREDENTIALS_DIRECTORY}/config.yaml" ++ c.extraArgs);
          Restart = "always";
          LoadCredential = if c.config != null then
            "config.yaml:${sops.templates.${goskUnitName name}.path}"
          else
            "";
          DynamicUser = "yes";
        };
      }])) cfg.processors;
  };

  meta.maintainers = with maintainers; [ munnik ];
}

But this results in an error:

   error: attribute 'path' missing
       at /nix/store/b643glfy3wal6pwz7rwk8r7h56qn0bjj-source/modules/nixos/services/gosk.nix:122:28:
          121|           LoadCredential = if c.config != null then
          122|             "config.yaml:${sops.templates.${goskUnitName name}.path}"
             |                            ^
          123|           else

I tried several ways to write this but I can’t get it working. How do I refer to the sops file?

Don’t mix rec with the module system’s fixpoint stuff; it’ll only lead to confusion.

"config.yaml:${config.sops.templates.${goskUnitName name}.path}"

For more explanation, I have produced this wiki article.

3 Likes