From 36ce48152edab0a9dddca8971f78413c1a731fdc Mon Sep 17 00:00:00 2001 From: Benjamin Gaussorgues Date: Wed, 22 Oct 2025 11:02:37 +0200 Subject: [PATCH] feat(config): allow to override some config values by hostname Signed-off-by: Benjamin Gaussorgues --- build/psalm-baseline.xml | 4 ---- config/config.sample.php | 26 ++++++++++++++++++++++++++ lib/private/Config.php | 23 +++++++++++++++++++++-- lib/private/Snowflake/Generator.php | 7 ++++++- tests/lib/Snowflake/GeneratorTest.php | 14 ++++++++++++-- 5 files changed, 65 insertions(+), 9 deletions(-) diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 7e3f9ab604ce3..20b47367c09cb 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -3522,10 +3522,6 @@ delete($key)]]> set($key, $value)]]> - - - - diff --git a/config/config.sample.php b/config/config.sample.php index 411e46c46c409..f2e5fa2be1091 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -45,6 +45,17 @@ */ 'instanceid' => '', + /** + * This is a unique identifier for your server. + * It is useful when your Nextcloud instance is spread between different servers. + * Once it's set it shouldn't be changed. + * + * Value must be an integer, comprised between 0 and 1023. + * + * This value should be overriden by hostname in $CONFIG_HOSTNAME + */ + 'serverid' => -1, + /** * The salt used to hash all passwords, auto-generated by the Nextcloud * installer. (There are also per-user salts.) If you lose this salt, you lose @@ -2813,3 +2824,18 @@ */ 'enable_lazy_objects' => true, ]; + +/** + * CONFIG_HOSTNAME allows to set specific configuration value on specific hosts. + * It should be used when configuration is stored on a shared filesystem between several servers. + * + * Only options listed in \OC\Config::HOST_OVERRIDE_CONFIG can be defined here. + */ +$CONFIG_HOSTNAME = [ + 'hostname_a' => [ + 'serverid' => 42, + ], + 'hostname_b' => [ + 'serverid' => 43, + ], +]; diff --git a/lib/private/Config.php b/lib/private/Config.php index a9eb58a186646..4f1fe4941be12 100644 --- a/lib/private/Config.php +++ b/lib/private/Config.php @@ -15,6 +15,10 @@ */ class Config { public const ENV_PREFIX = 'NC_'; + // List configurations that can be overriden based on server hostname + private const HOST_OVERRIDE_CONFIG = [ + 'serverid', + ]; /** @var array Associative array ($key => $value) */ protected $cache = []; @@ -199,7 +203,7 @@ private function readData() { // Include file and merge config foreach ($configFiles as $file) { - unset($CONFIG); + $CONFIG = $CONFIG_HOSTNAME = null; // Invalidate opcache (only if the timestamp changed) if (function_exists('opcache_invalidate')) { @@ -226,6 +230,10 @@ private function readData() { } try { + /** + * @var ?array $CONFIG + * @var ?array $CONFIG_HOSTNAME + */ include $file; } finally { // Close the file pointer and release the lock @@ -241,9 +249,20 @@ private function readData() { } throw new \Exception($errorMessage); } - if (isset($CONFIG) && is_array($CONFIG)) { + if (is_array($CONFIG)) { $this->cache = array_merge($this->cache, $CONFIG); } + if (is_array($CONFIG_HOSTNAME) && !empty($CONFIG_HOSTNAME)) { + $hostname = gethostname(); + if (isset($CONFIG_HOSTNAME[$hostname]) && is_array($CONFIG_HOSTNAME[$hostname])) { + $filteredConfig = array_filter( + $CONFIG_HOSTNAME[$hostname], + fn ($key) => in_array($key, self::HOST_OVERRIDE_CONFIG), + ARRAY_FILTER_USE_KEY, + ); + $this->cache = array_merge($this->cache, $filteredConfig); + } + } } // grab any "NC_" environment variables diff --git a/lib/private/Snowflake/Generator.php b/lib/private/Snowflake/Generator.php index f378482a315dd..b3fcaeee7dbe5 100644 --- a/lib/private/Snowflake/Generator.php +++ b/lib/private/Snowflake/Generator.php @@ -10,6 +10,7 @@ namespace OC\Snowflake; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; use OCP\Snowflake\IGenerator; use Override; @@ -23,6 +24,7 @@ final class Generator implements IGenerator { public function __construct( private readonly ITimeFactory $timeFactory, + private readonly IConfig $config, ) { } @@ -100,7 +102,10 @@ private function getCurrentTime(): array { } private function getServerId(): int { - return crc32(gethostname() ?: random_bytes(8)); + $serverid = $this->config->getSystemValueInt('serverid', -1); + return $serverid > 0 + ? $serverid + : crc32(gethostname() ?: random_bytes(8)); } private function isCli(): bool { diff --git a/tests/lib/Snowflake/GeneratorTest.php b/tests/lib/Snowflake/GeneratorTest.php index 748d0f190422a..382c1bfbd4bb9 100644 --- a/tests/lib/Snowflake/GeneratorTest.php +++ b/tests/lib/Snowflake/GeneratorTest.php @@ -13,6 +13,7 @@ use OC\Snowflake\Decoder; use OC\Snowflake\Generator; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; use OCP\Snowflake\IGenerator; use PHPUnit\Framework\Attributes\DataProvider; use Test\TestCase; @@ -22,12 +23,17 @@ */ class GeneratorTest extends TestCase { private Decoder $decoder; + private IConfig|MockObject $config; public function setUp():void { $this->decoder = new Decoder(); + $this->config = $this->createMock(IConfig::class); + $this->config->method('getSystemValueInt') + ->with('serverid') + ->willReturn(42); } public function testGenerator(): void { - $generator = new Generator(new TimeFactory()); + $generator = new Generator(new TimeFactory(), $this->config); $snowflakeId = $generator->nextId(); $data = $this->decoder->decode($generator->nextId()); @@ -45,6 +51,9 @@ public function testGenerator(): void { // Check CLI $this->assertTrue($data['isCli']); + + // Check serverId + $this->assertEquals(42, $data['serverId']); } #[DataProvider('provideSnowflakeData')] @@ -53,11 +62,12 @@ public function testGeneratorWithFixedTime(string $date, int $expectedSeconds, i $timeFactory = $this->createMock(ITimeFactory::class); $timeFactory->method('now')->willReturn($dt); - $generator = new Generator($timeFactory); + $generator = new Generator($timeFactory, $this->config); $data = $this->decoder->decode($generator->nextId()); $this->assertEquals($expectedSeconds, ($data['createdAt']->format('U') - IGenerator::TS_OFFSET)); $this->assertEquals($expectedMilliseconds, (int)$data['createdAt']->format('v')); + $this->assertEquals(42, $data['serverId']); } public static function provideSnowflakeData(): array {