feat(config): allow to override some config values by hostname

Signed-off-by: Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
This commit is contained in:
Benjamin Gaussorgues 2025-10-22 11:02:37 +02:00
parent f06168f6c4
commit 36ce48152e
No known key found for this signature in database
5 changed files with 65 additions and 9 deletions

View file

@ -3522,10 +3522,6 @@
<code><![CDATA[$this->delete($key)]]></code>
<code><![CDATA[$this->set($key, $value)]]></code>
</InvalidOperand>
<UndefinedVariable>
<code><![CDATA[$CONFIG]]></code>
<code><![CDATA[$CONFIG]]></code>
</UndefinedVariable>
</file>
<file src="lib/private/Console/Application.php">
<NoInterfaceProperties>

View file

@ -45,6 +45,17 @@ $CONFIG = [
*/
'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 @@ $CONFIG = [
*/
'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,
],
];

View file

@ -15,6 +15,10 @@ use OCP\HintException;
*/
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 @@ class Config {
// 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 @@ class Config {
}
try {
/**
* @var ?array $CONFIG
* @var ?array $CONFIG_HOSTNAME
*/
include $file;
} finally {
// Close the file pointer and release the lock
@ -241,9 +249,20 @@ class Config {
}
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

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace OC\Snowflake;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\Snowflake\IGenerator;
use Override;
@ -23,6 +24,7 @@ use Override;
final class Generator implements IGenerator {
public function __construct(
private readonly ITimeFactory $timeFactory,
private readonly IConfig $config,
) {
}
@ -100,7 +102,10 @@ final class Generator implements IGenerator {
}
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 {

View file

@ -13,6 +13,7 @@ use OC\AppFramework\Utility\TimeFactory;
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 @@ use Test\TestCase;
*/
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 @@ class GeneratorTest extends TestCase {
// Check CLI
$this->assertTrue($data['isCli']);
// Check serverId
$this->assertEquals(42, $data['serverId']);
}
#[DataProvider('provideSnowflakeData')]
@ -53,11 +62,12 @@ class GeneratorTest extends TestCase {
$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 {