Compare commits

...
Author SHA1 Message Date
Darshan 94c511f39a lint. 2026-03-05 14:03:30 +05:30
Darshan 7d72fd77e5 fix: test. 2026-03-05 13:58:42 +05:30
Darshan 7477b43653 fix: correct tag. 2026-03-05 13:38:14 +05:30
Darshan 569505c54d readd: local source mounts. 2026-03-05 13:21:49 +05:30
DarshanandGitHub e0d2db0c24 Merge pull request #11458 from appwrite/simplify-dev-merge 2026-03-05 13:13:43 +05:30
Darshan dc66be4b2c Merge branch 'feat-installer' into 'simplify-dev'. 2026-03-05 13:12:15 +05:30
Darshan ccd43c9c2a add: timeout back with correct fix. 2026-02-09 15:54:06 +05:30
Darshan fa6e8b8158 add: timeout back. 2026-02-09 15:41:45 +05:30
Darshan 96d94c9452 update: cleanup on redirects. 2026-02-09 14:57:37 +05:30
Darshan 6454ef55ad fix: router bug for in network connection. 2026-02-09 14:47:47 +05:30
Darshan d502320a94 update: simpler dev testing. 2026-02-09 14:47:26 +05:30
Darshan 850bd81511 fix: local image tagging. 2026-02-04 17:54:26 +05:30
9 changed files with 120 additions and 22 deletions
+5
View File
@@ -26,4 +26,9 @@ test-results
docker-compose.web-installer.yml
.env.web-installer
docker-compose.web-installer.yml.**.backup
# local installer output dir
appwrite/
# screenshots
tests/playwright/screenshots
+2 -1
View File
@@ -115,7 +115,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
}
if (!in_array($host, $platformHostnames)) {
$routerProtectionEnabled = System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'disabled') === 'enabled';
if ($routerProtectionEnabled && !in_array($host, $platformHostnames)) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView);
}
+3
View File
@@ -392,5 +392,8 @@ const COOKIE_NAME_PREVIEW = 'a_jwt_console';
const CACHE_RECONNECT_MAX_RETRIES = 2;
const CACHE_RECONNECT_RETRY_DELAY = 1000;
/* Web installer */
const LOCAL_API_TIMEOUT = 30 * 1000; // 30 seconds
// Project status
const PROJECT_STATUS_ACTIVE = 'active';
@@ -851,6 +851,14 @@
const retryButton = event.target.closest('[data-install-retry]');
if (consoleButton) {
fetch('/install/cleanup', {
method: 'POST',
headers: withCsrfHeader({
'Content-Type': 'application/json'
})
}).catch(() => {});
// Redirect immediately
redirectToApp();
return;
}
@@ -0,0 +1,42 @@
<?php
namespace Appwrite\Platform\Installer\Http\Installer;
use Appwrite\Platform\Installer\Server;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Http\Adapter\Swoole\Response;
use Utopia\Platform\Action;
class Cleanup extends Action
{
public static function getName(): string
{
return 'installerCleanup';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/install/cleanup')
->desc('Cleanup installer container')
->inject('request')
->inject('response')
->callback($this->action(...));
}
public function action(Request $request, Response $response): void
{
if (!Validate::validateCsrf($request)) {
$response->setStatusCode(Response::STATUS_CODE_BAD_REQUEST);
$response->json(['success' => false, 'message' => 'Invalid CSRF token']);
return;
}
// Remove the installer container
$container = escapeshellarg(Server::DEFAULT_CONTAINER);
@exec("docker rm -f $container >/dev/null 2>&1");
$response->json(['success' => true]);
}
}
+22 -8
View File
@@ -153,7 +153,7 @@ class Server
->inject('response')
->action($errorHandler->action(...));
$adapter = new class($host, $port, ['worker_num' => 1]) extends SwooleAdapter {
$adapter = new class ($host, $port, ['worker_num' => 1]) extends SwooleAdapter {
public function getNativeServer(): SwooleServer
{
return $this->server;
@@ -198,14 +198,27 @@ class Server
private function cleanupWebInstallerFiles(): void
{
$cwd = getcwd();
if ($cwd === false) {
return;
$baseDir = null;
try {
$cfg = $this->state->buildConfig();
if ($cfg->isLocal() && !empty($cfg->getHostPath())) {
$baseDir = rtrim($cfg->getHostPath(), '/') . '/appwrite';
}
} catch (\Throwable) {
// Fall back to cwd
}
if ($baseDir === null) {
$cwd = getcwd();
if ($cwd === false) {
return;
}
$baseDir = $cwd;
}
$filesToRemove = [
$cwd . '/.env.web-installer',
$cwd . '/docker-compose.web-installer.yml',
$baseDir . '/.env.web-installer',
$baseDir . '/docker-compose.web-installer.yml',
];
foreach ($filesToRemove as $file) {
@@ -241,7 +254,7 @@ class Server
}
}
private function ensureLocalInstallerTag(string $source, string $target): void
private function ensureCorrectTag(string $source, string $target): void
{
$sourceArg = escapeshellarg($source);
$targetArg = escapeshellarg($target);
@@ -259,7 +272,7 @@ class Server
if (!$this->dockerImageExists($image)) {
$this->buildDockerInstallerImage($image);
}
$this->ensureLocalInstallerTag($image, 'appwrite/appwrite:local');
$this->ensureCorrectTag($image, 'appwrite/appwrite:local');
$port = (string)self::INSTALLER_WEB_PORT;
$entrypoint = isset($opts['upgrade']) ? 'upgrade' : 'install';
@@ -290,6 +303,7 @@ class Server
'-p', "127.0.0.1:$port:" . self::INSTALLER_WEB_PORT,
'--volume', '/var/run/docker.sock:/var/run/docker.sock',
'--volume', "$volumePath:/usr/src/code:rw",
'--volume', "$volumePath:$volumePath:rw",
];
$args[] = '-e';
$args[] = 'APPWRITE_INSTALLER_CONFIG=' . $configJson;
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Installer\Services;
use Appwrite\Platform\Installer\Http\Installer\Cleanup;
use Appwrite\Platform\Installer\Http\Installer\Complete;
use Appwrite\Platform\Installer\Http\Installer\Install;
use Appwrite\Platform\Installer\Http\Installer\Shutdown;
@@ -19,6 +20,7 @@ class Http extends Service
$this->addAction(View::getName(), new View());
$this->addAction(Status::getName(), new Status());
$this->addAction(Validate::getName(), new Validate());
$this->addAction(Cleanup::getName(), new Cleanup());
$this->addAction(Complete::getName(), new Complete());
$this->addAction(Shutdown::getName(), new Shutdown());
$this->addAction(Install::getName(), new Install());
+21 -11
View File
@@ -471,6 +471,11 @@ class Install extends Action
): void {
$isLocalInstall = $this->isLocalInstall();
$this->applyLocalPaths($isLocalInstall, false);
if ($isLocalInstall && !is_dir($this->path)) {
if (!@mkdir($this->path, 0755, true)) {
throw new \Exception('Can\'t create directory ' . $this->path);
}
}
$isCLI = php_sapi_name() === 'cli';
if ($isLocalInstall) {
@@ -490,10 +495,9 @@ class Install extends Action
$database = $input['_APP_DB_ADAPTER'] ?? 'mongodb';
$version = \defined('APP_VERSION_STABLE') ? APP_VERSION_STABLE : 'latest';
if ($isLocalInstall) {
$version = 'local';
}
$version = $isLocalInstall
? 'local'
: (defined('APP_VERSION_STABLE') ? APP_VERSION_STABLE : 'latest');
$assistantKey = (string) ($input['_APP_ASSISTANT_OPENAI_API_KEY'] ?? '');
$enableAssistant = trim($assistantKey) !== '';
@@ -505,8 +509,8 @@ class Install extends Action
->setParam('organization', $organization)
->setParam('image', $image)
->setParam('database', $database)
->setParam('hostPath', $this->hostPath)
->setParam('enableAssistant', $enableAssistant);
->setParam('enableAssistant', $enableAssistant)
->setParam('hostPath', $this->hostPath);
$templateForEnv->setParam('vars', $input);
@@ -588,7 +592,12 @@ class Install extends Action
}
} else {
if ($isCLI) {
Console::success('Installation files created. Run "docker compose up -d" to start Appwrite');
if ($isLocalInstall) {
$composePath = $this->path . '/' . $this->getComposeFileName();
Console::success('Installation files created in ' . $this->path . '. Run "docker compose -f ' . $composePath . ' up -d" to start Appwrite');
} else {
Console::success('Installation files created. Run "docker compose up -d" to start Appwrite');
}
}
}
} catch (\Throwable $e) {
@@ -822,7 +831,7 @@ class Install extends Action
{
$client = new Client();
$client
->setTimeout(30000)
->setTimeout(LOCAL_API_TIMEOUT)
->setConnectTimeout(10000)
->addHeader('Content-Type', 'application/json')
->addHeader('X-Appwrite-Project', 'console')
@@ -1043,8 +1052,9 @@ class Install extends Action
if (!$force && $this->hostPath !== '') {
return;
}
$this->path = '/usr/src/code';
$this->hostPath = $this->getInstallerHostPath();
$hostPath = rtrim($this->hostPath, '/');
$this->path = $hostPath . '/appwrite';
}
protected function readExistingCompose(): string
@@ -1066,12 +1076,12 @@ class Install extends Action
protected function getComposeFileName(): string
{
return $this->isLocalInstall() ? 'docker-compose.web-installer.yml' : 'docker-compose.yml';
return 'docker-compose.yml';
}
protected function getEnvFileName(): string
{
return $this->isLocalInstall() ? '.env.web-installer' : '.env';
return '.env';
}
private function isInstallationComplete(int $port): bool
@@ -2,6 +2,7 @@
namespace Tests\Unit\Platform\Modules\Installer;
use Appwrite\Platform\Installer\Http\Installer\Cleanup;
use Appwrite\Platform\Installer\Http\Installer\Complete;
use Appwrite\Platform\Installer\Http\Installer\Error;
use Appwrite\Platform\Installer\Http\Installer\Install;
@@ -41,10 +42,11 @@ class ModuleTest extends TestCase
$service = reset($services);
$actions = $service->getActions();
$this->assertCount(6, $actions);
$this->assertCount(7, $actions);
$this->assertArrayHasKey('installerView', $actions);
$this->assertArrayHasKey('installerStatus', $actions);
$this->assertArrayHasKey('installerValidate', $actions);
$this->assertArrayHasKey('installerCleanup', $actions);
$this->assertArrayHasKey('installerComplete', $actions);
$this->assertArrayHasKey('installerShutdown', $actions);
$this->assertArrayHasKey('installerInstall', $actions);
@@ -97,6 +99,17 @@ class ModuleTest extends TestCase
$this->assertActionInjects($action, ['request', 'response', 'installerState']);
}
public function testCleanupAction(): void
{
$action = $this->getAction('installerCleanup');
$this->assertEquals('installerCleanup', Cleanup::getName());
$this->assertEquals(Action::HTTP_REQUEST_METHOD_POST, $action->getHttpMethod());
$this->assertEquals('/install/cleanup', $action->getHttpPath());
$this->assertEquals(Action::TYPE_DEFAULT, $action->getType());
$this->assertActionInjects($action, ['request', 'response']);
}
public function testShutdownAction(): void
{
$action = $this->getAction('installerShutdown');
@@ -138,7 +151,7 @@ class ModuleTest extends TestCase
*/
public function testRouteRegistration(): void
{
$platform = new class(new Module()) extends Platform {};
$platform = new class (new Module()) extends Platform {};
$platform->init(Service::TYPE_HTTP);
// If we get here without exceptions, route registration succeeded