627 lines
20 KiB
Nix
627 lines
20 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.komodo-periphery;
|
|
settingsFormat = pkgs.formats.toml { };
|
|
|
|
actualRepoDir = if cfg.repoDir != null then cfg.repoDir else "${cfg.rootDirectory}/repos";
|
|
actualStackDir = if cfg.stackDir != null then cfg.stackDir else "${cfg.rootDirectory}/stacks";
|
|
actualBuildDir = if cfg.buildDir != null then cfg.buildDir else "${cfg.rootDirectory}/builds";
|
|
|
|
genFinalSettings =
|
|
let
|
|
actualServerEnabled =
|
|
if cfg.inbound.serverEnabled != null then
|
|
cfg.inbound.serverEnabled
|
|
else
|
|
(cfg.outbound.coreAddress == "");
|
|
|
|
hasAnyPrivateKey = cfg.auth.privateKey != "";
|
|
|
|
baseSettings = {
|
|
default_terminal_command = cfg.defaultTerminalCommand;
|
|
disable_terminals = cfg.disableTerminals;
|
|
disable_container_terminals = cfg.disableContainerTerminals;
|
|
|
|
stats_polling_rate = cfg.statsPollingRate;
|
|
container_stats_polling_rate = cfg.containerStatsPollingRate;
|
|
legacy_compose_cli = cfg.legacyComposeCli;
|
|
|
|
include_disk_mounts = cfg.includeDiskMounts;
|
|
exclude_disk_mounts = cfg.excludeDiskMounts;
|
|
|
|
# When a private key is explicitly configured, it's passed via env var.
|
|
# Otherwise, use the default file path so Periphery auto-generates a key.
|
|
private_key = if !hasAnyPrivateKey then "file:${cfg.rootDirectory}/keys/periphery.key" else "";
|
|
core_public_keys = [ ];
|
|
passkeys = [ ];
|
|
|
|
server_enabled = actualServerEnabled;
|
|
port = cfg.inbound.port;
|
|
bind_ip = cfg.inbound.bindIp;
|
|
allowed_ips = cfg.inbound.allowedIps;
|
|
ssl_enabled = cfg.inbound.ssl.enable;
|
|
}
|
|
// {
|
|
core_address = cfg.outbound.coreAddress;
|
|
connect_as = cfg.outbound.connectAs;
|
|
onboarding_key = "";
|
|
}
|
|
// {
|
|
logging = {
|
|
level = cfg.logging.level;
|
|
stdio = cfg.logging.stdio;
|
|
opentelemetry_service_name = cfg.logging.opentelemetryServiceName;
|
|
opentelemetry_scope_name = cfg.logging.opentelemetryScopeName;
|
|
pretty = cfg.logging.pretty;
|
|
}
|
|
// lib.optionalAttrs (cfg.logging.otlpEndpoint != "") {
|
|
otlp_endpoint = cfg.logging.otlpEndpoint;
|
|
};
|
|
pretty_startup_config = cfg.prettyStartupConfig;
|
|
}
|
|
// cfg.extraSettings;
|
|
in
|
|
lib.filterAttrsRecursive (_: v: v != null && v != { } && v != [ ] && v != "") baseSettings;
|
|
|
|
configFile =
|
|
if cfg.configFile == null then
|
|
settingsFormat.generate "komodo-periphery.toml" genFinalSettings
|
|
else
|
|
cfg.configFile;
|
|
in
|
|
{
|
|
imports = with lib; [
|
|
(mkRenamedOptionModule
|
|
[ "services" "komodo-periphery" "port" ]
|
|
[ "services" "komodo-periphery" "inbound" "port" ]
|
|
)
|
|
(mkRenamedOptionModule
|
|
[ "services" "komodo-periphery" "bindIp" ]
|
|
[ "services" "komodo-periphery" "inbound" "bindIp" ]
|
|
)
|
|
(mkRenamedOptionModule
|
|
[ "services" "komodo-periphery" "allowedIps" ]
|
|
[ "services" "komodo-periphery" "inbound" "allowedIps" ]
|
|
)
|
|
(mkRenamedOptionModule
|
|
[ "services" "komodo-periphery" "ssl" "enable" ]
|
|
[ "services" "komodo-periphery" "inbound" "ssl" "enable" ]
|
|
)
|
|
(mkRenamedOptionModule
|
|
[ "services" "komodo-periphery" "ssl" "keyFile" ]
|
|
[ "services" "komodo-periphery" "inbound" "ssl" "keyFile" ]
|
|
)
|
|
(mkRenamedOptionModule
|
|
[ "services" "komodo-periphery" "ssl" "certFile" ]
|
|
[ "services" "komodo-periphery" "inbound" "ssl" "certFile" ]
|
|
)
|
|
(mkRenamedOptionModule
|
|
[ "services" "komodo-periphery" "serverEnabled" ]
|
|
[ "services" "komodo-periphery" "inbound" "serverEnabled" ]
|
|
)
|
|
(mkRenamedOptionModule
|
|
[ "services" "komodo-periphery" "disableContainerExec" ]
|
|
[ "services" "komodo-periphery" "disableContainerTerminals" ]
|
|
)
|
|
(mkRemovedOptionModule [ "services" "komodo-periphery" "passkeys" ]
|
|
"services.komodo-periphery.passkeys has been removed. Use passkeyFiles for v1.X compatibility, or migrate to auth.privateKey and auth.corePublicKeys (v2.0+)."
|
|
)
|
|
(mkRemovedOptionModule [ "services" "komodo-periphery" "outbound" "onboardingKey" ]
|
|
"services.komodo-periphery.outbound.onboardingKey has been removed. Use outbound.onboardingKeyFile instead for better security."
|
|
)
|
|
];
|
|
|
|
options.services.komodo-periphery = {
|
|
enable = lib.mkEnableOption "Periphery, a multi-server Docker and Git deployment agent by Komodo";
|
|
|
|
package = lib.mkPackageOption pkgs "komodo" { };
|
|
|
|
dockerHost = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = ''
|
|
Docker host socket URI to use for container operations.
|
|
If null, uses the default Docker socket and automatically enables Docker.
|
|
Set this to use alternative container runtimes like Podman or remote Docker hosts.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
# Podman rootless
|
|
"unix:///run/user/1000/podman/podman.sock"
|
|
|
|
# Podman rootful
|
|
"unix:///run/podman/podman.sock"
|
|
|
|
# Remote Docker
|
|
"tcp://192.168.1.100:2375"
|
|
'';
|
|
};
|
|
|
|
configFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = ''
|
|
Path to the Periphery configuration file.
|
|
|
|
- If `null` (default), a configuration file will be automatically generated
|
|
from the module options (recommended for most users).
|
|
- If set to a path, that file will be used directly as the configuration file.
|
|
- You can also use `pkgs.writeText` to write configuration inline in your NixOS configuration.
|
|
|
|
When using a custom config file, all other module options (except `package`, `user`, `group`,
|
|
`environment`, and `environmentFile`) will be ignored.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
# Option 1: Use an existing config file
|
|
"/etc/komodo/periphery.toml"
|
|
|
|
# Option 2: Write config inline using pkgs.writeText
|
|
pkgs.writeText "periphery.toml" '''
|
|
root_directory = "/var/lib/komodo-periphery"
|
|
|
|
logging.level = "info"
|
|
logging.stdio = "standard"
|
|
'''
|
|
'';
|
|
};
|
|
|
|
rootDirectory = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = "/var/lib/komodo-periphery";
|
|
description = "Root directory for Komodo Periphery data.";
|
|
};
|
|
|
|
repoDir = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
defaultText = lib.literalExpression ''"''${config.services.komodo-periphery.rootDirectory}/repos"'';
|
|
description = "Directory for Komodo Periphery to manage repos. If null, defaults to `\${rootDirectory}/repos`.";
|
|
};
|
|
|
|
stackDir = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
defaultText = lib.literalExpression ''"''${config.services.komodo-periphery.rootDirectory}/stacks"'';
|
|
description = "Directory for Komodo Periphery to manage stacks. If null, defaults to `\${rootDirectory}/stacks`.";
|
|
};
|
|
|
|
buildDir = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
defaultText = lib.literalExpression ''"''${config.services.komodo-periphery.rootDirectory}/builds"'';
|
|
description = "Directory for Komodo Periphery to manage builds. If null, defaults to `\${rootDirectory}/builds`.";
|
|
};
|
|
|
|
logging = {
|
|
level = lib.mkOption {
|
|
type = lib.types.enum [
|
|
"off"
|
|
"error"
|
|
"warn"
|
|
"info"
|
|
"debug"
|
|
"trace"
|
|
];
|
|
default = "info";
|
|
description = "Logging verbosity level.";
|
|
};
|
|
|
|
stdio = lib.mkOption {
|
|
type = lib.types.enum [
|
|
"standard"
|
|
"json"
|
|
"none"
|
|
];
|
|
default = "standard";
|
|
description = "Logging format for stdout/stderr.";
|
|
};
|
|
|
|
otlpEndpoint = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = "OpenTelemetry OTLP endpoint for traces.";
|
|
example = "http://localhost:4317";
|
|
};
|
|
|
|
opentelemetryServiceName = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "Periphery";
|
|
description = "(v2.0+) OpenTelemetry service name attached to telemetry.";
|
|
};
|
|
|
|
opentelemetryScopeName = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "Komodo";
|
|
description = "(v2.0+) OpenTelemetry scope name attached to telemetry.";
|
|
};
|
|
|
|
pretty = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "(v2.0+) Enable human-readable logging (multi-line).";
|
|
};
|
|
};
|
|
|
|
prettyStartupConfig = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "(v2.0+) Enable human-readable startup config log (multi-line).";
|
|
};
|
|
|
|
passkeyFiles = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = ''
|
|
Path to file containing passkeys (v1.X compatibility).
|
|
This will be passed via `PERIPHERY_PASSKEYS_FILE` environment variable.
|
|
Consider migrating to the new v2.0+ authentication mechanism.
|
|
'';
|
|
example = "/run/secrets/komodo-passkey";
|
|
};
|
|
|
|
extraSettings = lib.mkOption {
|
|
type = settingsFormat.type;
|
|
default = { };
|
|
description = "Extra settings to add to the generated TOML config.";
|
|
example = {
|
|
secrets.GITHUB_TOKEN = "ghp_xxxx";
|
|
};
|
|
};
|
|
|
|
defaultTerminalCommand = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "bash";
|
|
description = "(v2.0+) Default terminal command used to initialize the shell.";
|
|
example = "zsh";
|
|
};
|
|
|
|
disableTerminals = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Disable remote shell access through Periphery.";
|
|
};
|
|
|
|
disableContainerTerminals = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "(v2.0+) Disable remote container shell access through Periphery.";
|
|
};
|
|
|
|
statsPollingRate = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "5-sec";
|
|
description = "System stats polling interval.";
|
|
example = "10-sec";
|
|
};
|
|
|
|
containerStatsPollingRate = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "30-sec";
|
|
description = "Container stats polling interval.";
|
|
example = "1-min";
|
|
};
|
|
|
|
legacyComposeCli = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Use `docker-compose` instead of `docker compose`.";
|
|
};
|
|
|
|
includeDiskMounts = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = "Only include these mount paths in disk reporting.";
|
|
example = [
|
|
"/mnt/data"
|
|
"/mnt/backup"
|
|
];
|
|
};
|
|
|
|
excludeDiskMounts = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = "Exclude these mount paths from disk reporting.";
|
|
example = [
|
|
"/tmp"
|
|
"/boot"
|
|
];
|
|
};
|
|
|
|
auth = {
|
|
privateKey = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = ''
|
|
(v2.0+) Private key used with the Noise handshake.
|
|
|
|
Use `file:/path/to/file` prefix to load from a file. If the file doesn't exist,
|
|
Periphery will generate and write a new key to the path.
|
|
|
|
If empty, defaults to `file:''${rootDirectory}/keys/periphery.key`.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
# Reference a secret file
|
|
"file:''${config.age.secrets.komodo-private-key.path}"
|
|
|
|
# Or direct path
|
|
"file:/run/secrets/komodo-private-key"
|
|
'';
|
|
};
|
|
|
|
corePublicKeys = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = ''
|
|
(v2.0+) Accepted public keys to allow Core(s) to connect.
|
|
Accepts Spki base64 DER directly or can reference files using `file:/path/to/core.pub` prefix.
|
|
You can mix direct keys and file references.
|
|
|
|
For better security, use the `file:` prefix to reference secret files.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
[
|
|
# Direct key value
|
|
"MCowBQYDK2VuAyEATZgrjGHeF0KJUe0+n77+qAWOg3YzEzXOmQWaRgO3OGQ="
|
|
# Reference secret files
|
|
"file:''${config.age.secrets.komodo-core1-pub.path}"
|
|
"file:''${config.age.secrets.komodo-core2-pub.path}"
|
|
]
|
|
'';
|
|
};
|
|
};
|
|
|
|
inbound = {
|
|
serverEnabled = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.bool;
|
|
default = null;
|
|
description = ''
|
|
Enable the inbound connection server for Core -> Periphery connections.
|
|
If null, defaults to false when `outbound.coreAddress` is defined, otherwise true.
|
|
'';
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 8120;
|
|
description = "Port for the inbound Periphery server to listen on.";
|
|
};
|
|
|
|
bindIp = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "[::]";
|
|
description = ''
|
|
IP address the periphery server will bind to.
|
|
The default allows external IPv4 and IPv6 connections.
|
|
'';
|
|
};
|
|
|
|
allowedIps = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = ''
|
|
Limit the IP addresses which can connect to Periphery.
|
|
Supports IPv4/IPv6 addresses and subnets.
|
|
Empty list allows all connections.
|
|
'';
|
|
example = [
|
|
"::ffff:12.34.56.78"
|
|
"10.0.10.0/24"
|
|
];
|
|
};
|
|
|
|
ssl = {
|
|
enable = lib.mkEnableOption "(v2.0+) SSL/TLS support for inbound connections" // {
|
|
default = true;
|
|
};
|
|
|
|
keyFile = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = "${cfg.rootDirectory}/ssl/key.pem";
|
|
defaultText = lib.literalExpression ''"''${config.services.komodo-periphery.rootDirectory}/ssl/key.pem"'';
|
|
description = ''
|
|
Path to SSL key file.
|
|
If the file doesn't exist and SSL is enabled, self-signed keys will be generated using openssl.
|
|
'';
|
|
};
|
|
|
|
certFile = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = "${cfg.rootDirectory}/ssl/cert.pem";
|
|
defaultText = lib.literalExpression ''"''${config.services.komodo-periphery.rootDirectory}/ssl/cert.pem"'';
|
|
description = ''
|
|
Path to SSL certificate file.
|
|
If the file doesn't exist and SSL is enabled, self-signed certificate will be generated using openssl.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
outbound = {
|
|
coreAddress = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = ''
|
|
(v2.0+) The address of Komodo Core. Must be reachable from this host.
|
|
If provided, Periphery will operate in outbound mode.
|
|
'';
|
|
example = "demo.komo.do";
|
|
};
|
|
|
|
connectAs = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = ''
|
|
(v2.0+) The Server this Periphery agent should connect as.
|
|
Must match an existing Server name or id.
|
|
'';
|
|
example = "server-name";
|
|
};
|
|
|
|
onboardingKeyFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = ''
|
|
(v2.0+) Path to file containing the onboarding key.
|
|
This will be passed via `PERIPHERY_ONBOARDING_KEY_FILE` environment variable.
|
|
'';
|
|
example = lib.literalExpression ''"''${config.age.secrets.komodo-onboarding.path}"'';
|
|
};
|
|
};
|
|
|
|
user = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "komodo-periphery";
|
|
description = "User under which the Periphery agent runs.";
|
|
};
|
|
|
|
group = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "komodo-periphery";
|
|
description = "Group under which the Periphery agent runs.";
|
|
};
|
|
|
|
environmentFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = "Environment file for additional configuration via environment variables.";
|
|
example = "/run/secrets/komodo-periphery.env";
|
|
};
|
|
|
|
environment = lib.mkOption {
|
|
type = lib.types.attrsOf lib.types.str;
|
|
default = { };
|
|
description = "Environment variables to set for the service.";
|
|
example = {
|
|
RUST_LOG = "komodo=debug";
|
|
DOCKER_HOST = "unix:///var/run/docker.sock";
|
|
};
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
virtualisation.docker.enable = lib.mkDefault (cfg.dockerHost == null);
|
|
|
|
users.users.${cfg.user} = lib.mkIf (cfg.user == "komodo-periphery") {
|
|
isSystemUser = true;
|
|
group = cfg.group;
|
|
description = "Komodo Periphery service user";
|
|
home = cfg.rootDirectory;
|
|
extraGroups = lib.optional (cfg.dockerHost == null) "docker";
|
|
};
|
|
|
|
users.groups.${cfg.group} = lib.mkIf (cfg.group == "komodo-periphery") { };
|
|
|
|
systemd.tmpfiles.settings."10-komodo-periphery" = {
|
|
"${cfg.rootDirectory}".d = {
|
|
mode = "0755";
|
|
user = cfg.user;
|
|
group = cfg.group;
|
|
};
|
|
"${actualRepoDir}".d = {
|
|
mode = "0755";
|
|
user = cfg.user;
|
|
group = cfg.group;
|
|
};
|
|
"${actualStackDir}".d = {
|
|
mode = "0755";
|
|
user = cfg.user;
|
|
group = cfg.group;
|
|
};
|
|
"${actualBuildDir}".d = {
|
|
mode = "0755";
|
|
user = cfg.user;
|
|
group = cfg.group;
|
|
};
|
|
"${cfg.rootDirectory}/keys".d = {
|
|
mode = "0700";
|
|
user = cfg.user;
|
|
group = cfg.group;
|
|
};
|
|
"${cfg.rootDirectory}/ssl".d = {
|
|
mode = "0700";
|
|
user = cfg.user;
|
|
group = cfg.group;
|
|
};
|
|
};
|
|
|
|
systemd.services.komodo-periphery = {
|
|
description = "Komodo Periphery - Multi-server Docker and Git deployment agent";
|
|
after = [
|
|
"network-online.target"
|
|
]
|
|
++ lib.optional (cfg.dockerHost == null) "docker.service";
|
|
wants = [
|
|
"network-online.target"
|
|
]
|
|
++ lib.optional (cfg.dockerHost == null) "docker.service";
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
SupplementaryGroups = lib.mkIf (cfg.dockerHost == null) [ "docker" ];
|
|
Restart = "on-failure";
|
|
RestartSec = "10s";
|
|
WorkingDirectory = cfg.rootDirectory;
|
|
|
|
ExecStart = lib.escapeShellArgs [
|
|
(lib.getExe' cfg.package "periphery")
|
|
"--config-path"
|
|
configFile
|
|
];
|
|
|
|
Environment = lib.mapAttrsToList (name: value: "${name}=${value}") (
|
|
lib.optionalAttrs (cfg.configFile == null) {
|
|
PERIPHERY_ROOT_DIRECTORY = cfg.rootDirectory;
|
|
PERIPHERY_REPO_DIR = actualRepoDir;
|
|
PERIPHERY_STACK_DIR = actualStackDir;
|
|
PERIPHERY_BUILD_DIR = actualBuildDir;
|
|
}
|
|
// lib.optionalAttrs (cfg.configFile == null && cfg.inbound.ssl.enable) {
|
|
PERIPHERY_SSL_KEY_FILE = cfg.inbound.ssl.keyFile;
|
|
PERIPHERY_SSL_CERT_FILE = cfg.inbound.ssl.certFile;
|
|
}
|
|
// lib.optionalAttrs (cfg.dockerHost != null) {
|
|
DOCKER_HOST = cfg.dockerHost;
|
|
}
|
|
// lib.optionalAttrs (cfg.passkeyFiles != null) {
|
|
PERIPHERY_PASSKEYS_FILE = cfg.passkeyFiles;
|
|
}
|
|
// lib.optionalAttrs (cfg.auth.privateKey != "") {
|
|
PERIPHERY_PRIVATE_KEY = cfg.auth.privateKey;
|
|
}
|
|
// lib.optionalAttrs (cfg.auth.corePublicKeys != [ ]) {
|
|
PERIPHERY_CORE_PUBLIC_KEYS = lib.concatStringsSep "," cfg.auth.corePublicKeys;
|
|
}
|
|
// lib.optionalAttrs (cfg.outbound.onboardingKeyFile != null) {
|
|
PERIPHERY_ONBOARDING_KEY_FILE = cfg.outbound.onboardingKeyFile;
|
|
}
|
|
// cfg.environment
|
|
);
|
|
|
|
ExecSearchPath = lib.mkIf (!cfg.disableTerminals) [
|
|
"/run/current-system/sw/bin"
|
|
"/run/wrappers/bin"
|
|
];
|
|
|
|
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
|
|
|
|
StateDirectory = "komodo-periphery";
|
|
StateDirectoryMode = "0755";
|
|
|
|
NoNewPrivileges = true;
|
|
PrivateTmp = true;
|
|
ProtectSystem = "full";
|
|
ProtectHome = "read-only";
|
|
};
|
|
};
|
|
};
|
|
|
|
meta.maintainers = with lib.maintainers; [ channinghe ];
|
|
}
|