Skip to content

Add the ability to apply an options 'overlay' at run-time; enabling update of listen IP and port#1623

Merged
jpdillingham merged 25 commits into
masterfrom
runtime-config
Jan 31, 2026
Merged

Add the ability to apply an options 'overlay' at run-time; enabling update of listen IP and port#1623
jpdillingham merged 25 commits into
masterfrom
runtime-config

Conversation

@jpdillingham

@jpdillingham jpdillingham commented Jan 30, 2026

Copy link
Copy Markdown
Member

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 /options endpoint 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:

curl --location --request PATCH 'http://<ip>:<port>/api/v0/options' \
--header 'X-API-Key: <an api key with administrator role>' \
--header 'Content-Type: application/json' \
--data '{
    "soulseek": {
        "listenPort":  55555,
        "listenIpAddress":  "localhost"
    }
}'

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.

@jpdillingham jpdillingham changed the title Add the ability to apply an options 'overlay' at run-time Add the ability to apply an options 'overlay' at run-time; enabling update of listen IP and port Jan 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 OptionsOverlay model and a VolatileOverlayConfigurationSource<T> configuration provider, wired into Program so overlays supersede all other configuration sources.
  • Adds a PATCH /api/v0/options endpoint that validates and applies an OptionsOverlay, returning a redacted view of the effective overlay while honoring RemoteConfiguration and 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.

Comment thread src/slskd/Core/API/Controllers/OptionsController.cs
Comment on lines +65 to +99
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));
}
}
}

Copilot AI Jan 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/slskd/Core/API/Controllers/OptionsController.cs Outdated
Comment thread src/slskd/Core/Extensions.cs Outdated
Comment thread src/slskd/Common/Configuration/VolatileOverlayConfigurationSource.cs Outdated
Comment thread src/slskd/Core/OptionsOverlay.cs Outdated
jpdillingham and others added 7 commits January 30, 2026 09:44
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>
@jpdillingham jpdillingham merged commit 09eabbc into master Jan 31, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants