diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 0f4df352bd..f160e38b94 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -11,6 +11,8 @@ $httpsPort = $this->getParam('httpsPort', ''); $version = $this->getParam('version', ''); $organization = $this->getParam('organization', ''); $image = $this->getParam('image', ''); +$appwriteImage = $this->getParam('appwriteImage', ''); +$useHostSourceMount = $this->getParam('useHostSourceMount', false); $enableAssistant = $this->getParam('enableAssistant', false); $dbService = $this->getParam('database', 'mongodb'); $allowedDbServices = ['mariadb', 'mongodb', 'postgresql']; @@ -18,6 +20,9 @@ if (!\in_array($dbService, $allowedDbServices, true)) { $dbService = 'mongodb'; } $hostPath = rtrim($this->getParam('hostPath', ''), '/'); +if ($appwriteImage === '') { + $appwriteImage = $organization . '/' . $image . ':' . $version; +} ?>services: traefik: image: traefik:3.6 @@ -46,7 +51,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - appwrite appwrite: - image: /: + image: container_name: appwrite <<: *x-logging restart: unless-stopped @@ -67,7 +72,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - traefik.http.routers.appwrite_api_https.service=appwrite_api - traefik.http.routers.appwrite_api_https.tls=true volumes: - + - ":/usr/src/code:rw" - appwrite-uploads:/storage/uploads:rw @@ -214,7 +219,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - traefik.http.routers.appwrite_console_https.tls=true appwrite-realtime: - image: /: + image: entrypoint: realtime container_name: appwrite-realtime <<: *x-logging @@ -261,7 +266,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_LOGGING_CONFIG appwrite-worker-audits: - image: /: + image: entrypoint: worker-audits <<: *x-logging container_name: appwrite-worker-audits @@ -291,7 +296,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_LOGGING_CONFIG appwrite-worker-webhooks: - image: /: + image: entrypoint: worker-webhooks <<: *x-logging container_name: appwrite-worker-webhooks @@ -323,7 +328,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_LOGGING_CONFIG appwrite-worker-deletes: - image: /: + image: entrypoint: worker-deletes <<: *x-logging container_name: appwrite-worker-deletes @@ -390,7 +395,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_EMAIL_CERTIFICATES appwrite-worker-databases: - image: /: + image: entrypoint: worker-databases <<: *x-logging container_name: appwrite-worker-databases @@ -420,7 +425,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_LOGGING_CONFIG appwrite-worker-builds: - image: /: + image: entrypoint: worker-builds <<: *x-logging container_name: appwrite-worker-builds @@ -492,7 +497,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_DOMAIN_SITES appwrite-worker-certificates: - image: /: + image: entrypoint: worker-certificates <<: *x-logging container_name: appwrite-worker-certificates @@ -533,7 +538,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_LOGGING_CONFIG appwrite-worker-functions: - image: /: + image: entrypoint: worker-functions <<: *x-logging container_name: appwrite-worker-functions @@ -577,7 +582,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_LOGGING_CONFIG appwrite-worker-mails: - image: /: + image: entrypoint: worker-mails <<: *x-logging container_name: appwrite-worker-mails @@ -616,7 +621,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_OPTIONS_FORCE_HTTPS appwrite-worker-messaging: - image: /: + image: entrypoint: worker-messaging <<: *x-logging container_name: appwrite-worker-messaging @@ -672,7 +677,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_STORAGE_WASABI_BUCKET appwrite-worker-migrations: - image: /: + image: entrypoint: worker-migrations <<: *x-logging container_name: appwrite-worker-migrations @@ -711,7 +716,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET appwrite-task-maintenance: - image: /: + image: entrypoint: maintenance <<: *x-logging container_name: appwrite-task-maintenance @@ -755,7 +760,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_MAINTENANCE_RETENTION_SCHEDULES appwrite-task-stats-resources: - image: /: + image: container_name: appwrite-task-stats-resources entrypoint: stats-resources <<: *x-logging @@ -788,7 +793,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_STATS_RESOURCES_INTERVAL appwrite-worker-stats-resources: - image: /: + image: entrypoint: worker-stats-resources container_name: appwrite-worker-stats-resources <<: *x-logging @@ -820,7 +825,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_STATS_RESOURCES_INTERVAL appwrite-worker-stats-usage: - image: /: + image: entrypoint: worker-stats-usage container_name: appwrite-worker-stats-usage <<: *x-logging @@ -852,7 +857,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_USAGE_AGGREGATION_INTERVAL appwrite-task-scheduler-functions: - image: /: + image: entrypoint: schedule-functions container_name: appwrite-task-scheduler-functions <<: *x-logging @@ -881,7 +886,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_DB_ADAPTER appwrite-task-scheduler-executions: - image: /: + image: entrypoint: schedule-executions container_name: appwrite-task-scheduler-executions <<: *x-logging @@ -910,7 +915,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/'); - _APP_DB_ADAPTER appwrite-task-scheduler-messages: - image: /: + image: entrypoint: schedule-messages container_name: appwrite-task-scheduler-messages <<: *x-logging diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index af768444f2..0cd2a219b1 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -106,7 +106,7 @@ class Install extends Action file_put_contents($this->path . '/' . $composeFileName . '.' . $time . '.backup', $data); $compose = new Compose($data); $appwrite = $compose->getService('appwrite'); - $oldVersion = $appwrite?->getImageVersion(); + $oldImage = $appwrite?->getImage() ?? null; try { $ports = $compose->getService('traefik')->getPorts(); } catch (\Throwable $th) { @@ -117,7 +117,7 @@ class Install extends Action Console::warning('Traefik not found. Falling back to default ports.'); } - if ($oldVersion) { + if (!empty($oldImage)) { foreach ($compose->getServices() as $service) { if (!$service) { continue; @@ -509,12 +509,14 @@ class Install extends Action $this->applyLocalPaths($isLocalInstall, false); $isCLI = php_sapi_name() === 'cli'; - if ($isLocalInstall || $isUpgrade) { - $useExistingConfig = false; - } else { - $useExistingConfig = file_exists($this->path . '/' . $this->getComposeFileName()) - && file_exists($this->path . '/' . $this->getEnvFileName()); - } + $existingComposePath = $this->path . '/' . $this->getComposeFileName(); + $existingEnvPath = $this->path . '/' . $this->getEnvFileName(); + $useExistingConfig = $this->shouldReuseExistingConfig( + $isLocalInstall, + $isUpgrade, + file_exists($existingComposePath) && file_exists($existingEnvPath), + $this->readExistingComposeImage() + ); if ($isLocalInstall) { $image = 'appwrite'; @@ -531,6 +533,8 @@ class Install extends Action $version = 'local'; } + $appwriteImage = $this->resolveComposeAppwriteImage($isLocalInstall, $organization, $image, $version); + $assistantKey = (string) ($input['_APP_ASSISTANT_OPENAI_API_KEY'] ?? ''); $enableAssistant = trim($assistantKey) !== ''; @@ -540,8 +544,10 @@ class Install extends Action ->setParam('version', $version) ->setParam('organization', $organization) ->setParam('image', $image) + ->setParam('appwriteImage', $appwriteImage) ->setParam('database', $database) ->setParam('hostPath', $this->hostPath) + ->setParam('useHostSourceMount', $isLocalInstall) ->setParam('enableAssistant', $enableAssistant); $templateForEnv->setParam('vars', $input); @@ -1096,6 +1102,63 @@ class Install extends Action return !empty($data) ? $data : ''; } + protected function readExistingComposeImage(): ?string + { + $data = $this->readExistingCompose(); + + if ($data === '') { + return null; + } + + try { + $compose = new Compose($data); + return $compose->getService('appwrite')?->getImage(); + } catch (\Throwable) { + return null; + } + } + + protected function shouldUseLocalAppwriteImage(): bool + { + return \trim((string) \getenv('_APP_VERSION')) === 'dev' && $this->dockerImageExists('appwrite-dev'); + } + + protected function resolveComposeAppwriteImage(bool $isLocalInstall, string $organization, string $image, string $version): string + { + if ($isLocalInstall || $this->shouldUseLocalAppwriteImage()) { + return 'appwrite-dev'; + } + + return $organization . '/' . $image . ':' . $version; + } + + protected function shouldReuseExistingConfig(bool $isLocalInstall, bool $isUpgrade, bool $configFilesExist, ?string $existingImage = null): bool + { + if ($isLocalInstall || $isUpgrade || !$configFilesExist) { + return false; + } + + // Avoid sticky mutable or legacy dev tags from previous failed installer runs. + if ($existingImage !== null && $this->isStaleMutableAppwriteImageReference($existingImage)) { + return false; + } + + return true; + } + + protected function isStaleMutableAppwriteImageReference(string $image): bool + { + return \in_array(\strtolower(\trim($image)), ['appwrite/appwrite:dev', 'appwrite/appwrite:unknown'], true); + } + + protected function dockerImageExists(string $image): bool + { + $result = 1; + $output = []; + \exec('docker image inspect ' . \escapeshellarg($image) . ' >/dev/null 2>&1', $output, $result); + return $result === 0; + } + protected function generatePasswordValue(string $varName, Password $password): string { $value = $password->generate(); diff --git a/tests/unit/Platform/Tasks/InstallTest.php b/tests/unit/Platform/Tasks/InstallTest.php new file mode 100644 index 0000000000..d54192193c --- /dev/null +++ b/tests/unit/Platform/Tasks/InstallTest.php @@ -0,0 +1,57 @@ +assertSame('appwrite-dev', $this->resolveComposeAppwriteImageForTest(false, 'appwrite', 'appwrite', 'dev', true)); + } + + public function testShouldNotReuseExistingConfigForDevTaggedCompose(): void + { + $this->assertFalse($this->shouldReuseExistingConfigForTest(false, false, true, 'appwrite/appwrite:dev')); + } + + public function testShouldReuseExistingConfigForLocalAppwriteImageReference(): void + { + $this->assertTrue($this->shouldReuseExistingConfigForTest(false, false, true, 'appwrite-dev')); + } + + private function resolveComposeAppwriteImageForTest(bool $isLocalInstall, string $organization, string $image, string $version, bool $useLocalAppwriteImage): string + { + $install = new class ($useLocalAppwriteImage) extends Install { + public function __construct(private bool $useLocalAppwriteImage) + { + } + + public function resolveComposeAppwriteImageForTest(bool $isLocalInstall, string $organization, string $image, string $version): string + { + return $this->resolveComposeAppwriteImage($isLocalInstall, $organization, $image, $version); + } + + protected function shouldUseLocalAppwriteImage(): bool + { + return $this->useLocalAppwriteImage; + } + }; + + return $install->resolveComposeAppwriteImageForTest($isLocalInstall, $organization, $image, $version); + } + + private function shouldReuseExistingConfigForTest(bool $isLocalInstall, bool $isUpgrade, bool $configFilesExist, ?string $existingImage = null): bool + { + $install = new class () extends Install { + public function shouldReuseExistingConfigForTest(bool $isLocalInstall, bool $isUpgrade, bool $configFilesExist, ?string $existingImage = null): bool + { + return $this->shouldReuseExistingConfig($isLocalInstall, $isUpgrade, $configFilesExist, $existingImage); + } + }; + + return $install->shouldReuseExistingConfigForTest($isLocalInstall, $isUpgrade, $configFilesExist, $existingImage); + } +}