feat(topology): generalize icon definitions and assertions

This commit is contained in:
oddlama 2024-03-18 23:37:26 +01:00
parent 23564f980e
commit c0cccc298d
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
9 changed files with 170 additions and 45 deletions

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<g>
<g>
<path d="M168,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C180,189.412,174.624,184.036,168,184.036z"/>
</g>
</g>
<g>
<g>
<path d="M256,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C268,189.412,262.624,184.036,256,184.036z"/>
</g>
</g>
<g>
<g>
<path d="M212,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C224,189.412,218.624,184.036,212,184.036z"/>
</g>
</g>
<g>
<g>
<path d="M460,0H52C23.28,0,0,23.28,0,52v408c0,28.72,23.28,52,52,52h408c28.72,0,52-23.28,52-52V52C512,23.28,488.72,0,460,0z
M444,284h-88.024c-2.212,0-3.976,1.64-3.976,3.848V348H160v-60.152c0-2.208-1.616-3.848-3.828-3.848H68V68h44v92.184
c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h20v92.184c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h20v92.184
c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h20v92.184c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h20v92.184
c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h20v92.184c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h20v92.184
c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h44V284z"/>
</g>
</g>
<g>
<g>
<path d="M124,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C136,189.412,130.624,184.036,124,184.036z"/>
</g>
</g>
<g>
<g>
<path d="M388,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C400,189.412,394.624,184.036,388,184.036z"/>
</g>
</g>
<g>
<g>
<path d="M344,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C356,189.412,350.624,184.036,344,184.036z"/>
</g>
</g>
<g>
<g>
<path d="M300,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C312,189.412,306.624,184.036,300,184.036z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -14,7 +14,7 @@ in {
openssh = mkIf config.services.openssh.enable { openssh = mkIf config.services.openssh.enable {
hidden = mkDefault true; # Causes a lot of much clutter hidden = mkDefault true; # Causes a lot of much clutter
name = "OpenSSH"; name = "OpenSSH";
icon = "openssh"; icon = "services.openssh";
info = "port: ${concatStringsSep ", " (map toString config.services.openssh.ports)}"; info = "port: ${concatStringsSep ", " (map toString config.services.openssh.ports)}";
}; };
@ -25,7 +25,7 @@ in {
in in
mkIf config.services.vaultwarden.enable { mkIf config.services.vaultwarden.enable {
name = "Vaultwarden"; name = "Vaultwarden";
icon = "vaultwarden"; icon = "services.vaultwarden";
info = mkIf (domain != null) domain; info = mkIf (domain != null) domain;
details.listen = mkIf (address != null && port != null) {text = "${address}:${toString port}";}; details.listen = mkIf (address != null && port != null) {text = "${address}:${toString port}";};
}; };

View file

@ -45,10 +45,18 @@ in {
default = config.networking.hostName; default = config.networking.hostName;
type = types.str; type = types.str;
}; };
isMainModule = mkOption {
description = "Whether this is the toplevel topology module.";
readOnly = true;
internal = true;
default = false;
type = types.bool;
};
}; };
config.topology = { config.topology = {
# Ensure a node exists for this host # Ensure a node exists for this host
nodes.${config.topology.id}.type = "nixos"; nodes.${config.topology.id}.deviceType = "nixos";
}; };
} }

View file

@ -7,7 +7,11 @@ f: {
(lib) (lib)
concatStringsSep concatStringsSep
filterAttrs filterAttrs
getAttrFromPath
hasAttrByPath
hasPrefix
init init
mapAttrs
mapAttrs' mapAttrs'
mkOption mkOption
nameValuePair nameValuePair
@ -30,22 +34,38 @@ f: {
}; };
}; };
}; };
mkIcons = name:
mkOption {
description = "Defines icons for ${name}.";
default = {};
type = types.attrsOf iconType;
};
in in
f { f {
options.icons = { options.icons = mkOption {
interfaces = mkIcons "network interface types"; description = "All predefined icons by category.";
services = mkIcons "services"; default = {};
type = types.attrsOf (types.attrsOf iconType);
}; };
config.icons = { config = {
interfaces = iconsFromFiles ../icons/interfaces; # The necessary assertion for an icon type
services = iconsFromFiles ../icons/services; lib.assertions.iconValid = icon: path: {
assertion = icon != null -> (!hasPrefix "/" icon -> hasAttrByPath (splitString "." icon) config.icons);
message = "topology: ${path} refers to an unknown icon icons.${icon}";
};
# The function that returns the image path for an icon option
lib.icons.get = icon: let
attrPath = splitString "." icon;
in
if icon == null
then null
# Icon is a path
else if hasPrefix "/" icon
then icon
# Icon is a valid icon name
else if hasAttrByPath attrPath config.icons
then (getAttrFromPath attrPath config.icons).file
# Icon is not valid
else throw "Reference to unknown icon ${icon} detected. Aborting.";
icons =
mapAttrs (n: _: iconsFromFiles ../icons/${n})
(filterAttrs (_: v: v == "directory") (builtins.readDir ../icons));
}; };
} }

View file

@ -9,6 +9,7 @@ f: {
flatten flatten
flip flip
mkDefault mkDefault
mkIf
mkOption mkOption
types types
; ;
@ -48,8 +49,9 @@ in
}; };
icon = mkOption { icon = mkOption {
description = "The icon for this interface. Must be a valid entry in icons.interfaces. If null, an icon will be selected based on the type."; description = "The icon representing this interface's type. Must be a path to an image or a valid icon name (<category>.<name>). By default an icon will be selected based on the type.";
type = types.nullOr types.str; type = types.nullOr (types.either types.path types.str);
default = null;
}; };
addresses = mkOption { addresses = mkOption {
@ -90,14 +92,9 @@ in
}; };
config = { config = {
icon = mkDefault ( # Set the default icon, if an icon exists with a matching name
{ icon = mkIf (config.topology.isMainModule && config.icons.interfaces ? ${submod.config.type}) (
ethernet = "ethernet"; mkDefault ("interfaces." + submod.config.type)
wireguard = "wireguard";
wifi = "wifi";
}
.${submod.config.type}
or null
); );
}; };
})); }));
@ -116,10 +113,8 @@ in
assertion = interface.network != null -> config.networks ? ${interface.network}; assertion = interface.network != null -> config.networks ? ${interface.network};
message = "topology: nodes.${node.id}.interfaces.${interface.id} refers to an unknown network '${interface.network}'"; message = "topology: nodes.${node.id}.interfaces.${interface.id} refers to an unknown network '${interface.network}'";
} }
{ (config.lib.assertions.iconValid
assertion = interface.icon != null -> config.icons.interfaces ? ${interface.icon}; interface.icon "nodes.${node.id}.interfaces.${interface.id}")
message = "topology: nodes.${node.id}.interfaces.${interface.id} refers to an unknown icon icons.interfaces.${interface.icon}";
}
] ]
++ flip map interface.physicalConnections ( ++ flip map interface.physicalConnections (
physicalConnection: { physicalConnection: {

View file

@ -5,7 +5,12 @@ f: {
}: let }: let
inherit inherit
(lib) (lib)
attrValues
flatten
flip
literalExpression literalExpression
mkDefault
mkIf
mkOption mkOption
types types
; ;
@ -33,22 +38,52 @@ in
defaultText = literalExpression ''"<name>"''; defaultText = literalExpression ''"<name>"'';
}; };
icon = mkOption {
description = "The icon representing this node. Usually shown next to the name. Must be a path to an image or a valid icon name (<category>.<name>).";
type = types.nullOr (types.either types.path types.str);
default = null;
};
# FIXME: TODO emoji / icon # FIXME: TODO emoji / icon
# FIXME: TODO hardware description "Odroid H3" # FIXME: TODO hardware description "Odroid H3"
# FIXME: TODO hardware image # FIXME: TODO hardware image
# FIXME: TODO are these good types? how about nixos vs router vs ... # FIXME: TODO are these good types? how about nixos vs router vs ...
type = mkOption { deviceType = mkOption {
description = "TODO"; description = "TODO";
type = types.enum ["nixos" "microvm" "nixos-container"]; type = types.enum ["nixos" "microvm" "nixos-container"];
}; };
deviceIcon = mkOption {
description = "The icon representing this node's type. Must be a path to an image or a valid icon name (<category>.<name>). By default an icon will be selected based on the deviceType.";
type = types.nullOr (types.either types.path types.str);
default = null;
};
parent = mkOption { parent = mkOption {
description = "The id of the parent node, if this node has a parent."; description = "The id of the parent node, if this node has a parent.";
default = null; default = null;
type = types.nullOr types.str; type = types.nullOr types.str;
}; };
}; };
config = {
# Set the default icon, if an icon exists with a matching name
deviceIcon = mkIf (config.topology.isMainModule && config.icons.devices ? ${nodeSubmod.config.deviceType}) (
mkDefault ("interfaces." + nodeSubmod.config.deviceType)
);
};
})); }));
}; };
config = {
assertions = flatten (
flip map (attrValues config.nodes) (
node: [
(config.lib.assertions.iconValid
node.deviceIcon "nodes.${node.id}.deviceIcon")
]
)
);
};
} }

View file

@ -8,6 +8,8 @@ f: {
attrValues attrValues
flatten flatten
flip flip
mkDefault
mkIf
mkOption mkOption
types types
; ;
@ -40,8 +42,8 @@ in
}; };
icon = mkOption { icon = mkOption {
description = "The icon for this service. Must be a valid entry in icons.services."; description = "The icon for this service. Must be a path to an image or a valid icon name (<category>.<name>).";
type = types.nullOr types.str; type = types.nullOr (types.either types.path types.str);
default = null; default = null;
}; };
@ -77,6 +79,13 @@ in
})); }));
}; };
}; };
config = {
# Set the default icon, if an icon exists with a matching name
icon = mkIf (config.topology.isMainModule && config.icons.services ? ${submod.config.id}) (
mkDefault ("services." + submod.config.id)
);
};
})); }));
}; };
}; };
@ -88,10 +97,8 @@ in
node: node:
flip map (attrValues node.services) ( flip map (attrValues node.services) (
service: [ service: [
{ (config.lib.assertions.iconValid
assertion = service.icon != null -> config.icons.services ? ${service.icon}; service.icon "nodes.${node.id}.services.${service.id}")
message = "topology: nodes.${node.id}.services.${service.id} refers to an unknown icon icons.services.${service.icon}";
}
] ]
) )
)); ));

View file

@ -93,6 +93,14 @@ in {
}; };
}); });
}; };
topology.isMainModule = mkOption {
description = "Whether this is the toplevel topology module.";
readOnly = true;
internal = true;
default = true;
type = types.bool;
};
}; };
config = let config = let

View file

@ -34,11 +34,6 @@
''; '';
in "${drv}/${name}.svg"; in "${drv}/${name}.svg";
getIcon = registry: iconName:
if iconName == null
then null
else config.icons.${registry}.${iconName}.file or null;
html = rec { html = rec {
mkImage = twAttrs: file: mkImage = twAttrs: file:
if file == null if file == null
@ -110,7 +105,7 @@
<div tw="flex flex-row items-center my-2"> <div tw="flex flex-row items-center my-2">
<div tw="flex flex-row flex-none bg-[${color}] w-4 h-1"></div> <div tw="flex flex-row flex-none bg-[${color}] w-4 h-1"></div>
<div tw="flex flex-row flex-none items-center bg-[${color}] text-[#101419] rounded-lg px-2 py-1 w-46 h-8 mr-4"> <div tw="flex flex-row flex-none items-center bg-[${color}] text-[#101419] rounded-lg px-2 py-1 w-46 h-8 mr-4">
${mkImage "w-6 h-6 mr-2" (getIcon "interfaces" interface.icon)} ${mkImage "w-6 h-6 mr-2" (config.lib.icons.get interface.icon)}
<span tw="font-bold">${interface.id}</span> <span tw="font-bold">${interface.id}</span>
</div> </div>
<span>addrs: ${toString interface.addresses}</span> <span>addrs: ${toString interface.addresses}</span>
@ -140,7 +135,7 @@
'' ''
<div tw="flex flex-col mx-4 mt-4 bg-[#21262e] rounded-lg p-2"> <div tw="flex flex-col mx-4 mt-4 bg-[#21262e] rounded-lg p-2">
<div tw="flex flex-row items-center"> <div tw="flex flex-row items-center">
${mkImage "w-16 h-16 mr-4 rounded-lg" (getIcon "services" service.icon)} ${mkImage "w-16 h-16 mr-4 rounded-lg" (config.lib.icons.get service.icon)}
<div tw="flex flex-col grow"> <div tw="flex flex-col grow">
<h1 tw="text-xl font-bold m-0">${service.name}</h1> <h1 tw="text-xl font-bold m-0">${service.name}</h1>
${optionalString (service.info != "") ''<p tw="text-base m-0">${service.info}</p>''} ${optionalString (service.info != "") ''<p tw="text-base m-0">${service.info}</p>''}
@ -158,7 +153,7 @@
<div tw="flex flex-row mx-6 my-2"> <div tw="flex flex-row mx-6 my-2">
<h2 tw="grow text-4xl font-bold">${node.name}</h2> <h2 tw="grow text-4xl font-bold">${node.name}</h2>
<div tw="flex grow"></div> <div tw="flex grow"></div>
<h2 tw="text-4xl">${node.type}</h2> <h2 tw="text-4xl">${node.deviceType}</h2>
</div> </div>
''; '';