Improvements and fixes

* Updated README, added LICENSE
* New services: memcached, phpcacheadmin
* Polished commands
* ContainerManager now persists state
This commit is contained in:
2022-09-28 01:11:14 +02:00
parent 0b66b826f7
commit 2ec5081832
15 changed files with 898 additions and 20 deletions

View File

@ -39,6 +39,16 @@ class ExecCommand extends Command
$output->writeln(sprintf(" <comment>%s</> - <info>%s</>", $script, $meta['info']??"?"));
}
return self::SUCCESS;
} elseif ($input->getOption("script")) {
$script = $serviceInfo['scripts'][$command]['execute']??null;
if (!$script) {
$output->writeln("<error>No such script</>");
return self::FAILURE;
}
$cmdl = [ is_array($script)?join("; ", $script):$script ];
$containerManager->execute($serviceInfo, $instanceName, $cmdl);
} else {
$cmdl = [ $input->getArgument("execute") ];
array_push($cmdl, ...$input->getArgument("arguments"));

View File

@ -22,10 +22,21 @@ class FindCommand extends Command
$serviceRegistry = $this->getServiceRegistry();
$services = $serviceRegistry->findAllServices();
$query = $input->getArgument("service");
$output->writeln("Available services:");
$output->writeln($query?"Services matching <options=bold>{$query}</>:":"Available services:");
foreach ($services as $service) {
$output->writeln(sprintf(" <comment>%s</> - <info>%s</>", $service['name'], $service['description']??"?"));
$tags = $service['tags']??[];
// Filter out, if we have a query
if ($query && !((in_array($query,$tags) || str_contains($service['description'],$query) || $service['name']==$query)))
continue;
if ($tags) {
$tags = '#'.join("</>,<fg=cyan>#", $tags)."</>";
} else {
$tags = "";
}
$output->writeln(sprintf(" <comment>%s</>: <info>%s</> <fg=cyan>%s</>", $service['name'], $service['description']??"?", $tags));
}
return self::SUCCESS;

View File

@ -39,9 +39,11 @@ class StartCommand extends Command
'portoffset' => $input->getOption("portoffset")
];
$output->write("Starting...\r");
$info = $containerManager->startService($serviceInfo, $options);
$output->writeln("Started <fg=cyan>{$serviceName}</><<fg=cyan>{$instanceName}</>>");
$output->writeln("Started <fg=cyan>{$serviceName}</>[<fg=cyan>{$instanceName}</>]");
foreach ($info['ports'] as $info=>$port) {
$output->writeln(" <info>{$info}</>: <comment>{$port}</comment>");
}

View File

@ -5,6 +5,7 @@ namespace NoccyLabs\Serverctl\Commands;
use NoccyLabs\Spinner\Spinner;
use NoccyLabs\Spinner\Style\BrailleDotsStyle;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@ -19,7 +20,7 @@ class StatusCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output)
{
/*
$spinner = new Spinner(style: BrailleDotsStyle::class, fps: 15);
$output->write(" Getting status\r");
@ -27,7 +28,26 @@ class StatusCommand extends Command
if ($output->isDecorated()) $output->write("({$spinner})\r");
usleep(10000);
}
*/
$containerManager = $this->getContainerManager();
$table = new Table($output);
$table->setStyle('compact');
$table->setHeaders([ "Service", "Instance", "Purpose", "Port" ]);
$running = $containerManager->getRunningServices();
$s = 0;
foreach ($running as $service) {
$s++;
$i = 0;
foreach ($service['ports'] as $portInfo=>$portNumber) {
$table->addRow([ ($i==0)?$service['service']['name']:"", ($i==0)?$service['instance']:"", $portInfo, $portNumber ]);
$i++;
}
}
$table->render();
$output->writeln("<options=bold>{$s}</> running services.");
return self::SUCCESS;
}

View File

@ -2,6 +2,8 @@
namespace NoccyLabs\Serverctl\Commands;
use Exception;
use RuntimeException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@ -33,7 +35,15 @@ class StopCommand extends Command
return self::FAILURE;
}
$containerManager->stopService($serviceInfo, $instanceName);
$output->write("Stopping...\r");
try {
$containerManager->stopService($serviceInfo, $instanceName);
$output->writeln("Stopped service <fg=cyan>{$serviceName}</>[<fg=cyan>{$instanceName}</>]");
} catch (RuntimeException $e) {
$output->writeln("<error>".$e->getMessage()."</>");
return self::FAILURE;
}
return self::SUCCESS;
}

View File

@ -2,14 +2,89 @@
namespace NoccyLabs\Serverctl\Container;
use RuntimeException;
class ContainerManager
{
private string $dataPath;
private string $stateFile;
private array $autoEnv = [];
public function __construct(?string $dataPath=null)
{
$this->dataPath = $dataPath ?? (getenv("HOME")."/.var/serverctl");
$this->stateFile = $this->dataPath . "/state.json";
$this->setupAutoEnv();
}
private function setupAutoEnv()
{
$ifs = net_get_interfaces();
if ($ifs) {
$dockerHostIp = null;
if (array_key_exists('docker0', $ifs)) {
foreach ($ifs['docker0']['unicast'] as $uc) {
if ($uc['family'] == 2) {
$dockerHostIp = $uc['address'];
break;
}
}
} else {
// Find a usable interface
foreach ($ifs as $if=>$conf) {
if (!array_key_exists('unicast', $conf)) continue;
foreach ($conf['unicast'] as $uc) {
if ($uc['family'] == 2) {
$dockerHostIp = $uc['address'];
break;
}
}
}
}
$this->autoEnv['DOCKER_HOST'] = $dockerHostIp;
}
}
private function updateStateFile(string $instanceId, ?array $options)
{
if (!file_exists($this->stateFile)) {
touch($this->stateFile);
}
$fd = fopen($this->stateFile, 'r+');
if (!flock($fd, LOCK_EX)) {
throw new \RuntimeException("flock fail");
}
// Read the state from the file, seek to end first to get size
fseek($fd, 0, SEEK_END);
$flen = ftell($fd);
fseek($fd, 0, SEEK_SET);
if ($flen > 0) {
$body = fread($fd, $flen);
// Parse the state
$state = (array)((@json_decode($body, true))??[]);
} else {
$state = [];
}
// Update node in state
if ($options === null) {
unset($state[$instanceId]);
} else {
$state[$instanceId] = $options;
}
// Reencode the state, truncate the file and write it
$body = json_encode($state, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
fseek($fd, 0, SEEK_SET);
ftruncate($fd, 0);
fwrite($fd, $body);
// TODO: release lock
flock($fd, LOCK_UN);
}
/**
@ -43,8 +118,9 @@ class ContainerManager
$mappedPorts = [];
foreach ($ports as $port) {
$portNumber = intval($port['port']) + $portOffset;
$target = array_key_exists('target',$port)?intval($port['target']):intval($port['port']);
$args[] = '-p';
$args[] = $portNumber;
$args[] = $portNumber.":".$target;
$mappedPorts[$port['info']] = $portNumber;
}
@ -61,20 +137,34 @@ class ContainerManager
// Get environment
$envs = (array)($service['environment']??[]);
$parsedEnv = [];
foreach ($envs as $env=>$value) {
$args[] = '-e';
// TODO: use environment if set (override)
$envval = getenv($env, true);
if ($envval) $value = $envval;
$value = $this->expandString($value);
$args[] = sprintf("%s=%s", $env, $value);
$parsedEnv[$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);
//echo "$ {$cmdl}\n";
exec($cmdl, $out, $ret);
if ($ret != 0) {
echo join("\n",$out)."\n";
throw new RuntimeException("Docker returned non-zero exit code");
}
$this->updateStateFile($containerName, [
'command' => $cmdl,
'instance' => $instanceName,
'environment' => $parsedEnv,
'service' => $service,
'ports' => $mappedPorts
]);
return [
'ports' => $mappedPorts
@ -97,8 +187,14 @@ class ContainerManager
$cmdl = 'docker '.join(' ',array_map('escapeshellarg', $args));
echo "$ {$cmdl}\n";
passthru($cmdl);
//echo "$ {$cmdl}\n";
exec($cmdl, $out, $ret);
if ($ret != 0) {
echo join("\n",$out)."\n";
throw new RuntimeException("Docker returned non-zero exitcode");
}
$this->updateStateFile($containerName, null);
}
@ -128,7 +224,30 @@ class ContainerManager
*/
public function getRunningServices(): array
{
return [];
if (!file_exists($this->stateFile)) {
return [];
}
$fd = fopen($this->stateFile, 'r+');
if (!flock($fd, LOCK_SH)) {
throw new \RuntimeException("flock fail");
}
// Read the state from the file, seek to end first to get size
fseek($fd, 0, SEEK_END);
$flen = ftell($fd);
fseek($fd, 0, SEEK_SET);
$ret = [];
if ($flen > 0) {
$body = fread($fd, $flen);
// Parse the state
$ret = (array)((@json_decode($body, true))??[]);
} else {
return [];
}
return $ret;
}
public function getServiceDataPath(array $service)
@ -136,4 +255,15 @@ class ContainerManager
return $this->dataPath."/".$service['name'];
}
}
private function expandString(string $input): string
{
return preg_replace_callback('<\$\{(.+?)\}>i', function ($m) {
$k = $m[1];
if (array_key_exists($k, $this->autoEnv)) {
return $this->autoEnv[$k];
}
return getenv($k);
}, $input);
}
}