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

@ -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);
}
}