mirror of
https://github.com/appwrite/appwrite
synced 2026-05-24 01:18:37 +00:00
Merge pull request #8667 from appwrite/multi-region-support
Multi region support
This commit is contained in:
commit
6cd9fc72d9
15 changed files with 91 additions and 40 deletions
1
.env
1
.env
|
|
@ -15,6 +15,7 @@ _APP_SYSTEM_TEAM_EMAIL=team@appwrite.io
|
||||||
_APP_EMAIL_SECURITY=security@appwrite.io
|
_APP_EMAIL_SECURITY=security@appwrite.io
|
||||||
_APP_EMAIL_CERTIFICATES=certificates@appwrite.io
|
_APP_EMAIL_CERTIFICATES=certificates@appwrite.io
|
||||||
_APP_SYSTEM_RESPONSE_FORMAT=
|
_APP_SYSTEM_RESPONSE_FORMAT=
|
||||||
|
_APP_CUSTOM_DOMAIN_DENY_LIST=
|
||||||
_APP_OPTIONS_ABUSE=disabled
|
_APP_OPTIONS_ABUSE=disabled
|
||||||
_APP_OPTIONS_ROUTER_PROTECTION=disabled
|
_APP_OPTIONS_ROUTER_PROTECTION=disabled
|
||||||
_APP_OPTIONS_FORCE_HTTPS=disabled
|
_APP_OPTIONS_FORCE_HTTPS=disabled
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,15 @@ return [
|
||||||
'question' => 'Enter your Appwrite hostname',
|
'question' => 'Enter your Appwrite hostname',
|
||||||
'filter' => ''
|
'filter' => ''
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => '_APP_CUSTOM_DOMAIN_DENY_LIST',
|
||||||
|
'description' => 'List of reserved or prohibited domains when configuring custom domains.',
|
||||||
|
'introduction' => '',
|
||||||
|
'default' => 'example.com,test.com,app.example.com',
|
||||||
|
'required' => false,
|
||||||
|
'question' => '',
|
||||||
|
'filter' => ''
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'name' => '_APP_DOMAIN_FUNCTIONS',
|
'name' => '_APP_DOMAIN_FUNCTIONS',
|
||||||
'description' => 'A domain to use for function preview URLs. Setting to empty turns off function preview URLs.',
|
'description' => 'A domain to use for function preview URLs. Setting to empty turns off function preview URLs.',
|
||||||
|
|
|
||||||
|
|
@ -408,6 +408,7 @@ App::get('/v1/migrations/appwrite/report')
|
||||||
->inject('project')
|
->inject('project')
|
||||||
->inject('user')
|
->inject('user')
|
||||||
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
|
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
|
||||||
|
|
||||||
$appwrite = new Appwrite($projectID, $endpoint, $key);
|
$appwrite = new Appwrite($projectID, $endpoint, $key);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,14 @@ App::post('/v1/projects')
|
||||||
|
|
||||||
$databases = Config::getParam('pools-database', []);
|
$databases = Config::getParam('pools-database', []);
|
||||||
|
|
||||||
|
if ($region !== 'default') {
|
||||||
|
$databaseKeys = System::getEnv('_APP_DATABASE_KEYS', '');
|
||||||
|
$keys = explode(',', $databaseKeys);
|
||||||
|
$databases = array_filter($keys, function ($value) use ($region) {
|
||||||
|
return str_contains($value, $region);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$databaseOverride = System::getEnv('_APP_DATABASE_OVERRIDE');
|
$databaseOverride = System::getEnv('_APP_DATABASE_OVERRIDE');
|
||||||
$index = \array_search($databaseOverride, $databases);
|
$index = \array_search($databaseOverride, $databases);
|
||||||
if ($index !== false) {
|
if ($index !== false) {
|
||||||
|
|
@ -205,17 +213,17 @@ App::post('/v1/projects')
|
||||||
$dsn = new DSN('mysql://' . $dsn);
|
$dsn = new DSN('mysql://' . $dsn);
|
||||||
}
|
}
|
||||||
|
|
||||||
$adapter = $pools->get($dsn->getHost())->pop()->getResource();
|
|
||||||
$dbForProject = new Database($adapter, $cache);
|
|
||||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||||
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
|
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
|
||||||
|
|
||||||
$projectTables = !\in_array($dsn->getHost(), $sharedTables);
|
$projectTables = !\in_array($dsn->getHost(), $sharedTables);
|
||||||
$sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1);
|
$sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1);
|
||||||
$sharedTablesV2 = !$projectTables && !$sharedTablesV1;
|
$sharedTablesV2 = !$projectTables && !$sharedTablesV1;
|
||||||
$sharedTables = $sharedTablesV1 || $sharedTablesV2;
|
$sharedTables = $sharedTablesV1 || $sharedTablesV2;
|
||||||
|
|
||||||
if (!$sharedTablesV2) {
|
if (!$sharedTablesV2) {
|
||||||
|
$adapter = $pools->get($dsn->getHost())->pop()->getResource();
|
||||||
|
$dbForProject = new Database($adapter, $cache);
|
||||||
|
|
||||||
if ($sharedTables) {
|
if ($sharedTables) {
|
||||||
$dbForProject
|
$dbForProject
|
||||||
->setSharedTables(true)
|
->setSharedTables(true)
|
||||||
|
|
|
||||||
|
|
@ -55,14 +55,25 @@ App::post('/v1/proxy/rules')
|
||||||
->inject('dbForPlatform')
|
->inject('dbForPlatform')
|
||||||
->inject('dbForProject')
|
->inject('dbForProject')
|
||||||
->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) {
|
->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) {
|
||||||
|
|
||||||
$mainDomain = System::getEnv('_APP_DOMAIN', '');
|
$mainDomain = System::getEnv('_APP_DOMAIN', '');
|
||||||
if ($domain === $mainDomain) {
|
if ($domain === $mainDomain) {
|
||||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your main domain to specific resource. Please use subdomain or a different domain.');
|
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your main domain to specific resource. Please use subdomain or a different domain.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
|
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS');
|
||||||
if ($functionsDomain != '' && str_ends_with($domain, $functionsDomain)) {
|
$denyListDomains = System::getEnv('_APP_CUSTOM_DOMAIN_DENY_LIST');
|
||||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your functions domain or it\'s subdomain to specific resource. Please use different domain.');
|
|
||||||
|
if (!empty($denyListDomains)) {
|
||||||
|
$functionsDomain .= ',' . $denyListDomains;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deniedDomains = array_map('trim', explode(',', $functionsDomain));
|
||||||
|
|
||||||
|
foreach ($deniedDomains as $deniedDomain) {
|
||||||
|
if (str_ends_with($domain, $deniedDomain)) {
|
||||||
|
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your functions domain or its subdomain to a specific resource. Please use a different domain.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($domain === 'localhost' || $domain === APP_HOSTNAME_INTERNAL) {
|
if ($domain === 'localhost' || $domain === APP_HOSTNAME_INTERNAL) {
|
||||||
|
|
|
||||||
|
|
@ -72,11 +72,19 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($route->isEmpty()) {
|
if ($route->isEmpty()) {
|
||||||
if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) {
|
|
||||||
|
$appDomainFunctionsFallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
|
||||||
|
$appDomainFunctions = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
|
||||||
|
|
||||||
|
if (!empty($appDomainFunctionsFallback) && \str_ends_with($host, $appDomainFunctionsFallback)) {
|
||||||
|
$appDomainFunctions = $appDomainFunctionsFallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($host === $appDomainFunctions) {
|
||||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.');
|
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (\str_ends_with($host, System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))) {
|
if (\str_ends_with($host, $appDomainFunctions)) {
|
||||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain is not connected to any Appwrite resource yet. Please configure custom domain or function domain to allow this request.');
|
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain is not connected to any Appwrite resource yet. Please configure custom domain or function domain to allow this request.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -534,7 +534,7 @@ App::init()
|
||||||
$data = $cache->load($key, $timestamp);
|
$data = $cache->load($key, $timestamp);
|
||||||
|
|
||||||
if (!empty($data) && !$cacheLog->isEmpty()) {
|
if (!empty($data) && !$cacheLog->isEmpty()) {
|
||||||
$parts = explode('/', $cacheLog->getAttribute('resourceType'));
|
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
|
||||||
$type = $parts[0] ?? null;
|
$type = $parts[0] ?? null;
|
||||||
|
|
||||||
if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) {
|
if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) {
|
||||||
|
|
|
||||||
16
composer.lock
generated
16
composer.lock
generated
|
|
@ -3497,16 +3497,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "utopia-php/database",
|
"name": "utopia-php/database",
|
||||||
"version": "0.64.1",
|
"version": "0.64.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/utopia-php/database.git",
|
"url": "https://github.com/utopia-php/database.git",
|
||||||
"reference": "6530a8a6d3c1fe92d0f9a92f0f05eda698d92e0b"
|
"reference": "dc9c4a68c93e8bea2dfaa76d1ba308be539998bd"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/6530a8a6d3c1fe92d0f9a92f0f05eda698d92e0b",
|
"url": "https://api.github.com/repos/utopia-php/database/zipball/dc9c4a68c93e8bea2dfaa76d1ba308be539998bd",
|
||||||
"reference": "6530a8a6d3c1fe92d0f9a92f0f05eda698d92e0b",
|
"reference": "dc9c4a68c93e8bea2dfaa76d1ba308be539998bd",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -3547,9 +3547,9 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/utopia-php/database/issues",
|
"issues": "https://github.com/utopia-php/database/issues",
|
||||||
"source": "https://github.com/utopia-php/database/tree/0.64.1"
|
"source": "https://github.com/utopia-php/database/tree/0.64.2"
|
||||||
},
|
},
|
||||||
"time": "2025-04-02T00:35:29+00:00"
|
"time": "2025-04-09T07:53:05+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "utopia-php/domains",
|
"name": "utopia-php/domains",
|
||||||
|
|
@ -8126,7 +8126,7 @@
|
||||||
],
|
],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"stability-flags": {},
|
"stability-flags": [],
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
|
|
@ -8150,5 +8150,5 @@
|
||||||
"platform-overrides": {
|
"platform-overrides": {
|
||||||
"php": "8.3"
|
"php": "8.3"
|
||||||
},
|
},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.2.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,7 @@ services:
|
||||||
- _APP_DATABASE_SHARED_TABLES_V1
|
- _APP_DATABASE_SHARED_TABLES_V1
|
||||||
- _APP_DATABASE_SHARED_NAMESPACE
|
- _APP_DATABASE_SHARED_NAMESPACE
|
||||||
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT
|
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT
|
||||||
|
- _APP_CUSTOM_DOMAIN_DENY_LIST
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -351,6 +351,7 @@ class Event
|
||||||
*/
|
*/
|
||||||
public function trigger(): string|bool
|
public function trigger(): string|bool
|
||||||
{
|
{
|
||||||
|
|
||||||
if ($this->paused) {
|
if ($this->paused) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -360,6 +361,7 @@ class Event
|
||||||
|
|
||||||
// Merge the base payload with any trimmed values
|
// Merge the base payload with any trimmed values
|
||||||
$payload = array_merge($this->preparePayload(), $this->trimPayload());
|
$payload = array_merge($this->preparePayload(), $this->trimPayload());
|
||||||
|
|
||||||
return $this->publisher->enqueue($queue, $payload);
|
return $this->publisher->enqueue($queue, $payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,15 +47,22 @@ class Maintenance extends Action
|
||||||
|
|
||||||
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
|
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
|
||||||
|
|
||||||
$dbForPlatform->foreach('projects', function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) {
|
$dbForPlatform->foreach(
|
||||||
$queueForDeletes
|
'projects',
|
||||||
->setType(DELETE_TYPE_MAINTENANCE)
|
[
|
||||||
->setProject($project)
|
Query::equal('region', System::getEnv('_APP_REGION', 'default'))
|
||||||
->setUsageRetentionHourlyDateTime(DateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
|
],
|
||||||
->trigger();
|
function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) {
|
||||||
}, [
|
$queueForDeletes
|
||||||
Query::limit(100),
|
->setType(DELETE_TYPE_MAINTENANCE)
|
||||||
]);
|
->setProject($project)
|
||||||
|
->setUsageRetentionHourlyDateTime(DateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
|
||||||
|
->trigger();
|
||||||
|
},
|
||||||
|
[
|
||||||
|
Query::limit(100),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
$queueForDeletes
|
$queueForDeletes
|
||||||
->setType(DELETE_TYPE_MAINTENANCE)
|
->setType(DELETE_TYPE_MAINTENANCE)
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,8 @@ class StatsResources extends Action
|
||||||
* For each project that were accessed in last 24 hours
|
* For each project that were accessed in last 24 hours
|
||||||
*/
|
*/
|
||||||
$this->foreachDocument($this->dbForPlatform, 'projects', [
|
$this->foreachDocument($this->dbForPlatform, 'projects', [
|
||||||
Query::greaterThanEqual('accessedAt', DateTime::format($last24Hours))
|
Query::greaterThanEqual('accessedAt', DateTime::format($last24Hours)),
|
||||||
|
Query::equal('region', System::getEnv('_APP_REGION', 'default'))
|
||||||
], function ($project) use ($queue) {
|
], function ($project) use ($queue) {
|
||||||
$queue
|
$queue
|
||||||
->setProject($project)
|
->setProject($project)
|
||||||
|
|
|
||||||
|
|
@ -494,21 +494,22 @@ class Deletes extends Action
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Database $dbForPlatform
|
* @param Database $dbForPlatform
|
||||||
* @param Document $document
|
* @param Document $document
|
||||||
* @return void
|
* @return void
|
||||||
* @throws Authorization
|
* @throws Authorization
|
||||||
* @throws DatabaseException
|
* @throws DatabaseException
|
||||||
* @throws Conflict
|
* @throws Conflict
|
||||||
* @throws Restricted
|
* @throws Restricted
|
||||||
* @throws Structure
|
* @throws Structure
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
private function deleteProjectsByTeam(Database $dbForPlatform, callable $getProjectDB, CertificatesAdapter $certificates, Document $document): void
|
protected function deleteProjectsByTeam(Database $dbForPlatform, callable $getProjectDB, CertificatesAdapter $certificates, Document $document): void
|
||||||
{
|
{
|
||||||
|
|
||||||
$projects = $dbForPlatform->find('projects', [
|
$projects = $dbForPlatform->find('projects', [
|
||||||
Query::equal('teamInternalId', [$document->getInternalId()])
|
Query::equal('teamInternalId', [$document->getInternalId()]),
|
||||||
|
Query::equal('region', [System::getEnv('_APP_REGION', 'default')])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
foreach ($projects as $project) {
|
foreach ($projects as $project) {
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,6 @@ class StatsResources extends Action
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($project->getAttribute('database'))) {
|
if (empty($project->getAttribute('database'))) {
|
||||||
var_dump($payload);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ class Webhooks extends Action
|
||||||
$this->errors = [];
|
$this->errors = [];
|
||||||
$payload = $message->getPayload() ?? [];
|
$payload = $message->getPayload() ?? [];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (empty($payload)) {
|
if (empty($payload)) {
|
||||||
throw new Exception('Missing payload');
|
throw new Exception('Missing payload');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue