diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index d3f397e0f46c..3fbd21ee4f1d 100644 --- a/nixos/doc/manual/release-notes/rl-2611.section.md +++ b/nixos/doc/manual/release-notes/rl-2611.section.md @@ -28,6 +28,8 @@ - [FlapAlerted](https://github.com/Kioubit/FlapAlerted), detects BGP flapping events and provides statistics based on BGP update messages. Available as [services.flap-alerted](#opt-services.flap-alerted.enable). +- [gocron](https://github.com/flohoss/gocron), a task scheduler with web interface. Available as [services.gocron](#opt-services.gocron.enable). + - [Unpackerr](https://unpackerr.zip), extracts downloads for Radarr, Sonarr, Lidarr, Readarr, and/or a Watch folder. Available as [services.unpackerr](#opt-services.unpackerr.enable). - [Matrix Authentication Service](https://github.com/element-hq/matrix-authentication-service) is an OAuth2.0 and OpenID Connect provider for Matrix homeservers (such as Synapse). It replaces standard password authentication with modern OpenID Connect flows, and can delegate authentication to upstream OIDC providers. Available as [services.matrix-authentication-service](#opt-services.matrix-authentication-service.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 2535896d4790..692b4462c928 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1491,6 +1491,7 @@ ./services/scheduling/atd.nix ./services/scheduling/cron.nix ./services/scheduling/fcron.nix + ./services/scheduling/gocron.nix ./services/scheduling/prefect.nix ./services/scheduling/scx-loader.nix ./services/scheduling/scx.nix diff --git a/nixos/modules/services/scheduling/gocron.nix b/nixos/modules/services/scheduling/gocron.nix new file mode 100644 index 000000000000..9916d9fd9da7 --- /dev/null +++ b/nixos/modules/services/scheduling/gocron.nix @@ -0,0 +1,173 @@ +{ + options, + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.gocron; + settingsFormat = pkgs.formats.yaml { }; + gocronConf = settingsFormat.generate "gocron.yaml" cfg.settings; + defaultUser = "gocron"; + defaultGroup = "gocron"; + timeZone = config.time.timeZone; + + hardeningOptions = lib.mkOption { + description = "Configuration for hardening the systemd service."; + type = lib.types.submodule { + options = { + ProtectHome = lib.mkOption { + description = '' + Whether to make the home directories inaccessible to the service. + See for more details. + ''; + type = lib.types.either lib.types.str lib.types.bool; + default = true; + example = "read-only"; + }; + ProtectSystem = lib.mkOption { + description = '' + Whether to make several system directories inaccessible to the service. + See for more details. + ''; + type = lib.types.either lib.types.str lib.types.bool; + default = true; + example = "full"; + }; + }; + }; + }; +in +{ + + options.services.gocron = { + enable = lib.mkEnableOption "gocron, a task scheduler"; + + package = lib.mkOption { + default = pkgs.gocron; + defaultText = lib.literalExpression "pkgs.gocron"; + type = lib.types.package; + description = '' + gocron package to use. + ''; + }; + + openFirewall = lib.mkOption { + description = "Whether to open the firewall port to access the web ui."; + type = lib.types.bool; + default = false; + }; + + user = lib.mkOption { + description = "Unix User to run the server under"; + type = lib.types.str; + default = defaultUser; + }; + + group = lib.mkOption { + description = "Unix Group to run the server under"; + type = lib.types.str; + default = defaultGroup; + }; + + extraGroups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "backup" ]; + description = '' + Additional groups for the systemd service. + ''; + }; + + hardening = hardeningOptions; + + settings = lib.mkOption { + # Setting this type allows for correct merging behavior + type = settingsFormat.type; + default = { }; + description = '' + Configuration for gocron, see + + for supported settings. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = !lib.hasAttr "software" cfg.settings; + message = "Software installation configuration is only supported for traditional distros by upstream."; + } + ]; + + services.gocron.settings = { + time_zone = if timeZone != null then timeZone else lib.mkDefault "Etc/UTC"; + server = { + address = lib.mkDefault "127.0.0.1"; + port = lib.mkDefault 8156; + }; + db.location = lib.mkDefault "/var/lib/gocron"; + }; + + systemd.services.gocron = { + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${lib.getExe pkgs.gocron} --config '${gocronConf}'"; + User = cfg.user; + Group = cfg.group; + DeviceAllow = ""; + LockPersonality = true; + MemoryDenyWriteExecute = true; + MountAPIVFS = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateNetwork = lib.mkDefault false; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = cfg.hardening.ProtectHome; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = cfg.hardening.ProtectSystem; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + UMask = "0077"; + StateDirectory = lib.mkIf (cfg.settings.db.location == "/var/lib/gocron") "gocron"; + SystemCallArchitectures = "native"; + SystemCallErrorNumber = "EPERM"; + SystemCallFilter = [ + "@system-service" + ]; + }; + }; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.settings.server.port ]; + + users.users.${cfg.user} = { + isSystemUser = true; + inherit (cfg) group; + }; + + users.groups.${cfg.group} = { }; + + meta = { + buildDocsInSandbox = true; + maintainers = with lib.maintainers; [ juliusfreudenberger ]; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 0bc01b90551f..71a18ed9e343 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -702,6 +702,7 @@ in gobgpd = runTest ./gobgpd.nix; gocd-agent = runTest ./gocd-agent.nix; gocd-server = runTest ./gocd-server.nix; + gocron = runTest ./gocron.nix; gocryptfs = runTest ./gocryptfs.nix; gokapi = runTest ./gokapi.nix; gollum = runTest ./gollum.nix; diff --git a/nixos/tests/gocron.nix b/nixos/tests/gocron.nix new file mode 100644 index 000000000000..2625dc474b53 --- /dev/null +++ b/nixos/tests/gocron.nix @@ -0,0 +1,58 @@ +{ pkgs, ... }: + +{ + name = "gocron"; + meta.maintainers = with pkgs.lib.maintainers; [ juliusfreudenberger ]; + + nodes.machine = + { pkgs, ... }: + { + environment.systemPackages = [ pkgs.jq ]; + services.gocron = { + enable = true; + settings = { + jobs = [ + { + name = "Test job"; + disabled_cron = true; + commands = [ + "echo 'Job runs successfully'" + ]; + } + ]; + }; + }; + }; + + testScript = '' + def gocron_is_up(_) -> bool: + status, _ = machine.execute("curl --fail http://localhost:8156") + return status == 0 + + def job_is_available() -> bool: + status, output = machine.execute("curl http://localhost:8156/api/jobs | jq '. | length'") + return status == 0 and int(output) == 1 + + def start_job(): + machine.succeed("curl -X POST http://localhost:8156/api/jobs/Test%20job") + + def job_ran_successfully() -> bool: + output = machine.succeed("curl http://localhost:8156/api/runs/Test%20job | jq '.[0].status_id, .[0].logs.[2].message'") + split_output = output.split('\n') + ran_successfully = int(split_output[0]) == 3 + log_message_as_expected = "Job runs not successfully" in split_output[1] + return ran_successfully and log_message_as_expected + + machine.wait_for_unit("gocron.service") + machine.wait_for_open_port(8156) + with machine.nested("Waiting for UI to work"): + retry(gocron_is_up) + + with machine.nested("Test job"): + if not job_is_available(): + exit(1) + start_job() + if not job_ran_successfully(): + exit(1) + ''; +} diff --git a/pkgs/by-name/go/gocron/package.nix b/pkgs/by-name/go/gocron/package.nix new file mode 100644 index 000000000000..bc9ea1e8dd31 --- /dev/null +++ b/pkgs/by-name/go/gocron/package.nix @@ -0,0 +1,109 @@ +{ + lib, + fetchFromGitHub, + nodejs, + fetchYarnDeps, + yarnConfigHook, + yarnBuildHook, + yarnInstallHook, + stdenv, + buildGoModule, + makeWrapper, + bash, + versionCheckHook, + nix-update-script, + nixosTests, +}: + +buildGoModule (finalAttrs: { + + pname = "gocron"; + version = "0.9.14"; + __structuredAttrs = true; + + src = fetchFromGitHub { + owner = "flohoss"; + repo = "gocron"; + tag = "v${finalAttrs.version}"; + hash = "sha256-LKjK5V+WrzTJlWPytafy8Ypva41RW4/12aSGaJj572I="; + }; + + gocron-web = stdenv.mkDerivation (finalAttrsWebassets: { + pname = "${finalAttrs.pname}-web"; + src = "${finalAttrs.src}/web"; + inherit (finalAttrs) version; + + yarnOfflineCache = fetchYarnDeps { + yarnLock = finalAttrsWebassets.src + "/yarn.lock"; + hash = "sha256-f0xnF9gd3c0KPrORPVkApyWPy+DazyzHeQu32wWybiw="; + }; + + nativeBuildInputs = [ + yarnConfigHook + yarnBuildHook + yarnInstallHook + nodejs + ]; + + preBuild = '' + yarn types + ''; + + postBuild = '' + mv dist/ $out + ''; + + }); + + vendorHash = "sha256-VbmS9Fh0pr/dUB+pZBqKbi4bu6Do/3TRr9uI3TmGsOM="; + + postPatch = '' + substituteInPlace handlers/web.go \ + --replace-fail "web/assets" "${finalAttrs.gocron-web}/assets" \ + --replace-fail "web/static" "${finalAttrs.gocron-web}/static" \ + --replace-fail "web/index.html" "${finalAttrs.gocron-web}/index.html" + substituteInPlace main.go \ + --replace-fail '"github.com/flohoss/gocron/internal/software"' "" \ + --replace-fail "software.Install()" "" + ''; + + ldflags = [ + "-s" + "-w" + "-X github.com/flohoss/gocron/internal/buildinfo.Version=${finalAttrs.version}" + ]; + + nativeBuildInputs = [ + makeWrapper + ]; + + postInstall = '' + wrapProgram $out/bin/gocron --prefix PATH : ${ + lib.makeBinPath [ + bash + ] + } + ''; + + nativeInstallCheckInputs = [ versionCheckHook ]; + doInstallCheck = true; + + passthru.updateScript = nix-update-script { + extraArgs = [ + "--subpackage" + "gocron-web" + ]; + }; + passthru.tests = nixosTests.gocron; + + meta = { + description = "Task scheduler built with Go and Vue.js."; + homepage = "https://github.com/flohoss/gocron"; + license = lib.licenses.mit; + mainProgram = "gocron"; + maintainers = with lib.maintainers; [ + juliusfreudenberger + ]; + }; + +})