456 lines
13 KiB
Nix
456 lines
13 KiB
Nix
{ pkgs, lib }:
|
|
let
|
|
inherit (builtins) isAttrs hasAttr;
|
|
inherit (lib)
|
|
any
|
|
concatMapStringsSep
|
|
concatStringsSep
|
|
escapeShellArg
|
|
;
|
|
shb = rec {
|
|
# Replace secrets in a file.
|
|
# - userConfig is an attrset that will produce a config file.
|
|
# - resultPath is the location the config file should have on the filesystem.
|
|
# - generator is a function taking two arguments name and value and returning path in the nix
|
|
# nix store where the
|
|
replaceSecrets =
|
|
{
|
|
userConfig,
|
|
resultPath,
|
|
generator,
|
|
user ? null,
|
|
permissions ? "u=r,g=r,o=",
|
|
}:
|
|
let
|
|
configWithTemplates = withReplacements userConfig;
|
|
|
|
nonSecretConfigFile = generator "template" configWithTemplates;
|
|
|
|
replacements = getReplacements userConfig;
|
|
in
|
|
replaceSecretsScript {
|
|
file = nonSecretConfigFile;
|
|
inherit resultPath replacements;
|
|
inherit user permissions;
|
|
};
|
|
|
|
replaceSecretsFormatAdapter = format: format.generate;
|
|
replaceSecretsGeneratorAdapter =
|
|
generator: name: value:
|
|
pkgs.writeText "generator " (generator value);
|
|
toEnvVar = replaceSecretsGeneratorAdapter (
|
|
v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; })
|
|
);
|
|
|
|
template =
|
|
file: newPath: replacements:
|
|
replaceSecretsScript {
|
|
inherit file replacements;
|
|
resultPath = newPath;
|
|
};
|
|
|
|
genReplacement =
|
|
secret:
|
|
let
|
|
t =
|
|
{
|
|
transform ? null,
|
|
...
|
|
}:
|
|
if isNull transform then x: x else transform;
|
|
in
|
|
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
|
|
|
|
replaceSecretsScript =
|
|
{
|
|
file,
|
|
resultPath,
|
|
replacements,
|
|
user ? null,
|
|
permissions ? "u=r,g=r,o=",
|
|
}:
|
|
let
|
|
templatePath = resultPath + ".template";
|
|
|
|
# We check that the files containing the secrets have the
|
|
# correct permissions for us to read them in this separate
|
|
# step. Otherwise, the $(cat ...) commands inside the sed
|
|
# replacements could fail but not fail individually but
|
|
# not fail the whole script.
|
|
checkPermissions = concatMapStringsSep "\n" (
|
|
pattern: "cat ${pattern.source} > /dev/null"
|
|
) replacements;
|
|
|
|
generatedReplacements = map genReplacement replacements;
|
|
|
|
replaceScript = pkgs.writers.writePython3 "replace-secret" { } ''
|
|
import argparse
|
|
import pathlib
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--file", required=True, type=pathlib.Path)
|
|
parser.add_argument("--marker", required=True)
|
|
parser.add_argument("--replacement", required=True)
|
|
return parser.parse_args()
|
|
|
|
|
|
args = parse_args()
|
|
content = args.file.read_text()
|
|
args.file.write_text(content.replace(args.marker, args.replacement))
|
|
'';
|
|
|
|
replaceCommands = concatMapStringsSep "\n" (pattern: ''
|
|
${replaceScript} \
|
|
--file ${escapeShellArg resultPath} \
|
|
--marker ${escapeShellArg pattern.name} \
|
|
--replacement "${pattern.value}"
|
|
'') generatedReplacements;
|
|
|
|
replaceCmd =
|
|
if replacements == [ ] then
|
|
"cat ${escapeShellArg templatePath} > ${escapeShellArg resultPath}"
|
|
else
|
|
''
|
|
cat ${escapeShellArg templatePath} > ${escapeShellArg resultPath}
|
|
${replaceCommands}
|
|
'';
|
|
in
|
|
''
|
|
set -euo pipefail
|
|
|
|
${checkPermissions}
|
|
|
|
mkdir -p $(dirname ${templatePath})
|
|
ln -fs ${file} ${templatePath}
|
|
rm -f ${resultPath}
|
|
touch ${resultPath}
|
|
''
|
|
+ (lib.optionalString (user != null) ''
|
|
chown ${user} ${resultPath}
|
|
'')
|
|
+ ''
|
|
${replaceCmd}
|
|
chmod ${permissions} ${resultPath}
|
|
'';
|
|
|
|
secretFileType = lib.types.submodule {
|
|
options = {
|
|
source = lib.mkOption {
|
|
type = lib.types.path;
|
|
description = "File containing the value.";
|
|
};
|
|
|
|
transform = lib.mkOption {
|
|
type = lib.types.raw;
|
|
description = "An optional function to transform the secret.";
|
|
default = null;
|
|
example = lib.literalExpression ''
|
|
v: "prefix-$${v}-suffix"
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
secretName =
|
|
names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
|
|
|
|
withReplacements =
|
|
attrs:
|
|
let
|
|
valueOrReplacement =
|
|
name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name;
|
|
in
|
|
mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs;
|
|
|
|
getReplacements =
|
|
attrs:
|
|
let
|
|
addNameField =
|
|
name: value:
|
|
if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; };
|
|
|
|
secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs;
|
|
in
|
|
collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
|
|
|
|
# Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists.
|
|
mapAttrsRecursiveCond =
|
|
# A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set.
|
|
cond:
|
|
# A function, given a list of attribute names and a value, returns a new value.
|
|
f:
|
|
# Attribute set or list to recursively map over.
|
|
set:
|
|
let
|
|
recurse =
|
|
path: val:
|
|
if builtins.isAttrs val && cond val then
|
|
lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val
|
|
else if builtins.isList val && cond val then
|
|
lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val
|
|
else
|
|
f path val;
|
|
in
|
|
recurse [ ] set;
|
|
|
|
# Like lib.attrsets.collect but also recurses on lists.
|
|
collect =
|
|
# Given an attribute's value, determine if recursion should stop.
|
|
pred:
|
|
# The attribute set to recursively collect.
|
|
attrs:
|
|
if pred attrs then
|
|
[ attrs ]
|
|
else if builtins.isAttrs attrs then
|
|
lib.lists.concatMap (collect pred) (lib.attrsets.attrValues attrs)
|
|
else if builtins.isList attrs then
|
|
lib.lists.concatMap (collect pred) attrs
|
|
else
|
|
[ ];
|
|
|
|
indent =
|
|
i: str:
|
|
lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str);
|
|
|
|
# Generator for XML
|
|
formatXML =
|
|
{
|
|
enclosingRoot ? null,
|
|
}:
|
|
{
|
|
type =
|
|
with lib.types;
|
|
let
|
|
valueType =
|
|
nullOr (oneOf [
|
|
bool
|
|
int
|
|
float
|
|
str
|
|
path
|
|
(attrsOf valueType)
|
|
(listOf valueType)
|
|
])
|
|
// {
|
|
description = "XML value";
|
|
};
|
|
in
|
|
valueType;
|
|
|
|
generate =
|
|
name: value:
|
|
pkgs.callPackage (
|
|
{ runCommand, python3 }:
|
|
runCommand "config"
|
|
{
|
|
value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; });
|
|
passAsFile = [ "value" ];
|
|
}
|
|
(
|
|
pkgs.writers.writePython3 "dict2xml"
|
|
{
|
|
libraries = with python3.pkgs; [
|
|
python
|
|
dict2xml
|
|
];
|
|
}
|
|
''
|
|
import os
|
|
import json
|
|
from dict2xml import dict2xml
|
|
|
|
with open(os.environ["valuePath"]) as f:
|
|
content = json.loads(f.read())
|
|
if content is None:
|
|
print("Could not parse env var valuePath as json")
|
|
os.exit(2)
|
|
with open(os.environ["out"], "w") as out:
|
|
out.write(dict2xml(content))
|
|
''
|
|
)
|
|
) { };
|
|
|
|
};
|
|
|
|
parseXML =
|
|
xml:
|
|
let
|
|
xmlToJsonFile = pkgs.callPackage (
|
|
{ runCommand, python3 }:
|
|
runCommand "config"
|
|
{
|
|
inherit xml;
|
|
passAsFile = [ "xml" ];
|
|
}
|
|
(
|
|
pkgs.writers.writePython3 "xml2json"
|
|
{
|
|
libraries = with python3.pkgs; [ python ];
|
|
}
|
|
''
|
|
import os
|
|
import json
|
|
from collections import ChainMap
|
|
from xml.etree import ElementTree
|
|
|
|
|
|
def xml_to_dict_recursive(root):
|
|
all_descendants = list(root)
|
|
if len(all_descendants) == 0:
|
|
return {root.tag: root.text}
|
|
else:
|
|
merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants))
|
|
return {root.tag: dict(merged_dict)}
|
|
|
|
|
|
with open(os.environ["xmlPath"]) as f:
|
|
root = ElementTree.XML(f.read())
|
|
xml = xml_to_dict_recursive(root)
|
|
j = json.dumps(xml)
|
|
|
|
with open(os.environ["out"], "w") as out:
|
|
out.write(j)
|
|
''
|
|
)
|
|
) { };
|
|
in
|
|
builtins.fromJSON (builtins.readFile xmlToJsonFile);
|
|
|
|
renameAttrName =
|
|
attrset: from: to:
|
|
(lib.attrsets.filterAttrs (name: v: name == from) attrset)
|
|
// {
|
|
${to} = attrset.${from};
|
|
};
|
|
|
|
# Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix
|
|
# with a nicer diff display function.
|
|
check =
|
|
{ pkgs, tests }:
|
|
let
|
|
formatValue =
|
|
val:
|
|
if (builtins.isList val || builtins.isAttrs val) then
|
|
builtins.toJSON val
|
|
else
|
|
builtins.toString val;
|
|
|
|
resultToString =
|
|
{
|
|
name,
|
|
expected,
|
|
result,
|
|
}:
|
|
builtins.readFile (
|
|
pkgs.runCommand "nix-flake-tests-error"
|
|
{
|
|
expected = formatValue expected;
|
|
result = formatValue result;
|
|
passAsFile = [
|
|
"expected"
|
|
"result"
|
|
];
|
|
}
|
|
''
|
|
echo "${name} failed (- expected, + result)" > $out
|
|
cp ''${expectedPath} ''${expectedPath}.json
|
|
cp ''${resultPath} ''${resultPath}.json
|
|
${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out
|
|
''
|
|
);
|
|
|
|
results = pkgs.lib.runTests tests;
|
|
in
|
|
if results != [ ] then
|
|
builtins.throw (concatStringsSep "\n" (map resultToString (lib.traceValSeq results)))
|
|
else
|
|
pkgs.runCommand "nix-flake-tests-success" { } "echo > $out";
|
|
|
|
genConfigOutOfBandSystemd =
|
|
{
|
|
config,
|
|
configLocation,
|
|
generator,
|
|
user ? null,
|
|
permissions ? "u=r,g=r,o=",
|
|
}:
|
|
{
|
|
loadCredentials = getLoadCredentials "source" config;
|
|
preStart = lib.mkBefore (replaceSecrets {
|
|
userConfig = updateToLoadCredentials "source" "$CREDENTIALS_DIRECTORY" config;
|
|
resultPath = configLocation;
|
|
inherit generator;
|
|
inherit user permissions;
|
|
});
|
|
};
|
|
|
|
updateToLoadCredentials =
|
|
sourceField: rootDir: attrs:
|
|
let
|
|
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
|
|
|
valueOrLoadCredential =
|
|
path: value:
|
|
if !(hasPlaceholderField value) then
|
|
value
|
|
else
|
|
value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; };
|
|
in
|
|
mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs;
|
|
|
|
getLoadCredentials =
|
|
sourceField: attrs:
|
|
let
|
|
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
|
|
|
addPathField =
|
|
path: value: if !(hasPlaceholderField value) then value else value // { inherit path; };
|
|
|
|
secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs;
|
|
|
|
allSecrets = collect (v: hasPlaceholderField v) secretsWithPath;
|
|
|
|
genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}";
|
|
in
|
|
map genLoadCredentials allSecrets;
|
|
|
|
anyNotNull = any (x: x != null);
|
|
|
|
mkJellyfinPlugin =
|
|
{
|
|
pname,
|
|
version,
|
|
hash,
|
|
url,
|
|
}:
|
|
pkgs.callPackage (
|
|
{ stdenv, fetchzip }:
|
|
stdenv.mkDerivation (finalAttrs: {
|
|
inherit pname version;
|
|
|
|
src = fetchzip {
|
|
inherit url hash;
|
|
stripRoot = false;
|
|
};
|
|
|
|
dontBuild = true;
|
|
|
|
installPhase = ''
|
|
mkdir $out
|
|
cp -r . $out
|
|
'';
|
|
})
|
|
) { };
|
|
|
|
update =
|
|
attr: fn: attrset:
|
|
attrset // { ${attr} = fn attrset.${attr}; };
|
|
|
|
};
|
|
in
|
|
shb
|
|
// {
|
|
homepage = pkgs.callPackage ./homepage.nix { inherit shb; };
|
|
}
|