Files
nixos/modules/util/container-services.nix
2024-07-02 10:56:58 +03:00

225 lines
6.4 KiB
Nix

{ pkgs, lib, config, ... }:
let
cfg = config.container-services;
healthcheck = with pkgs.lib; {
test = mkOption {
type = types.oneOf types.str (types.listOf types.str);
};
interval = mkOption {
type = types.str;
default = "30s";
};
startupInterval = mkOption {
type = types.str;
default = "30s";
};
startPeriod = mkOption {
type = types.str;
default = "0s";
};
};
service = with pkgs.lib; {
name = mkOption {
type = types.nilOr types.str;
default = nil;
};
alias = mkOption {
type = types.nilOr types.str;
default = nil;
};
dependsOn = mkOption {
type = types.listOf service;
default = [ ];
};
image = mkOption {
type = types.str;
};
volumes = mkOption {
type = types.listOf types.str;
default = [ ];
};
ports = mkOption {
type = types.listOf types.str;
default = [ ];
};
environment = mkOption {
type = types.attrsOf types.str;
default = { };
};
environmentFiles = mkOption {
type = types.listOf types.str;
default = [ ];
};
cmd = mkOption {
type = types.nilOr (types.listOf types.str);
default = nil;
};
healthCheck = mkOption {
type = types.nilOr (types.submodule healthcheck);
default = nil;
};
};
pod = with pkgs.lib; {
name = mkOption {
type = types.nilOr str;
default = nil;
};
description = mkOption {
type = types.nilOr str;
default = nil;
};
services = mkOption {
type = types.attrsOf (types.submodule service);
default = { };
};
};
backend = config.virtualisation.oci-containers.backend;
volumeName = pod: name: "${pod.name}-${name}";
volumeServiceName = pod: name: "${backend}-volume-${volumeName pod name}";
volumeServiceRef = pod: name: config.systemd.services."${backend}-volume-${volumeName pod name}".name + ".service";
serviceName = pod: service: "${backend}-${pod.name}-${service.name}";
serviceRef = pod: service: config.systemd.services."${backend}-${pod.name}-${service.name}".name + ".service";
networkName = pod: "${pod.name}-default";
networkServiceName = pod: "${backend}-network-${pod.name}";
networkServiceRef = pod: config.systemd.services."${backend}-network-${pod.name}".name + ".service";
podName = pod: "${backend}-pod-${pod.name}";
podRef = pod: config.systemd.targets."${backend}-pod-${pod.name}".name + ".target";
namedVolumes = service: lib.filter (volume: (! ((lib.hasPrefix "/" volume) || (lib.hasPrefix "./" volume)))) (lib.map (volume: lib.head (lib.splitString ":" volume)) service.volumes);
oneShotService = { pod, description, script }: {
path = [ (if backend == "podman" then pkgs.podman else pkgs.docker) ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
inherit description;
inherit script;
partOf = [ podRef pod ];
wantedBy = [ podRef pod ];
};
in
{
options = with pkgs.lib; {
container-services = mkOption {
type = types.attrsOf (types.submodule pod);
default = { };
};
};
config = {
container-services = lib.mapAttrs
(name: pod: {
name = pod.name or name;
services = lib.mapAttrs
(name: service: {
name = service.name or name;
})
pod.services;
})
cfg;
systemd.targets = lib.mapAttrs' (_: pod:
lib.nameValuePair (podName pod) {
wantedBy = [ "multi-user.target" ];
unitConfig = {
Description = pod.description or pod.name;
};
}
);
systemd.services = lib.foldl
(services: pod: lib.mergeAttrsList [
services
{
"${networkServiceName pod}" = oneShotService {
description = "Network for ${pod.name}";
script = ''
${backend} network inspect ${networkName pod} || ${backend} network create ${networkName pod}
'';
};
}
(lib.mapAttrs'
(_: service:
let
dependencies =
(lib.map (service: serviceRef pod.name service.name) service.dependsOn) ++
(lib.map (name: volumeServiceRef pod.name name) (namedVolumes service)) ++
(networkServiceRef pod);
in
lib.nameValuePair (serviceName pod service) {
serviceConfig = {
Restart = lib.mkOverride 500 "always";
};
after = dependencies;
requires = dependencies;
partOf = [ (podRef pod) ];
wantedBy = [ (podRef pod) ];
}
)
pod.services)
(lib.listToAttrs (lib.flatten (
lib.map
(service: (
lib.map
(volume: {
name = volumeServiceName pod volume;
value = oneShotService {
description = "Volume ${volume} of ${pod.name}";
script = ''
${backend} volume inspect ${volumeName pod volume} || ${backend} volume create ${volumeName pod volume}
'';
};
})
namedVolumes)
)
pod.services)))
])
{ }
cfg;
virtualisation.oci-containers.containers = lib.foldl
(containers: pod: containers // (
lib.mapAttrs' (_: service:
lib.nameValuePair (serviceName pod service) {
image = service.image;
volumes = service.volumes;
log-driver = "journal";
ports = service.ports;
environment = service.environment;
environmentFiles = service.environmentFiles;
cmd = service.cmd;
extraOptions = [
"--network-alias=${service.alias or service.name}"
"--network=${networkName pod}"
] ++ (if (service.healthCheck != null) then [
"--health-cmd=${
if (builtins.isList service.healthCheck.test)
then builtins.toJSON service.healthCheck.test
else service.healthCheck.test
}"
"--health-interval=${service.healthCheck.interval}"
"--health-startup-interval=${service.healthCheck.startupInterval}"
"--health-start-period=${service.healthCheck.startPeriod}"
] else [ ]);
}
)
))
{ }
cfg;
};
}