From c0cccc298dbe4b8accd85cf6222a9e946656afec Mon Sep 17 00:00:00 2001 From: oddlama Date: Mon, 18 Mar 2024 23:37:26 +0100 Subject: [PATCH] feat(topology): generalize icon definitions and assertions --- topology/icons/devices/ethernet.svg | 57 +++++++++++++++++++++ topology/nixos/extractors/services.nix | 4 +- topology/nixos/module.nix | 10 +++- topology/options/icons.nix | 46 ++++++++++++----- topology/options/interfaces.nix | 23 ++++----- topology/options/nodes.nix | 37 ++++++++++++- topology/options/services.nix | 19 ++++--- topology/topology/default.nix | 8 +++ topology/topology/renderers/svg/default.nix | 11 ++-- 9 files changed, 170 insertions(+), 45 deletions(-) create mode 100644 topology/icons/devices/ethernet.svg diff --git a/topology/icons/devices/ethernet.svg b/topology/icons/devices/ethernet.svg new file mode 100644 index 0000000..1d8045a --- /dev/null +++ b/topology/icons/devices/ethernet.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/topology/nixos/extractors/services.nix b/topology/nixos/extractors/services.nix index 3302676..bccd744 100644 --- a/topology/nixos/extractors/services.nix +++ b/topology/nixos/extractors/services.nix @@ -14,7 +14,7 @@ in { openssh = mkIf config.services.openssh.enable { hidden = mkDefault true; # Causes a lot of much clutter name = "OpenSSH"; - icon = "openssh"; + icon = "services.openssh"; info = "port: ${concatStringsSep ", " (map toString config.services.openssh.ports)}"; }; @@ -25,7 +25,7 @@ in { in mkIf config.services.vaultwarden.enable { name = "Vaultwarden"; - icon = "vaultwarden"; + icon = "services.vaultwarden"; info = mkIf (domain != null) domain; details.listen = mkIf (address != null && port != null) {text = "${address}:${toString port}";}; }; diff --git a/topology/nixos/module.nix b/topology/nixos/module.nix index 3e71c87..6888947 100644 --- a/topology/nixos/module.nix +++ b/topology/nixos/module.nix @@ -45,10 +45,18 @@ in { default = config.networking.hostName; type = types.str; }; + + isMainModule = mkOption { + description = "Whether this is the toplevel topology module."; + readOnly = true; + internal = true; + default = false; + type = types.bool; + }; }; config.topology = { # Ensure a node exists for this host - nodes.${config.topology.id}.type = "nixos"; + nodes.${config.topology.id}.deviceType = "nixos"; }; } diff --git a/topology/options/icons.nix b/topology/options/icons.nix index 4bb94f7..d2ff600 100644 --- a/topology/options/icons.nix +++ b/topology/options/icons.nix @@ -7,7 +7,11 @@ f: { (lib) concatStringsSep filterAttrs + getAttrFromPath + hasAttrByPath + hasPrefix init + mapAttrs mapAttrs' mkOption nameValuePair @@ -30,22 +34,38 @@ f: { }; }; }; - - mkIcons = name: - mkOption { - description = "Defines icons for ${name}."; - default = {}; - type = types.attrsOf iconType; - }; in f { - options.icons = { - interfaces = mkIcons "network interface types"; - services = mkIcons "services"; + options.icons = mkOption { + description = "All predefined icons by category."; + default = {}; + type = types.attrsOf (types.attrsOf iconType); }; - config.icons = { - interfaces = iconsFromFiles ../icons/interfaces; - services = iconsFromFiles ../icons/services; + config = { + # The necessary assertion for an icon type + 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)); }; } diff --git a/topology/options/interfaces.nix b/topology/options/interfaces.nix index 7679f9e..d3e265d 100644 --- a/topology/options/interfaces.nix +++ b/topology/options/interfaces.nix @@ -9,6 +9,7 @@ f: { flatten flip mkDefault + mkIf mkOption types ; @@ -48,8 +49,9 @@ in }; 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."; - type = types.nullOr types.str; + description = "The icon representing this interface's type. Must be a path to an image or a valid icon name (.). By default an icon will be selected based on the type."; + type = types.nullOr (types.either types.path types.str); + default = null; }; addresses = mkOption { @@ -90,14 +92,9 @@ in }; config = { - icon = mkDefault ( - { - ethernet = "ethernet"; - wireguard = "wireguard"; - wifi = "wifi"; - } - .${submod.config.type} - or null + # Set the default icon, if an icon exists with a matching name + icon = mkIf (config.topology.isMainModule && config.icons.interfaces ? ${submod.config.type}) ( + mkDefault ("interfaces." + submod.config.type) ); }; })); @@ -116,10 +113,8 @@ in assertion = interface.network != null -> config.networks ? ${interface.network}; message = "topology: nodes.${node.id}.interfaces.${interface.id} refers to an unknown network '${interface.network}'"; } - { - assertion = interface.icon != null -> config.icons.interfaces ? ${interface.icon}; - message = "topology: nodes.${node.id}.interfaces.${interface.id} refers to an unknown icon icons.interfaces.${interface.icon}"; - } + (config.lib.assertions.iconValid + interface.icon "nodes.${node.id}.interfaces.${interface.id}") ] ++ flip map interface.physicalConnections ( physicalConnection: { diff --git a/topology/options/nodes.nix b/topology/options/nodes.nix index 13c4942..4a34ea7 100644 --- a/topology/options/nodes.nix +++ b/topology/options/nodes.nix @@ -5,7 +5,12 @@ f: { }: let inherit (lib) + attrValues + flatten + flip literalExpression + mkDefault + mkIf mkOption types ; @@ -33,22 +38,52 @@ in defaultText = literalExpression ''""''; }; + 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 (.)."; + type = types.nullOr (types.either types.path types.str); + default = null; + }; + # FIXME: TODO emoji / icon # FIXME: TODO hardware description "Odroid H3" # FIXME: TODO hardware image # FIXME: TODO are these good types? how about nixos vs router vs ... - type = mkOption { + deviceType = mkOption { description = "TODO"; 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 (.). By default an icon will be selected based on the deviceType."; + type = types.nullOr (types.either types.path types.str); + default = null; + }; + parent = mkOption { description = "The id of the parent node, if this node has a parent."; default = null; 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") + ] + ) + ); + }; } diff --git a/topology/options/services.nix b/topology/options/services.nix index 4851d3f..e770396 100644 --- a/topology/options/services.nix +++ b/topology/options/services.nix @@ -8,6 +8,8 @@ f: { attrValues flatten flip + mkDefault + mkIf mkOption types ; @@ -40,8 +42,8 @@ in }; icon = mkOption { - description = "The icon for this service. Must be a valid entry in icons.services."; - type = types.nullOr types.str; + description = "The icon for this service. Must be a path to an image or a valid icon name (.)."; + type = types.nullOr (types.either types.path types.str); 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: flip map (attrValues node.services) ( service: [ - { - assertion = service.icon != null -> config.icons.services ? ${service.icon}; - message = "topology: nodes.${node.id}.services.${service.id} refers to an unknown icon icons.services.${service.icon}"; - } + (config.lib.assertions.iconValid + service.icon "nodes.${node.id}.services.${service.id}") ] ) )); diff --git a/topology/topology/default.nix b/topology/topology/default.nix index e295283..67d7dc7 100644 --- a/topology/topology/default.nix +++ b/topology/topology/default.nix @@ -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 diff --git a/topology/topology/renderers/svg/default.nix b/topology/topology/renderers/svg/default.nix index f56c10e..34921ea 100644 --- a/topology/topology/renderers/svg/default.nix +++ b/topology/topology/renderers/svg/default.nix @@ -34,11 +34,6 @@ ''; in "${drv}/${name}.svg"; - getIcon = registry: iconName: - if iconName == null - then null - else config.icons.${registry}.${iconName}.file or null; - html = rec { mkImage = twAttrs: file: if file == null @@ -110,7 +105,7 @@
- ${mkImage "w-6 h-6 mr-2" (getIcon "interfaces" interface.icon)} + ${mkImage "w-6 h-6 mr-2" (config.lib.icons.get interface.icon)} ${interface.id}
addrs: ${toString interface.addresses} @@ -140,7 +135,7 @@ ''
- ${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)}

${service.name}

${optionalString (service.info != "") ''

${service.info}

''} @@ -158,7 +153,7 @@

${node.name}

-

${node.type}

+

${node.deviceType}

'';