Syncthing in Kubernetes

Folks - I’m building a Helm chart to install Syncthing in Kubernetes. Because A) Syncthing listens on 22000, and B) out-of-the-box k8s NodePort services typically start in the >=32000 range, then it requires to configure Syncthing to communicate on >=32000 per: Syncthing Configuration — Syncthing v1.28.0 documentation.

This can be done in the UI but - automating it at install time is non-trivial since Syncthing appears to only accept one config XML in its entirety.

My question is - is there a way with Syncthing to supplement the default /var/syncthing/config/config.xml file with individual setting overrides? I don’t see a command line arg for this one setting under syncthing serve --help (which is perfectly reasonable otherwise there would have to be dozens of args which would be undesirable.)

If Syncthing only accepts the full XML then the Helm chart has to include an entire XML and the chart has to interpolate this one setting into it. This is fragile from a chart perspective for obvious reasons.

It’s more natural from a Kubernetes/Helm perspective to support the following:

  1. Adopt the Linux configuration directory approach (i.e. conf.d) where all files in the directory are processed in name order allowing override files to exist with the default configs. Then the Helm chart could mount this additional config as a separate file into the same directory as the default config.
  2. Or - support env vars for all these settings as overrides
  3. Or - support a configuration override file (e.g. via command line arg like --additional-configs=/path/to/file)

Is anyone running Syncthing in k8s and if so - have you addressed this consideration and if so how? Thanks in advance for any help.

You can use the CLI to change it after creating the config.xml (when Syncthing is already running):

syncthing cli config options raw-listen-addresses 0 set ...

Or simply create a basic config.xml containing only the few settings you need to override, then let Syncthing respect that when first generating its certificate and stuff:

syncthing generate --gui-user=...

It will flesh out your small snippet to a full configuration file. See Syncthing — Syncthing documentation for details.

Other ways that come to mind: It’s XML, thus there should be many tools that can query / alter its contents programatically via XPath or similar.

oh interesting. I was just researching using the REST API in a sidecar container after syncthing comes up

curl -X PATCH -H 'X-API-Key: THEKEY'\
  --data '{"listenAddresses": ["tcp://0.0.0.0:32222", "quic://0.0.0.0:32222", "dynamic+https://relays.syncthing.net/endpoint"]}'\
  http://localhost:8384/rest/config/options

but - starting with a minimal config and letting syncthing complete it seems better. Thanks.

P.S. It apears that generate creates a full file in this case:

bin/syncthing generate --config=TESTME --gui-user=TESTME
2024/10/30 09:05:11 INFO: Generating ECDSA key and certificate for syncthing...
2024/10/30 09:05:11 INFO: Device ID: OAFUC23-F3HTDK4-MFMED7Q-FSF6RJU-QDGPRTT-2VWGNMY-OWYV4I4-TYN4TA6
2024/10/30 09:05:11 INFO: Default folder created and/or linked to new config
2024/10/30 09:05:11 INFO: Updated GUI authentication user name: TESTME

Then:

wc -l TESTME/config.xml 

Produces:

172 TESTME/config.xml

And 172 lines is about what you get when you just run it for the first time with no args (syncthing serve)

Syncthing will accept a config.xml with only a couple of settings in it and substitute defaults for the rest when reading.

ahhh. This is cleanest - the helm chart can create a config like:

<configuration version="37">
    <options>
        <listenAddress>tcp://0.0.0.0:32222</listenAddress> 
        <listenAddress>quic://0.0.0.0:32222</listenAddress>
    </options>
</configuration>

…and then just let the server start.

Thanks to all!

so far so good - now I would like to override the default device name of “syncthing-0”. I tried this in the stub XML

<configuration version="37">
    <device name="my.k8s.cluster.dns">
    </device>
</configuration>

But it ignored that.

I don’t think you can do that, since the name is in the device section for the local device ID which is not known ahead of time.

(The default name is the host name, which in your case comes from the statefulset I guess.)

1 Like

Yep - In k8s it would be nice to assign to syncthing a logical device name (i.e. the cluster dns name that is running it.)

From a brief perusal of the code, it looks like this would be pretty localized. It could be done a couple ways:

  1. One way would be to support a new tag in the partial config file that is merged into the top <device> tag in the generated config. E.g.:

     <configuration version="37">
         <override device name="foo">
     </configuration>
    
  2. Another way would be with a new cmdline arg (that is only interrogated on initial generation of the config):

    syncthing serve --override-device-name=foo ...
    

Or both… Or other… Do you think the maintainers would accept a PR? If so I will investigate the LOE. Thanks.

There’s been a recent similar discussion, for another field. My opinion from there still stands: I don’t think it’s viable to add special cases for single fields (either as command line args or special config fields), it should be generic. A few options to approach that were mentioned, I don’t have time to right now to search for them. The building blocks are already there though. Already now you could do it after startup either through syncthing cli config or equivalently the rest API. And if it needs to be before startup, you could e.g. add an operation to the cli to just populate the config, but not start. Then extend syncthing cli config to be able to modify config directly instead of through the API. There are probably other options, the point is the building blocks are there to do something generic.

I get it. Do you think if the startup merge functionality were refactored to be a true merge - that would be a viable solution? Right now there are a few “special cases” in the merge handling but conceptually the logic could be changed to:

  1. Read partial config and convert to Go struct
  2. Generate default (as go struct)
  3. Merge partial into default accepting only populated (non-nil) values
  4. Write result as XML

I have not research Go struct merging (other than two minutes just now) but its likely a common problem with a good solution somewhere…

What you describe to me sounds like what we do. I don’t think this is a problem of “implementing true merge”, at least I don’t see a true merge conceptually: Your example shows an empty device with just a name, while the identifying and defying property of a device is its ID. While we could add code such that a single empty device with a name means that is the local device, and treat it accordingly, that’s an implicit, magic (as in it’s happening because we wish for it, not following some rule), unexpected behaviour. What’s wrong with setting the name after generating the initial config?

I wouldn’t say there’s anything wrong with it. It’s just more idiomatic in the Helm/Kubernetes world to say (conceptually) here is configuration for this workload and be done.

What we presently have is here is some initial configuration for this workload and then wait for the workload to reach some state (config.xml fully generated) and then poke it with some additional configuration because the startup configuration doesn’t support all configurables.

Hey I’m not complaining - this is a great piece of software. As a k8s integrator I’m just wanting it to play nice with Helm and Kubernetes. This particular case is complex because the devce ID is derived - it appears - from PKI materials and is coupled with the “friendly” device name. So that makes it particularly hard for the Helm chart to intervene with. And with Kubernetes / Helm you want everything to be automatiable, declarative, and GitOps-enabled…

Yeah, Syncthing is not a stateless application. Consider running it like you would something like PostgreSQL. There you’ll find an entire ecosystem of operators and stuff to manage it once it’s running to create users and roles and databases and such, and frankly Syncthing is kind of similar. I could see an operator doing the things you’re envisioning, together with SyncedFolder and PeerDevice CRDs and so on, though this is obviously at an entirely different effort level than what you’re looking at right now.

For something like a helm chart I’d probably allow the user to have control by explicitly specifying the device ID certificates and config (stub or not), or let Syncthing manage it. It’s more difficult to do some sort of middle road.

1 Like

Yep - I’ll take the approach for now to configure what is supported on startup then have a sidecar that waits for Syncthing to be ready and then makes any remaining tweaks using the REST API. Thanks for everyone’s help.

We have the var LocalDeviceID = repeatedDeviceID(0xff) as a defined special case value already. I could see us adding some code to parse a config section mentioning that special ID as applying to the local ID of the running instance, and treating the section as such. But it’s a very specific solution for a very small set of use cases.

Obvious problem if we added it: What to do when device sections are present for both the LOCAL-ID special value and the real device ID?