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 Jul 23, 2023
1 parent 7b5e394 commit 33a8f1c
Showing 1 changed file with 85 additions and 17 deletions.
102 changes: 85 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 = mapAttrsToList (name: folder: folder.id) cfg.settings.folders;

updateConfig = pkgs.writers.writeDash "merge-syncthing-config" ''
jq = "${pkgs.jq}/bin/jq";
updateConfig = pkgs.writers.writeBash "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,91 @@ 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 almost identical.
Hence we iterate them using lib.pipe and generate shell commands for both at
the sime time. */
(lib.pipe ["folders" "devices"] [
# For the two above, create an attribute set with all the variables needed
# for writing the shell commands
(map (conf_type: {
inherit conf_type;
new_conf_IDs = lib.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}";
}))
# Now for each of these attributes, write the curl commands that are
# identical to both folders and devices.
(map (s:
lib.optionalString (s.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 ${s.new_conf_IDs}; do
new_cfg=$(echo ${escapeShellArg (builtins.toJSON s.conf)} | ${jq} '.[] | select(.${s.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.
#
# What's not documented, is that using PUT will remove objects that
# don't exist in the array given. That's why we use here `POST`, and
# only if s.override == true then we DELETE the relevant folders
# afterwards.
#
curl -d "$new_cfg" -X POST ${s.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
*/
+ lib.optionalString s.override ''
old_conf_${s.conf_type}_ids="$(curl -X GET ${s.baseAddress} | ${jq} --raw-output '.[].${s.GET_IdAttrName}')"
for id in ''${old_conf_${s.conf_type}_ids}; do
if echo ${s.new_conf_IDs} | grep -q $id; then
continue
else
curl -X DELETE ${s.baseAddress}/$id
fi
done
''
))
(lib.concatStringsSep "\n")
]) +
/* Now we update the other settings defined in cleanedConfig which are not
"folders" or "devices". */
(lib.pipe cleanedConfig [
builtins.attrNames
(lib.subtractLists ["folders" "devices"])
(map (subOption: ''
curl -X PUT -d ${lib.escapeShellArg (builtins.toJSON cleanedConfig.${subOption})} \
${cfg.guiAddress}/rest/config/${subOption}
''))
(lib.concatStringsSep "\n")
]) + ''
# 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 33a8f1c

Please sign in to comment.