feat: try rework
This commit is contained in:
parent
1ddbd3b8b6
commit
ecf10628c3
51 changed files with 1941 additions and 445 deletions
40
intermediate/dhcp.nix
Normal file
40
intermediate/dhcp.nix
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
let
|
||||
lib = import <nixpkgs/lib>;
|
||||
net = import ../config/network.nix;
|
||||
networkDevicesValidation = import ../validation/network_devices.nix;
|
||||
|
||||
devices = networkDevicesValidation.getDevices net;
|
||||
localDevices = networkDevicesValidation.getLocalDevices devices;
|
||||
|
||||
reservationRecords = lib.concatLists (lib.mapAttrsToList (deviceName: device:
|
||||
if device ? reservation then
|
||||
let
|
||||
reservation = networkDevicesValidation.validateReservationShape deviceName device.reservation;
|
||||
in
|
||||
[
|
||||
{
|
||||
ip-address = device.ip;
|
||||
hw-address = reservation.hw_address;
|
||||
hostname = reservation.hostname;
|
||||
}
|
||||
]
|
||||
else
|
||||
[]
|
||||
) localDevices);
|
||||
|
||||
ensureUnique = fieldName: values:
|
||||
let
|
||||
uniqueValues = lib.unique values;
|
||||
in
|
||||
if builtins.length uniqueValues != builtins.length values then
|
||||
throw "Duplicate DHCP reservation ${fieldName} found."
|
||||
else
|
||||
null;
|
||||
|
||||
_uniqueIps = ensureUnique "ip-address" (map (reservation: reservation.ip-address) reservationRecords);
|
||||
_uniqueHwAddresses = ensureUnique "hw-address" (map (reservation: reservation.hw-address) reservationRecords);
|
||||
_uniqueHostnames = ensureUnique "hostname" (map (reservation: reservation.hostname) reservationRecords);
|
||||
in
|
||||
rec {
|
||||
reservations = reservationRecords;
|
||||
}
|
||||
49
intermediate/dns.nix
Normal file
49
intermediate/dns.nix
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
let
|
||||
lib = import <nixpkgs/lib>;
|
||||
net = import ../config/network.nix;
|
||||
end = import ../config/endpoints.nix;
|
||||
endpointValidation = import ../validation/endpoints.nix;
|
||||
networkDevicesValidation = import ../validation/network_devices.nix;
|
||||
|
||||
localDomain =
|
||||
if net ? local_domain && builtins.isString net.local_domain && net.local_domain != "" then
|
||||
net.local_domain
|
||||
else
|
||||
throw "config/network.nix must define local_domain as a non-empty string.";
|
||||
|
||||
localIngressIp =
|
||||
if net ? devices && builtins.isAttrs net.devices && net.devices ? self && net.devices.self ? ip && builtins.isString net.devices.self.ip then
|
||||
net.devices.self.ip
|
||||
else
|
||||
throw "config/network.nix must define devices.self.ip as local ingress IP for local endpoint DNS mapping.";
|
||||
|
||||
endpoints = endpointValidation.validateEndpointsShape end;
|
||||
devices = networkDevicesValidation.getDevices net;
|
||||
localDevices = networkDevicesValidation.getLocalDevices devices;
|
||||
|
||||
matchesLocalDomain = domain:
|
||||
domain == localDomain || lib.hasSuffix ".${localDomain}" domain;
|
||||
|
||||
deviceMappings = builtins.listToAttrs (lib.mapAttrsToList (name: device: {
|
||||
name = "${name}.${localDomain}";
|
||||
value = device.ip;
|
||||
}) localDevices);
|
||||
|
||||
localEndpointDomains = lib.unique (map (endpoint: endpoint.domain) (lib.filter (endpoint: matchesLocalDomain endpoint.domain) endpoints));
|
||||
endpointMappings = builtins.listToAttrs (map (domain: {
|
||||
name = domain;
|
||||
value = localIngressIp;
|
||||
}) localEndpointDomains);
|
||||
|
||||
mergedMappings = deviceMappings // endpointMappings;
|
||||
|
||||
_localEndpointConflicts = map (domain:
|
||||
if deviceMappings ? ${domain} && deviceMappings.${domain} != endpointMappings.${domain} then
|
||||
throw "DNS mapping conflict for '${domain}' between device-derived and endpoint-derived values."
|
||||
else
|
||||
null
|
||||
) (builtins.attrNames endpointMappings);
|
||||
in
|
||||
rec {
|
||||
dnsMappings = mergedMappings;
|
||||
}
|
||||
297
intermediate/nginx.nix
Normal file
297
intermediate/nginx.nix
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
let
|
||||
lib = import <nixpkgs/lib>;
|
||||
net = import ../config/network.nix;
|
||||
end = import ../config/endpoints.nix;
|
||||
endpointValidation = import ../validation/endpoints.nix;
|
||||
autoSshValidation = import ../validation/auto_ssh.nix;
|
||||
|
||||
endpoints = endpointValidation.validateEndpointsShape end;
|
||||
|
||||
subnet =
|
||||
if net ? network && builtins.isAttrs net.network && net.network ? subnet && builtins.isString net.network.subnet then
|
||||
net.network.subnet
|
||||
else
|
||||
throw "config/network.nix must define network.subnet as a string.";
|
||||
|
||||
localDomain =
|
||||
if net ? local_domain && builtins.isString net.local_domain then
|
||||
net.local_domain
|
||||
else
|
||||
throw "config/network.nix must define local_domain as a string.";
|
||||
|
||||
devices = autoSshValidation.getDevices net;
|
||||
autoSshDevices = autoSshValidation.getAutoSshDevices devices;
|
||||
autoSshDomains = autoSshValidation.getAutoSshDomains autoSshDevices;
|
||||
|
||||
matchesDomain = domain: candidate:
|
||||
domain == candidate || lib.hasSuffix ".${candidate}" domain;
|
||||
|
||||
classifyExposure = domain:
|
||||
let
|
||||
isLocal = matchesDomain domain localDomain;
|
||||
isRemote = lib.any (candidate: matchesDomain domain candidate) autoSshDomains;
|
||||
in
|
||||
if isLocal && isRemote then
|
||||
throw "Domain '${domain}' is both local and remote according to config/network.nix."
|
||||
else if isLocal then
|
||||
"local"
|
||||
else if isRemote then
|
||||
"external"
|
||||
else
|
||||
throw "Domain '${domain}' is neither local nor mapped to an auto_ssh remote domain.";
|
||||
|
||||
validateEndpointSemantics = index: endpoint:
|
||||
let
|
||||
_forceSslPort =
|
||||
if endpoint.force_ssl && endpoint.port == 80 then
|
||||
throw "Endpoint at index ${toString index} cannot use force_ssl=true on port 80."
|
||||
else
|
||||
null;
|
||||
_proxyType =
|
||||
if endpoint.type == "proxy" && endpoint.content.type != "service" then
|
||||
throw "Proxy endpoint at index ${toString index} must use content.type = 'service'."
|
||||
else
|
||||
null;
|
||||
in
|
||||
endpoint // {
|
||||
exposure = classifyExposure endpoint.domain;
|
||||
};
|
||||
|
||||
validatedEndpoints = lib.imap0 validateEndpointSemantics endpoints;
|
||||
|
||||
normalizeWebPrefix = endpoint:
|
||||
if endpoint == "/" then
|
||||
"/"
|
||||
else if lib.hasSuffix "/" endpoint then
|
||||
endpoint
|
||||
else
|
||||
throw "Web endpoint '${endpoint}' must end with '/' so it can be used as a file prefix.";
|
||||
|
||||
joinWebPath = endpointPrefix: relativePath:
|
||||
let
|
||||
prefix = normalizeWebPrefix endpointPrefix;
|
||||
relativeNoContext = builtins.unsafeDiscardStringContext relativePath;
|
||||
_ =
|
||||
if builtins.isString relativeNoContext && relativeNoContext != "" && builtins.substring 0 1 relativeNoContext != "/" then
|
||||
null
|
||||
else
|
||||
throw "Web store file path '${toString relativeNoContext}' must be a non-empty relative path.";
|
||||
in
|
||||
builtins.unsafeDiscardStringContext "${prefix}${relativeNoContext}";
|
||||
|
||||
expandWebEndpoint = endpoint:
|
||||
if endpoint.type != "web" then
|
||||
[ endpoint ]
|
||||
else
|
||||
let
|
||||
files =
|
||||
if endpoint.content ? files && builtins.isList endpoint.content.files then
|
||||
endpoint.content.files
|
||||
else
|
||||
throw "Web endpoint '${endpoint.domain}${endpoint.endpoint}' must define content.files as a list.";
|
||||
|
||||
_ =
|
||||
if files != [] then
|
||||
null
|
||||
else
|
||||
throw "Web endpoint '${endpoint.domain}${endpoint.endpoint}' resolves to zero files. Check config/web.nix roots and data files.";
|
||||
in
|
||||
map (file:
|
||||
endpoint // {
|
||||
endpoint = joinWebPath endpoint.endpoint file.path;
|
||||
content = {
|
||||
status = file.status;
|
||||
contentType = file.contentType;
|
||||
filePath = file.filePath;
|
||||
};
|
||||
}
|
||||
) files;
|
||||
|
||||
mappedEndpoints = lib.concatLists (map expandWebEndpoint validatedEndpoints);
|
||||
|
||||
hostKey = endpoint: "${endpoint.domain}|${toString endpoint.port}|${if endpoint.force_ssl then "tls" else "plain"}";
|
||||
hostKeyWithoutTls = endpoint: "${endpoint.domain}|${toString endpoint.port}";
|
||||
|
||||
groupedByHost = lib.foldl' (acc: endpoint:
|
||||
let
|
||||
key = hostKey endpoint;
|
||||
current = if acc ? ${key} then acc.${key} else [];
|
||||
in
|
||||
acc // { ${key} = current ++ [ endpoint ]; }
|
||||
) {} mappedEndpoints;
|
||||
|
||||
groupedByHostWithoutTls = lib.foldl' (acc: endpoint:
|
||||
let
|
||||
key = hostKeyWithoutTls endpoint;
|
||||
current = if acc ? ${key} then acc.${key} else [];
|
||||
in
|
||||
acc // { ${key} = current ++ [ endpoint.force_ssl ]; }
|
||||
) {} mappedEndpoints;
|
||||
|
||||
groupedByDomain = lib.foldl' (acc: endpoint:
|
||||
let
|
||||
key = endpoint.domain;
|
||||
current = if acc ? ${key} then acc.${key} else [];
|
||||
in
|
||||
acc // { ${key} = current ++ [ endpoint.force_ssl ]; }
|
||||
) {} mappedEndpoints;
|
||||
|
||||
_tlsConsistency = lib.mapAttrs (_: tlsValues:
|
||||
let
|
||||
uniqueValues = lib.unique tlsValues;
|
||||
in
|
||||
if builtins.length uniqueValues > 1 then
|
||||
throw "Found incompatible TLS definitions for the same domain+port."
|
||||
else
|
||||
null
|
||||
) groupedByHostWithoutTls;
|
||||
|
||||
_domainTlsConsistency = lib.mapAttrs (domain: tlsValues:
|
||||
let
|
||||
uniqueValues = lib.unique tlsValues;
|
||||
in
|
||||
if builtins.length uniqueValues > 1 then
|
||||
throw "Found incompatible TLS definitions for domain '${domain}'."
|
||||
else
|
||||
null
|
||||
) groupedByDomain;
|
||||
|
||||
ensureUniquePaths = key: routes:
|
||||
let
|
||||
paths = map (route: route.endpoint) routes;
|
||||
uniquePaths = lib.unique paths;
|
||||
in
|
||||
if builtins.length paths != builtins.length uniquePaths then
|
||||
throw "Duplicate endpoint paths found for host key ${key}."
|
||||
else
|
||||
routes;
|
||||
|
||||
ensureExposureConsistency = key: routes:
|
||||
let
|
||||
exposures = lib.unique (map (route: route.exposure) routes);
|
||||
in
|
||||
if builtins.length exposures != 1 then
|
||||
throw "Mixed exposure policy found for host key ${key}."
|
||||
else
|
||||
routes;
|
||||
|
||||
sortRoutes = routes:
|
||||
builtins.sort (a: b: builtins.stringLength a.endpoint > builtins.stringLength b.endpoint) routes;
|
||||
|
||||
sanitizeHostKey = key:
|
||||
builtins.replaceStrings [ "." "|" ":" "/" ] [ "_" "_" "_" "_" ] key;
|
||||
|
||||
escaped = value:
|
||||
builtins.replaceStrings [ "\\" "\"" ] [ "\\\\" "\\\"" ] value;
|
||||
|
||||
mkLocation = route:
|
||||
if route.type == "proxy" then
|
||||
let
|
||||
localNodeIp =
|
||||
if devices ? self && devices.self ? ip && builtins.isString devices.self.ip then
|
||||
devices.self.ip
|
||||
else
|
||||
throw "config/network.nix must define devices.self.ip as a string.";
|
||||
proxyIp =
|
||||
if route.content.ip == localNodeIp then
|
||||
"127.0.0.1"
|
||||
else
|
||||
route.content.ip;
|
||||
in
|
||||
{
|
||||
name = route.endpoint;
|
||||
value = {
|
||||
proxyPass = "http://${proxyIp}:${toString route.content.port}";
|
||||
proxyWebsockets = route.content.proxyWebsockets;
|
||||
};
|
||||
}
|
||||
else
|
||||
let
|
||||
statusValue =
|
||||
if route.content ? status && builtins.isInt route.content.status then
|
||||
route.content.status
|
||||
else
|
||||
throw "Web endpoint '${route.domain}${route.endpoint}' must define content.status as int.";
|
||||
contentTypeValue =
|
||||
if route.content ? contentType && builtins.isString route.content.contentType && route.content.contentType != "" then
|
||||
route.content.contentType
|
||||
else
|
||||
throw "Web endpoint '${route.domain}${route.endpoint}' must define content.contentType as non-empty string.";
|
||||
filePathValue =
|
||||
if route.content ? filePath && (builtins.isString route.content.filePath || builtins.isPath route.content.filePath) then
|
||||
route.content.filePath
|
||||
else
|
||||
throw "Web endpoint '${route.domain}${route.endpoint}' must define content.filePath as string or path.";
|
||||
_status =
|
||||
if statusValue == 200 then
|
||||
null
|
||||
else
|
||||
throw "Web endpoint '${route.domain}${route.endpoint}' must use status 200 for static file serving.";
|
||||
in
|
||||
{
|
||||
name = "= ${route.endpoint}";
|
||||
value = {
|
||||
alias = toString filePathValue;
|
||||
extraConfig = ''
|
||||
types { }
|
||||
default_type ${contentTypeValue};
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
mkVirtualHost = key: routes:
|
||||
let
|
||||
checkedRoutes = ensureExposureConsistency key (ensureUniquePaths key routes);
|
||||
sortedRoutes = sortRoutes checkedRoutes;
|
||||
headRoute = builtins.head sortedRoutes;
|
||||
hostName = sanitizeHostKey key;
|
||||
locations = builtins.listToAttrs (map mkLocation sortedRoutes);
|
||||
base = {
|
||||
serverName = headRoute.domain;
|
||||
listen = [
|
||||
({
|
||||
addr = "0.0.0.0";
|
||||
port = headRoute.port;
|
||||
} // lib.optionalAttrs headRoute.force_ssl { ssl = true; })
|
||||
];
|
||||
locations = locations;
|
||||
};
|
||||
exposureConfig =
|
||||
if headRoute.exposure == "external" then
|
||||
{
|
||||
extraConfig = ''
|
||||
allow all;
|
||||
'';
|
||||
}
|
||||
else
|
||||
{
|
||||
extraConfig = ''
|
||||
allow ${subnet};
|
||||
deny all;
|
||||
'';
|
||||
};
|
||||
sslConfig =
|
||||
if headRoute.force_ssl then
|
||||
{
|
||||
enableACME = true;
|
||||
forceSSL = true;
|
||||
}
|
||||
else
|
||||
{};
|
||||
in
|
||||
{
|
||||
name = hostName;
|
||||
value = base // exposureConfig // sslConfig;
|
||||
};
|
||||
|
||||
virtualHostsData = builtins.listToAttrs (lib.mapAttrsToList mkVirtualHost groupedByHost);
|
||||
|
||||
nginxUsedPorts =
|
||||
lib.unique (map (route: route.port) mappedEndpoints);
|
||||
|
||||
acmeDomains =
|
||||
lib.unique (map (route: route.domain) (lib.filter (route: route.force_ssl) mappedEndpoints));
|
||||
in
|
||||
rec {
|
||||
inherit validatedEndpoints mappedEndpoints virtualHostsData nginxUsedPorts acmeDomains;
|
||||
}
|
||||
78
intermediate/remote.nix
Normal file
78
intermediate/remote.nix
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
let
|
||||
lib = import <nixpkgs/lib>;
|
||||
net = import ../config/network.nix;
|
||||
end = import ../config/endpoints.nix;
|
||||
endpointValidation = import ../validation/endpoints.nix;
|
||||
autoSshValidation = import ../validation/auto_ssh.nix;
|
||||
|
||||
endpoints = endpointValidation.validateEndpointsShape end;
|
||||
|
||||
devices = autoSshValidation.getDevices net;
|
||||
autoSshDevices = autoSshValidation.getAutoSshDevices devices;
|
||||
|
||||
matchesDomain = endpointDomain: remoteDomain:
|
||||
if builtins.isString endpointDomain && builtins.isString remoteDomain then
|
||||
endpointDomain == remoteDomain || lib.hasSuffix ".${remoteDomain}" endpointDomain
|
||||
else
|
||||
throw "Endpoint and remote domains must be strings.";
|
||||
|
||||
getRemotePortMap = device:
|
||||
autoSshValidation.getRemotePortMap device;
|
||||
|
||||
resolveRemotePort = remotePortMap: endpoint:
|
||||
let
|
||||
localPort =
|
||||
if endpoint ? port then
|
||||
endpoint.port
|
||||
else
|
||||
throw "Endpoint is missing required field: port.";
|
||||
overrides = lib.filter (entry: entry.localPort == localPort) remotePortMap;
|
||||
in
|
||||
if overrides != [] then
|
||||
(builtins.head overrides).remotePort
|
||||
else
|
||||
localPort;
|
||||
|
||||
mapEndpointToForward = remotePortMap: endpoint: {
|
||||
remote = resolveRemotePort remotePortMap endpoint;
|
||||
localAddress = "localhost";
|
||||
localPort = endpoint.port;
|
||||
};
|
||||
|
||||
endpointForHttpRedirect = endpoint:
|
||||
endpoint // { port = 80; };
|
||||
|
||||
portsByRemote = lib.mapAttrs (_: device:
|
||||
let
|
||||
remoteDomain =
|
||||
if device ? domain then
|
||||
device.domain
|
||||
else
|
||||
throw "Auto SSH device is missing required field: domain.";
|
||||
remotePortMap = getRemotePortMap device;
|
||||
matchedEndpoints =
|
||||
lib.filter (endpoint:
|
||||
if endpoint.force_ssl && endpoint.port == 80 then
|
||||
throw "Endpoint '${endpoint.domain}${endpoint.endpoint}' cannot use force_ssl=true on port 80."
|
||||
else if endpoint.type == "proxy" && endpoint.content.type != "service" then
|
||||
throw "Proxy endpoint '${endpoint.domain}${endpoint.endpoint}' must use content.type = 'service'."
|
||||
else
|
||||
matchesDomain endpoint.domain remoteDomain
|
||||
) endpoints;
|
||||
forwards = lib.concatMap (endpoint:
|
||||
let
|
||||
baseForward = mapEndpointToForward remotePortMap endpoint;
|
||||
httpRedirectForward = mapEndpointToForward remotePortMap (endpointForHttpRedirect endpoint);
|
||||
in
|
||||
if endpoint.force_ssl then
|
||||
[ baseForward httpRedirectForward ]
|
||||
else
|
||||
[ baseForward ]
|
||||
) matchedEndpoints;
|
||||
in
|
||||
lib.unique forwards
|
||||
) autoSshDevices;
|
||||
in
|
||||
rec {
|
||||
inherit portsByRemote;
|
||||
}
|
||||
163
intermediate/secrets.nix
Normal file
163
intermediate/secrets.nix
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
let
|
||||
lib = import <nixpkgs/lib>;
|
||||
secretsConfig = (import ../validation/secrets.nix).getSecretsConfig (import ../config/secrets.nix);
|
||||
|
||||
isSopsEncrypted = filePath:
|
||||
let
|
||||
content = builtins.readFile filePath;
|
||||
in
|
||||
lib.hasInfix "\nsops:" content
|
||||
|| lib.hasInfix "\n\"sops\":" content
|
||||
|| lib.hasInfix "\"sops\":" content
|
||||
|| builtins.substring 0 5 content == "sops:";
|
||||
|
||||
extractJsonKeys = filePath:
|
||||
let
|
||||
content = builtins.readFile (builtins.toString filePath);
|
||||
parsed = builtins.fromJSON content;
|
||||
|
||||
recurseKeys = prefix: obj:
|
||||
if builtins.isAttrs obj then
|
||||
lib.concatMap (name:
|
||||
if name == "sops" then
|
||||
[]
|
||||
else
|
||||
let
|
||||
newPrefix = if prefix == "" then name else "${prefix}.${name}";
|
||||
in
|
||||
recurseKeys newPrefix obj.${name}
|
||||
) (builtins.attrNames obj)
|
||||
else
|
||||
[ prefix ];
|
||||
in
|
||||
recurseKeys "" parsed;
|
||||
|
||||
getRuntimePath = path:
|
||||
"/run/secrets/${builtins.concatStringsSep "_" path}";
|
||||
|
||||
defaultMetadata = {
|
||||
path = null;
|
||||
owner = null;
|
||||
group = null;
|
||||
mode = null;
|
||||
};
|
||||
|
||||
# Step 1: Convert path/string leaves to a normalized attrset shape.
|
||||
normalizeLeaf = path: node:
|
||||
if builtins.isString node || builtins.isPath node then
|
||||
{
|
||||
file = node;
|
||||
keys = null;
|
||||
metadata = defaultMetadata;
|
||||
}
|
||||
else if builtins.isAttrs node && node ? file then
|
||||
{
|
||||
file = node.file;
|
||||
keys = node.keys or null;
|
||||
metadata = {
|
||||
path = node.path or null;
|
||||
owner = node.owner or null;
|
||||
group = node.group or null;
|
||||
mode = node.mode or null;
|
||||
};
|
||||
}
|
||||
else
|
||||
throw "Invalid secret leaf at ${builtins.concatStringsSep "." path}: must be string, path, or attrset with 'file'";
|
||||
|
||||
# Step 2: Flatten normalized leaf structure into entries.
|
||||
flattenTree = path: node:
|
||||
if builtins.isAttrs node && !(node ? file) then
|
||||
lib.concatMap (name:
|
||||
flattenTree (path ++ [ name ]) node.${name}
|
||||
) (builtins.attrNames node)
|
||||
else
|
||||
let
|
||||
normalized = normalizeLeaf path node;
|
||||
in
|
||||
[ {
|
||||
inherit path;
|
||||
file = normalized.file;
|
||||
keys = normalized.keys;
|
||||
metadata = normalized.metadata;
|
||||
} ];
|
||||
|
||||
entries = flattenTree [] secretsConfig;
|
||||
|
||||
# Step 3a: Look for keys when type is JSON and keys are not explicitly provided.
|
||||
enrichedEntries = map (entry:
|
||||
let
|
||||
isJson = builtins.match ".*\\.json$" (builtins.toString entry.file) != null;
|
||||
extractedKeys = if isJson && entry.keys == null then extractJsonKeys entry.file else null;
|
||||
in
|
||||
entry // { keys = if extractedKeys != null then extractedKeys else entry.keys; }
|
||||
) entries;
|
||||
|
||||
isReady = entry:
|
||||
if !builtins.pathExists entry.file then
|
||||
false
|
||||
else if builtins.match ".*\\.(ya?ml|json)$" (builtins.toString entry.file) != null then
|
||||
true
|
||||
else
|
||||
isSopsEncrypted entry.file;
|
||||
|
||||
readyEntries = builtins.filter isReady enrichedEntries;
|
||||
missingEntries = builtins.filter (entry: !(isReady entry)) enrichedEntries;
|
||||
|
||||
# Step 3b: Expand key lists to per-secret entries and build sops.secrets attrset.
|
||||
mkSopsSecrets = sourceEntries:
|
||||
let
|
||||
expanded = lib.concatMap (entry:
|
||||
if entry.keys != null && entry.keys != [] then
|
||||
map (key: entry // { singleKey = key; }) entry.keys
|
||||
else
|
||||
[ (entry // { singleKey = null; }) ]
|
||||
) sourceEntries;
|
||||
|
||||
mkEntry = entry:
|
||||
let
|
||||
normalizedKeyName = if entry.singleKey != null then
|
||||
builtins.replaceStrings [ "." ] [ "_" ] entry.singleKey
|
||||
else
|
||||
null;
|
||||
|
||||
secretName = if entry.singleKey != null then
|
||||
builtins.concatStringsSep "_" (entry.path ++ [ normalizedKeyName ])
|
||||
else
|
||||
builtins.concatStringsSep "_" entry.path;
|
||||
|
||||
isJson = builtins.match ".*\\.json$" (builtins.toString entry.file) != null;
|
||||
isYaml = builtins.match ".*\\.ya?ml$" (builtins.toString entry.file) != null;
|
||||
|
||||
sopsKey =
|
||||
if entry.singleKey == null then
|
||||
null
|
||||
else
|
||||
builtins.replaceStrings [ "." ] [ "/" ] entry.singleKey;
|
||||
in
|
||||
{
|
||||
name = secretName;
|
||||
value = {
|
||||
sopsFile = entry.file;
|
||||
# Compatibility: current sops-install-secrets key traversal expects
|
||||
# YAML map shape for nested key extraction. JSON documents are valid
|
||||
# YAML, so parse .json via "yaml" to support nested keys.
|
||||
format = if isJson || isYaml then "yaml" else "binary";
|
||||
path = if entry.metadata.path != null then
|
||||
entry.metadata.path
|
||||
else
|
||||
getRuntimePath (entry.path ++ lib.optional (normalizedKeyName != null) normalizedKeyName);
|
||||
owner = if entry.metadata.owner != null then entry.metadata.owner else "root";
|
||||
group = if entry.metadata.group != null then entry.metadata.group else "root";
|
||||
mode = if entry.metadata.mode != null then entry.metadata.mode else "0400";
|
||||
} // lib.optionalAttrs (sopsKey != null) {
|
||||
key = sopsKey;
|
||||
};
|
||||
};
|
||||
in
|
||||
builtins.listToAttrs (map mkEntry expanded);
|
||||
in
|
||||
{
|
||||
source = secretsConfig;
|
||||
byName = mkSopsSecrets readyEntries;
|
||||
missing = missingEntries;
|
||||
}
|
||||
27
intermediate/storage.nix
Normal file
27
intermediate/storage.nix
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
let
|
||||
lib = import <nixpkgs/lib>;
|
||||
storageValidation = import ../validation/storage.nix;
|
||||
storageConfig = storageValidation.getStorageConfig (import ../config/storage.nix);
|
||||
|
||||
mkFileSystem = name: entry:
|
||||
let
|
||||
extraAttrs =
|
||||
if entry ? extra then
|
||||
entry.extra
|
||||
else
|
||||
{};
|
||||
in
|
||||
{
|
||||
name = entry.path;
|
||||
value = {
|
||||
device = entry.source;
|
||||
fsType = entry.type;
|
||||
options = entry.options;
|
||||
} // extraAttrs;
|
||||
};
|
||||
|
||||
fileSystems = builtins.listToAttrs (lib.mapAttrsToList mkFileSystem storageConfig);
|
||||
in
|
||||
rec {
|
||||
inherit fileSystems;
|
||||
}
|
||||
102
intermediate/web.nix
Normal file
102
intermediate/web.nix
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
let
|
||||
lib = import <nixpkgs/lib>;
|
||||
webConfig = import ../config/web.nix;
|
||||
webValidation = import ../validation/web.nix;
|
||||
|
||||
stores = webValidation.getStores webConfig;
|
||||
|
||||
stripPrefix = prefix: value:
|
||||
let
|
||||
prefixLen = builtins.stringLength prefix;
|
||||
valueLen = builtins.stringLength value;
|
||||
in
|
||||
if valueLen >= prefixLen && builtins.substring 0 prefixLen value == prefix then
|
||||
builtins.substring prefixLen (valueLen - prefixLen) value
|
||||
else
|
||||
value;
|
||||
|
||||
trimLeadingSlash = value:
|
||||
if builtins.stringLength value > 0 && builtins.substring 0 1 value == "/" then
|
||||
builtins.substring 1 (builtins.stringLength value - 1) value
|
||||
else
|
||||
value;
|
||||
|
||||
findFiles = dir:
|
||||
let
|
||||
entries = builtins.readDir dir;
|
||||
processEntry = name: type:
|
||||
let
|
||||
path = dir + "/${name}";
|
||||
in
|
||||
if type == "directory" then
|
||||
findFiles path
|
||||
else if type == "regular" then
|
||||
[ path ]
|
||||
else
|
||||
[];
|
||||
in
|
||||
lib.concatLists (lib.mapAttrsToList processEntry entries);
|
||||
|
||||
detectContentType = relativePath:
|
||||
if lib.hasSuffix ".html" relativePath then "text/html"
|
||||
else if lib.hasSuffix ".css" relativePath then "text/css"
|
||||
else if lib.hasSuffix ".js" relativePath then "application/javascript"
|
||||
else if lib.hasSuffix ".json" relativePath then "application/json"
|
||||
else if lib.hasSuffix ".svg" relativePath then "image/svg+xml"
|
||||
else if lib.hasSuffix ".png" relativePath then "image/png"
|
||||
else if lib.hasSuffix ".jpg" relativePath || lib.hasSuffix ".jpeg" relativePath then "image/jpeg"
|
||||
else if lib.hasSuffix ".gif" relativePath then "image/gif"
|
||||
else if lib.hasSuffix ".webp" relativePath then "image/webp"
|
||||
else if lib.hasSuffix ".ico" relativePath then "image/x-icon"
|
||||
else if lib.hasSuffix ".txt" relativePath then "text/plain"
|
||||
else if lib.hasSuffix ".xml" relativePath then "application/xml"
|
||||
else if lib.hasSuffix ".pdf" relativePath then "application/pdf"
|
||||
else if lib.hasSuffix ".wasm" relativePath then "application/wasm"
|
||||
else "application/octet-stream";
|
||||
|
||||
mkStorePath = storeName: rootPath:
|
||||
builtins.path {
|
||||
path = rootPath;
|
||||
name = "web-store-${storeName}";
|
||||
};
|
||||
|
||||
mkStoreFileEntries = storeName: store:
|
||||
let
|
||||
storeRoot = mkStorePath storeName store.root;
|
||||
files = findFiles storeRoot;
|
||||
|
||||
mkFileEntry = filePath:
|
||||
let
|
||||
filePathString = toString filePath;
|
||||
relative = trimLeadingSlash (stripPrefix (toString storeRoot) filePathString);
|
||||
in
|
||||
{
|
||||
path = relative;
|
||||
filePath = filePath;
|
||||
contentType = detectContentType relative;
|
||||
status = 200;
|
||||
};
|
||||
|
||||
entries = map mkFileEntry files;
|
||||
sortedEntries = builtins.sort (a: b: a.path < b.path) entries;
|
||||
|
||||
paths = map (entry: entry.path) sortedEntries;
|
||||
uniquePaths = lib.unique paths;
|
||||
_ =
|
||||
if builtins.length paths == builtins.length uniquePaths then
|
||||
null
|
||||
else
|
||||
throw "config/web.nix store '${storeName}' produces duplicate relative file paths in root '${toString store.root}'.";
|
||||
in
|
||||
sortedEntries;
|
||||
|
||||
storeFilesByName = lib.mapAttrs mkStoreFileEntries stores;
|
||||
|
||||
storePayloads = lib.mapAttrs (_: files: {
|
||||
type = "store";
|
||||
files = files;
|
||||
}) storeFilesByName;
|
||||
in
|
||||
rec {
|
||||
inherit stores storeFilesByName storePayloads;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue