diff --git a/README.MD b/README.MD index c6e9c21f..d9b91db0 100644 --- a/README.MD +++ b/README.MD @@ -56,7 +56,7 @@ Here are a few key differences compared to the original Valet: - Rabbitmq (on/off mode) - Xdebug (on/off mode) - Memcache (on/off mode) -- Elasticsearch v6, ~~v7~~, ~~v8~~ (on/off mode) +- Elasticsearch v6, v7, ~~v8~~ (on/off mode) using Docker - Opensearch (on/off mode) - ~~Ioncube~~ - Rewrite/unrewrite public domain to local environment @@ -70,6 +70,7 @@ Here are a few key differences compared to the original Valet: - Use command `valet-plus elasticsearch|es use ` instead of `valet-plus use elasticsearch|es `. - Use `127.0.0.1` as Redis host instead of `/tmp/redis.sock`. - Choose which binaries to install (default all) and self-update on `valet-plus install` command. +- Adds dependency on Docker for Elasticsearch, see https://docs.docker.com/desktop/install/mac-install/ diff --git a/cli/ValetPlus/AbstractDockerService.php b/cli/ValetPlus/AbstractDockerService.php new file mode 100644 index 00000000..388de3b8 --- /dev/null +++ b/cli/ValetPlus/AbstractDockerService.php @@ -0,0 +1,202 @@ +cli = $cli; + $this->files = $files; + } + + /** + * Returns a collection of names of the running docker containers. + * + * @return Collection + */ + public function getAllRunningContainers(): Collection + { + $command = 'docker ps --format=\'{{.Names}}\''; + $onError = function ($exitCode, $errorOutput) { + output($errorOutput); + + throw new DomainException('Docker was unable to check which containers are running.'); + }; + + return collect(array_filter(explode(PHP_EOL, $this->cli->runAsUser($command, $onError)))); + } + + /** + * Runs a docker command from the path where the docker-compose.yml is located. + * + * @param $command + * @param $dir + * @return $this + */ + public function runCommand($command, $dir) + { + $cwd = getcwd(); + @chdir($dir); + $this->cli->runAsUser($command); + @chdir($cwd); + + return $this; + } + + /** + * Starts the docker container by the service's name. Creates the container first, if it doesn't exist yet. + * + * @param $name + * @return $this + */ + public function upContainer($name) + { + info("Docker up version {$name}..."); + $installPath = $this->getComposeInstallPath($name); + $installDir = $this->getComposeInstallDir($name); + + // Copy docker-compose.yml stub to installation/running path + if (!$this->files->isDir($installDir)) { + $this->files->mkdirAsUser($installDir); + } + $this->files->copyAsUser( + $this->getComposeStubPath($name), + $installPath + ); + + // Start the container the directory where the docker-compose.yml exists. + $this->runCommand( + 'docker compose up --detach', + $installDir + ); + + return $this; + } + + /** + * Stops the docker container by the service's name. + * + * @param $name + * @return $this + */ + public function stopContainer($name) + { + info("Docker stop version {$name}..."); + $this->runCommand( + 'docker compose stop', + $this->getComposeInstallDir($name) + ); + + return $this; + } + + /** + * Stop and remove containers, networks, images and volumes by the service's name. + * + * @param $name + * @return $this + */ + public function downContainer($name) + { + info("Docker down version {$name}..."); + $this->runCommand( + 'docker compose down --volumes --rmi all', + $this->getComposeInstallDir($name) + ); + + return $this; + } + + /** + * Returns the short class name in lowercase. + * + * @return string + */ + protected function getServiceName(): string + { + if (!$this->serviceName) { + try { + // We store the service's name in a property to prevent a lot of reflection (which is slow). + $this->serviceName = strtolower((new \ReflectionClass($this))->getShortName()); + } catch (\ReflectionException $reflectionException) { + echo 'Ohoh reflection exception'; + die(); + } + } + + return $this->serviceName; + } + + /** + * Returns path of the docker-compose.yml stub file for the service. + * + * @param $name + * @return string + */ + protected function getComposeStubPath($name): string + { + return sprintf( + static::DOCKER_COMPOSE_STUB, + $this->getServiceName(), + $name + ); + } + + /** + * Returns installation path of the docker-compose.yml stub file for the service. + * + * @param $name + * @return string + */ + protected function getComposeInstallPath($name): string + { + return sprintf( + static::DOCKER_COMPOSE_PATH, + $this->getServiceName(), + $name + ); + } + + /** + * Returns the directory of the installation path of the docker-compose.yml stub file for the service. + * + * @param $name + * @return string + */ + protected function getComposeInstallDir($name): string + { + return dirname($this->getComposeInstallPath($name)); + } +} diff --git a/cli/ValetPlus/AbstractService.php b/cli/ValetPlus/AbstractService.php index 6977d05e..3eb5bc0a 100644 --- a/cli/ValetPlus/AbstractService.php +++ b/cli/ValetPlus/AbstractService.php @@ -55,6 +55,7 @@ public function getConfigClassName(): string { if (!$this->configClassName) { try { + // We store the service's name in a property to prevent a lot of reflection (which is slow). $this->configClassName = strtolower((new \ReflectionClass($this))->getShortName()); } catch (\ReflectionException $reflectionException) { echo 'Ohoh reflection exception'; diff --git a/cli/ValetPlus/Elasticsearch.php b/cli/ValetPlus/Elasticsearch.php index 8ad0fe65..c45a1453 100644 --- a/cli/ValetPlus/Elasticsearch.php +++ b/cli/ValetPlus/Elasticsearch.php @@ -10,38 +10,38 @@ use Valet\Filesystem; use function Valet\info; -class Elasticsearch +class Elasticsearch extends AbstractDockerService { /** @var string */ - const NGINX_CONFIGURATION_STUB = __DIR__ . '/../stubs/elasticsearch.conf'; + const NGINX_CONFIGURATION_STUB = __DIR__ . '/../stubs/elasticsearch/elasticsearch.conf'; /** @var string */ const NGINX_CONFIGURATION_PATH = VALET_HOME_PATH . '/Nginx/elasticsearch.conf'; /** @var string */ - protected const ES_DEFAULT_VERSION = 'opensearch'; + protected const ES_DEFAULT_VERSION = 'opensearch'; // which is v2 in Brew, @todo; maybe support v1.2 using docker? /** @var string[] */ - protected const ES_SUPPORTED_VERSIONS = ['opensearch', 'elasticsearch@6']; + protected const ES_SUPPORTED_VERSIONS = ['opensearch', 'elasticsearch6', 'elasticsearch7']; + /** @var string[] */ + protected const ES_DOCKER_VERSIONS = ['elasticsearch6', 'elasticsearch7']; + /** @var string[] */ + protected const ES_EOL_VERSIONS = ['elasticsearch@6']; /** @var Brew */ protected $brew; - /** @var CommandLine */ - protected $cli; - /** @var Filesystem */ - protected $files; /** - * @param Brew $brew * @param CommandLine $cli * @param Filesystem $files + * @param Brew $brew */ public function __construct( - Brew $brew, CommandLine $cli, - Filesystem $files + Filesystem $files, + Brew $brew ) { - $this->brew = $brew; - $this->cli = $cli; - $this->files = $files; + parent::__construct($cli, $files); + + $this->brew = $brew; } /** @@ -54,15 +54,46 @@ public function getSupportedVersions() return static::ES_SUPPORTED_VERSIONS; } + /** + * Returns supported elasticsearch versions running in Docker. + * + * @return string[] + */ + public function getDockerVersions() + { + return static::ES_DOCKER_VERSIONS; + } + + /** + * Returns end-of-life elasticsearch versions. + * + * @return string[] + */ + public function getEolVersions() + { + return static::ES_EOL_VERSIONS; + } + /** * Returns if provided version is supported. * * @param $version * @return bool */ - public function isSupportedVersion($version) + public function isSupportedVersion($version): bool { - return in_array($version, static::ES_SUPPORTED_VERSIONS); + return in_array($version, $this->getSupportedVersions()); + } + + /** + * Returns is provided version is running as Docker container. If not, it's running natively (installed with Brew). + * + * @param $version + * @return bool + */ + public function isDockerVersion($version): bool + { + return in_array($version, $this->getDockerVersions()); } /** @@ -70,11 +101,13 @@ public function isSupportedVersion($version) * * @return string|null */ - public function getCurrentVersion() + public function getCurrentVersion(): ?string { - $runningServices = $this->brew->getAllRunningServices()->filter(function ($service) { - return $this->isSupportedVersion($service); - }); + $runningServices = $this->brew->getAllRunningServices() + ->merge($this->getAllRunningContainers()) + ->filter(function ($service) { + return $this->isSupportedVersion($service); + }); return $runningServices->first(); } @@ -120,6 +153,12 @@ public function stop($version = null) if (!$version) { return; } + + if ($this->isDockerVersion($version)) { + $this->stopContainer($version); + return; + } + if (!$this->brew->installed($version)) { return; } @@ -139,6 +178,13 @@ public function restart($version = null) if (!$version) { return; } + + if ($this->isDockerVersion($version)) { + $this->stopContainer($version); + $this->upContainer($version); + return; + } + if (!$this->brew->installed($version)) { return; } @@ -157,31 +203,38 @@ public function install($version = self::ES_DEFAULT_VERSION, $tld = 'test') { if (!$this->isSupportedVersion($version)) { throw new DomainException( - sprintf('Invalid Elasticsearch version given. Available versions: %s', implode(', ', static::ES_SUPPORTED_VERSIONS)) + sprintf( + 'Invalid Elasticsearch version given. Available versions: %s', + implode(', ', static::ES_SUPPORTED_VERSIONS) + ) ); } - // todo; install java dependency? and remove other java deps? seems like there can be only one running. - // opensearch requires openjdk (installed automatically) - // elasticsearch@6 requires openjdk@17 (installed automatically) - // seems like there can be only one openjdk when installing. after installing it doesn't matter. - // if this dependency is installed we need to launch es with this java version, see https://github.com/Homebrew/homebrew-core/issues/100260 + if (!$this->isDockerVersion($version)) { + // For Docker versions we don't need to anything here. - $this->brew->ensureInstalled($version); + // todo; install java dependency? and remove other java deps? seems like there can be only one running. + // opensearch requires openjdk (installed automatically) + // elasticsearch@6 requires openjdk@17 (installed automatically) + // seems like there can be only one openjdk when installing. after installing it doesn't matter. + // if this dependency is installed we need to launch es with this java version, see https://github.com/Homebrew/homebrew-core/issues/100260 - // todo: switch config still needed? > not between opensearch and elasticsearch@6 - // ==> opensearch - //Data: /usr/local/var/lib/opensearch/ - //Logs: /usr/local/var/log/opensearch/*.log - //Plugins: /usr/local/var/opensearch/plugins/ - //Config: /usr/local/etc/opensearch/ - // ==> elasticsearch@6 - //Data: /usr/local/var/lib/elasticsearch/ - //Logs: /usr/local/var/log/elasticsearch/*.log - //Plugins: /usr/local/var/elasticsearch/plugins/ - //Config: /usr/local/etc/elasticsearch/ + $this->brew->ensureInstalled($version); - // todo; add support for adding plugins like 'analysis-icu' and 'analysis-phonetic'? + // todo: switch config still needed? > not between opensearch and elasticsearch@6 + // ==> opensearch + //Data: /usr/local/var/lib/opensearch/ + //Logs: /usr/local/var/log/opensearch/*.log + //Plugins: /usr/local/var/opensearch/plugins/ + //Config: /usr/local/etc/opensearch/ + // ==> elasticsearch@6 + //Data: /usr/local/var/lib/elasticsearch/ + //Logs: /usr/local/var/log/elasticsearch/*.log + //Plugins: /usr/local/var/elasticsearch/plugins/ + //Config: /usr/local/etc/elasticsearch/ + + // todo; add support for adding plugins like 'analysis-icu' and 'analysis-phonetic'? + } $this->restart($version); $this->updateDomain($tld); @@ -195,12 +248,17 @@ public function uninstall() // Remove nginx domain listen file. $this->files->unlink(static::NGINX_CONFIGURATION_PATH); - foreach ($this->getSupportedVersions() as $version) { + $versions = array_merge($this->getSupportedVersions(), $this->getEolVersions()); + foreach ($versions as $version) { $this->stop($version); - $this->brew->uninstallFormula($version); + if ($this->isDockerVersion($version)) { + $this->downContainer($version); + } else { + $this->brew->uninstallFormula($version); + } } - // Elasticsearch files + // Legacy elasticsearch files if (file_exists(BREW_PREFIX . '/var/elasticsearch')) { $this->files->rmDirAndContents(BREW_PREFIX . '/var/elasticsearch'); } diff --git a/cli/includes/facades.php b/cli/includes/facades.php index 6021c486..472e53e2 100644 --- a/cli/includes/facades.php +++ b/cli/includes/facades.php @@ -62,3 +62,6 @@ class Binary extends ValetPlusFacade class DriverConfigurator extends ValetPlusFacade { } +class Docker extends ValetPlusFacade +{ +} diff --git a/cli/stubs/elasticsearch.conf b/cli/stubs/elasticsearch/elasticsearch.conf similarity index 100% rename from cli/stubs/elasticsearch.conf rename to cli/stubs/elasticsearch/elasticsearch.conf diff --git a/cli/stubs/elasticsearch/elasticsearch6/docker-compose.yml b/cli/stubs/elasticsearch/elasticsearch6/docker-compose.yml new file mode 100644 index 00000000..358dbb7f --- /dev/null +++ b/cli/stubs/elasticsearch/elasticsearch6/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3' +services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:6.8.23 + container_name: elasticsearch6 + ports: + - 9200:9200 + - 9300:9300 + environment: + - discovery.type=single-node + - xpack.security.enabled=false + volumes: + - esdata:/usr/share/elasticsearch/data +volumes: + esdata: + driver: local diff --git a/cli/stubs/elasticsearch/elasticsearch7/docker-compose.yml b/cli/stubs/elasticsearch/elasticsearch7/docker-compose.yml new file mode 100644 index 00000000..e85a36b4 --- /dev/null +++ b/cli/stubs/elasticsearch/elasticsearch7/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3' +services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.15 + container_name: elasticsearch7 + ports: + - 9200:9200 + - 9300:9300 + environment: + - discovery.type=single-node + - xpack.security.enabled=false + volumes: + - esdata:/usr/share/elasticsearch/data +volumes: + esdata: + driver: local diff --git a/cli/valet.php b/cli/valet.php index cb9201dc..cada3456 100755 --- a/cli/valet.php +++ b/cli/valet.php @@ -534,10 +534,12 @@ * Elasticsearch/opensearch services. */ $esVersions = Elasticsearch::getSupportedVersions(); + $esDockerVersions = Elasticsearch::getDockerVersions(); + $esCurrentVersion = Elasticsearch::getCurrentVersion(); $app ->command('elasticsearch', function (InputInterface $input, OutputInterface $output, $mode, $targetVersion = null) { $modes = ['install', 'use', 'on', 'enable', 'off', 'disable', 'uninstall']; - $targetVersion = $targetVersion ?? 'opensearch'; + $targetVersion = $targetVersion ?? 'opensearch'; //@todo only when we don't have any installed versions, if we do pick the first installed? if (!in_array($mode, $modes)) { throw new Exception('Mode not found. Available modes: ' . implode(', ', $modes)); @@ -568,7 +570,11 @@ PhpFpm::restart(); Nginx::restart(); }) - ->descriptions('Enable/disable/switch Elasticsearch') + ->descriptions( + 'Enable/disable/switch Elasticsearch. ' . + 'The versions [' . implode(', ', $esDockerVersions) . '] require Docker. ' . + ($esCurrentVersion !== null ? "\n " . 'Current running version: ' . $esCurrentVersion : '') + ) ->setAliases(['es']) ->addArgument('mode', InputArgument::REQUIRED, 'Available modes: ' . implode(', ', ['install', 'use', 'on', 'enable', 'off', 'disable', 'uninstall'])) ->addArgument('targetVersion', InputArgument::OPTIONAL, "Version to use, supported versions: " . implode(', ', $esVersions), null);