Add the ability to apply an options 'overlay' at run-time; enabling update of listen IP and port#1623
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a volatile, run‑time configuration overlay mechanism so that selected options (currently Soulseek listen IP and port) can be updated programmatically via an authenticated HTTP API without touching the YAML config file.
Changes:
- Introduces an
OptionsOverlaymodel and aVolatileOverlayConfigurationSource<T>configuration provider, wired intoProgramso overlays supersede all other configuration sources. - Adds a
PATCH /api/v0/optionsendpoint that validates and applies anOptionsOverlay, returning a redacted view of the effective overlay while honoringRemoteConfigurationand admin authorization. - Switches the YAML configuration update endpoint to
PUT, adds backup creation before writing, and updates the web client library to call the new verb and use improved validation helpers.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/web/src/lib/options.js | Updates the front-end options client to send YAML updates via PUT /options/yaml, aligning with the backend verb change. |
| src/slskd/Program.cs | Adds static accessors and helpers for the current OptionsOverlay and registers the VolatileOverlayConfigurationSource<OptionsOverlay> as the last configuration provider. |
| src/slskd/Core/OptionsOverlay.cs | Defines the OptionsOverlay model and nested SoulseekOptionsPatch type with appropriate validation attributes for listen IP and port. |
| src/slskd/Core/Extensions.cs | Adds a redaction extension for OptionsOverlay that deep-clones and redacts secrets similarly to the existing Options redaction. |
| src/slskd/Core/API/Controllers/OptionsController.cs | Introduces the PATCH /options overlay endpoint, updates logging conventions, tightens remote-config checks, creates YAML backups on update, and switches the YAML update route to PUT. |
| src/slskd/Common/Validation/ValidationExtensions.cs | Adds GetResultString to flatten composite validation results into a single-line message for logs and HTTP responses. |
| src/slskd/Common/Validation/Extensions.cs | Extends validation helpers with TryValidate(this OptionsOverlay ...) to reuse the existing validation pattern for the new overlay type. |
| src/slskd/Common/Configuration/VolatileOverlayConfigurationSource.cs | Implements a new configuration provider/source pair that maps a runtime overlay object into configuration keys and triggers reloads when overlays change. |
| src/slskd/Common/Configuration/DefaultValueConfigurationSource.cs | Documents that its mapping logic is intentionally kept in lockstep with the new volatile overlay provider. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| void Map(Type type, string path, object instance) | ||
| { | ||
| var props = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); | ||
|
|
||
| foreach (PropertyInfo property in props) | ||
| { | ||
| var key = ConfigurationPath.Combine(path, property.Name.ToLowerInvariant()); | ||
|
|
||
| if (property.PropertyType.Namespace.StartsWith(Namespace)) | ||
| { | ||
| Map(property.PropertyType, key, property.GetValue(instance)); | ||
| } | ||
| else | ||
| { | ||
| // don't add array values to the configuration; these are additive across providers | ||
| // and the default value from the class is "stuck", so adding them again results in duplicates. | ||
| if (!property.PropertyType.IsArray) | ||
| { | ||
| var value = property.GetValue(instance); | ||
|
|
||
| if (value != null) | ||
| { | ||
| Data[key] = value.ToString(); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| // serialize array defaults and stick them on the parent key | ||
| // (not indexed by array position). this value is "stuck", and | ||
| // we want to show that in the config debug view. this isn't really | ||
| // functional, just illustrative. | ||
| Data[key] = JsonSerializer.Serialize(property.GetValue(instance)); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
VolatileOverlayConfigurationProvider.Map recurses into nested types even when the provided instance is null, because property.GetValue(instance) is called without a null check before recursing. If an overlay specifies a top-level object as null (e.g. Soulseek is null) while CurrentValue is non‑null, this will cause a NullReferenceException when the configuration provider attempts to traverse that subtree. Consider short‑circuiting the recursion when instance is null and/or only calling Map for nested properties when the corresponding value is non‑null, so that overlays with partially null sections don’t crash the configuration reload.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…rce.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Currently the only way to update options while the application is running is to write to the YAML configuration file. This modification is technically possible to do programmatically but for security reasons users need to be very intentional about it, and whatever is performing the update needs to handle serialization and deserialization itself.
To provide easy programmatic configuration changes, I've added a new
PATCH /optionsendpoint that applies a configuration 'overlay' on top of the configuration that is derived from all other sources. This overly is volatile, meaning the values are discarded when the application restarts.To apply an overlay:
Currently only the listen IP address and port for the Soulseek network can be overlaid.
This addition is being made explicitly to improve support for VPNs which might dynamically assign forwarded ports after connection.