163 lines
5.3 KiB
Nix
163 lines
5.3 KiB
Nix
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;
|
|
}
|