Skip to content

Commit

Permalink
nixos/syncthing: Use API to merge / override configurations
Browse files Browse the repository at this point in the history
If one sets either of `override{Device,folder}s` to false, the jq `*`
operator doesn't merge well the devices and folders, creating duplicate
IDs for folders as observed in NixOS#230146. This PR makes the script iterate
via a Bash for loop the devices and folders IDs and merges the keys
using upstream's `curl -X PATCH` support for single objects.

Hence this commit fixes NixOS#230146.
  • Loading branch information
doronbehar committed Jun 29, 2023
1 parent e3bf448 commit badf9cf
Showing 1 changed file with 65 additions and 17 deletions.
82 changes: 65 additions & 17 deletions nixos/modules/services/networking/syncthing.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ let
devices = mapAttrsToList (_: device: device // {
deviceID = device.id;
}) cfg.settings.devices;
devicesIDs = mapAttrsToList (name: device: device.id) cfg.settings.devices;

folders = mapAttrsToList (_: folder: folder //
throwIf (folder?rescanInterval || folder?watch || folder?watchDelay) ''
Expand All @@ -28,12 +29,13 @@ let
}) (filterAttrs (_: folder:
folder.enable
) cfg.settings.folders);
foldersIDs = builtins.map (folder: folder.id) folders;

updateConfig = pkgs.writers.writeDash "merge-syncthing-config" ''
jq = "${pkgs.jq}/bin/jq";
updateConfig = pkgs.writers.writeDash "merge-syncthing-config" (''
set -efu
# be careful not to leak secrets in the filesystem or in process listings
umask 0077
# get the api key by parsing the config.xml
Expand All @@ -51,25 +53,71 @@ let
--retry 1000 --retry-delay 1 --retry-all-errors \
"$@"
}
# query the old config
old_cfg=$(curl ${cfg.guiAddress}/rest/config)
# generate the new config by merging with the NixOS config options
new_cfg=$(printf '%s\n' "$old_cfg" | ${pkgs.jq}/bin/jq -c ${escapeShellArg ''. * ${builtins.toJSON cleanedConfig} * {
"devices": ('${escapeShellArg (builtins.toJSON devices)}'${optionalString (cfg.settings.devices == {} || ! cfg.overrideDevices) " + .devices"}),
"folders": ('${escapeShellArg (builtins.toJSON folders)}'${optionalString (cfg.settings.folders == {} || ! cfg.overrideFolders) " + .folders"})
}''})

# send the new config
curl -X PUT -d "$new_cfg" ${cfg.guiAddress}/rest/config

'' +
/* Syncthing's rest API for the folders and devices is identical. Hence we
iterate the cfg.settings.{devices,folder} Nix variables using the
conf_type which is either "devices" or "folders": */
(lib.concatStringsSep "\n" (map (conf_type: let
new_conf_IDs = (concatStringsSep " " {
devices = devicesIDs;
folders = foldersIDs;
}.${conf_type});
override = {
devices = cfg.overrideDevices;
folders = cfg.overrideFolders;
}.${conf_type};
conf = {
inherit devices folders;
}.${conf_type};
# The API's id naming is slightly different for devices and folders
GET_IdAttrName = {
devices = "deviceID";
folders = "id";
}.${conf_type};
# All URLs and curl commands are based on: https://docs.syncthing.net/rest/config.html
baseAddress = "${cfg.guiAddress}/rest/config/${conf_type}";
in (
(lib.optionalString (new_conf_IDs != []) ''
# We iterate the IDs list, and run a curl -X POST command for each, that
# should update that device/folder only.
for id in ${new_conf_IDs}; do
new_cfg=$(echo ${escapeShellArg (builtins.toJSON conf)} | ${jq} '.[] | select(.${GET_IdAttrName} == "'$id'")')
# Quoting https://docs.syncthing.net/rest/config.html: > PUT takes an
# array and POST a single object. In both cases if a given folder/device
# already exists, it’s replaced, otherwise a new one is added.
curl -d "$new_cfg" -X POST ${baseAddress}
done
'') +
/* If we need to override devices/folders, we iterate all currently configured
IDs, via another `curl -X GET`, and we delete all IDs that are not part of
the Nix configured list of IDs
*/
(optionalString override ''
old_conf_${conf_type}_ids="$(curl -X GET ${baseAddress} | ${jq} --raw-output '.[].${GET_IdAttrName}')"
for id in ''${old_conf_${conf_type}_ids}; do
if echo ${new_conf_IDs} | grep -q $id; then
continue
else
curl -X DELETE ${baseAddress}/$id
fi
done
'')
)) ["devices" "folders"])) +
/* Now we update the subOption "options", "gui" and "ldap" one by one, in
order to not override the folders and devices we handled in the previous
Nix Loop. */
(lib.concatStringsSep "\n" (map (subOption:
optionalString (builtins.hasAttr subOption cleanedConfig) ''
curl -X PUT -d ${escapeShellArg (builtins.toJSON cleanedConfig.${subOption})} \
${cfg.guiAddress}/rest/config/${subOption}
'') [ "options" "gui" "ldap" ])
) + ''
# restart Syncthing if required
if curl ${cfg.guiAddress}/rest/config/restart-required |
${pkgs.jq}/bin/jq -e .requiresRestart > /dev/null; then
${jq} -e .requiresRestart > /dev/null; then
curl -X POST ${cfg.guiAddress}/rest/system/restart
fi
'';
'');
in {
###### interface
options = {
Expand Down

0 comments on commit badf9cf

Please sign in to comment.