push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
# Athens {#module-athens}
*Source:* {file}`modules/services/development/athens.nix`
*Upstream documentation:* <https://docs.gomods.io/>
[Athens](https://github.com/gomods/athens)
is a Go module datastore and proxy
The main goal of Athens is providing a Go proxy (`$GOPROXY`) in regions without access to `https://proxy.golang.org` or to
improve the speed of Go module downloads for CI/CD systems.
## Configuring {#module-services-development-athens-configuring}
A complete list of options for the Athens module may be found
[here](#opt-services.athens.enable).
## Basic usage for a caching proxy configuration {#opt-services-development-athens-caching-proxy}
A very basic configuration for Athens that acts as a caching and forwarding HTTP proxy is:
```nix
{
services.athens = {
enable = true;
};
}
```
If you want to prevent Athens from writing to disk, you can instead configure it to cache modules only in memory:
```nix
{
services.athens = {
enable = true;
storageType = "memory";
};
}
```
To use the local proxy in Go builds (outside of `nix`), you can set the proxy as environment variable:
```nix
{
environment.variables = {
GOPROXY = "http://localhost:3000";
};
}
```
To also use the local proxy for Go builds happening in `nix` (with `buildGoModule`), the nix daemon can be configured to pass the GOPROXY environment variable to the `goModules` fixed-output derivation.
This can either be done via the nix-daemon systemd unit:
```nix
{ systemd.services.nix-daemon.environment.GOPROXY = "http://localhost:3000"; }
```
or via the [impure-env experimental feature](https://nix.dev/manual/nix/2.24/command-ref/conf-file#conf-impure-env):
```nix
{
nix.settings.experimental-features = [ "configurable-impure-env" ];
nix.settings.impure-env = "GOPROXY=http://localhost:3000";
}
```

View File

@@ -0,0 +1,994 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.athens;
athensConfig = lib.flip lib.recursiveUpdate cfg.extraConfig {
GoBinary = "${cfg.goBinary}/bin/go";
GoEnv = cfg.goEnv;
GoBinaryEnvVars = lib.mapAttrsToList (k: v: "${k}=${v}") cfg.goBinaryEnvVars;
GoGetWorkers = cfg.goGetWorkers;
GoGetDir = cfg.goGetDir;
ProtocolWorkers = cfg.protocolWorkers;
LogLevel = cfg.logLevel;
CloudRuntime = cfg.cloudRuntime;
EnablePprof = cfg.enablePprof;
PprofPort = ":${toString cfg.pprofPort}";
FilterFile = cfg.filterFile;
RobotsFile = cfg.robotsFile;
Timeout = cfg.timeout;
StorageType = cfg.storageType;
TLSCertFile = cfg.tlsCertFile;
TLSKeyFile = cfg.tlsKeyFile;
Port = ":${toString cfg.port}";
UnixSocket = cfg.unixSocket;
GlobalEndpoint = cfg.globalEndpoint;
BasicAuthUser = cfg.basicAuthUser;
BasicAuthPass = cfg.basicAuthPass;
ForceSSL = cfg.forceSSL;
ValidatorHook = cfg.validatorHook;
PathPrefix = cfg.pathPrefix;
NETRCPath = cfg.netrcPath;
GithubToken = cfg.githubToken;
HGRCPath = cfg.hgrcPath;
TraceExporter = cfg.traceExporter;
StatsExporter = cfg.statsExporter;
SumDBs = cfg.sumDBs;
NoSumPatterns = cfg.noSumPatterns;
DownloadMode = cfg.downloadMode;
NetworkMode = cfg.networkMode;
DownloadURL = cfg.downloadURL;
SingleFlightType = cfg.singleFlightType;
IndexType = cfg.indexType;
ShutdownTimeout = cfg.shutdownTimeout;
SingleFlight = {
Etcd = {
Endpoints = builtins.concatStringsSep "," cfg.singleFlight.etcd.endpoints;
};
Redis = {
Endpoint = cfg.singleFlight.redis.endpoint;
Password = cfg.singleFlight.redis.password;
LockConfig = {
TTL = cfg.singleFlight.redis.lockConfig.ttl;
Timeout = cfg.singleFlight.redis.lockConfig.timeout;
MaxRetries = cfg.singleFlight.redis.lockConfig.maxRetries;
};
};
RedisSentinel = {
Endpoints = cfg.singleFlight.redisSentinel.endpoints;
MasterName = cfg.singleFlight.redisSentinel.masterName;
SentinelPassword = cfg.singleFlight.redisSentinel.sentinelPassword;
LockConfig = {
TTL = cfg.singleFlight.redisSentinel.lockConfig.ttl;
Timeout = cfg.singleFlight.redisSentinel.lockConfig.timeout;
MaxRetries = cfg.singleFlight.redisSentinel.lockConfig.maxRetries;
};
};
};
Storage = {
CDN = {
Endpoint = cfg.storage.cdn.endpoint;
};
Disk = {
RootPath = cfg.storage.disk.rootPath;
};
GCP = {
ProjectID = cfg.storage.gcp.projectID;
Bucket = cfg.storage.gcp.bucket;
JSONKey = cfg.storage.gcp.jsonKey;
};
Minio = {
Endpoint = cfg.storage.minio.endpoint;
Key = cfg.storage.minio.key;
Secret = cfg.storage.minio.secret;
EnableSSL = cfg.storage.minio.enableSSL;
Bucket = cfg.storage.minio.bucket;
region = cfg.storage.minio.region;
};
Mongo = {
URL = cfg.storage.mongo.url;
DefaultDBName = cfg.storage.mongo.defaultDBName;
CertPath = cfg.storage.mongo.certPath;
Insecure = cfg.storage.mongo.insecure;
};
S3 = {
Region = cfg.storage.s3.region;
Key = cfg.storage.s3.key;
Secret = cfg.storage.s3.secret;
Token = cfg.storage.s3.token;
Bucket = cfg.storage.s3.bucket;
ForcePathStyle = cfg.storage.s3.forcePathStyle;
UseDefaultConfiguration = cfg.storage.s3.useDefaultConfiguration;
CredentialsEndpoint = cfg.storage.s3.credentialsEndpoint;
AwsContainerCredentialsRelativeURI = cfg.storage.s3.awsContainerCredentialsRelativeURI;
Endpoint = cfg.storage.s3.endpoint;
};
AzureBlob = {
AccountName = cfg.storage.azureblob.accountName;
AccountKey = cfg.storage.azureblob.accountKey;
ContainerName = cfg.storage.azureblob.containerName;
};
External = {
URL = cfg.storage.external.url;
};
};
Index = {
MySQL = {
Protocol = cfg.index.mysql.protocol;
Host = cfg.index.mysql.host;
Port = cfg.index.mysql.port;
User = cfg.index.mysql.user;
Password = cfg.index.mysql.password;
Database = cfg.index.mysql.database;
Params = {
parseTime = cfg.index.mysql.params.parseTime;
timeout = cfg.index.mysql.params.timeout;
};
};
Postgres = {
Host = cfg.index.postgres.host;
Port = cfg.index.postgres.port;
User = cfg.index.postgres.user;
Password = cfg.index.postgres.password;
Database = cfg.index.postgres.database;
Params = {
connect_timeout = cfg.index.postgres.params.connect_timeout;
sslmode = cfg.index.postgres.params.sslmode;
};
};
};
};
configFile = lib.pipe athensConfig [
(lib.filterAttrsRecursive (_k: v: v != null))
((pkgs.formats.toml { }).generate "config.toml")
];
in
{
meta = {
maintainers = pkgs.athens.meta.maintainers;
doc = ./athens.md;
};
options.services.athens = {
enable = lib.mkEnableOption "Go module datastore and proxy";
package = lib.mkOption {
default = pkgs.athens;
defaultText = lib.literalExpression "pkgs.athens";
example = "pkgs.athens";
description = "Which athens derivation to use";
type = lib.types.package;
};
goBinary = lib.mkOption {
type = lib.types.package;
default = pkgs.go;
defaultText = lib.literalExpression "pkgs.go";
example = "pkgs.go_1_23";
description = ''
The Go package used by Athens at runtime.
Athens primarily runs two Go commands:
1. `go mod download -json <module>@<version>`
2. `go list -m -json <module>@latest`
'';
};
goEnv = lib.mkOption {
type = lib.types.enum [
"development"
"production"
];
description = "Specifies the type of environment to run. One of 'development' or 'production'.";
default = "development";
example = "production";
};
goBinaryEnvVars = lib.mkOption {
type = lib.types.attrs;
description = "Environment variables to pass to the Go binary.";
example = ''
{ "GOPROXY" = "direct", "GODEBUG" = "true" }
'';
default = { };
};
goGetWorkers = lib.mkOption {
type = lib.types.int;
description = "Number of workers concurrently downloading modules.";
default = 10;
example = 32;
};
goGetDir = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = ''
Temporary directory that Athens will use to
fetch modules from VCS prior to persisting
them to a storage backend.
If the value is empty, Athens will use the
default OS temp directory.
'';
default = null;
example = "/tmp/athens";
};
protocolWorkers = lib.mkOption {
type = lib.types.int;
description = "Number of workers concurrently serving protocol paths.";
default = 30;
};
logLevel = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
"panic"
"fatal"
"error"
"warning"
"info"
"debug"
"trace"
]
);
description = ''
Log level for Athens.
Supports all logrus log levels (https://github.com/Sirupsen/logrus#level-logging)".
'';
default = "warning";
example = "debug";
};
cloudRuntime = lib.mkOption {
type = lib.types.enum [
"GCP"
"none"
];
description = ''
Specifies the Cloud Provider on which the Proxy/registry is running.
'';
default = "none";
example = "GCP";
};
enablePprof = lib.mkOption {
type = lib.types.bool;
description = "Enable pprof endpoints.";
default = false;
};
pprofPort = lib.mkOption {
type = lib.types.port;
description = "Port number for pprof endpoints.";
default = 3301;
example = 443;
};
filterFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = ''Filename for the include exclude filter.'';
default = null;
example = lib.literalExpression ''
pkgs.writeText "filterFile" '''
- github.com/azure
+ github.com/azure/azure-sdk-for-go
D golang.org/x/tools
'''
'';
};
robotsFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = ''Provides /robots.txt for net crawlers.'';
default = null;
example = lib.literalExpression ''pkgs.writeText "robots.txt" "# my custom robots.txt ..."'';
};
timeout = lib.mkOption {
type = lib.types.int;
description = "Timeout for external network calls in seconds.";
default = 300;
example = 3;
};
storageType = lib.mkOption {
type = lib.types.enum [
"memory"
"disk"
"mongo"
"gcp"
"minio"
"s3"
"azureblob"
"external"
];
description = "Specifies the type of storage backend to use.";
default = "disk";
};
tlsCertFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = "Path to the TLS certificate file.";
default = null;
example = "/etc/ssl/certs/athens.crt";
};
tlsKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = "Path to the TLS key file.";
default = null;
example = "/etc/ssl/certs/athens.key";
};
port = lib.mkOption {
type = lib.types.port;
default = 3000;
description = ''
Port number Athens listens on.
'';
example = 443;
};
unixSocket = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = ''
Path to the unix socket file.
If set, Athens will listen on the unix socket instead of TCP socket.
'';
default = null;
example = "/run/athens.sock";
};
globalEndpoint = lib.mkOption {
type = lib.types.str;
description = ''
Endpoint for a package registry in case of a proxy cache miss.
'';
default = "";
example = "http://upstream-athens.example.com:3000";
};
basicAuthUser = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Username for basic auth.
'';
default = null;
example = "user";
};
basicAuthPass = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Password for basic auth. Warning: this is stored in plain text in the config file.
'';
default = null;
example = "swordfish";
};
forceSSL = lib.mkOption {
type = lib.types.bool;
description = ''
Force SSL redirects for incoming requests.
'';
default = false;
};
validatorHook = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Endpoint to validate modules against.
Not used if empty.
'';
default = null;
example = "https://validation.example.com";
};
pathPrefix = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Sets basepath for all routes.
'';
default = null;
example = "/athens";
};
netrcPath = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = ''
Path to the .netrc file.
'';
default = null;
example = "/home/user/.netrc";
};
githubToken = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Creates .netrc file with the given token to be used for GitHub.
Warning: this is stored in plain text in the config file.
'';
default = null;
example = "ghp_1234567890";
};
hgrcPath = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = ''
Path to the .hgrc file.
'';
default = null;
example = "/home/user/.hgrc";
};
traceExporter = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
"jaeger"
"datadog"
]
);
description = ''
Trace exporter to use.
'';
default = null;
};
traceExporterURL = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
URL endpoint that traces will be sent to.
'';
default = null;
example = "http://localhost:14268";
};
statsExporter = lib.mkOption {
type = lib.types.nullOr (lib.types.enum [ "prometheus" ]);
description = "Stats exporter to use.";
default = null;
};
sumDBs = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = ''
List of fully qualified URLs that Athens will proxy
that the go command can use a checksum verifier.
'';
default = [ "https://sum.golang.org" ];
};
noSumPatterns = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = ''
List of patterns that Athens sum db proxy will return a 403 for.
'';
default = [ ];
example = [ "github.com/mycompany/*" ];
};
downloadMode = lib.mkOption {
type = lib.types.oneOf [
(lib.types.enum [
"sync"
"async"
"redirect"
"async_redirect"
"none"
])
(lib.types.strMatching "^file:.*$|^custom:.*$")
];
description = ''
Defines how Athens behaves when a module@version
is not found in storage. There are 7 options:
1. "sync": download the module synchronously and
return the results to the client.
2. "async": return 404, but asynchronously store the module
in the storage backend.
3. "redirect": return a 301 redirect status to the client
with the base URL as the DownloadRedirectURL from below.
4. "async_redirect": same as option number 3 but it will
asynchronously store the module to the backend.
5. "none": return 404 if a module is not found and do nothing.
6. "file:<path>": will point to an HCL file that specifies
any of the 5 options above based on different import paths.
7. "custom:<base64-encoded-hcl>" is the same as option 6
but the file is fully encoded in the option. This is
useful for using an environment variable in serverless
deployments.
'';
default = "async_redirect";
};
networkMode = lib.mkOption {
type = lib.types.enum [
"strict"
"offline"
"fallback"
];
description = ''
Configures how Athens will return the results
of the /list endpoint as it can be assembled from both its own
storage and the upstream VCS.
Note, that for better error messaging, this would also affect how other
endpoints behave.
Modes:
1. strict: merge VCS versions with storage versions, but fail if either of them fails.
2. offline: only get storage versions, never reach out to VCS.
3. fallback: only return storage versions, if VCS fails. Note this means that you may
see inconsistent results since fallback mode does a best effort of giving you what's
available at the time of requesting versions.
'';
default = "strict";
};
downloadURL = lib.mkOption {
type = lib.types.str;
description = "URL used if DownloadMode is set to redirect.";
default = "https://proxy.golang.org";
};
singleFlightType = lib.mkOption {
type = lib.types.enum [
"memory"
"etcd"
"redis"
"redis-sentinel"
"gcp"
"azureblob"
];
description = ''
Determines what mechanism Athens uses to manage concurrency flowing into the Athens backend.
'';
default = "memory";
};
indexType = lib.mkOption {
type = lib.types.enum [
"none"
"memory"
"mysql"
"postgres"
];
description = ''
Type of index backend Athens will use.
'';
default = "none";
};
shutdownTimeout = lib.mkOption {
type = lib.types.int;
description = ''
Number of seconds to wait for the server to shutdown gracefully.
'';
default = 60;
example = 1;
};
singleFlight = {
etcd = {
endpoints = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "URLs that determine all distributed etcd servers.";
default = [ ];
example = [ "localhost:2379" ];
};
};
redis = {
endpoint = lib.mkOption {
type = lib.types.str;
description = "URL of the redis server.";
default = "";
example = "localhost:6379";
};
password = lib.mkOption {
type = lib.types.str;
description = "Password for the redis server. Warning: this is stored in plain text in the config file.";
default = "";
example = "swordfish";
};
lockConfig = {
ttl = lib.mkOption {
type = lib.types.int;
description = "TTL for the lock in seconds.";
default = 900;
example = 1;
};
timeout = lib.mkOption {
type = lib.types.int;
description = "Timeout for the lock in seconds.";
default = 15;
example = 1;
};
maxRetries = lib.mkOption {
type = lib.types.int;
description = "Maximum number of retries for the lock.";
default = 10;
example = 1;
};
};
};
redisSentinel = {
endpoints = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "URLs that determine all distributed redis servers.";
default = [ ];
example = [ "localhost:26379" ];
};
masterName = lib.mkOption {
type = lib.types.str;
description = "Name of the sentinel master server.";
default = "";
example = "redis-1";
};
sentinelPassword = lib.mkOption {
type = lib.types.str;
description = "Password for the sentinel server. Warning: this is stored in plain text in the config file.";
default = "";
example = "swordfish";
};
lockConfig = {
ttl = lib.mkOption {
type = lib.types.int;
description = "TTL for the lock in seconds.";
default = 900;
example = 1;
};
timeout = lib.mkOption {
type = lib.types.int;
description = "Timeout for the lock in seconds.";
default = 15;
example = 1;
};
maxRetries = lib.mkOption {
type = lib.types.int;
description = "Maximum number of retries for the lock.";
default = 10;
example = 1;
};
};
};
};
storage = {
cdn = {
endpoint = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "hostname of the CDN server.";
example = "cdn.example.com";
default = null;
};
};
disk = {
rootPath = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = "Athens disk root folder.";
default = "/var/lib/athens";
};
};
gcp = {
projectID = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "GCP project ID.";
example = "my-project";
default = null;
};
bucket = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "GCP backend storage bucket.";
example = "my-bucket";
default = null;
};
jsonKey = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Base64 encoded GCP service account key. Warning: this is stored in plain text in the config file.";
default = null;
};
};
minio = {
endpoint = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Endpoint of the minio storage backend.";
example = "minio.example.com:9001";
default = null;
};
key = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Access key id for the minio storage backend.";
example = "minio";
default = null;
};
secret = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Secret key for the minio storage backend. Warning: this is stored in plain text in the config file.";
example = "minio123";
default = null;
};
enableSSL = lib.mkOption {
type = lib.types.bool;
description = "Enable SSL for the minio storage backend.";
default = false;
};
bucket = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Bucket name for the minio storage backend.";
example = "gomods";
default = null;
};
region = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Region for the minio storage backend.";
example = "us-east-1";
default = null;
};
};
mongo = {
url = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "URL of the mongo database.";
example = "mongodb://localhost:27017";
default = null;
};
defaultDBName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Name of the mongo database.";
example = "athens";
default = null;
};
certPath = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = "Path to the certificate file for the mongo database.";
example = "/etc/ssl/mongo.pem";
default = null;
};
insecure = lib.mkOption {
type = lib.types.bool;
description = "Allow insecure connections to the mongo database.";
default = false;
};
};
s3 = {
region = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Region of the S3 storage backend.";
example = "eu-west-3";
default = null;
};
key = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Access key id for the S3 storage backend.";
example = "minio";
default = null;
};
secret = lib.mkOption {
type = lib.types.str;
description = "Secret key for the S3 storage backend. Warning: this is stored in plain text in the config file.";
default = "";
};
token = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Token for the S3 storage backend. Warning: this is stored in plain text in the config file.";
default = null;
};
bucket = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Bucket name for the S3 storage backend.";
example = "gomods";
default = null;
};
forcePathStyle = lib.mkOption {
type = lib.types.bool;
description = "Force path style for the S3 storage backend.";
default = false;
};
useDefaultConfiguration = lib.mkOption {
type = lib.types.bool;
description = "Use default configuration for the S3 storage backend.";
default = false;
};
credentialsEndpoint = lib.mkOption {
type = lib.types.str;
description = "Credentials endpoint for the S3 storage backend.";
default = "";
};
awsContainerCredentialsRelativeURI = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Container relative url (used by fargate).";
default = null;
};
endpoint = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Endpoint for the S3 storage backend.";
default = null;
};
};
azureblob = {
accountName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Account name for the Azure Blob storage backend.";
default = null;
};
accountKey = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Account key for the Azure Blob storage backend. Warning: this is stored in plain text in the config file.";
default = null;
};
containerName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Container name for the Azure Blob storage backend.";
default = null;
};
};
external = {
url = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "URL of the backend storage layer.";
example = "https://athens.example.com";
default = null;
};
};
};
index = {
mysql = {
protocol = lib.mkOption {
type = lib.types.str;
description = "Protocol for the MySQL database.";
default = "tcp";
};
host = lib.mkOption {
type = lib.types.str;
description = "Host for the MySQL database.";
default = "localhost";
};
port = lib.mkOption {
type = lib.types.port;
description = "Port for the MySQL database.";
default = 3306;
};
user = lib.mkOption {
type = lib.types.str;
description = "User for the MySQL database.";
default = "root";
};
password = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Password for the MySQL database. Warning: this is stored in plain text in the config file.";
default = null;
};
database = lib.mkOption {
type = lib.types.str;
description = "Database name for the MySQL database.";
default = "athens";
};
params = {
parseTime = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Parse time for the MySQL database.";
default = "true";
};
timeout = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Timeout for the MySQL database.";
default = "30s";
};
};
};
postgres = {
host = lib.mkOption {
type = lib.types.str;
description = "Host for the Postgres database.";
default = "localhost";
};
port = lib.mkOption {
type = lib.types.port;
description = "Port for the Postgres database.";
default = 5432;
};
user = lib.mkOption {
type = lib.types.str;
description = "User for the Postgres database.";
default = "postgres";
};
password = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Password for the Postgres database. Warning: this is stored in plain text in the config file.";
default = null;
};
database = lib.mkOption {
type = lib.types.str;
description = "Database name for the Postgres database.";
default = "athens";
};
params = {
connect_timeout = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Connect timeout for the Postgres database.";
default = "30s";
};
sslmode = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "SSL mode for the Postgres database.";
default = "disable";
};
};
};
};
extraConfig = lib.mkOption {
type = lib.types.attrs;
description = ''
Extra configuration options for the athens config file.
'';
default = { };
};
};
config = lib.mkIf cfg.enable {
systemd.services.athens = {
description = "Athens Go module proxy";
documentation = [ "https://docs.gomods.io" ];
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Restart = "on-abnormal";
Nice = 5;
ExecStart = ''${cfg.package}/bin/athens -config_file=${configFile}'';
KillMode = "mixed";
KillSignal = "SIGINT";
TimeoutStopSec = cfg.shutdownTimeout;
LimitNOFILE = 1048576;
LimitNPROC = 512;
DynamicUser = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectHome = "read-only";
ProtectSystem = "full";
ReadWritePaths = lib.mkIf (
cfg.storage.disk.rootPath != null && (!lib.hasPrefix "/var/lib/" cfg.storage.disk.rootPath)
) [ cfg.storage.disk.rootPath ];
StateDirectory = lib.mkIf (lib.hasPrefix "/var/lib/" cfg.storage.disk.rootPath) [
(lib.removePrefix "/var/lib/" cfg.storage.disk.rootPath)
];
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
NoNewPrivileges = true;
};
};
networking.firewall = {
allowedTCPPorts =
lib.optionals (cfg.unixSocket == null) [ cfg.port ]
++ lib.optionals cfg.enablePprof [ cfg.pprofPort ];
};
};
}

View File

@@ -0,0 +1,38 @@
# Blackfire profiler {#module-services-blackfire}
*Source:* {file}`modules/services/development/blackfire.nix`
*Upstream documentation:* <https://blackfire.io/docs/introduction>
[Blackfire](https://blackfire.io) is a proprietary tool for profiling applications. There are several languages supported by the product but currently only PHP support is packaged in Nixpkgs. The back-end consists of a module that is loaded into the language runtime (called *probe*) and a service (*agent*) that the probe connects to and that sends the profiles to the server.
To use it, you will need to enable the agent and the probe on your server. The exact method will depend on the way you use PHP but here is an example of NixOS configuration for PHP-FPM:
```nix
let
php = pkgs.php.withExtensions ({ enabled, all }: enabled ++ (with all; [ blackfire ]));
in
{
# Enable the probe extension for PHP-FPM.
services.phpfpm = {
phpPackage = php;
};
# Enable and configure the agent.
services.blackfire-agent = {
enable = true;
settings = {
# You will need to get credentials at https://blackfire.io/my/settings/credentials
# You can also use other options described in https://blackfire.io/docs/up-and-running/configuration/agent
server-id = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
server-token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
};
};
# Make the agent run on start-up.
# (WantedBy= from the upstream unit not respected: https://github.com/NixOS/nixpkgs/issues/81138)
# Alternately, you can start it manually with `systemctl start blackfire-agent`.
systemd.services.blackfire-agent.wantedBy = [ "phpfpm-foo.service" ];
}
```
On your developer machine, you will also want to install [the client](https://blackfire.io/docs/up-and-running/installation#install-a-profiling-client) (see `blackfire` package) or the browser extension to actually trigger the profiling.

View File

@@ -0,0 +1,66 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.blackfire-agent;
agentConfigFile = lib.generators.toINI { } {
blackfire = cfg.settings;
};
agentSock = "blackfire/agent.sock";
in
{
meta = {
maintainers = pkgs.blackfire.meta.maintainers;
doc = ./blackfire.md;
};
options = {
services.blackfire-agent = {
enable = lib.mkEnableOption "Blackfire profiler agent";
settings = lib.mkOption {
description = ''
See <https://blackfire.io/docs/up-and-running/configuration/agent>
'';
type = lib.types.submodule {
freeformType = with lib.types; attrsOf str;
options = {
server-id = lib.mkOption {
type = lib.types.str;
description = ''
Sets the server id used to authenticate with Blackfire
You can find your personal server-id at <https://blackfire.io/my/settings/credentials>
'';
};
server-token = lib.mkOption {
type = lib.types.str;
description = ''
Sets the server token used to authenticate with Blackfire
You can find your personal server-token at <https://blackfire.io/my/settings/credentials>
'';
};
};
};
};
};
};
config = lib.mkIf cfg.enable {
environment.etc."blackfire/agent".text = agentConfigFile;
services.blackfire-agent.settings.socket = "unix:///run/${agentSock}";
systemd.packages = [
pkgs.blackfire
];
};
}

View File

@@ -0,0 +1,58 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.bloop;
in
{
options.services.bloop = {
extraOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"-J-Xmx2G"
"-J-XX:MaxInlineLevel=20"
"-J-XX:+UseParallelGC"
];
description = ''
Specifies additional command line argument to pass to bloop
java process.
'';
};
install = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to install a user service for the Bloop server.
The service must be manually started for each user with
"systemctl --user start bloop".
'';
};
};
config = lib.mkIf (cfg.install) {
systemd.user.services.bloop = {
description = "Bloop Scala build server";
environment = {
PATH = lib.mkForce "${lib.makeBinPath [ config.programs.java.package ]}";
};
serviceConfig = {
Type = "forking";
ExecStart = "${pkgs.bloop}/bin/bloop start";
ExecStop = "${pkgs.bloop}/bin/bloop exit";
Restart = "always";
};
};
environment.systemPackages = [ pkgs.bloop ];
};
}

View File

@@ -0,0 +1,114 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.corteza;
in
{
options.services.corteza = {
enable = lib.mkEnableOption "Corteza, a low-code platform";
package = lib.mkPackageOption pkgs "corteza" { };
address = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
description = ''
IP for the HTTP server.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 80;
description = ''
Port for the HTTP server.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = "Whether to open ports in the firewall.";
};
user = lib.mkOption {
type = lib.types.str;
default = "corteza";
description = "The user to run Corteza under.";
};
group = lib.mkOption {
type = lib.types.str;
default = "corteza";
description = "The group to run Corteza under.";
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = lib.types.attrsOf lib.types.str;
options = {
HTTP_WEBAPP_ENABLED = lib.mkEnableOption "webapps" // {
default = true;
apply = toString;
};
};
};
default = { };
description = ''
Configuration for Corteza, will be passed as environment variables.
See <https://docs.cortezaproject.org/corteza-docs/2024.9/devops-guide/references/configuration/server.html>.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !cfg.settings ? HTTP_ADDR;
message = "Use `services.corteza.address` and `services.corteza.port` instead.";
}
];
warnings = lib.optional (!cfg.settings ? DB_DSN) ''
A database connection string is not set.
Corteza will create a temporary SQLite database in memory, but it will not persist data.
For production use, set `services.corteza.settings.DB_DSN`.
'';
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
systemd.services.corteza = {
description = "Corteza";
documentation = [ "https://docs.cortezaproject.org/" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
HTTP_WEBAPP_BASE_DIR = "./webapp";
HTTP_ADDR = "${cfg.address}:${toString cfg.port}";
}
// cfg.settings;
path = [ pkgs.dart-sass ];
serviceConfig = {
WorkingDirectory = cfg.package;
User = cfg.user;
Group = cfg.group;
ExecStart = "${lib.getExe cfg.package} serve-api";
};
};
users = {
groups.${cfg.group} = { };
users.${cfg.user} = {
inherit (cfg) group;
isSystemUser = true;
};
};
};
meta.maintainers = with lib.maintainers; [
prince213
];
}

View File

@@ -0,0 +1,161 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.distccd;
in
{
options = {
services.distccd = {
enable = lib.mkEnableOption "distccd, a distributed C/C++ compiler";
allowedClients = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "127.0.0.1" ];
example = [
"127.0.0.1"
"192.168.0.0/24"
"10.0.0.0/24"
];
description = ''
Client IPs which are allowed to connect to distccd in CIDR notation.
Anyone who can connect to the distccd server can run arbitrary
commands on that system as the distcc user, therefore you should use
this judiciously.
'';
};
jobTimeout = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
description = ''
Maximum duration, in seconds, of a single compilation request.
'';
};
logLevel = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
"critical"
"error"
"warning"
"notice"
"info"
"debug"
]
);
default = "warning";
description = ''
Set the minimum severity of error that will be included in the log
file. Useful if you only want to see error messages rather than an
entry for each connection.
'';
};
maxJobs = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
description = ''
Maximum number of tasks distccd should execute at lib.any time.
'';
};
nice = lib.mkOption {
type = lib.types.nullOr (lib.types.ints.between (-20) 19);
default = null;
description = ''
Niceness of the compilation tasks.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Opens the specified TCP port for distcc.
'';
};
package = lib.mkPackageOption pkgs "distcc" { };
port = lib.mkOption {
type = lib.types.port;
default = 3632;
description = ''
The TCP port which distccd will listen on.
'';
};
stats = {
enable = lib.mkEnableOption "statistics reporting via HTTP server";
port = lib.mkOption {
type = lib.types.port;
default = 3633;
description = ''
The TCP port which the distccd statistics HTTP server will listen
on.
'';
};
};
zeroconf = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to register via mDNS/DNS-SD
'';
};
};
};
config = lib.mkIf cfg.enable {
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ] ++ lib.optionals cfg.stats.enable [ cfg.stats.port ];
};
systemd.services.distccd = {
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
description = "Distributed C, C++ and Objective-C compiler";
documentation = [ "man:distccd(1)" ];
serviceConfig = {
User = "distcc";
Group = "distcc";
# FIXME: I'd love to get rid of `--enable-tcp-insecure` here, but I'm
# not sure how I'm supposed to get distccd to "accept" running a binary
# (the compiler) that's outside of /usr/lib.
ExecStart = pkgs.writeShellScript "start-distccd" ''
export PATH="${pkgs.distccMasquerade}/bin"
${cfg.package}/bin/distccd \
--no-detach \
--daemon \
--enable-tcp-insecure \
--port ${toString cfg.port} \
${lib.optionalString (cfg.jobTimeout != null) "--job-lifetime ${toString cfg.jobTimeout}"} \
${lib.optionalString (cfg.logLevel != null) "--log-level ${cfg.logLevel}"} \
${lib.optionalString (cfg.maxJobs != null) "--jobs ${toString cfg.maxJobs}"} \
${lib.optionalString (cfg.nice != null) "--nice ${toString cfg.nice}"} \
${lib.optionalString cfg.stats.enable "--stats"} \
${lib.optionalString cfg.stats.enable "--stats-port ${toString cfg.stats.port}"} \
${lib.optionalString cfg.zeroconf "--zeroconf"} \
${lib.concatMapStrings (c: "--allow ${c} ") cfg.allowedClients}
'';
};
};
users = {
groups.distcc.gid = config.ids.gids.distcc;
users.distcc = {
description = "distccd user";
group = "distcc";
uid = config.ids.uids.distcc;
};
};
};
}

View File

@@ -0,0 +1,113 @@
{
lib,
pkgs,
config,
...
}:
let
settingsFormat = pkgs.formats.yaml { };
# gemstash uses a yaml config where the keys are ruby symbols,
# which means they start with ':'. This would be annoying to use
# on the nix side, so we rewrite plain names instead.
prefixColon =
s:
lib.listToAttrs (
map (attrName: {
name = ":${attrName}";
value = if lib.isAttrs s.${attrName} then prefixColon s."${attrName}" else s."${attrName}";
}) (lib.attrNames s)
);
# parse the port number out of the tcp://ip:port bind setting string
parseBindPort = bind: lib.strings.toInt (lib.last (lib.strings.splitString ":" bind));
cfg = config.services.gemstash;
in
{
options.services.gemstash = {
enable = lib.mkEnableOption "gemstash, a cache for rubygems.org and a private gem server";
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to open the firewall for the port in {option}`services.gemstash.bind`.
'';
};
settings = lib.mkOption {
default = { };
description = ''
Configuration for Gemstash. The details can be found at in
[gemstash documentation](https://github.com/rubygems/gemstash/blob/master/man/gemstash-configuration.5.md).
Each key set here is automatically prefixed with ":" to match the gemstash expectations.
'';
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
base_path = lib.mkOption {
type = lib.types.path;
default = "/var/lib/gemstash";
description = "Path to store the gem files and the sqlite database. If left unchanged, the directory will be created.";
};
bind = lib.mkOption {
type = lib.types.str;
default = "tcp://0.0.0.0:9292";
description = "Host and port combination for the server to listen on.";
};
db_adapter = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
"sqlite3"
"postgres"
"mysql"
"mysql2"
]
);
default = null;
description = "Which database type to use. For choices other than sqlite3, the dbUrl has to be specified as well.";
};
db_url = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "The database to connect to when using postgres, mysql, or mysql2.";
};
};
};
};
};
config = lib.mkIf cfg.enable {
users = {
users.gemstash = {
group = "gemstash";
isSystemUser = true;
};
groups.gemstash = { };
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
(parseBindPort cfg.settings.bind)
];
systemd.services.gemstash = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = lib.mkMerge [
{
ExecStart = "${pkgs.gemstash}/bin/gemstash start --no-daemonize --config-file ${settingsFormat.generate "gemstash.yaml" (prefixColon cfg.settings)}";
NoNewPrivileges = true;
User = "gemstash";
Group = "gemstash";
PrivateTmp = true;
RestrictSUIDSGID = true;
LockPersonality = true;
}
(lib.mkIf (cfg.settings.base_path == "/var/lib/gemstash") {
StateDirectory = "gemstash";
})
];
};
};
}

View File

@@ -0,0 +1,97 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.hoogle;
hoogleEnv = pkgs.buildEnv {
name = "hoogle";
paths = [ (cfg.haskellPackages.ghcWithHoogle cfg.packages) ];
};
in
{
options.services.hoogle = {
enable = lib.mkEnableOption "Haskell documentation server";
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = ''
Port number Hoogle will be listening to.
'';
};
packages = lib.mkOption {
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = hp: [ ];
defaultText = lib.literalExpression "hp: []";
example = lib.literalExpression "hp: with hp; [ text lens ]";
description = ''
The Haskell packages to generate documentation for.
The option value is a function that takes the package set specified in
the {var}`haskellPackages` option as its sole parameter and
returns a list of packages.
'';
};
haskellPackages = lib.mkOption {
description = "Which haskell package set to use.";
type = lib.types.attrs;
default = pkgs.haskellPackages;
defaultText = lib.literalExpression "pkgs.haskellPackages";
};
home = lib.mkOption {
type = lib.types.str;
description = "Url for hoogle logo";
default = "https://hoogle.haskell.org";
};
host = lib.mkOption {
type = lib.types.str;
description = "Set the host to bind on.";
default = "127.0.0.1";
};
extraOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "--no-security-headers" ];
description = ''
Additional command-line arguments to pass to
{command}`hoogle server`
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.hoogle = {
description = "Haskell documentation server";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Restart = "always";
ExecStart = ''
${hoogleEnv}/bin/hoogle server --local --port ${toString cfg.port} --home ${cfg.home} --host ${cfg.host} \
${lib.concatStringsSep " " cfg.extraOptions}
'';
DynamicUser = true;
ProtectHome = true;
RuntimeDirectory = "hoogle";
WorkingDirectory = "%t/hoogle";
};
};
};
}

View File

@@ -0,0 +1,238 @@
{
config,
lib,
pkgs,
options,
...
}:
let
cfg = config.services.jupyter;
package = pkgs.python3.withPackages (
ps:
[
cfg.package
]
++ cfg.extraPackages
);
kernels = (
pkgs.jupyter-kernel.create {
definitions = if cfg.kernels != null then cfg.kernels else pkgs.jupyter-kernel.default;
}
);
notebookConfig = pkgs.writeText "jupyter_server_config.py" ''
${cfg.notebookConfig}
c.ServerApp.password = "${cfg.password}"
'';
in
{
meta.maintainers = with lib.maintainers; [
aborsu
b-m-f
];
options.services.jupyter = {
enable = lib.mkEnableOption "Jupyter development server";
ip = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = ''
IP address Jupyter will be listening on.
'';
};
package = lib.mkPackageOption pkgs [
"python3"
"pkgs"
"jupyter"
] { };
extraPackages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
example = lib.literalExpression ''
[
pkgs.python3.pkgs.nbconvert
pkgs.python3.pkgs.playwright
]
'';
description = "Extra packages to be available in the jupyter runtime environment";
};
extraEnvironmentVariables = lib.mkOption {
description = "Extra environment variables to be set in the runtime context of jupyter notebook";
default = { };
example = lib.literalExpression ''
{
PLAYWRIGHT_BROWSERS_PATH = "''${pkgs.playwright-driver.browsers}";
PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = "true";
}
'';
inherit (options.environment.variables) type apply;
};
command = lib.mkOption {
type = lib.types.str;
default = "jupyter notebook";
example = "jupyter lab";
description = ''
Which command the service runs. Note that not all jupyter packages
have all commands, e.g. `jupyter lab` isn't present in the `notebook` package.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 8888;
description = ''
Port number Jupyter will be listening on.
'';
};
notebookDir = lib.mkOption {
type = lib.types.str;
default = "~/";
description = ''
Root directory for notebooks.
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "jupyter";
description = ''
Name of the user used to run the jupyter service.
For security reason, jupyter should really not be run as root.
If not set (jupyter), the service will create a jupyter user with appropriate settings.
'';
example = "aborsu";
};
group = lib.mkOption {
type = lib.types.str;
default = "jupyter";
description = ''
Name of the group used to run the jupyter service.
Use this if you want to create a group of users that are able to view the notebook directory's content.
'';
example = "users";
};
password = lib.mkOption {
type = lib.types.str;
description = ''
Password to use with notebook.
Can be generated following: <https://jupyter-server.readthedocs.io/en/stable/operators/public-server.html#preparing-a-hashed-password>
'';
example = "argon2:$argon2id$v=19$m=10240,t=10,p=8$48hF+vTUuy1LB83/GzNhUg$J1nx4jPWD7PwOJHs5OtDW8pjYK2s0c1R3rYGbSIKB54";
};
notebookConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Raw jupyter config.
Please use the password configuration option to set a password instead of passing it in here.
'';
};
kernels = lib.mkOption {
type = lib.types.nullOr (
lib.types.attrsOf (
lib.types.submodule (
import ./kernel-options.nix {
inherit lib pkgs;
}
)
)
);
default = null;
example = lib.literalExpression ''
{
python3 = let
env = (pkgs.python3.withPackages (pythonPackages: with pythonPackages; [
ipykernel
pandas
scikit-learn
]));
in {
displayName = "Python 3 for machine learning";
argv = [
"''${env.interpreter}"
"-m"
"ipykernel_launcher"
"-f"
"{connection_file}"
];
language = "python";
logo32 = "''${env.sitePackages}/ipykernel/resources/logo-32x32.png";
logo64 = "''${env.sitePackages}/ipykernel/resources/logo-64x64.png";
extraPaths = {
"cool.txt" = pkgs.writeText "cool" "cool content";
};
};
}
'';
description = ''
Declarative kernel config.
Kernels can be declared in any language that supports and has the required
dependencies to communicate with a jupyter server.
In python's case, it means that ipykernel package must always be included in
the list of packages of the targeted environment.
'';
};
};
config = lib.mkMerge [
(lib.mkIf cfg.enable {
systemd.services.jupyter = {
description = "Jupyter development server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
# TODO: Patch notebook so we can explicitly pass in a shell
path = [ pkgs.bash ]; # needed for sh in cell magic to work
environment = {
JUPYTER_PATH = toString kernels;
}
// cfg.extraEnvironmentVariables;
serviceConfig = {
Restart = "always";
ExecStart = ''
${package}/bin/${cfg.command} \
--no-browser \
--ip=${cfg.ip} \
--port=${toString cfg.port} --port-retries 0 \
--notebook-dir=${cfg.notebookDir} \
--JupyterApp.config_file=${notebookConfig}
'';
User = cfg.user;
Group = cfg.group;
WorkingDirectory = "~";
};
};
})
(lib.mkIf (cfg.enable && (cfg.group == "jupyter")) {
users.groups.jupyter = { };
})
(lib.mkIf (cfg.enable && (cfg.user == "jupyter")) {
users.extraUsers.jupyter = {
inherit (cfg) group;
home = "/var/lib/jupyter";
createHome = true;
isSystemUser = true;
useDefaultShell = true; # needed so that the user can start a terminal.
};
})
];
}

View File

@@ -0,0 +1,79 @@
# Options that can be used for creating a jupyter kernel.
{ lib, pkgs }:
{
freeformType = (pkgs.formats.json { }).type;
options = {
displayName = lib.mkOption {
type = lib.types.str;
default = "";
example = lib.literalExpression ''
"Python 3"
"Python 3 for Data Science"
'';
description = ''
Name that will be shown to the user.
'';
};
argv = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = [
"{customEnv.interpreter}"
"-m"
"ipykernel_launcher"
"-f"
"{connection_file}"
];
description = ''
Command and arguments to start the kernel.
'';
};
language = lib.mkOption {
type = lib.types.str;
example = "python";
description = ''
Language of the environment. Typically the name of the binary.
'';
};
env = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = {
OMP_NUM_THREADS = "1";
};
description = ''
Environment variables to set for the kernel.
'';
};
logo32 = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = lib.literalExpression ''"''${env.sitePackages}/ipykernel/resources/logo-32x32.png"'';
description = ''
Path to 32x32 logo png.
'';
};
logo64 = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = lib.literalExpression ''"''${env.sitePackages}/ipykernel/resources/logo-64x64.png"'';
description = ''
Path to 64x64 logo png.
'';
};
extraPaths = lib.mkOption {
type = lib.types.attrsOf lib.types.path;
default = { };
example = lib.literalExpression ''"{ examples = ''${env.sitePack}/IRkernel/kernelspec/kernel.js"; }'';
description = ''
Extra paths to link in kernel directory
'';
};
};
}

View File

@@ -0,0 +1,215 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.jupyterhub;
kernels = (
pkgs.jupyter-kernel.create {
definitions = if cfg.kernels != null then cfg.kernels else pkgs.jupyter-kernel.default;
}
);
jupyterhubConfig = pkgs.writeText "jupyterhub_config.py" ''
c.JupyterHub.bind_url = "http://${cfg.host}:${toString cfg.port}"
c.JupyterHub.authenticator_class = "${cfg.authentication}"
c.JupyterHub.spawner_class = "${cfg.spawner}"
c.SystemdSpawner.default_url = '/lab'
c.SystemdSpawner.cmd = "${cfg.jupyterlabEnv}/bin/jupyterhub-singleuser"
c.SystemdSpawner.environment = {
'JUPYTER_PATH': '${kernels}'
}
${cfg.extraConfig}
'';
in
{
meta.maintainers = with lib.maintainers; [ costrouc ];
options.services.jupyterhub = {
enable = lib.mkEnableOption "Jupyterhub development server";
authentication = lib.mkOption {
type = lib.types.str;
default = "jupyterhub.auth.PAMAuthenticator";
description = ''
Jupyterhub authentication to use
There are many authenticators available including: oauth, pam,
ldap, kerberos, etc.
'';
};
spawner = lib.mkOption {
type = lib.types.str;
default = "systemdspawner.SystemdSpawner";
description = ''
Jupyterhub spawner to use
There are many spawners available including: local process,
systemd, docker, kubernetes, yarn, batch, etc.
'';
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Extra contents appended to the jupyterhub configuration
Jupyterhub configuration is a normal python file using
Traitlets. <https://jupyterhub.readthedocs.io/en/stable/getting-started/config-basics.html>. The
base configuration of this module was designed to have sane
defaults for configuration but you can override anything since
this is a python file.
'';
example = ''
c.SystemdSpawner.mem_limit = '8G'
c.SystemdSpawner.cpu_limit = 2.0
'';
};
jupyterhubEnv = lib.mkOption {
type = lib.types.package;
default = pkgs.python3.withPackages (
p: with p; [
jupyterhub
jupyterhub-systemdspawner
]
);
defaultText = lib.literalExpression ''
pkgs.python3.withPackages (p: with p; [
jupyterhub
jupyterhub-systemdspawner
])
'';
description = ''
Python environment to run jupyterhub
Customizing will affect the packages available in the hub and
proxy. This will allow packages to be available for the
extraConfig that you may need. This will not normally need to
be changed.
'';
};
jupyterlabEnv = lib.mkOption {
type = lib.types.package;
default = pkgs.python3.withPackages (
p: with p; [
jupyterhub
jupyterlab
]
);
defaultText = lib.literalExpression ''
pkgs.python3.withPackages (p: with p; [
jupyterhub
jupyterlab
])
'';
description = ''
Python environment to run jupyterlab
Customizing will affect the packages available in the
jupyterlab server and the default kernel provided. This is the
way to customize the jupyterlab extensions and jupyter
notebook extensions. This will not normally need to
be changed.
'';
};
kernels = lib.mkOption {
type = lib.types.nullOr (
lib.types.attrsOf (
lib.types.submodule (
import ../jupyter/kernel-options.nix {
inherit lib pkgs;
}
)
)
);
default = null;
example = lib.literalExpression ''
{
python3 = let
env = (pkgs.python3.withPackages (pythonPackages: with pythonPackages; [
ipykernel
pandas
scikit-learn
]));
in {
displayName = "Python 3 for machine learning";
argv = [
"''${env.interpreter}"
"-m"
"ipykernel_launcher"
"-f"
"{connection_file}"
];
language = "python";
logo32 = "''${env}/''${env.sitePackages}/ipykernel/resources/logo-32x32.png";
logo64 = "''${env}/''${env.sitePackages}/ipykernel/resources/logo-64x64.png";
};
}
'';
description = ''
Declarative kernel config
Kernels can be declared in any language that supports and has
the required dependencies to communicate with a jupyter server.
In python's case, it means that ipykernel package must always be
included in the list of packages of the targeted environment.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 8000;
description = ''
Port number Jupyterhub will be listening on
'';
};
host = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
description = ''
Bind IP JupyterHub will be listening on
'';
};
stateDirectory = lib.mkOption {
type = lib.types.str;
default = "jupyterhub";
description = ''
Directory for jupyterhub state (token + database)
'';
};
};
config = lib.mkMerge [
(lib.mkIf cfg.enable {
systemd.services.jupyterhub = {
description = "Jupyterhub development server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Restart = "always";
ExecStart = "${cfg.jupyterhubEnv}/bin/jupyterhub --config ${jupyterhubConfig}";
User = "root";
StateDirectory = cfg.stateDirectory;
WorkingDirectory = "/var/lib/${cfg.stateDirectory}";
};
};
})
];
}

View File

@@ -0,0 +1,61 @@
# Livebook {#module-services-livebook}
[Livebook](https://livebook.dev/) is a web application for writing
interactive and collaborative code notebooks.
## Basic Usage {#module-services-livebook-basic-usage}
Enabling the `livebook` service creates a user
[`systemd`](https://www.freedesktop.org/wiki/Software/systemd/) unit
which runs the server.
```nix
{ ... }:
{
services.livebook = {
enableUserService = true;
environment = {
LIVEBOOK_PORT = 20123;
LIVEBOOK_PASSWORD = "mypassword";
};
# See note below about security
environmentFile = "/var/lib/livebook.env";
};
}
```
::: {.note}
The Livebook server has the ability to run any command as the user it
is running under, so securing access to it with a password is highly
recommended.
Putting the password in the Nix configuration like above is an easy way to get
started but it is not recommended in the real world because the resulting
environment variables can be read by unprivileged users. A better approach
would be to put the password in some secure user-readable location and set
`environmentFile = /home/user/secure/livebook.env`.
:::
The [Livebook
documentation](https://hexdocs.pm/livebook/readme.html#environment-variables)
lists all the applicable environment variables. It is recommended to at least
set `LIVEBOOK_PASSWORD` or `LIVEBOOK_TOKEN_ENABLED=false`.
### Extra dependencies {#module-services-livebook-extra-dependencies}
By default, the Livebook service is run with minimum dependencies, but
some features require additional packages. For example, the machine
learning Kinos require `gcc` and `gnumake`. To add these, use
`extraPackages`:
```nix
{
services.livebook.extraPackages = with pkgs; [
gcc
gnumake
];
}
```

View File

@@ -0,0 +1,126 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.livebook;
in
{
options.services.livebook = {
# Since livebook doesn't have a granular permission system (a user
# either has access to all the data or none at all), the decision
# was made to run this as a user service. If that changes in the
# future, this can be changed to a system service.
enableUserService = lib.mkEnableOption "a user service for Livebook";
package = lib.mkPackageOption pkgs "livebook" { };
environment = lib.mkOption {
type =
with lib.types;
attrsOf (
nullOr (oneOf [
bool
int
str
])
);
default = { };
description = ''
Environment variables to set.
Livebook is configured through the use of environment variables. The
available configuration options can be found in the [Livebook
documentation](https://hexdocs.pm/livebook/readme.html#environment-variables).
Note that all environment variables set through this configuration
parameter will be readable by anyone with access to the host
machine. Therefore, sensitive information like {env}`LIVEBOOK_PASSWORD`
or {env}`LIVEBOOK_COOKIE` should never be set using this configuration
option, but should instead use
[](#opt-services.livebook.environmentFile). See the documentation for
that option for more information.
Any environment variables specified in the
[](#opt-services.livebook.environmentFile) will supersede environment
variables specified in this option.
'';
example = lib.literalExpression ''
{
LIVEBOOK_PORT = 8080;
}
'';
};
environmentFile = lib.mkOption {
type = with lib.types; nullOr lib.types.path;
default = null;
description = ''
Additional environment file as defined in {manpage}`systemd.exec(5)`.
Secrets like {env}`LIVEBOOK_PASSWORD` (which is used to specify the
password needed to access the livebook site) or {env}`LIVEBOOK_COOKIE`
(which is used to specify the
[cookie](https://www.erlang.org/doc/reference_manual/distributed.html#security)
used to connect to the running Elixir system) may be passed to the
service without making them readable to everyone with access to
systemctl by using this configuration parameter.
Note that this file needs to be available on the host on which
`livebook` is running.
For security purposes, this file should contain at least
{env}`LIVEBOOK_PASSWORD` or {env}`LIVEBOOK_TOKEN_ENABLED=false`.
See the [Livebook
documentation](https://hexdocs.pm/livebook/readme.html#environment-variables)
and the [](#opt-services.livebook.environment) configuration parameter
for further options.
'';
example = "/var/lib/livebook.env";
};
extraPackages = lib.mkOption {
type = with lib.types; listOf package;
default = [ ];
description = ''
Extra packages to make available to the Livebook service.
'';
example = lib.literalExpression "with pkgs; [ gcc gnumake ]";
};
};
config = lib.mkIf cfg.enableUserService {
systemd.user.services.livebook = {
serviceConfig = {
Restart = "always";
EnvironmentFile = cfg.environmentFile;
ExecStart = "${cfg.package}/bin/livebook start";
KillMode = "mixed";
# Fix for the issue described here:
# https://github.com/livebook-dev/livebook/issues/2691
#
# Without this, the livebook service fails to start and gets
# stuck running a `cat /dev/urandom | tr | fold` pipeline.
IgnoreSIGPIPE = false;
};
environment = lib.mapAttrs (
name: value: if lib.isBool value then lib.boolToString value else toString value
) cfg.environment;
path = [ pkgs.bash ] ++ cfg.extraPackages;
wantedBy = [ "default.target" ];
};
};
meta = {
doc = ./livebook.md;
maintainers = with lib.maintainers; [
munksgaard
scvalex
];
};
}

View File

@@ -0,0 +1,61 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.lorri;
socketPath = "lorri/daemon.socket";
in
{
options = {
services.lorri = {
enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
Enables the daemon for `lorri`, a nix-shell replacement for project
development. The socket-activated daemon starts on the first request
issued by the `lorri` command.
'';
};
package = lib.mkPackageOption pkgs "lorri" { };
};
};
config = lib.mkIf cfg.enable {
systemd.user.sockets.lorri = {
description = "Socket for Lorri Daemon";
wantedBy = [ "sockets.target" ];
socketConfig = {
ListenStream = "%t/${socketPath}";
RuntimeDirectory = "lorri";
};
};
systemd.user.services.lorri = {
description = "Lorri Daemon";
requires = [ "lorri.socket" ];
after = [ "lorri.socket" ];
path = with pkgs; [
config.nix.package
git
gnutar
gzip
];
serviceConfig = {
ExecStart = "${cfg.package}/bin/lorri daemon";
PrivateTmp = true;
ProtectSystem = "full";
Restart = "on-failure";
};
};
environment.systemPackages = [
cfg.package
pkgs.direnv
];
};
}

View File

@@ -0,0 +1,106 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.services.nixseparatedebuginfod;
url = "127.0.0.1:${toString cfg.port}";
in
{
options = {
services.nixseparatedebuginfod = {
enable = lib.mkEnableOption "separatedebuginfod, a debuginfod server providing source and debuginfo for nix packages";
port = lib.mkOption {
description = "port to listen";
default = 1949;
type = lib.types.port;
};
nixPackage = lib.mkOption {
type = lib.types.package;
default = pkgs.nix;
defaultText = lib.literalExpression "pkgs.nix";
description = ''
The version of nix that nixseparatedebuginfod should use as client for the nix daemon. It is strongly advised to use nix version >= 2.18, otherwise some debug info may go missing.
'';
};
allowOldNix = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Do not fail evaluation when {option}`services.nixseparatedebuginfod.nixPackage` is older than nix 2.18.
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.allowOldNix || (lib.versionAtLeast cfg.nixPackage.version "2.18");
message = "nixseparatedebuginfod works better when `services.nixseparatedebuginfod.nixPackage` is set to nix >= 2.18 (instead of ${cfg.nixPackage.name}). Set `services.nixseparatedebuginfod.allowOldNix` to bypass.";
}
];
systemd.services.nixseparatedebuginfod = {
wantedBy = [ "multi-user.target" ];
wants = [ "nix-daemon.service" ];
after = [ "nix-daemon.service" ];
path = [ cfg.nixPackage ];
serviceConfig = {
ExecStart = [ "${pkgs.nixseparatedebuginfod}/bin/nixseparatedebuginfod -l ${url}" ];
Restart = "on-failure";
CacheDirectory = "nixseparatedebuginfod";
# nix does not like DynamicUsers in allowed-users
User = "nixseparatedebuginfod";
Group = "nixseparatedebuginfod";
# hardening
# Filesystem stuff
ProtectSystem = "strict"; # Prevent writing to most of /
ProtectHome = true; # Prevent accessing /home and /root
PrivateTmp = true; # Give an own directory under /tmp
PrivateDevices = true; # Deny access to most of /dev
ProtectKernelTunables = true; # Protect some parts of /sys
ProtectControlGroups = true; # Remount cgroups read-only
RestrictSUIDSGID = true; # Prevent creating SETUID/SETGID files
PrivateMounts = true; # Give an own mount namespace
RemoveIPC = true;
UMask = "0077";
# Capabilities
CapabilityBoundingSet = ""; # Allow no capabilities at all
NoNewPrivileges = true; # Disallow getting more capabilities. This is also implied by other options.
# Kernel stuff
ProtectKernelModules = true; # Prevent loading of kernel modules
SystemCallArchitectures = "native"; # Usually no need to disable this
ProtectKernelLogs = true; # Prevent access to kernel logs
ProtectClock = true; # Prevent setting the RTC
# Networking
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
# Misc
LockPersonality = true; # Prevent change of the personality
ProtectHostname = true; # Give an own UTS namespace
RestrictRealtime = true; # Prevent switching to RT scheduling
MemoryDenyWriteExecute = true; # Maybe disable this for interpreters like python
RestrictNamespaces = true;
};
};
users.users.nixseparatedebuginfod = {
isSystemUser = true;
group = "nixseparatedebuginfod";
};
users.groups.nixseparatedebuginfod = { };
nix.settings = lib.optionalAttrs (lib.versionAtLeast config.nix.package.version "2.4") {
extra-allowed-users = [ "nixseparatedebuginfod" ];
};
environment.debuginfodServers = [ "http://${url}" ];
};
}

View File

@@ -0,0 +1,97 @@
{
pkgs,
lib,
config,
utils,
...
}:
let
cfg = config.services.nixseparatedebuginfod2;
url = "127.0.0.1:${toString cfg.port}";
in
{
options = {
services.nixseparatedebuginfod2 = {
enable = lib.mkEnableOption "nixseparatedebuginfod2, a debuginfod server providing source and debuginfo for nix packages";
port = lib.mkOption {
description = "port to listen";
default = 1950;
type = lib.types.port;
};
package = lib.mkPackageOption pkgs "nixseparatedebuginfod2" { };
substituter = lib.mkOption {
description = "nix substituter to fetch debuginfo from. Either http/https substituters, or `local:` to use debuginfo present in the local store.";
default = "https://cache.nixos.org";
example = "local:";
type = lib.types.str;
};
cacheExpirationDelay = lib.mkOption {
description = "keep unused cache entries for this long. A number followed by a unit";
default = "1d";
type = lib.types.str;
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.nixseparatedebuginfod2 = {
wantedBy = [ "multi-user.target" ];
path = [ config.nix.package ];
serviceConfig = {
ExecStart = [
(utils.escapeSystemdExecArgs [
(lib.getExe cfg.package)
"--listen-address"
url
"--substituter"
cfg.substituter
"--expiration"
cfg.cacheExpirationDelay
])
];
Restart = "on-failure";
CacheDirectory = "nixseparatedebuginfod2";
DynamicUser = true;
# hardening
# Filesystem stuff
ProtectSystem = "strict"; # Prevent writing to most of /
ProtectHome = true; # Prevent accessing /home and /root
PrivateTmp = true; # Give an own directory under /tmp
PrivateDevices = true; # Deny access to most of /dev
ProtectKernelTunables = true; # Protect some parts of /sys
ProtectControlGroups = true; # Remount cgroups read-only
RestrictSUIDSGID = true; # Prevent creating SETUID/SETGID files
PrivateMounts = true; # Give an own mount namespace
RemoveIPC = true;
UMask = "0077";
# Capabilities
CapabilityBoundingSet = ""; # Allow no capabilities at all
NoNewPrivileges = true; # Disallow getting more capabilities. This is also implied by other options.
# Kernel stuff
ProtectKernelModules = true; # Prevent loading of kernel modules
SystemCallArchitectures = "native"; # Usually no need to disable this
SystemCallFilter = "@system-service";
ProtectKernelLogs = true; # Prevent access to kernel logs
ProtectClock = true; # Prevent setting the RTC
ProtectProc = "noaccess";
ProcSubset = "pid";
# Networking
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
# Misc
LockPersonality = true; # Prevent change of the personality
ProtectHostname = true; # Give an own UTS namespace
RestrictRealtime = true; # Prevent switching to RT scheduling
MemoryDenyWriteExecute = true; # Maybe disable this for interpreters like python
RestrictNamespaces = true;
};
};
environment.debuginfodServers = [ "http://${url}" ];
};
}

View File

@@ -0,0 +1,108 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.rstudio-server;
rserver-conf = builtins.toFile "rserver.conf" ''
server-working-dir=${cfg.serverWorkingDir}
www-address=${cfg.listenAddr}
${cfg.rserverExtraConfig}
'';
rsession-conf = builtins.toFile "rsession.conf" ''
${cfg.rsessionExtraConfig}
'';
in
{
meta.maintainers = with lib.maintainers; [
jbedo
cfhammill
];
options.services.rstudio-server = {
enable = lib.mkEnableOption "RStudio server";
serverWorkingDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/rstudio-server";
description = ''
Default working directory for server (server-working-dir in rserver.conf).
'';
};
listenAddr = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = ''
Address to listen on (www-address in rserver.conf).
'';
};
package = lib.mkPackageOption pkgs "rstudio-server" {
example = "rstudioServerWrapper.override { packages = [ pkgs.rPackages.ggplot2 ]; }";
};
rserverExtraConfig = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Extra contents for rserver.conf.
'';
};
rsessionExtraConfig = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Extra contents for resssion.conf.
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.rstudio-server = {
description = "Rstudio server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
restartTriggers = [
rserver-conf
rsession-conf
];
serviceConfig = {
Restart = "on-failure";
Type = "forking";
ExecStart = "${cfg.package}/bin/rserver";
StateDirectory = "rstudio-server";
RuntimeDirectory = "rstudio-server";
};
};
environment.etc = {
"rstudio/rserver.conf".source = rserver-conf;
"rstudio/rsession.conf".source = rsession-conf;
"pam.d/rstudio".source = "/etc/pam.d/login";
};
environment.systemPackages = [ cfg.package ];
users = {
users.rstudio-server = {
uid = config.ids.uids.rstudio-server;
description = "rstudio-server";
group = "rstudio-server";
};
groups.rstudio-server = {
gid = config.ids.gids.rstudio-server;
};
};
};
}

View File

@@ -0,0 +1,49 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.vsmartcard-vpcd;
in
{
options.services.vsmartcard-vpcd = {
enable = lib.mkEnableOption "Virtual smart card driver.";
port = lib.mkOption {
type = lib.types.port;
default = 35963;
description = ''
Port number vpcd will be listening on.
'';
};
hostname = lib.mkOption {
type = lib.types.str;
default = "/dev/null";
description = ''
Hostname of a waiting vpicc server vpcd will be connecting to. Use /dev/null for listening mode.
'';
};
};
config = lib.mkIf cfg.enable {
services.pcscd.readerConfigs = [
''
FRIENDLYNAME "Virtual PCD"
DEVICENAME ${cfg.hostname}:0x${lib.toHexString cfg.port}
LIBPATH ${pkgs.vsmartcard-vpcd}/var/lib/pcsc/drivers/serial/libifdvpcd.so
CHANNELID 0x${lib.toHexString cfg.port}
''
];
environment.systemPackages = [ pkgs.vsmartcard-vpcd ];
};
meta.maintainers = with lib.maintainers; [ stargate01 ];
}

View File

@@ -0,0 +1,364 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.zammad;
settingsFormat = pkgs.formats.yaml { };
filterNull = lib.filterAttrs (_: v: v != null);
serviceConfig = {
Type = "simple";
Restart = "always";
User = cfg.user;
Group = cfg.group;
PrivateTmp = true;
StateDirectory = "zammad";
WorkingDirectory = package;
};
environment = {
RAILS_ENV = "production";
NODE_ENV = "production";
RAILS_SERVE_STATIC_FILES = "true";
RAILS_LOG_TO_STDOUT = "true";
REDIS_URL = "redis://${cfg.redis.host}:${toString cfg.redis.port}";
};
databaseConfig = settingsFormat.generate "database.yml" cfg.database.settings;
package = cfg.package.override {
dataDir = cfg.dataDir;
};
in
{
options = {
services.zammad = {
enable = lib.mkEnableOption "Zammad, a web-based, open source user support/ticketing solution";
package = lib.mkPackageOption pkgs "zammad" { };
user = lib.mkOption {
type = lib.types.str;
default = "zammad";
description = ''
Name of the Zammad user.
'';
};
group = lib.mkOption {
type = lib.types.str;
default = "zammad";
description = ''
Name of the Zammad group.
'';
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/zammad";
description = ''
Path to a folder that will contain Zammad working directory.
'';
};
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
example = "192.168.23.42";
description = "Host address.";
};
openPorts = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to open firewall ports for Zammad";
};
port = lib.mkOption {
type = lib.types.port;
default = 3000;
description = "Web service port.";
};
websocketPort = lib.mkOption {
type = lib.types.port;
default = 6042;
description = "Websocket service port.";
};
redis = {
createLocally = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to create a local redis automatically.";
};
name = lib.mkOption {
type = lib.types.str;
default = "zammad";
description = ''
Name of the redis server. Only used if `createLocally` is set to true.
'';
};
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = ''
Redis server address.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 6379;
description = "Port of the redis server.";
};
};
database = {
host = lib.mkOption {
type = lib.types.str;
default = "/run/postgresql";
description = ''
Database host address.
'';
};
port = lib.mkOption {
type = lib.types.nullOr lib.types.port;
default = null;
description = "Database port. Use `null` for default port.";
};
name = lib.mkOption {
type = lib.types.str;
default = "zammad";
description = ''
Database name.
'';
};
user = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "zammad";
description = "Database user.";
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/keys/zammad-dbpassword";
description = ''
A file containing the password for {option}`services.zammad.database.user`.
'';
};
createLocally = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to create a local database automatically.";
};
settings = lib.mkOption {
type = settingsFormat.type;
default = { };
example = lib.literalExpression ''
{
}
'';
description = ''
The {file}`database.yml` configuration file as key value set.
See \<TODO\>
for list of configuration parameters.
'';
};
};
secretKeyBaseFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/keys/secret_key_base";
description = ''
The path to a file containing the
`secret_key_base` secret.
Zammad uses `secret_key_base` to encrypt
the cookie store, which contains session data, and to digest
user auth tokens.
Needs to be a 64 byte long string of hexadecimal
characters. You can generate one by running
```
openssl rand -hex 64 >/path/to/secret_key_base_file
```
This should be a string, not a nix path, since nix paths are
copied into the world-readable nix store.
'';
};
};
};
config = lib.mkIf cfg.enable {
services.zammad.database.settings = {
production = lib.mapAttrs (_: v: lib.mkDefault v) (filterNull {
adapter = "postgresql";
database = cfg.database.name;
pool = 50;
timeout = 5000;
encoding = "utf8";
username = cfg.database.user;
host = cfg.database.host;
port = cfg.database.port;
});
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openPorts [
config.services.zammad.port
config.services.zammad.websocketPort
];
users.users.${cfg.user} = {
group = "${cfg.group}";
isSystemUser = true;
};
users.groups.${cfg.group} = { };
assertions = [
{
assertion =
cfg.database.createLocally -> cfg.database.user == "zammad" && cfg.database.name == "zammad";
message = "services.zammad.database.user must be set to \"zammad\" if services.zammad.database.createLocally is set to true";
}
{
assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
message = "a password cannot be specified if services.zammad.database.createLocally is set to true";
}
{
assertion = cfg.redis.createLocally -> cfg.redis.host == "localhost";
message = "the redis host must be localhost if services.zammad.redis.createLocally is set to true";
}
];
services.postgresql = lib.optionalAttrs (cfg.database.createLocally) {
enable = true;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
{
name = cfg.database.user;
ensureDBOwnership = true;
}
];
};
services.redis = lib.optionalAttrs cfg.redis.createLocally {
servers."${cfg.redis.name}" = {
enable = true;
port = cfg.redis.port;
};
};
systemd.services.zammad-web = {
inherit environment;
serviceConfig = serviceConfig // {
# loading all the gems takes time
TimeoutStartSec = 1200;
};
after = [
"network.target"
"systemd-tmpfiles-setup.service"
]
++ lib.optionals (cfg.database.createLocally) [
"postgresql.target"
]
++ lib.optionals cfg.redis.createLocally [
"redis-${cfg.redis.name}.service"
];
requires = lib.optionals (cfg.database.createLocally) [
"postgresql.target"
];
description = "Zammad web";
wantedBy = [ "multi-user.target" ];
preStart = ''
# config file
cat ${databaseConfig} > ${cfg.dataDir}/config/database.yml
${lib.optionalString (cfg.database.passwordFile != null) ''
{
echo -n " password: "
cat ${cfg.database.passwordFile}
} >> ${cfg.dataDir}/config/database.yml
''}
${lib.optionalString (cfg.secretKeyBaseFile != null) ''
{
echo "production: "
echo -n " secret_key_base: "
cat ${cfg.secretKeyBaseFile}
} > ${cfg.dataDir}/config/secrets.yml
''}
# needed for cleanup
shopt -s extglob
# cleanup state directory from module before refactoring in
# https://github.com/NixOS/nixpkgs/pull/277456
if [[ -e ${cfg.dataDir}/node_modules ]]; then
rm -rf ${cfg.dataDir}/!("tmp"|"config"|"log"|"state_dir_migrated"|"db_seeded")
rm -rf ${cfg.dataDir}/config/!("database.yml"|"secrets.yml")
# state directory cleanup required --> zammad was already installed --> do not seed db
echo true > ${cfg.dataDir}/db_seeded
fi
SEEDED=$(cat ${cfg.dataDir}/db_seeded)
if [[ $SEEDED != "true" ]]; then
echo "Initialize database"
./bin/rake --no-system db:migrate
./bin/rake --no-system db:seed
echo true > ${cfg.dataDir}/db_seeded
else
echo "Migrate database"
./bin/rake --no-system db:migrate
fi
echo "Done"
'';
script = "./script/rails server -b ${cfg.host} -p ${toString cfg.port}";
};
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.dataDir}/config 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.dataDir}/tmp 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.dataDir}/log 0750 ${cfg.user} ${cfg.group} - -"
"f ${cfg.dataDir}/config/secrets.yml 0640 ${cfg.user} ${cfg.group} - -"
"f ${cfg.dataDir}/config/database.yml 0640 ${cfg.user} ${cfg.group} - -"
"f ${cfg.dataDir}/db_seeded 0640 ${cfg.user} ${cfg.group} - -"
];
systemd.services.zammad-websocket = {
inherit serviceConfig environment;
after = [ "zammad-web.service" ];
requires = [ "zammad-web.service" ];
description = "Zammad websocket";
wantedBy = [ "multi-user.target" ];
script = "./script/websocket-server.rb -b ${cfg.host} -p ${toString cfg.websocketPort} start";
};
systemd.services.zammad-worker = {
inherit serviceConfig environment;
after = [ "zammad-web.service" ];
requires = [ "zammad-web.service" ];
description = "Zammad background worker";
wantedBy = [ "multi-user.target" ];
script = "./script/background-worker.rb start";
};
};
meta.maintainers = with lib.maintainers; [
taeer
netali
];
}