diff --git a/composer.json b/composer.json index d79f8ea..baa37d4 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,9 @@ "illuminate/support": "^10.3", "league/flysystem": "^3.0", "symfony/console": "^6.2", - "symfony/finder": "^6.2" + "symfony/finder": "^6.2", + "symfony/process": "^6.2", + "vlucas/phpdotenv": "^5.5" }, "require-dev": { "mockery/mockery": "^1.5.1", diff --git a/devstack b/devstack index 7425536..3b81f35 100755 --- a/devstack +++ b/devstack @@ -7,13 +7,30 @@ if (file_exists($autoload = __DIR__ . '/vendor/autoload.php')) { require dirname(__DIR__, 2) . '/autoload.php'; } -use Symfony\Component\Console\Application; +use Webteractive\Devstack\App; +use Webteractive\Devstack\Commands\MySql; +use Webteractive\Devstack\Commands\Redis; +use Webteractive\Devstack\Commands\Shell; use Webteractive\Devstack\Commands\Config; -use Webteractive\Devstack\Commands\DownloadRuntimes; use Webteractive\Devstack\Commands\InitStack; +use Webteractive\Devstack\Commands\RunPHPCommand; +use Webteractive\Devstack\Commands\DownloadRuntimes; +use Webteractive\Devstack\Commands\RunComposerCommands; +use Webteractive\Devstack\RegisterDockerComposeCommands; +use Webteractive\Devstack\Commands\RunLaravelArtisanCommand; + +$app = new App('Devstack', '1.1.5'); -$app = new Application; $app->add(new InitStack); $app->add(new Config); $app->add(new DownloadRuntimes); +$app->add(new RunLaravelArtisanCommand); +$app->add(new RunPHPCommand); +$app->add(new RunComposerCommands); +$app->add(new Shell); +$app->add(new MySql); +$app->add(new Redis); + +RegisterDockerComposeCommands::register($app); + $app->run(); \ No newline at end of file diff --git a/src/App.php b/src/App.php new file mode 100644 index 0000000..24f1b60 --- /dev/null +++ b/src/App.php @@ -0,0 +1,43 @@ +setName(static::$name); + $this->setVersion($version); + parent::__construct($name, $version); + } + + /** + * @return string + */ + public function getHelp(): string + { + return static::$logo . "\n\n" . parent::getHelp(); + } +} diff --git a/src/CommandSignature.php b/src/CommandSignature.php index de7b234..44ceb05 100644 --- a/src/CommandSignature.php +++ b/src/CommandSignature.php @@ -4,8 +4,8 @@ use Illuminate\Support\Str; use InvalidArgumentException; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; class CommandSignature { diff --git a/src/Commands/Base.php b/src/Commands/Base.php index 07472b3..b13988e 100644 --- a/src/Commands/Base.php +++ b/src/Commands/Base.php @@ -2,14 +2,16 @@ namespace Webteractive\Devstack\Commands; +use Webteractive\Devstack\CommandSignature; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\Question; -use Webteractive\Devstack\CommandSignature; abstract class Base extends Command { + protected $fullCommandSignature; + protected $name; protected $signature; @@ -20,12 +22,12 @@ abstract class Base extends Command protected $hidden = false; - protected $input; + protected InputInterface $input; - protected $output; + protected OutputInterface $output; public function __construct() - { + { if (isset($this->signature)) { $this->setup(); } else { @@ -43,20 +45,32 @@ public function setup() { [$name, $arguments, $options] = CommandSignature::parse($this->signature); + + if ($this->shouldIgnoreValidationErrors()) { + $this->ignoreValidationErrors(); + } + parent::__construct($name); $this->getDefinition()->addArguments($arguments); $this->getDefinition()->addOptions($options); } + public function shouldIgnoreValidationErrors(): bool + { + return false; + } + protected function execute(InputInterface $input, OutputInterface $output) { return $this->setIO($input, $output) ->handle(); } - public function setIO($input, $output) + public function setIO(InputInterface $input, OutputInterface $output) { + global $argv; + $this->fullCommandSignature = array_slice($argv, 2); $this->input = $input; $this->output = $output; return $this; @@ -115,6 +129,11 @@ public function line($message = '') return $this; } + public function lineBreak() + { + return $this->line(''); + } + public function info($message = '') { $this->output->writeln(empty($message) ? '' : "{$message}"); diff --git a/src/Commands/Config.php b/src/Commands/Config.php index 22a68b5..a043570 100644 --- a/src/Commands/Config.php +++ b/src/Commands/Config.php @@ -2,13 +2,7 @@ namespace Webteractive\Devstack\Commands; -use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; use Webteractive\Devstack\ShouldConfigure; -use Webteractive\Devstack\WithStorage; class Config extends Base { diff --git a/src/Commands/DownloadRuntimes.php b/src/Commands/DownloadRuntimes.php index 6583130..6e95ac0 100644 --- a/src/Commands/DownloadRuntimes.php +++ b/src/Commands/DownloadRuntimes.php @@ -2,8 +2,8 @@ namespace Webteractive\Devstack\Commands; -use Webteractive\Devstack\RuntimeDownloader; use Webteractive\Devstack\ShouldConfigure; +use Webteractive\Devstack\RuntimeDownloader; class DownloadRuntimes extends Base { diff --git a/src/Commands/InitStack.php b/src/Commands/InitStack.php index 6580821..16bc733 100644 --- a/src/Commands/InitStack.php +++ b/src/Commands/InitStack.php @@ -2,10 +2,10 @@ namespace Webteractive\Devstack\Commands; -use Webteractive\Devstack\RuntimeDownloader; use Webteractive\Devstack\File; -use Webteractive\Devstack\PublicRuntimeDownloader; use Webteractive\Devstack\ShouldConfigure; +use Webteractive\Devstack\RuntimeDownloader; +use Webteractive\Devstack\PublicRuntimeDownloader; class InitStack extends Base { diff --git a/src/Commands/MySql.php b/src/Commands/MySql.php new file mode 100644 index 0000000..ffdeb72 --- /dev/null +++ b/src/Commands/MySql.php @@ -0,0 +1,79 @@ +mysql container.'; + + public function handle(): int + { + $defaults = [ + 'password' => 'password', + 'user' => 'user', + 'database' => null, + ]; + $envPath = $this->input->getOption('env') ?? getcwd(); + if (file_exists($envPath . '/.env')) { + $env = new Env($envPath); + $defaults['password'] = $env->get('DB_PASSWORD'); + $defaults['user'] = $env->get('DB_USERNAME'); + $defaults['database'] = $env->get('DB_DATABASE'); + $this->lineBreak(); + $this->line('Found an .env file in your current working directory, values will now be used as defaults.'); + $this->lineBreak(); + } else { + $this->lineBreak(); + $this->line('Unable to find an .env file in your current working directory, now using the defaults.'); + $this->line('If these were changed in your docker-compose.yml file, please supply it as a command'); + $this->line('flag or add an .env file and add it there.'); + + $this->lineBreak(); + $this->line('If you want to use the .env route, create a .env file and add the variables below including the values:'); + $this->line('DB_USERNAME='); + $this->line('DB_PASSWORD='); + $this->line('DB_DATABASE='); + $this->lineBreak(); + $this->line('If your .env is located somewhere else, you may use the --env=/path/to/your/.env flag.'); + $this->line('For example, devstack mysql --env=/path/to/your/.env/directory.'); + $this->lineBreak(); + $this->line('Finally for the command flag, just do devstack mysql --user=the_user --password=the_password --database=the_db.'); + $this->line('For more details on the devsack mysql command flags, run devstack help mysql.'); + $this->lineBreak(); + } + + + $password = $this->input->getOption('password') ?? $defaults['password']; + $user = $this->input->getOption('user') ?? $defaults['user']; + $database = $this->input->getOption('database') ?? $defaults['database']; + + $bashCommand = []; + $bashCommand[] = "MYSQL_PWD={$password}"; + $bashCommand[] = "mysql -u {$user}"; + if ($database) { + $bashCommand[] = $database; + } + + $this->handleTerminationSignals( + $process = Process::prepareFromShell('docker compose exec -it mysql bash -c "' . join(' ', $bashCommand) . '"') + ); + + $process->setTty(true) + ->setTimeout(60 * 60 * 2) + ->setIdleTimeout(60 * 60 * 8) + ->run(); + + return static::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Commands/Redis.php b/src/Commands/Redis.php new file mode 100644 index 0000000..1162a37 --- /dev/null +++ b/src/Commands/Redis.php @@ -0,0 +1,29 @@ +redis container.'; + + public function handle(): int + { + + $this->handleTerminationSignals( + $process = Process::prepareFromShell('docker compose exec -it redis redis-cli') + ); + + $process->setTty(true) + ->setTimeout(60 * 60 * 2) + ->setIdleTimeout(60 * 60 * 8) + ->run(); + + return static::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Commands/RunComposerCommands.php b/src/Commands/RunComposerCommands.php new file mode 100644 index 0000000..54cad63 --- /dev/null +++ b/src/Commands/RunComposerCommands.php @@ -0,0 +1,42 @@ +handleTerminationSignals( + $process = Process::prepare(array_merge($command, $this->fullCommandSignature)) + ); + + $process->setTty(true)->run(); + + return static::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Commands/RunDockerCommands.php b/src/Commands/RunDockerCommands.php new file mode 100644 index 0000000..2c43d5d --- /dev/null +++ b/src/Commands/RunDockerCommands.php @@ -0,0 +1,37 @@ +signature = $name; + $this->description = $description; + + $this->ignoreValidationErrors(); + + parent::__construct(); + } + + public function handle(): int + { + $commandSignature = array_merge( + ['docker', 'compose', $this->getName()], + $this->fullCommandSignature + ); + + $this->handleTerminationSignals( + $process = Process::prepare($commandSignature) + ); + + $process->setTty(true)->run(); + + return static::SUCCESS; + } +} diff --git a/src/Commands/RunLaravelArtisanCommand.php b/src/Commands/RunLaravelArtisanCommand.php new file mode 100644 index 0000000..4181467 --- /dev/null +++ b/src/Commands/RunLaravelArtisanCommand.php @@ -0,0 +1,42 @@ +handleTerminationSignals( + $process = Process::prepare(array_merge($command, $this->fullCommandSignature)) + ); + + $process->setTty(true)->run(); + + return static::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Commands/RunPhpCommand.php b/src/Commands/RunPhpCommand.php new file mode 100644 index 0000000..3cf649a --- /dev/null +++ b/src/Commands/RunPhpCommand.php @@ -0,0 +1,41 @@ +handleTerminationSignals( + $process = Process::prepare(array_merge($command, $this->fullCommandSignature)) + ); + + $process->setTty(true)->run(); + + return static::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Commands/Shell.php b/src/Commands/Shell.php new file mode 100644 index 0000000..bc13adb --- /dev/null +++ b/src/Commands/Shell.php @@ -0,0 +1,39 @@ +input->getOption('root')) { + $command[] = '-u'; + $command[] = 'dev'; + } + + $this->handleTerminationSignals( + $process = Process::prepare(array_merge($command, ['app', 'bash'])) + ); + + $process->setTty(true) + ->setTimeout(60 * 60 * 2) + ->setIdleTimeout(60 * 60 * 8) + ->run(); + + return static::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Env.php b/src/Env.php new file mode 100644 index 0000000..485ef04 --- /dev/null +++ b/src/Env.php @@ -0,0 +1,21 @@ +dotenv = Dotenv::createImmutable($path)->safeLoad(); + } + + public function get($key, $default = null) + { + return Arr::get($this->dotenv, $key, $default); + } +} diff --git a/src/Fs.php b/src/Fs.php index 69d2c43..25fdb89 100644 --- a/src/Fs.php +++ b/src/Fs.php @@ -2,7 +2,6 @@ namespace Webteractive\Devstack; -use League\Flysystem\DirectoryListing; use League\Flysystem\Filesystem; use League\Flysystem\Local\LocalFilesystemAdapter; diff --git a/src/Helpers.php b/src/Helpers.php deleted file mode 100644 index f2d6880..0000000 --- a/src/Helpers.php +++ /dev/null @@ -1,8 +0,0 @@ - 'Build or rebuild services.', + 'config' => 'Parse, resolve and render compose file in canonical format.', + 'cp' => 'Copy files/folders between a service container and the local filesystem.', + 'create' => 'Creates containers for a service.', + 'down' => 'Stop and remove containers, networks.', + 'events' => 'Receive real time events from containers.', + 'exec' => 'Execute a command in a running container.', + 'images' => 'List images used by the created containers.', + 'kill' => 'Force stop service containers.', + 'logs' => 'View output from containers.', + 'ls' => 'List running compose projects.', + 'pause' => 'Pause services.', + 'port' => 'Print the public port for a port binding.', + 'ps' => 'List containers.', + 'pull' => 'Pull service images.', + 'push' => 'Push service images', + 'restart' => 'Restart service containers.', + 'rm' => 'Removes stopped service containers.', + 'run' => 'Run a one-off command on a service.', + 'start' => 'Start services.', + 'stop' => 'Stop services.', + 'top' => 'Display the running processes.', + 'unpause' => 'Unpause services.', + 'up' => 'Create and start containers.', + 'version' => 'Show the Docker Compose version information.', + ]; + + public static function register(Application $app, $prefix = 'runtime') + { + foreach (static::$commands as $name => $description) { + $app->add(new RunDockerCommands($name, $description)); + } + } +} \ No newline at end of file diff --git a/src/WithSignalHandlers.php b/src/WithSignalHandlers.php new file mode 100644 index 0000000..ef48ae4 --- /dev/null +++ b/src/WithSignalHandlers.php @@ -0,0 +1,22 @@ +signal($signal); + }); + } + + public function handleTerminationSignals(Process $process) + { + $this->handleSignal($process, SIGINT); + // $this->handleSignal($process, SIGSTOP); + // $this->handleSignal($process, SIGKILL); + } +} \ No newline at end of file