1{
2 lib,
3 pkgs,
4 config,
5 ...
6}:
7
8let
9 cfg = config.services.peertube-runner;
10
11 settingsFormat = pkgs.formats.toml { };
12 configFile = settingsFormat.generate "config.toml" cfg.settings;
13
14 env = {
15 NODE_ENV = "production";
16 XDG_CONFIG_HOME = "/var/lib/peertube-runner";
17 XDG_CACHE_HOME = "/var/cache/peertube-runner";
18 # peertube-runner makes its IPC socket in $XDG_DATA_HOME.
19 XDG_DATA_HOME = "/run/peertube-runner";
20 };
21in
22{
23 options.services.peertube-runner = {
24 enable = lib.mkEnableOption "peertube-runner";
25 package = lib.mkPackageOption pkgs [ "peertube" "runner" ] { };
26
27 user = lib.mkOption {
28 type = lib.types.str;
29 default = "prunner";
30 example = "peertube-runner";
31 description = "User account under which peertube-runner runs.";
32 };
33 group = lib.mkOption {
34 type = lib.types.str;
35 default = "prunner";
36 example = "peertube-runner";
37 description = "Group under which peertube-runner runs.";
38 };
39
40 settings = lib.mkOption {
41 type = settingsFormat.type;
42 default = { };
43 example = lib.literalExpression ''
44 {
45 jobs.concurrency = 4;
46 ffmpeg = {
47 threads = 0; # Let ffmpeg automatically choose.
48 nice = 5;
49 };
50 transcription.model = "large-v3";
51 }
52 '';
53 description = ''
54 Configuration for peertube-runner.
55
56 See available configuration options at <https://docs.joinpeertube.org/maintain/tools#configuration>.
57 '';
58 };
59 instancesToRegister = lib.mkOption {
60 type =
61 with lib.types;
62 attrsOf (submodule {
63 options = {
64 url = lib.mkOption {
65 type = lib.types.str;
66 example = "https://mypeertubeinstance.com";
67 description = "URL of the PeerTube instance.";
68 };
69 registrationTokenFile = lib.mkOption {
70 type = lib.types.path;
71 example = "/run/secrets/my-peertube-instance-registration-token";
72 description = ''
73 Path to a file containing a registration token for the PeerTube instance.
74
75 See how to generate registration tokens at <https://docs.joinpeertube.org/admin/remote-runners#manage-remote-runners>.
76 '';
77 };
78 runnerName = lib.mkOption {
79 type = lib.types.str;
80 example = "Transcription";
81 description = "Runner name declared to the PeerTube instance.";
82 };
83 runnerDescription = lib.mkOption {
84 type = with lib.types; nullOr str;
85 default = null;
86 example = "Runner for video transcription";
87 description = "Runner description declared to the PeerTube instance.";
88 };
89 };
90 });
91 default = { };
92 example = {
93 personal = {
94 url = "https://mypeertubeinstance.com";
95 registrationTokenFile = "/run/secrets/my-peertube-instance-registration-token";
96 runnerName = "Transcription";
97 runnerDescription = "Runner for video transcription";
98 };
99 };
100 description = "PeerTube instances to register this runner with.";
101 };
102
103 enabledJobTypes = lib.mkOption {
104 type = with lib.types; nonEmptyListOf str;
105 default = [
106 "vod-web-video-transcoding"
107 "vod-hls-transcoding"
108 "vod-audio-merge-transcoding"
109 "live-rtmp-hls-transcoding"
110 "video-studio-transcoding"
111 "video-transcription"
112 ];
113 example = [ "video-transcription" ];
114 description = "Job types that this runner will execute.";
115 };
116 };
117
118 config = lib.mkIf cfg.enable {
119 assertions = [
120 {
121 assertion = !(cfg.settings ? registeredInstances);
122 message = ''
123 `services.peertube-runner.settings.registeredInstances` cannot be used.
124 Instead, registered instances can be configured with `services.peertube-runner.instancesToRegister`.
125 '';
126 }
127 ];
128 warnings = lib.optional (cfg.instancesToRegister == { }) ''
129 `services.peertube-runner.instancesToRegister` is empty.
130 Instances cannot be manually registered using the command line.
131 '';
132
133 services.peertube-runner.settings = {
134 transcription = lib.mkIf (lib.elem "video-transcription" cfg.enabledJobTypes) {
135 engine = lib.mkDefault "whisper-ctranslate2";
136 enginePath = lib.mkDefault (lib.getExe pkgs.whisper-ctranslate2);
137 };
138 };
139
140 environment.systemPackages = [
141 (pkgs.writeShellScriptBin "peertube-runner" ''
142 ${lib.concatMapAttrsStringSep "\n" (name: value: ''export ${name}="${toString value}"'') env}
143
144 if [[ "$USER" == ${cfg.user} ]]; then
145 exec ${lib.getExe' cfg.package "peertube-runner"} "$@"
146 else
147 echo "This has to be run with the \`${cfg.user}\` user. Ex: \`sudo -u ${cfg.user} peertube-runner\`"
148 fi
149 '')
150 ];
151
152 systemd.services.peertube-runner = {
153 description = "peertube-runner daemon";
154 after = [
155 "network.target"
156 (lib.mkIf config.services.peertube.enable "peertube.service")
157 ];
158 wantedBy = [ "multi-user.target" ];
159
160 environment = env;
161 path = [ pkgs.ffmpeg-headless ];
162
163 script = ''
164 config_dir=$XDG_CONFIG_HOME/peertube-runner-nodejs/default
165 mkdir -p $config_dir
166 config_file=$config_dir/config.toml
167 cp -f --no-preserve=mode,ownership ${configFile} $config_file
168
169 ${lib.optionalString ((lib.length (lib.attrNames cfg.instancesToRegister)) > 0) ''
170 # Temp config directory for registration commands
171 temp_dir=$(mktemp --directory)
172 temp_config_dir=$temp_dir/peertube-runner-nodejs/default
173 mkdir -p $temp_config_dir
174 temp_config_file=$temp_config_dir/config.toml
175
176 mkdir -p $STATE_DIRECTORY/runner_tokens
177 ${lib.concatMapAttrsStringSep "\n" (instanceName: instance: ''
178 runner_token_file=$STATE_DIRECTORY/runner_tokens/${instanceName}
179
180 # Register any currenctly unregistered instances.
181 if [ ! -f $runner_token_file ] || [[ $(cat $runner_token_file) != ptrt-* ]]; then
182 # Server has to be running for registration.
183 XDG_CONFIG_HOME=$temp_dir ${lib.getExe' cfg.package "peertube-runner"} server &
184
185 XDG_CONFIG_HOME=$temp_dir ${lib.getExe' cfg.package "peertube-runner"} register \
186 --url ${lib.escapeShellArg instance.url} \
187 --registration-token "$(cat ${instance.registrationTokenFile})" \
188 --runner-name ${lib.escapeShellArg instance.runnerName} \
189 ${lib.optionalString (
190 instance.runnerDescription != null
191 ) ''--runner-description ${lib.escapeShellArg instance.runnerDescription}''}
192
193 # Kill the server
194 kill $!
195
196 ${lib.getExe pkgs.yq-go} -e ".registeredInstances[0].runnerToken" \
197 $temp_config_file > $runner_token_file
198 rm $temp_config_file
199 fi
200
201 echo "
202
203 [[registeredInstances]]
204 url = \"${instance.url}\"
205 runnerToken = \"$(cat $runner_token_file)\"
206 runnerName = \"${instance.runnerName}\"
207 ${lib.optionalString (
208 instance.runnerDescription != null
209 ) ''runnerDescription = \"${instance.runnerDescription}\"''}
210 " >> $config_file
211 '') cfg.instancesToRegister}
212 ''}
213
214 # Don't allow changes that won't persist.
215 chmod 440 $config_file
216
217 systemd-notify --ready
218 exec ${lib.getExe' cfg.package "peertube-runner"} server ${
219 lib.concatMapStringsSep " " (jobType: "--enable-job ${jobType}") cfg.enabledJobTypes
220 }
221 '';
222 serviceConfig = {
223 Type = "notify";
224 NotifyAccess = "all"; # for systemd-notify
225 Restart = "always";
226 RestartSec = 5;
227 SyslogIdentifier = "prunner";
228 User = cfg.user;
229 Group = cfg.group;
230 StateDirectory = "peertube-runner";
231 StateDirectoryMode = "0700";
232 CacheDirectory = "peertube-runner";
233 CacheDirectoryMode = "0700";
234 RuntimeDirectory = "peertube-runner";
235 RuntimeDirectoryMode = "0700";
236
237 ProtectSystem = "full";
238 NoNewPrivileges = true;
239 ProtectHome = true;
240 CapabilityBoundingSet = "~CAP_SYS_ADMIN";
241 };
242 };
243
244 users.users = lib.mkIf (cfg.user == "prunner") {
245 ${cfg.user} = {
246 isSystemUser = true;
247 group = cfg.group;
248 };
249 };
250 users.groups = lib.mkIf (cfg.group == "prunner") {
251 ${cfg.group} = { };
252 };
253 };
254
255 meta.maintainers = lib.teams.ngi.members;
256}