Improvements and fixes
* Updated README, added LICENSE * New services: memcached, phpcacheadmin * Polished commands * ContainerManager now persists state
This commit is contained in:
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user