mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-16 16:45:07 +00:00
8f12c83d21
ProjectSend prints the update instructions for the way this server was installed, and it knew two answers where it needed three: anything inside a container was handed `docker compose pull && docker compose up -d`. On the Compose stack that builds from a checkout there is no image behind those containers, so `pull` skips every ProjectSend service and `up -d` then finds them all current — the update reports success, changes nothing, and the dashboard goes on offering the same release. Reported by @mueller7382, who stayed on 2.0.0 that way while 2.1.0 was out (#1661). Those installations are now their own kind, told to `git pull` and rebuild, with the two steps a checkout needs that an image does not: its dependencies and its compiled frontend live outside git, so a release that moved either leaves them stale. Two signals decide it, in that order. The published image now declares itself with PROJECTSEND_IMAGE, which is the only evidence an operator bind-mounting over /var/www/html can neither hide nor forge; failing that — images published before this — a working tree in the install directory, which the image never has and the repository's own stack always does. getenv() rather than env(), because a cached configuration makes env() outside a config file return null, and the answer would flip silently on exactly the installs most likely to have cached it. The stale-code banner keeps treating both container kinds alike: what clears it is recreating the container, whichever way its image was built. The changelog also credits the reporter of #1663, which was missed when that entry was written.
97 lines
4.2 KiB
PHP
97 lines
4.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Platform\Installation;
|
|
|
|
/**
|
|
* Whether this installation runs from a container image, from a container
|
|
* the operator builds themselves, or from files on a server somebody
|
|
* administers directly.
|
|
*
|
|
* It exists because the application tells administrators how to upgrade, and
|
|
* the three answers have nothing in common. A published container is
|
|
* replaced — `docker compose pull && docker compose up -d`, with the
|
|
* entrypoint running the migrations on the way up. A container built from a
|
|
* checkout has to be given new code and rebuilt. A manual install is a
|
|
* sequence somebody performs by hand: back up, take the site down, unpack
|
|
* the release over the directory, migrate, refresh the caches, bring it back
|
|
* (INSTALL.md).
|
|
*
|
|
* Printing the wrong one of those is worse than printing nothing. For a
|
|
* manual install the container command names a tool they do not have, for a
|
|
* stack they are not running, at the exact moment they are trying to do the
|
|
* right thing — that was the behaviour before this class existed, when the
|
|
* command was a hardcoded string in two React components. For a stack built
|
|
* from a checkout it is worse still, because the command runs: `pull` skips
|
|
* services that have no image to pull and `up -d` then finds every container
|
|
* already current, so the update reports success and changes nothing, and
|
|
* the dashboard goes on offering the same release forever (#1661).
|
|
*
|
|
* Two signals, in order:
|
|
*
|
|
* 1. The published image sets PROJECTSEND_IMAGE. A positive marker set at
|
|
* build time is the only one a bind mount can neither forge nor hide.
|
|
* 2. Failing that — images published before that variable existed — a
|
|
* working tree in the install directory. The image is built from an
|
|
* unpacked release artifact and has none; the Compose stack in the
|
|
* repository bind-mounts the repository itself.
|
|
*
|
|
* Being in a container at all is the presence of the file a container
|
|
* runtime leaves in the root filesystem. It is a deliberately conservative
|
|
* signal: something exotic enough to run neither Docker nor Podman is
|
|
* reported as a manual install, which is the safer wrong answer of the
|
|
* three — the manual instructions are steps a person follows and checks for
|
|
* themselves, while the container commands are ones they would paste.
|
|
*/
|
|
class Installation
|
|
{
|
|
public function kind(): InstallationKind
|
|
{
|
|
if (! $this->inContainer()) {
|
|
return InstallationKind::Manual;
|
|
}
|
|
|
|
return $this->builtFromSource()
|
|
? InstallationKind::ContainerSource
|
|
: InstallationKind::Container;
|
|
}
|
|
|
|
/**
|
|
* Protected so a test can answer for it: there is no way to be in a
|
|
* container and not in one within a single test run.
|
|
*/
|
|
protected function inContainer(): bool
|
|
{
|
|
// Docker writes the first; Podman writes the second.
|
|
//
|
|
// Suppressed, and it has to stay that way. Shared hosting sets
|
|
// open_basedir to the webspace, and probing a path outside it is a
|
|
// warning rather than a false — which the framework's error handler
|
|
// turns into an exception, so the one call that asks which install
|
|
// this is took the whole dashboard down with it (#1663). Under `@`
|
|
// the warning is filtered and the probe answers false, which is the
|
|
// right answer anyway: a host that restricts PHP to a vhost
|
|
// directory is not the container image.
|
|
return @file_exists('/.dockerenv') || @file_exists('/run/.containerenv');
|
|
}
|
|
|
|
/**
|
|
* Protected for the same reason as inContainer(), and answered the same
|
|
* way in tests.
|
|
*/
|
|
protected function builtFromSource(): bool
|
|
{
|
|
// getenv() rather than env(): once the configuration is cached,
|
|
// env() outside a config file returns null, and the answer would
|
|
// silently flip on the installs most likely to have cached it.
|
|
if (getenv('PROJECTSEND_IMAGE') === '1') {
|
|
return false;
|
|
}
|
|
|
|
// A worktree checkout writes .git as a file rather than a
|
|
// directory, so ask whether it exists, not what it is.
|
|
return file_exists(base_path('.git'));
|
|
}
|
|
}
|