From da413c1a5dc3aa6f99983ff03a2a9bcfca6d0e15 Mon Sep 17 00:00:00 2001 From: Marvin Hinz Date: Thu, 2 Feb 2023 01:38:00 +0100 Subject: [PATCH 1/3] first draft oft import with urls and composer dependencies --- src/Command/ImportCommand.php | 144 ++++++++++++++++++++++++++++++++++ src/Importer/Importer.php | 23 ++++++ src/functions.php | 11 ++- 3 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 src/Command/ImportCommand.php diff --git a/src/Command/ImportCommand.php b/src/Command/ImportCommand.php new file mode 100644 index 000000000..d1e63c0c4 --- /dev/null +++ b/src/Command/ImportCommand.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Command; + +use Deployer\Deployer; +use Deployer\Importer\Importer; +use Deployer\Utility\Httpie; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\PhpProcess; +use Symfony\Component\Process\Process; + +class ImportCommand extends Command +{ + use CommandCommon; + + private InputInterface $input; + private OutputInterface $output; + + protected function configure() + { + $this + ->setName('import') + ->setDescription('Import a remote recipe') + ->addOption('mode', 'm', InputOption::VALUE_REQUIRED, 'Can be either url or composer') + ->addArgument('path', InputArgument::REQUIRED, 'Recipe file (can be URL or a path in the repo when using composer mode)') + ->addArgument('package', InputArgument::OPTIONAL, 'Composer package name (can have an appended version string)') + ->addArgument('repository', InputArgument::OPTIONAL, 'Composer package repository url') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->input = $input; + $this->output = $output; + + $io = new SymfonyStyle($input, $output); + $path = $input->getArgument('path'); + $package = $input->getArgument('package'); + $repository = $input->getArgument('repository'); + + if(!Deployer::isWorker()) { // is worker is not returning the correct value, as this command is runnning before the symfony console is initialized and running + // maybe the question should be asked only once (save the result), or not even be asked at all? + if (!$io->askQuestion(new ConfirmationQuestion("Do you really want to trust this remote recipe: $path?", true))) { + return 1; + } + } + + if(Importer::isUrl($path)) { + $this->importUrl($path); + } + + elseif(Importer::isRepo($path)) { + $this->importComposer($path, $package, $repository); + } + else { + throw new \Exception("Unrecognized path $path, make sure its a valid URL or a valid 'composer/package'"); + } + + return 0; + } + + protected function importUrl(string $path) + { + if ($data = Httpie::get($path)->send()) { + $tmpfname = tempnam("/tmp", "deployer_remote_recipe").".php"; + $tmpfhandle = fopen($tmpfname, "w"); + fwrite($tmpfhandle, $data); + fclose($tmpfhandle); + Deployer::get()->importer->import($tmpfname); + } else { + throw new \Exception("Could not download $path for import."); + } + } + + protected function importComposer(string $path, string $package, string $repository = null) + { + + if(!$this->composerJsonExists()) { + try { + $process = Process::fromShellCommandline("composer init --no-interaction --name deployer/project", dirname(DEPLOYER_DEPLOY_FILE)); + $output = trim($process->mustRun()->getOutput()); + } catch (RuntimeException $e) { + throw new \Exception($e->getMessage()); + } + echo "Initialized composer.json for you\n"; + } + + if($repository) { + $repoName = "deployer/".parse_url($repository, PHP_URL_HOST); + + try { + $process = Process::fromShellCommandline("composer config repositories.$repoName composer $repository", dirname(DEPLOYER_DEPLOY_FILE)); + $output = trim($process->mustRun()->getOutput()); + } catch (RuntimeException $e) { + throw new \Exception($e->getMessage()); + } + if ($this->output->isVerbose()) { + echo "Added repository to composer.json\n"; + } + } + + try { + $process = Process::fromShellCommandline("composer require --dev --no-plugins \"$package\"", dirname(DEPLOYER_DEPLOY_FILE)); + $output = trim($process->mustRun()->getOutput()); + } catch (RuntimeException $e) { + throw new \Exception($e->getMessage()); + } + if ($this->output->isVerbose()) { + echo "Added require-dev package to composer.json\n"; + } + + list($packageWithoutVersion, $version) = explode(":", $package); + $target = dirname(DEPLOYER_DEPLOY_FILE) . "/vendor/".$packageWithoutVersion."/".$path; + if(file_exists($target)) { + Importer::import($target); + } else { + throw new \Exception("Could not find imported composer file in ".$target); + } + } + + protected function getComposerJsonFile() + { + return dirname(DEPLOYER_DEPLOY_FILE) . "/composer.json"; + } + + private function composerJsonExists() + { + return file_exists($this->getComposerJsonFile()); + } + +} diff --git a/src/Importer/Importer.php b/src/Importer/Importer.php index c42b42bf7..52f48837c 100644 --- a/src/Importer/Importer.php +++ b/src/Importer/Importer.php @@ -8,6 +8,7 @@ namespace Deployer\Importer; +use Deployer\Command\ImportCommand; use Deployer\Deployer; use Deployer\Exception\ConfigurationException; use Deployer\Exception\Exception; @@ -15,6 +16,8 @@ use JsonSchema\Constraints\Factory; use JsonSchema\SchemaStorage; use JsonSchema\Validator; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Yaml\Yaml; use function array_filter; use function array_keys; @@ -102,6 +105,26 @@ public static function import($paths) } } + public static function isUrl(string $path): bool { + return (bool) preg_match('/^https:\/\//i', $path); + } + + public static function isRepo(string $path): bool { + list($repo, $version) = explode(":", $path); + return (bool) preg_match('@^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$@', $repo); + } + + public static function importUrl(string $url) + { + + (new ImportCommand())->run(new ArrayInput(['--mode'=> 'url', 'path' => $url]), new ConsoleOutput()); + } + + public static function importComposer(string $file, string $package, string $repo = null) + { + (new ImportCommand())->run(new ArrayInput(['--mode'=> 'composer', 'path' => $file, 'package' => $package, 'repository' => $repo]), new ConsoleOutput()); + } + protected static function hosts(array $hosts) { foreach ($hosts as $alias => $config) { diff --git a/src/functions.php b/src/functions.php index 34e0ba355..4cda098f4 100644 --- a/src/functions.php +++ b/src/functions.php @@ -152,9 +152,16 @@ function selectedHosts(): array * * @throws Exception */ -function import(string $file): void +function import(string $file, string $package = null, string $repo = null): void { - Importer::import($file); + if($file && $package) { + Importer::importComposer($file, $package, $repo); + } + elseif(Importer::isUrl($file)) { + Importer::importUrl($file); + } else { + Importer::import($file); + } } /** From 6c1cd35a3c6fef5545bd577f32e325de0e5e56cd Mon Sep 17 00:00:00 2001 From: Marvin Hinz Date: Thu, 2 Feb 2023 09:05:14 +0100 Subject: [PATCH 2/3] fix tests --- src/Command/ImportCommand.php | 37 +++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/Command/ImportCommand.php b/src/Command/ImportCommand.php index d1e63c0c4..74d0e6baf 100644 --- a/src/Command/ImportCommand.php +++ b/src/Command/ImportCommand.php @@ -19,7 +19,6 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Process\Exception\RuntimeException; -use Symfony\Component\Process\PhpProcess; use Symfony\Component\Process\Process; class ImportCommand extends Command @@ -28,8 +27,9 @@ class ImportCommand extends Command private InputInterface $input; private OutputInterface $output; + private string $cwd; - protected function configure() + protected function configure(): void { $this ->setName('import') @@ -46,10 +46,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->input = $input; $this->output = $output; - $io = new SymfonyStyle($input, $output); - $path = $input->getArgument('path'); - $package = $input->getArgument('package'); - $repository = $input->getArgument('repository'); + if(!defined('DEPLOYER_DEPLOY_FILE')) { + throw new \Exception("No deployfile present."); + } + + $this->cwd = dirname(DEPLOYER_DEPLOY_FILE); + + + $io = new SymfonyStyle($this->input, $output); + $path = $this->input->getArgument('path'); + $package = $this->input->getArgument('package'); + $repository = $this->input->getArgument('repository'); if(!Deployer::isWorker()) { // is worker is not returning the correct value, as this command is runnning before the symfony console is initialized and running // maybe the question should be asked only once (save the result), or not even be asked at all? @@ -72,7 +79,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - protected function importUrl(string $path) + protected function importUrl(string $path): void { if ($data = Httpie::get($path)->send()) { $tmpfname = tempnam("/tmp", "deployer_remote_recipe").".php"; @@ -85,12 +92,12 @@ protected function importUrl(string $path) } } - protected function importComposer(string $path, string $package, string $repository = null) + protected function importComposer(string $path, string $package, string $repository = null): void { if(!$this->composerJsonExists()) { try { - $process = Process::fromShellCommandline("composer init --no-interaction --name deployer/project", dirname(DEPLOYER_DEPLOY_FILE)); + $process = Process::fromShellCommandline("composer init --no-interaction --name deployer/project", $this->cwd); $output = trim($process->mustRun()->getOutput()); } catch (RuntimeException $e) { throw new \Exception($e->getMessage()); @@ -102,7 +109,7 @@ protected function importComposer(string $path, string $package, string $reposit $repoName = "deployer/".parse_url($repository, PHP_URL_HOST); try { - $process = Process::fromShellCommandline("composer config repositories.$repoName composer $repository", dirname(DEPLOYER_DEPLOY_FILE)); + $process = Process::fromShellCommandline("composer config repositories.$repoName composer $repository", $this->cwd); $output = trim($process->mustRun()->getOutput()); } catch (RuntimeException $e) { throw new \Exception($e->getMessage()); @@ -113,7 +120,7 @@ protected function importComposer(string $path, string $package, string $reposit } try { - $process = Process::fromShellCommandline("composer require --dev --no-plugins \"$package\"", dirname(DEPLOYER_DEPLOY_FILE)); + $process = Process::fromShellCommandline("composer require --dev --no-plugins \"$package\"", $this->cwd); $output = trim($process->mustRun()->getOutput()); } catch (RuntimeException $e) { throw new \Exception($e->getMessage()); @@ -123,7 +130,7 @@ protected function importComposer(string $path, string $package, string $reposit } list($packageWithoutVersion, $version) = explode(":", $package); - $target = dirname(DEPLOYER_DEPLOY_FILE) . "/vendor/".$packageWithoutVersion."/".$path; + $target = $this->cwd . "/vendor/".$packageWithoutVersion."/".$path; if(file_exists($target)) { Importer::import($target); } else { @@ -131,12 +138,12 @@ protected function importComposer(string $path, string $package, string $reposit } } - protected function getComposerJsonFile() + protected function getComposerJsonFile(): string { - return dirname(DEPLOYER_DEPLOY_FILE) . "/composer.json"; + return $this->cwd . "/composer.json"; } - private function composerJsonExists() + private function composerJsonExists(): bool { return file_exists($this->getComposerJsonFile()); } From 722ffcce7a9a1b9cc1f0a5d207617ba45e53266a Mon Sep 17 00:00:00 2001 From: Marvin Hinz Date: Thu, 2 Feb 2023 09:10:38 +0100 Subject: [PATCH 3/3] fix undefined array key error --- src/Importer/Importer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Importer/Importer.php b/src/Importer/Importer.php index 52f48837c..c4d30753a 100644 --- a/src/Importer/Importer.php +++ b/src/Importer/Importer.php @@ -110,7 +110,7 @@ public static function isUrl(string $path): bool { } public static function isRepo(string $path): bool { - list($repo, $version) = explode(":", $path); + list($repo, $version) = explode(":", $path.":"); return (bool) preg_match('@^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$@', $repo); }