Initial commit
This commit is contained in:
26
src/Commands/Command.php
Normal file
26
src/Commands/Command.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Serverctl\Commands;
|
||||
|
||||
use NoccyLabs\Serverctl\ConsoleApplication;
|
||||
use NoccyLabs\Serverctl\Container\ContainerManager;
|
||||
use NoccyLabs\Serverctl\Registry\ServiceRegistry;
|
||||
use Symfony\Component\Console\Command\Command as CommandCommand;
|
||||
|
||||
abstract class Command extends CommandCommand
|
||||
{
|
||||
public function getApplication(): ?ConsoleApplication
|
||||
{
|
||||
return parent::getApplication();
|
||||
}
|
||||
|
||||
protected function getServiceRegistry(): ServiceRegistry
|
||||
{
|
||||
return $this->getApplication()->getServiceRegistry();
|
||||
}
|
||||
|
||||
protected function getContainerManager(): ContainerManager
|
||||
{
|
||||
return $this->getApplication()->getContainerManager();
|
||||
}
|
||||
}
|
51
src/Commands/ExecCommand.php
Normal file
51
src/Commands/ExecCommand.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Serverctl\Commands;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(name:"exec", description:"Execute a shell command or script in the service")]
|
||||
class ExecCommand extends Command
|
||||
{
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->addOption("instance", "I", InputOption::VALUE_REQUIRED, "Specify the instance name", "default");
|
||||
$this->addOption("script", "s", InputOption::VALUE_NONE, "The command is a script (use 'list' for list)");
|
||||
$this->addArgument("service", InputArgument::REQUIRED, "The service name");
|
||||
$this->addArgument("execute", InputArgument::REQUIRED, "The command or script to execute");
|
||||
$this->addArgument("arguments", InputArgument::IS_ARRAY, "Arguments");
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$serviceRegistry = $this->getServiceRegistry();
|
||||
$containerManager = $this->getContainerManager();
|
||||
$instanceName = $input->getOption("instance");
|
||||
|
||||
$serviceName = $input->getArgument("service");
|
||||
$command = $input->getArgument("execute");
|
||||
|
||||
$serviceInfo = $serviceRegistry->findServiceByName($serviceName);
|
||||
|
||||
if ($command == "list") {
|
||||
$scripts = (array)($serviceInfo['scripts']??[]);
|
||||
$output->writeln("Available scripts:");
|
||||
foreach ($scripts as $script=>$meta) {
|
||||
$output->writeln(sprintf(" <comment>%s</> - <info>%s</>", $script, $meta['info']??"?"));
|
||||
}
|
||||
return self::SUCCESS;
|
||||
} else {
|
||||
$cmdl = [ $input->getArgument("execute") ];
|
||||
array_push($cmdl, ...$input->getArgument("arguments"));
|
||||
$containerManager->execute($serviceInfo, $instanceName, $cmdl);
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
}
|
33
src/Commands/FindCommand.php
Normal file
33
src/Commands/FindCommand.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Serverctl\Commands;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(name:"find", description:"Find defined services")]
|
||||
class FindCommand extends Command
|
||||
{
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->addArgument("service", InputArgument::OPTIONAL, "Search query");
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$serviceRegistry = $this->getServiceRegistry();
|
||||
|
||||
$services = $serviceRegistry->findAllServices();
|
||||
|
||||
$output->writeln("Available services:");
|
||||
foreach ($services as $service) {
|
||||
$output->writeln(sprintf(" <comment>%s</> - <info>%s</>", $service['name'], $service['description']??"?"));
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
45
src/Commands/StartCommand.php
Normal file
45
src/Commands/StartCommand.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Serverctl\Commands;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(name:"start", description:"Start a service")]
|
||||
class StartCommand extends Command
|
||||
{
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->addOption("instance", "I", InputOption::VALUE_REQUIRED, "Specify the instance name", "default");
|
||||
$this->addOption("portoffset", "p", InputOption::VALUE_REQUIRED, "Offset port numbers by value", 0);
|
||||
$this->addArgument("service", InputArgument::REQUIRED, "The service name");
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$serviceRegistry = $this->getServiceRegistry();
|
||||
$containerManager = $this->getContainerManager();
|
||||
|
||||
|
||||
$serviceName = $input->getArgument("service");
|
||||
|
||||
$serviceInfo = $serviceRegistry->findServiceByName($serviceName);
|
||||
if (!$serviceInfo) {
|
||||
$output->writeln("<error>No such service in registry</>");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$options = [
|
||||
'name' => $input->getOption("instance"),
|
||||
'portoffset' => $input->getOption("portoffset")
|
||||
];
|
||||
|
||||
$containerManager->startService($serviceInfo, $options);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
34
src/Commands/StatusCommand.php
Normal file
34
src/Commands/StatusCommand.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Serverctl\Commands;
|
||||
|
||||
use NoccyLabs\Spinner\Spinner;
|
||||
use NoccyLabs\Spinner\Style\BrailleDotsStyle;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(name:"status", description:"Show the running services")]
|
||||
class StatusCommand extends Command
|
||||
{
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
|
||||
$spinner = new Spinner(style: BrailleDotsStyle::class, fps: 15);
|
||||
|
||||
$output->write(" Getting status\r");
|
||||
for ($n = 0; $n < 250; $n++) {
|
||||
if ($output->isDecorated()) $output->write("({$spinner})\r");
|
||||
usleep(10000);
|
||||
}
|
||||
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
39
src/Commands/StopCommand.php
Normal file
39
src/Commands/StopCommand.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Serverctl\Commands;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(name:"stop", description:"Stop a running service")]
|
||||
class StopCommand extends Command
|
||||
{
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->addOption("instance", "I", InputOption::VALUE_REQUIRED, "Specify the instance name", "default");
|
||||
$this->addArgument("service", InputArgument::REQUIRED, "The service name");
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$serviceRegistry = $this->getServiceRegistry();
|
||||
$containerManager = $this->getContainerManager();
|
||||
|
||||
|
||||
$serviceName = $input->getArgument("service");
|
||||
|
||||
$serviceInfo = $serviceRegistry->findServiceByName($serviceName);
|
||||
if (!$serviceInfo) {
|
||||
$output->writeln("<error>No such service in registry</>");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$containerManager->stopService($serviceInfo, $input->getOption("instance"));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
48
src/ConsoleApplication.php
Normal file
48
src/ConsoleApplication.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Serverctl;
|
||||
|
||||
use NoccyLabs\Serverctl\Container\ContainerManager;
|
||||
use NoccyLabs\Serverctl\Registry\ServiceRegistry;
|
||||
use Symfony\Component\Console\Application;
|
||||
|
||||
class ConsoleApplication extends Application
|
||||
{
|
||||
|
||||
private ServiceRegistry $serviceRegistry;
|
||||
|
||||
private ContainerManager $containerManager;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct("Development server utility", "0.1.0");
|
||||
|
||||
$this->serviceRegistry = new ServiceRegistry(
|
||||
paths: [
|
||||
__DIR__."/../registry",
|
||||
dirname(realpath($GLOBALS['argv'][0]))."/registry",
|
||||
"/usr/share/serverctl/registry",
|
||||
getenv("HOME")."/.share/serverctl/registry"
|
||||
]
|
||||
);
|
||||
$this->containerManager = new ContainerManager(
|
||||
dataPath: null
|
||||
);
|
||||
|
||||
$this->add(new Commands\StartCommand());
|
||||
$this->add(new Commands\StopCommand());
|
||||
$this->add(new Commands\ExecCommand());
|
||||
$this->add(new Commands\FindCommand());
|
||||
$this->add(new Commands\StatusCommand());
|
||||
}
|
||||
|
||||
public function getServiceRegistry(): ServiceRegistry
|
||||
{
|
||||
return $this->serviceRegistry;
|
||||
}
|
||||
|
||||
public function getContainerManager(): ContainerManager
|
||||
{
|
||||
return $this->containerManager;
|
||||
}
|
||||
}
|
132
src/Container/ContainerManager.php
Normal file
132
src/Container/ContainerManager.php
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Serverctl\Container;
|
||||
|
||||
class ContainerManager
|
||||
{
|
||||
|
||||
private string $dataPath;
|
||||
|
||||
public function __construct(?string $dataPath=null)
|
||||
{
|
||||
$this->dataPath = $dataPath ?? (getenv("HOME")."/.var/serverctl");
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a service
|
||||
*
|
||||
* Instance options:
|
||||
* name: Instance name (default)
|
||||
* portoffset: Port number offset (0)
|
||||
*
|
||||
* @param array $service The service definition from the registry
|
||||
* @param array $options Instance options
|
||||
*/
|
||||
public function startService(array $service, array $options)
|
||||
{
|
||||
$args = [];
|
||||
|
||||
$serviceName = $service['name'];
|
||||
$instanceName = $options['name']??'default';
|
||||
|
||||
$containerName = "sm_".$serviceName."_".$instanceName;
|
||||
|
||||
$args[] = 'run';
|
||||
$args[] = '--rm'; // remove container after run
|
||||
$args[] = '-d';
|
||||
$args[] = '--name';
|
||||
$args[] = $containerName;
|
||||
|
||||
// Map the ports
|
||||
$ports = (array)($service['ports']??[]);
|
||||
foreach ($ports as $port) {
|
||||
$args[] = '-p';
|
||||
$args[] = $port['port'];
|
||||
}
|
||||
|
||||
// Get the paths to persist
|
||||
$volumes = (array)($service['persistence']??[]);
|
||||
$volumePath = $this->getServiceDataPath($service)."/".$instanceName;
|
||||
foreach ($volumes as $volume) {
|
||||
// volume { path, hint }
|
||||
$path = $volume['path'];
|
||||
$hint = $volume['hint'] ?? crc32($path);
|
||||
$args[] = '-v'; // add volume
|
||||
$args[] = $volumePath."/".$hint.":".$path;
|
||||
}
|
||||
|
||||
// Get environment
|
||||
$envs = (array)($service['environment']??[]);
|
||||
foreach ($envs as $env=>$value) {
|
||||
$args[] = '-e';
|
||||
// TODO: use environment if set (override)
|
||||
$args[] = sprintf("%s=%s", $env, $value);
|
||||
}
|
||||
|
||||
$args[] = $service['image'];
|
||||
|
||||
$cmdl = 'docker '.join(' ',array_map('escapeshellarg', $args));
|
||||
|
||||
// TODO: Write command line, env and meta to state file
|
||||
|
||||
echo "$ {$cmdl}\n";
|
||||
passthru($cmdl);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a service
|
||||
*/
|
||||
public function stopService(array $service, string $instanceName)
|
||||
{
|
||||
$args = [];
|
||||
|
||||
$serviceName = $service['name'];
|
||||
$instanceName = $options['name']??'default';
|
||||
$containerName = "sm_".$serviceName."_".$instanceName;
|
||||
|
||||
$args[] = 'stop';
|
||||
$args[] = $containerName;
|
||||
|
||||
$cmdl = 'docker '.join(' ',array_map('escapeshellarg', $args));
|
||||
|
||||
echo "$ {$cmdl}\n";
|
||||
passthru($cmdl);
|
||||
|
||||
}
|
||||
|
||||
public function execute(array $service, string $instanceName, array $command)
|
||||
{
|
||||
$args = [];
|
||||
|
||||
$serviceName = $service['name'];
|
||||
$instanceName = $options['name']??'default';
|
||||
$containerName = "sm_".$serviceName."_".$instanceName;
|
||||
|
||||
$args[] = 'exec';
|
||||
$args[] = '-it';
|
||||
$args[] = $containerName;
|
||||
|
||||
array_push($args, ...$command);
|
||||
|
||||
$cmdl = 'docker '.join(' ',array_map('escapeshellarg', $args));
|
||||
|
||||
echo "$ {$cmdl}\n";
|
||||
passthru($cmdl);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get running services
|
||||
*/
|
||||
public function getRunningServices(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getServiceDataPath(array $service)
|
||||
{
|
||||
return $this->dataPath."/".$service['name'];
|
||||
}
|
||||
|
||||
}
|
57
src/Registry/ServiceRegistry.php
Normal file
57
src/Registry/ServiceRegistry.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Serverctl\Registry;
|
||||
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
|
||||
class ServiceRegistry
|
||||
{
|
||||
|
||||
private $services = [];
|
||||
|
||||
public function __construct(array $paths)
|
||||
{
|
||||
foreach ($paths as $path) {
|
||||
if (is_dir($path)) {
|
||||
$this->readPath($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function readPath(string $path)
|
||||
{
|
||||
$iter = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator(
|
||||
$path
|
||||
)
|
||||
);
|
||||
|
||||
/** @var SplFileInfo $iteminfo */
|
||||
foreach ($iter as $itempath=>$iteminfo) {
|
||||
if (!fnmatch("*.json", $itempath)) continue;
|
||||
$json = file_get_contents($itempath);
|
||||
$parsed = json_decode($json, true);
|
||||
$this->services[$itempath] = $parsed;
|
||||
}
|
||||
|
||||
usort($this->services, function ($a,$b) {
|
||||
return $a['name'] <=> $b['name'];
|
||||
});
|
||||
}
|
||||
|
||||
public function findServiceByName(string $name): ?array
|
||||
{
|
||||
foreach ($this->services as $service) {
|
||||
if ($service['name'] === $name) return $service;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function findAllServices(): array
|
||||
{
|
||||
return $this->services;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user