lib/inspection.nix
Rama main · réplica de lectura del repositorio original.
{
lib,
config,
}: let
containers = config.services.podman.containers;
stackContainers = lib.filterAttrs (_: container: container.stack != null) containers;
stackNames = lib.unique (lib.mapAttrsToList (_: container: container.stack) stackContainers);
toStringValue = value:
if builtins.isPath value
then toString value
else if builtins.isBool value || builtins.isInt value || builtins.isFloat value
then toString value
else if value == null
then "null"
else value;
normalizeValue = value:
if builtins.isPath value
then toString value
else if builtins.isAttrs value
then lib.mapAttrs (_: normalizeValue) value
else if builtins.isList value
then map normalizeValue value
else value;
normalizeContainerDependency = dependency:
if lib.hasPrefix "podman-" dependency && lib.hasSuffix ".service" dependency
then lib.removeSuffix ".service" (lib.removePrefix "podman-" dependency)
else dependency;
normalizeExtraEnv = values: let
normalized = normalizeValue values;
in
lib.filterAttrs (_: value: value != null && !(builtins.isAttrs value && value == {})) (
lib.mapAttrs (
_: value:
if builtins.isAttrs value
then lib.filterAttrs (_: field: field != null) value
else value
)
normalized
);
mkContainer = name: container: let
network = container.network or null;
networkNames =
if network == null
then []
else if builtins.isList network
then network
else [network];
in
normalizeValue {
inherit name;
stack = container.stack;
image = container.image;
networks = networkNames;
port = container.port or null;
volumes = container.volumes or [];
volumeMap = container.volumeMap or {};
environment = container.environment or {};
extraEnv = normalizeExtraEnv (container.extraEnv or {});
environmentFile = container.environmentFile or [];
dependencies = {
requires = lib.unique (map normalizeContainerDependency (container.dependsOnContainer or []) ++ (container.dependsOn or []));
wants = lib.unique (map normalizeContainerDependency (container.wantsContainer or []) ++ (container.wants or []));
};
integrations = {
traefik = {
name = container.traefik.name or null;
serviceHost = container.traefik.serviceHost or null;
serviceUrl = container.traefik.serviceUrl or null;
expose = container.expose or null;
};
dashboard = container.dashboard or {};
};
};
stackData = lib.mapAttrsToList (name: container: mkContainer name container) stackContainers;
networks = lib.unique (lib.concatMap (container: container.networks) stackData);
edges =
lib.concatMap (
container:
map (dependency: {
from = container.name;
to = dependency;
kind = "requires";
})
container.dependencies.requires
++ map (dependency: {
from = container.name;
to = dependency;
kind = "wants";
})
container.dependencies.wants
++ map (network: {
from = container.name;
to = network;
kind = "network";
})
container.networks
++ lib.optional (container.integrations.traefik.name != null) {
from = container.name;
to = "route:${container.integrations.traefik.name}";
kind = "route";
}
)
stackData;
data = {
inherit networks edges;
containers = stackData;
};
renderContainer = container: let
field = label: value: let
labelWidth = 12;
padding = lib.concatStrings (lib.replicate (labelWidth - builtins.stringLength label) " ");
in " ${label}:${padding} ${value}";
formatList = values:
if values == []
then "none"
else lib.concatStringsSep ", " (map toStringValue values);
volumeLines = map (volume: " ${volume}") container.volumes;
environmentLines =
lib.mapAttrsToList (name: value: " ${name}=${toStringValue value}") container.environment;
extraEnvironmentLines =
lib.mapAttrsToList (
name: value:
" ${name}="
+ (
if builtins.isAttrs value
then builtins.toJSON value
else toStringValue value
)
)
container.extraEnv;
dashboardParts = lib.filter (value: value != null) [
(container.integrations.dashboard.name or null)
(container.integrations.dashboard.category or null)
(container.integrations.dashboard.icon or null)
];
traefikValue =
if container.integrations.traefik.name == null
then "not configured"
else "${toStringValue container.integrations.traefik.serviceUrl} (${
if container.integrations.traefik.expose == true
then "public"
else "private"
})";
dashboardValue =
if dashboardParts == []
then "not configured"
else lib.concatStringsSep " / " (map toStringValue dashboardParts);
in ''
${container.name} (${container.stack})
${field "Image" (toStringValue container.image)}
${field "Networks" (formatList container.networks)}
${field "Port" (
if container.port == null
then "not configured"
else toStringValue container.port
)}
${field "Requires" (formatList container.dependencies.requires)}
${field "Wants" (formatList container.dependencies.wants)}
${field "Traefik" traefikValue}
${field "Dashboard" dashboardValue}
Volumes:
${
if volumeLines == []
then " none"
else lib.concatStringsSep "\n" volumeLines
}
Environment (resolved):
${
if environmentLines == []
then " none"
else lib.concatStringsSep "\n" environmentLines
}
ExtraEnv sources:
${
if extraEnvironmentLines == []
then " none"
else lib.concatStringsSep "\n" extraEnvironmentLines
}
Environment files:
${
if container.environmentFile == []
then " none"
else lib.concatStringsSep "\n" (map (file: " ${file}") container.environmentFile)
}
'';
escapeDot = value:
lib.replaceStrings ["\\" "\"" "\n" "\r"] ["\\\\" "\\\"" "\\n" ""] (toString value);
renderDot = selectedContainers: let
selectedNetworks = lib.unique (lib.concatMap (container: container.networks) selectedContainers);
selectedNames = map (container: container.name) selectedContainers;
routedContainers = lib.filter (container: container.integrations.traefik.name != null) selectedContainers;
dependencies = lib.unique (lib.concatMap (container: container.dependencies.requires ++ container.dependencies.wants) selectedContainers);
externalDependencies = lib.filter (dependency: !(builtins.elem dependency selectedNames)) dependencies;
dependencyTarget = dependency:
if builtins.elem dependency selectedNames
then "container:${escapeDot dependency}"
else "dependency:${escapeDot dependency}";
nodes =
map (container: ''"container:${escapeDot container.name}" [label="${escapeDot container.name}\n${escapeDot container.image}"];'') selectedContainers
++ map (network: ''"network:${escapeDot network}" [shape=ellipse,label="${escapeDot network}"];'') selectedNetworks
++ map (container: ''"route:${escapeDot container.integrations.traefik.name}" [shape=component,label="${escapeDot container.integrations.traefik.serviceHost}\n${escapeDot container.integrations.traefik.serviceUrl}"];'') routedContainers
++ map (dependency: ''"dependency:${escapeDot dependency}" [shape=box,label="${escapeDot dependency} (outside selection)"];'') externalDependencies;
containerEdges =
lib.concatMap (
container:
map (dependency: ''"container:${escapeDot container.name}" -> "${dependencyTarget dependency}" [label="requires"];'') container.dependencies.requires
++ map (dependency: ''"container:${escapeDot container.name}" -> "${dependencyTarget dependency}" [style=dashed,label="wants"];'') container.dependencies.wants
++ map (network: ''"container:${escapeDot container.name}" -> "network:${escapeDot network}" [style=dotted,label="network"];'') container.networks
++ lib.optional (container.integrations.traefik.name != null) ''"container:${escapeDot container.name}" -> "route:${escapeDot container.integrations.traefik.name}" [label="route"];''
)
selectedContainers;
in ''
digraph nps {
rankdir=LR;
${lib.concatStringsSep "\n" (nodes ++ containerEdges)}
}
'';
mkRender = selectedContainers: {
summary = lib.concatMapStringsSep "\n" renderContainer selectedContainers;
json = builtins.toJSON {
networks = lib.unique (lib.concatMap (container: container.networks) selectedContainers);
containers = selectedContainers;
edges =
lib.filter (
edge:
builtins.elem edge.from (map (container: container.name) selectedContainers)
)
edges;
};
dot = renderDot selectedContainers;
};
rendersByStack = lib.genAttrs stackNames (
stack:
mkRender (lib.filter (container: container.stack == stack) stackData)
);
in {
inherit data;
render = (mkRender stackData) // {stacks = rendersByStack;};
}