cosmic checkpoint
This commit is contained in:
parent
4031a614a8
commit
f623faa0ef
26 changed files with 1163 additions and 538 deletions
365
home/util/containers.nix
Normal file
365
home/util/containers.nix
Normal file
|
@ -0,0 +1,365 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
containerOptions = {...}: {
|
||||
options = {
|
||||
pullPolicy = mkOption {
|
||||
type = with types; str;
|
||||
description = "Podman container pulling policy";
|
||||
default = "newer";
|
||||
};
|
||||
image = mkOption {
|
||||
type = with types; str;
|
||||
description = "OCI image to run.";
|
||||
example = "library/hello-world";
|
||||
};
|
||||
|
||||
imageFile = mkOption {
|
||||
type = with types; nullOr package;
|
||||
default = null;
|
||||
description = ''
|
||||
Path to an image file to load before running the image. This can
|
||||
be used to bypass pulling the image from the registry.
|
||||
|
||||
The `image` attribute must match the name and
|
||||
tag of the image contained in this file, as they will be used to
|
||||
run the container with that image. If they do not match, the
|
||||
image will be pulled from the registry as usual.
|
||||
'';
|
||||
example = literalExpression "pkgs.dockerTools.buildImage {...};";
|
||||
};
|
||||
|
||||
login = {
|
||||
username = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
description = "Username for login.";
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
description = "Path to file containing password.";
|
||||
example = "/etc/nixos/dockerhub-password.txt";
|
||||
};
|
||||
|
||||
registry = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
description = "Registry where to login to.";
|
||||
example = "https://docker.pkg.github.com";
|
||||
};
|
||||
};
|
||||
|
||||
cmd = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = [];
|
||||
description = "Commandline arguments to pass to the image's entrypoint.";
|
||||
example = literalExpression ''
|
||||
["--port=9000"]
|
||||
'';
|
||||
};
|
||||
|
||||
labels = mkOption {
|
||||
type = with types; attrsOf str;
|
||||
default = {};
|
||||
description = "Labels to attach to the container at runtime.";
|
||||
example = literalExpression ''
|
||||
{
|
||||
"traefik.https.routers.example.rule" = "Host(`example.container`)";
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
entrypoint = mkOption {
|
||||
type = with types; nullOr str;
|
||||
description = "Override the default entrypoint of the image.";
|
||||
default = null;
|
||||
example = "/bin/my-app";
|
||||
};
|
||||
|
||||
environment = mkOption {
|
||||
type = with types; attrsOf str;
|
||||
default = {};
|
||||
description = "Environment variables to set for this container.";
|
||||
example = literalExpression ''
|
||||
{
|
||||
DATABASE_HOST = "db.example.com";
|
||||
DATABASE_PORT = "3306";
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
environmentFiles = mkOption {
|
||||
type = with types; listOf path;
|
||||
default = [];
|
||||
description = "Environment files for this container.";
|
||||
example = literalExpression ''
|
||||
[
|
||||
/path/to/.env
|
||||
/path/to/.env.secret
|
||||
]
|
||||
'';
|
||||
};
|
||||
|
||||
log-driver = mkOption {
|
||||
type = types.str;
|
||||
default = "journald";
|
||||
description = ''
|
||||
Logging driver for the container. The default of
|
||||
`"journald"` means that the container's logs will be
|
||||
handled as part of the systemd unit.
|
||||
|
||||
For more details and a full list of logging drivers, refer to podman documentation.
|
||||
|
||||
For Docker:
|
||||
[Docker engine documentation](https://docs.docker.com/engine/reference/run/#logging-drivers---log-driver)
|
||||
|
||||
For Podman:
|
||||
Refer to the docker-run(1) man page.
|
||||
'';
|
||||
};
|
||||
|
||||
ports = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = [];
|
||||
description = ''
|
||||
Network ports to publish from the container to the outer host.
|
||||
|
||||
Valid formats:
|
||||
- `<ip>:<hostPort>:<containerPort>`
|
||||
- `<ip>::<containerPort>`
|
||||
- `<hostPort>:<containerPort>`
|
||||
- `<containerPort>`
|
||||
|
||||
Both `hostPort` and `containerPort` can be specified as a range of
|
||||
ports. When specifying ranges for both, the number of container
|
||||
ports in the range must match the number of host ports in the
|
||||
range. Example: `1234-1236:1234-1236/tcp`
|
||||
|
||||
When specifying a range for `hostPort` only, the `containerPort`
|
||||
must *not* be a range. In this case, the container port is published
|
||||
somewhere within the specified `hostPort` range.
|
||||
Example: `1234-1236:1234/tcp`
|
||||
|
||||
Refer to the
|
||||
[Docker engine documentation](https://docs.docker.com/engine/reference/run/#expose-incoming-ports) for full details.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
[
|
||||
"8080:9000"
|
||||
]
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
description = ''
|
||||
Override the username or UID (and optionally groupname or GID) used
|
||||
in the container.
|
||||
'';
|
||||
example = "nobody:nogroup";
|
||||
};
|
||||
|
||||
volumes = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = [];
|
||||
description = ''
|
||||
List of volumes to attach to this container.
|
||||
|
||||
Note that this is a list of `"src:dst"` strings to
|
||||
allow for `src` to refer to `/nix/store` paths, which
|
||||
would be difficult with an attribute set. There are
|
||||
also a variety of mount options available as a third
|
||||
field; please refer to the
|
||||
[docker engine documentation](https://docs.docker.com/engine/reference/run/#volume-shared-filesystems) for details.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
[
|
||||
"volume_name:/path/inside/container"
|
||||
"/path/on/host:/path/inside/container"
|
||||
]
|
||||
'';
|
||||
};
|
||||
|
||||
workdir = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
description = "Override the default working directory for the container.";
|
||||
example = "/var/lib/hello_world";
|
||||
};
|
||||
|
||||
dependsOn = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = [];
|
||||
description = ''
|
||||
Define which other containers this one depends on. They will be added to both After and Requires for the unit.
|
||||
|
||||
Use the same name as the attribute under `virtualisation.oci-containers.containers`.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
containers = {
|
||||
node1 = {};
|
||||
node2 = {
|
||||
dependsOn = [ "node1" ];
|
||||
}
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
hostname = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
description = "The hostname of the container.";
|
||||
example = "hello-world";
|
||||
};
|
||||
|
||||
preRunExtraOptions = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = [];
|
||||
description = "Extra options for podman that go before the `run` argument.";
|
||||
example = ["--runtime" "runsc"];
|
||||
};
|
||||
|
||||
extraOptions = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = [];
|
||||
description = "Extra options for podman run`.";
|
||||
example = literalExpression ''
|
||||
["--network=host"]
|
||||
'';
|
||||
};
|
||||
|
||||
autoStart = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
When enabled, the container is automatically started on boot.
|
||||
If this option is set to false, the container has to be started on-demand via its service.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
mkService = name: container: let
|
||||
podman = lib.getExe pkgs.podman;
|
||||
rm = lib.getExe' pkgs.coreutils "rm";
|
||||
printf = lib.getExe' pkgs.coreutils "printf";
|
||||
getId = ''"$(${lib.getExe' pkgs.coreutils "id"} -u)"'';
|
||||
dependsOn = map (x: "podman-${x}.service") container.dependsOn;
|
||||
escapedName = lib.escapeShellArg name;
|
||||
preStartScript = pkgs.writeShellApplication {
|
||||
name = "pre-start";
|
||||
runtimeInputs = [];
|
||||
text = ''
|
||||
${printf} "Running pre-start script.."
|
||||
${podman} rm -f ${name} || true
|
||||
${lib.optionalString (container.imageFile != null) ''
|
||||
${podman} load -i ${container.imageFile}
|
||||
''}
|
||||
${rm} -f /run/user/${getId}/podman-${escapedName}.ctr-id
|
||||
${printf} " success.\nStarting Podman...\n"
|
||||
'';
|
||||
};
|
||||
script = concatStringsSep " \\\n " (
|
||||
[
|
||||
"exec ${podman} "
|
||||
]
|
||||
++ map escapeShellArg container.preRunExtraOptions
|
||||
++ [
|
||||
"run"
|
||||
"--log-level=debug"
|
||||
"--rm"
|
||||
"--name=${escapedName}"
|
||||
"--log-driver=${container.log-driver}"
|
||||
]
|
||||
++ optional (container.entrypoint != null)
|
||||
"--entrypoint=${escapeShellArg container.entrypoint}"
|
||||
++ optional (container.hostname != null)
|
||||
"--hostname=${escapeShellArg container.hostname}"
|
||||
++ optional (container.pullPolicy != null)
|
||||
"--pull=${container.pullPolicy}"
|
||||
++ [
|
||||
"--cidfile=/run/user/${getId}/podman-${escapedName}.ctr-id"
|
||||
# "--sdnotify=ignore"
|
||||
"--cgroups=no-conmon"
|
||||
"--sdnotify=conmon"
|
||||
"-d"
|
||||
"--replace"
|
||||
]
|
||||
++ (mapAttrsToList (k: v: "-e ${escapeShellArg k}=${escapeShellArg v}") container.environment)
|
||||
++ map (f: "--env-file ${escapeShellArg f}") container.environmentFiles
|
||||
++ map (p: "-p ${escapeShellArg p}") container.ports
|
||||
++ optional (container.user != null) "-u ${escapeShellArg container.user}"
|
||||
++ map (v: "-v ${escapeShellArg v}") container.volumes
|
||||
++ (mapAttrsToList (k: v: "-l ${escapeShellArg k}=${escapeShellArg v}") container.labels)
|
||||
++ optional (container.workdir != null) "-w ${escapeShellArg container.workdir}"
|
||||
++ map escapeShellArg container.extraOptions
|
||||
++ [container.image]
|
||||
++ map escapeShellArg container.cmd
|
||||
);
|
||||
|
||||
inherit (lib) concatStringsSep escapeShellArg optional optionalAttrs optionalString mapAttrsToList;
|
||||
in {
|
||||
Unit = {
|
||||
WantedBy = [] ++ lib.optional (container.autoStart) "default.target"; # graphical-session instead maybe?
|
||||
After = dependsOn;
|
||||
Requires = dependsOn;
|
||||
# StopWhenUnneeded = true;
|
||||
};
|
||||
# TODO make network target..
|
||||
# wants = lib.optional (container.imageFile == null) "network-online.target";
|
||||
# after = lib.optionals (container.imageFile == null) [ "network-online.target" ]
|
||||
# ++ dependsOn;
|
||||
# environment = proxy_env;
|
||||
|
||||
Service = {
|
||||
### There is no generalized way of supporting `reload` for docker
|
||||
### containers. Some containers may respond well to SIGHUP sent to their
|
||||
### init process, but it is not guaranteed; some apps have other reload
|
||||
### mechanisms, some don't have a reload signal at all, and some docker
|
||||
### images just have broken signal handling. The best compromise in this
|
||||
### case is probably to leave ExecReload undefined, so `systemctl reload`
|
||||
### will at least result in an error instead of potentially undefined
|
||||
### behaviour.
|
||||
###
|
||||
### Advanced users can still override this part of the unit to implement
|
||||
### a custom reload handler, since the result of all this is a normal
|
||||
### systemd service from the perspective of the NixOS module system.
|
||||
###
|
||||
# ExecReload = ...;
|
||||
###
|
||||
ExecStartPre = ["${preStartScript}/bin/pre-start"];
|
||||
ExecStart = [
|
||||
"${pkgs.writeShellScript "start" script}"
|
||||
];
|
||||
ExecStop = [
|
||||
"${podman} stop --ignore --cidfile=/run/user/${getId}/podman-${escapedName}.ctr-id"
|
||||
"${podman} rm -f --ignore --cidfile=/run/user/${getId}/podman-${escapedName}.ctr-id"
|
||||
];
|
||||
# TimeoutStartSec = 0;
|
||||
# TimeoutStopSec = 120;
|
||||
# Restart = "always";
|
||||
Environment = ["PODMAN_SYSTEMD_UNIT=podman-${name}.service"];
|
||||
Type = "notify";
|
||||
NotifyAccess = "all";
|
||||
# Type = "exec";
|
||||
};
|
||||
};
|
||||
|
||||
cfg = config.containers;
|
||||
inherit (lib) mapAttrs' nameValuePair mkOption types mkIf literalExpression;
|
||||
in {
|
||||
options.containers = mkOption {
|
||||
default = {};
|
||||
type = with types; attrsOf (submodule containerOptions);
|
||||
};
|
||||
|
||||
config = mkIf (cfg != {}) {
|
||||
systemd.user.services = mapAttrs' (k: v: nameValuePair "podman-${k}" (mkService k v)) cfg;
|
||||
};
|
||||
}
|
176
home/util/cosmic.nix
Normal file
176
home/util/cosmic.nix
Normal file
|
@ -0,0 +1,176 @@
|
|||
{
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}: let
|
||||
inherit (lib) filterAttrs concatStrings concatStringsSep mapAttrsToList concatLists foldlAttrs concatMapAttrs mapAttrs mapAttrs' nameValuePair boolToString;
|
||||
inherit (builtins) typeOf toString stringLength;
|
||||
|
||||
# build up serialisation machinery from here for various types
|
||||
|
||||
# list -> array
|
||||
array = a: "[${concatStringsSep "," a}]";
|
||||
# attrset -> hashmap
|
||||
_assoc = a: mapAttrsToList (name: val: "${name}: ${val}") a;
|
||||
assoc = a: '' {
|
||||
${concatStringsSep ",\n" (concatLists (map _assoc a))}
|
||||
}'';
|
||||
# attrset -> struct
|
||||
_struct_kv = k: v:
|
||||
if v == null
|
||||
then ""
|
||||
else (concatStringsSep ":" [k (serialise.${typeOf v} v)]);
|
||||
_struct_concat = s:
|
||||
foldlAttrs (
|
||||
acc: k: v:
|
||||
if stringLength acc > 0
|
||||
then concatStringsSep ", " [acc (_struct_kv k v)]
|
||||
else _struct_kv k v
|
||||
) ""
|
||||
s;
|
||||
_struct_filt = s:
|
||||
_struct_concat (filterAttrs (k: v: v != null) s);
|
||||
struct = s: "(${_struct_filt s})";
|
||||
toQuotedString = s: ''"${toString s}"'';
|
||||
|
||||
# make an attrset for struct serialisation
|
||||
serialise = {
|
||||
int = toString;
|
||||
float = toString;
|
||||
bool = boolToString;
|
||||
string = toString;
|
||||
path = toString;
|
||||
null = toString;
|
||||
set = struct;
|
||||
list = array;
|
||||
};
|
||||
|
||||
# define the key for a keybind
|
||||
defineBinding = binding:
|
||||
struct {
|
||||
inherit (binding) modifiers;
|
||||
key =
|
||||
if isNull binding.key
|
||||
then null
|
||||
else toQuotedString binding.key;
|
||||
};
|
||||
|
||||
# map keybinding from list of attrset to hashmap of (mod,key): action
|
||||
_mapBindings = bindings:
|
||||
map (
|
||||
inner: {"${defineBinding inner}" = maybeToString (checkAction inner.action);}
|
||||
)
|
||||
bindings;
|
||||
mapBindings = bindings:
|
||||
assoc (_mapBindings bindings);
|
||||
|
||||
# check a keybinding's action
|
||||
# escape with quotes if it's a Spawn action
|
||||
checkAction = a:
|
||||
if typeOf a == "set" && a.type == "Spawn"
|
||||
then {
|
||||
inherit (a) type;
|
||||
data = toQuotedString a.data;
|
||||
}
|
||||
else a;
|
||||
|
||||
maybeToString = s:
|
||||
if typeOf s == "set"
|
||||
then concatStrings [s.type "(" (toString s.data) ")"]
|
||||
else s;
|
||||
|
||||
mapCosmicSettings = application: options:
|
||||
mapAttrs' (k: v:
|
||||
nameValuePair "cosmic/${application}/v${options.version}/${k}" {
|
||||
enable = true;
|
||||
text = serialise.${typeOf v} v;
|
||||
})
|
||||
options.option;
|
||||
# deconstructKeybindAttr = attr: mapAttrs' (
|
||||
# name: value:
|
||||
# ) attr;
|
||||
# rec_attr = lib.types.attrsOf (lib.types.oneOf lib.types.string rec_attr);
|
||||
in {
|
||||
options.cosmic = {
|
||||
enable = with lib;
|
||||
mkOption {
|
||||
default = false;
|
||||
type = types.bool;
|
||||
};
|
||||
|
||||
defaultKeybindings = with lib;
|
||||
mkOption {
|
||||
default = true;
|
||||
type = types.bool;
|
||||
};
|
||||
|
||||
# binds = with lib;
|
||||
# mkOption {
|
||||
# default = {};
|
||||
# type = with types; rec_attr;
|
||||
# };
|
||||
|
||||
keybindings = with lib;
|
||||
mkOption {
|
||||
default = [];
|
||||
type = with types;
|
||||
listOf (submodule {
|
||||
options = {
|
||||
modifiers = mkOption {
|
||||
type = listOf str;
|
||||
default = [];
|
||||
};
|
||||
key = mkOption {
|
||||
type = nullOr str;
|
||||
default = null;
|
||||
};
|
||||
action = mkOption {
|
||||
type = either str (submodule {
|
||||
options = {
|
||||
type = mkOption {
|
||||
type = str;
|
||||
};
|
||||
data = mkOption {
|
||||
type = oneOf [str int];
|
||||
default = "";
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
otherSettings = with lib;
|
||||
mkOption {
|
||||
default = {};
|
||||
type = with types;
|
||||
attrsOf (submodule {
|
||||
options = {
|
||||
version = mkOption {
|
||||
type = str;
|
||||
default = "1";
|
||||
};
|
||||
option = mkOption {
|
||||
type = attrsOf anything;
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
# TODO we need to split between system_actions, workspaces, custom
|
||||
config.xdg.configFile =
|
||||
{
|
||||
"cosmic/com.system76.CosmicSettings.Shortcuts/v1/custom".text = mapBindings config.cosmic.keybindings;
|
||||
"cosmic/com.system76.CosmicSettings.Shortcuts/v1/defaults" = {
|
||||
text = "{}";
|
||||
enable = !config.cosmic.defaultKeybindings;
|
||||
};
|
||||
}
|
||||
// concatMapAttrs (
|
||||
application: options:
|
||||
mapCosmicSettings application options
|
||||
)
|
||||
config.cosmic.otherSettings;
|
||||
}
|
|
@ -4,15 +4,22 @@
|
|||
...
|
||||
}: let
|
||||
inherit (builtins) getAttr stringLength substring;
|
||||
inherit (lib) mkOption getExe;
|
||||
inherit (lib) mkOption getExe listToAttrs attrsToList imap;
|
||||
inherit (lib.attrsets) filterAttrs mapAttrs mapAttrs' nameValuePair;
|
||||
inherit (lib.strings) concatStringsSep toUpper;
|
||||
|
||||
enumerate = a:
|
||||
listToAttrs (imap (id: {
|
||||
name,
|
||||
value,
|
||||
}: {
|
||||
inherit name;
|
||||
value = value // {inherit id;};
|
||||
}) (attrsToList a));
|
||||
make-app-profiles = cfg:
|
||||
mapAttrs' (name: cfg:
|
||||
nameValuePair "home-manager-webapp-${name}" {
|
||||
inherit (cfg) id;
|
||||
|
||||
userChrome = ''
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
|
@ -82,10 +89,10 @@ in {
|
|||
description = "The URL of the webapp to launch.";
|
||||
};
|
||||
|
||||
id = mkOption {
|
||||
type = int;
|
||||
description = "The Firefox profile ID to set.";
|
||||
};
|
||||
# id = mkOption {
|
||||
# type = int;
|
||||
# description = "The Firefox profile ID to set.";
|
||||
# };
|
||||
|
||||
hidden = mkOption {
|
||||
type = bool;
|
||||
|
@ -174,7 +181,7 @@ in {
|
|||
};
|
||||
|
||||
config = {
|
||||
programs.firefox.profiles = make-app-profiles config.programs.firefox.webapps;
|
||||
programs.firefox.profiles = make-app-profiles (enumerate config.programs.firefox.webapps);
|
||||
|
||||
xdg.desktopEntries =
|
||||
mapAttrs (name: cfg: {
|
||||
|
|
|
@ -7,32 +7,84 @@
|
|||
inherit (lib) mkOption;
|
||||
inherit (lib.attrsets) mapAttrs mapAttrs' nameValuePair;
|
||||
inherit (lib) getExe getExe';
|
||||
# make a firefox webapp entry for the client app
|
||||
# make a firefox webapp + hidden .desktop entry for the client app
|
||||
make-firefox = cfg:
|
||||
mapAttrs' (
|
||||
name: cfg:
|
||||
nameValuePair "${name}-client"
|
||||
{
|
||||
inherit (cfg) name id;
|
||||
inherit (cfg) name;
|
||||
url = "http://127.0.0.1:${builtins.toString cfg.port}";
|
||||
extraSettings = config.programs.firefox.profiles.default.settings;
|
||||
hidden = true;
|
||||
}
|
||||
)
|
||||
cfg;
|
||||
# make a systemd service for running the backend
|
||||
make-systemd = cfg:
|
||||
# make a systemd service for running the frontend
|
||||
make-systemd-service = cfg:
|
||||
mapAttrs' (
|
||||
name: cfg:
|
||||
if
|
||||
(cfg.service
|
||||
!= null)
|
||||
then
|
||||
nameValuePair "${cfg.name}-frontend" {
|
||||
Unit = {
|
||||
Description = "${cfg.name} Frontend";
|
||||
WantedBy = lib.mkForce [];
|
||||
};
|
||||
|
||||
Service = cfg.service;
|
||||
}
|
||||
else nameValuePair "" {}
|
||||
)
|
||||
cfg;
|
||||
# modify systemd units to be PartOf this target
|
||||
modify-systemd-services = cfg:
|
||||
lib.listToAttrs (lib.flatten (lib.mapAttrsToList (
|
||||
name: cfg: (map (
|
||||
req: {
|
||||
name = "${req}";
|
||||
value = {
|
||||
Unit = {
|
||||
PartOf = "${lib.toLower cfg.name}.target";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
cfg.requires.services)
|
||||
)
|
||||
cfg));
|
||||
modify-quadlets = cfg:
|
||||
lib.listToAttrs (lib.flatten (lib.mapAttrsToList (
|
||||
name: cfg: (map (
|
||||
req: {
|
||||
name = "${req}";
|
||||
value = {
|
||||
unitConfig = {
|
||||
PartOf = "${lib.toLower cfg.name}.target";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
cfg.requires.containers)
|
||||
)
|
||||
cfg));
|
||||
# make a systemd target to collate dependencies
|
||||
make-systemd-target = cfg:
|
||||
mapAttrs (
|
||||
name: cfg: {
|
||||
Unit = {
|
||||
Description = "${cfg.name} Backend";
|
||||
Description = "${cfg.name} Target";
|
||||
WantedBy = lib.mkForce [];
|
||||
Requires =
|
||||
(map (req: req + ".service") cfg.requires.services)
|
||||
++ (map (req: "podman-" + req + ".service") cfg.requires.containers);
|
||||
};
|
||||
Service = cfg.service;
|
||||
}
|
||||
)
|
||||
cfg;
|
||||
# make desktop shortcuts and a script which will handle starting both the above
|
||||
# make desktop shortcuts and a script which will handle starting everything
|
||||
make-xdg = cfg:
|
||||
mapAttrs (
|
||||
name: cfg: {
|
||||
|
@ -42,45 +94,72 @@
|
|||
notify-send = "${getExe' pkgs.libnotify "notify-send"} -a \"${cfg.name}\"";
|
||||
systemctl = "${getExe' pkgs.systemd "systemctl"}";
|
||||
dex = "${getExe pkgs.dex}";
|
||||
curl = "${getExe pkgs.curl}";
|
||||
podman = "${getExe pkgs.podman}";
|
||||
makeContainerCheck = container: ''[ "$(${podman} inspect -f {{.State.Health.Status}} ${container})" == "healthy" ]'';
|
||||
# makeContainerCheck = container: ''
|
||||
# [ ${podman} inspect -f {{.State.Status}} ${container})" != "running" ]
|
||||
# '';
|
||||
containerChecks =
|
||||
if (cfg.requires.containers != [])
|
||||
then
|
||||
''
|
||||
container_checks() {
|
||||
if ''
|
||||
+ (lib.concatMapStringsSep " && "
|
||||
(container: makeContainerCheck container)
|
||||
cfg.requires.containers)
|
||||
+ ''
|
||||
; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
''
|
||||
else ''
|
||||
container_checks() {
|
||||
return 0
|
||||
}
|
||||
'';
|
||||
in
|
||||
pkgs.writeShellScript "${name}"
|
||||
''
|
||||
set -euo pipefail
|
||||
${notify-send} "Launching backend.." "Please be patient."
|
||||
${systemctl} --user start ${name}
|
||||
attempts=0
|
||||
success=false
|
||||
|
||||
while [[ $attempts -lt $((20*9)) ]]; do
|
||||
if [[ $(${curl} -sf http://127.0.0.1:${builtins.toString cfg.port} --output /dev/null; printf $?) -eq 0 ]]; then
|
||||
${notify-send} "Backend up." "Launching client.."
|
||||
success=true
|
||||
break
|
||||
else
|
||||
attempts=$((attempts+1))
|
||||
if [[ $(($attempts % 20)) -eq 0 ]]; then
|
||||
${notify-send} "Launching backend.." "Still launching.. ($((attempts/2))s)"
|
||||
fi
|
||||
exit_error() {
|
||||
${notify-send} -w "Failure" $1
|
||||
exit 1
|
||||
}
|
||||
|
||||
${containerChecks}
|
||||
|
||||
${notify-send} "Launching ${name} backend.." "Please be patient."
|
||||
${systemctl} --user start ${name}.target || exit_error "Failed to launch!"
|
||||
|
||||
checks=0
|
||||
until container_checks; do
|
||||
sleep 2
|
||||
checks=$((checks+1))
|
||||
if [ $((checks%10)) -eq 0 ]; then
|
||||
${notify-send} "Waiting for backend."
|
||||
fi
|
||||
if [ $checks -ge 60 ]; then
|
||||
${systemctl} --no-block --user stop ${name}.target
|
||||
exit_error "Failed to launch!"
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [[ ! $success ]]; then
|
||||
${notify-send} "Failure" "Failed to launch backend!"
|
||||
${systemctl} --user kill ${name}
|
||||
exit 1
|
||||
fi
|
||||
|
||||
${notify-send} "Launching ${name}.."
|
||||
${dex} -w ~/.nix-profile/share/applications/${name}-client.desktop
|
||||
|
||||
${notify-send} "Goodbye" "Shutting down."
|
||||
${systemctl} --user stop ${name}
|
||||
${systemctl} --user stop ${name}.target
|
||||
exit 0
|
||||
''}";
|
||||
}
|
||||
)
|
||||
cfg;
|
||||
cfg = config.localWebApps;
|
||||
in {
|
||||
options.localWebApps = mkOption {
|
||||
default = {};
|
||||
|
@ -104,29 +183,59 @@ in {
|
|||
default = null;
|
||||
};
|
||||
|
||||
id = mkOption {
|
||||
type = int;
|
||||
description = "Firefox profile ID for the webapp's client";
|
||||
requires = mkOption {
|
||||
type = nullOr (submodule {
|
||||
options = {
|
||||
containers = mkOption {
|
||||
type = listOf str;
|
||||
default = [];
|
||||
};
|
||||
services = mkOption {
|
||||
type = listOf str;
|
||||
default = [];
|
||||
};
|
||||
};
|
||||
});
|
||||
default = null;
|
||||
description = "Containers or services this app requires.";
|
||||
};
|
||||
|
||||
service = mkOption {
|
||||
type = nullOr (submodule {
|
||||
options = {
|
||||
execStartPre = mkOption {
|
||||
type = nullOr str;
|
||||
default = null;
|
||||
};
|
||||
execStart = mkOption {
|
||||
type = nullOr str;
|
||||
default = null;
|
||||
};
|
||||
execStop = mkOption {
|
||||
type = nullOr str;
|
||||
default = null;
|
||||
};
|
||||
};
|
||||
});
|
||||
default = null;
|
||||
description = "Submodule containing exec[StartPre/Start/Stop] commands for any required systemd service";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = int;
|
||||
description = "Local port the webapp should host on.";
|
||||
};
|
||||
|
||||
service = mkOption {
|
||||
type = attrsOf str;
|
||||
description = "The service settings for systemd user service. Requires at least ExecStart.";
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
config = {
|
||||
programs.firefox.webapps = make-firefox config.localWebApps;
|
||||
programs.firefox.webapps = make-firefox cfg;
|
||||
|
||||
systemd.user.services = make-systemd config.localWebApps;
|
||||
systemd.user.targets = make-systemd-target cfg;
|
||||
systemd.user.services = (make-systemd-service cfg) // (modify-systemd-services cfg);
|
||||
services.podman.containers = modify-quadlets cfg;
|
||||
|
||||
xdg.desktopEntries = make-xdg config.localWebApps;
|
||||
xdg.desktopEntries = make-xdg cfg;
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue