2022-09-27 12:29:56 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace NoccyLabs\Serverctl\Container;
|
|
|
|
|
2022-09-28 01:11:14 +02:00
|
|
|
use RuntimeException;
|
|
|
|
|
2022-09-27 12:29:56 +02:00
|
|
|
class ContainerManager
|
|
|
|
{
|
|
|
|
|
|
|
|
private string $dataPath;
|
|
|
|
|
2022-09-28 01:11:14 +02:00
|
|
|
private string $stateFile;
|
|
|
|
|
|
|
|
private array $autoEnv = [];
|
|
|
|
|
2022-09-27 12:29:56 +02:00
|
|
|
public function __construct(?string $dataPath=null)
|
|
|
|
{
|
|
|
|
$this->dataPath = $dataPath ?? (getenv("HOME")."/.var/serverctl");
|
2022-09-28 01:11:14 +02:00
|
|
|
$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);
|
2022-09-27 12:29:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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';
|
2022-09-27 12:47:30 +02:00
|
|
|
$portOffset = intval($options['portoffset']??0);
|
2022-09-27 12:29:56 +02:00
|
|
|
|
|
|
|
$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']??[]);
|
2022-09-27 12:47:30 +02:00
|
|
|
$mappedPorts = [];
|
2022-09-27 12:29:56 +02:00
|
|
|
foreach ($ports as $port) {
|
2022-09-27 12:47:30 +02:00
|
|
|
$portNumber = intval($port['port']) + $portOffset;
|
2022-09-28 01:11:14 +02:00
|
|
|
$target = array_key_exists('target',$port)?intval($port['target']):intval($port['port']);
|
2022-09-27 12:29:56 +02:00
|
|
|
$args[] = '-p';
|
2022-09-28 01:11:14 +02:00
|
|
|
$args[] = $portNumber.":".$target;
|
2022-09-27 12:47:30 +02:00
|
|
|
$mappedPorts[$port['info']] = $portNumber;
|
2022-09-27 12:29:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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']??[]);
|
2022-09-28 01:11:14 +02:00
|
|
|
$parsedEnv = [];
|
2022-09-27 12:29:56 +02:00
|
|
|
foreach ($envs as $env=>$value) {
|
|
|
|
$args[] = '-e';
|
2022-09-28 01:11:14 +02:00
|
|
|
$envval = getenv($env, true);
|
|
|
|
if ($envval) $value = $envval;
|
|
|
|
$value = $this->expandString($value);
|
2022-09-27 12:29:56 +02:00
|
|
|
$args[] = sprintf("%s=%s", $env, $value);
|
2022-09-28 01:11:14 +02:00
|
|
|
$parsedEnv[$env] = $value;
|
2022-09-27 12:29:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$args[] = $service['image'];
|
|
|
|
|
|
|
|
$cmdl = 'docker '.join(' ',array_map('escapeshellarg', $args));
|
|
|
|
|
2022-09-28 01:11:14 +02:00
|
|
|
//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
|
|
|
|
]);
|
2022-09-27 12:29:56 +02:00
|
|
|
|
2022-09-27 12:47:30 +02:00
|
|
|
return [
|
|
|
|
'ports' => $mappedPorts
|
|
|
|
];
|
|
|
|
|
2022-09-27 12:29:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stop a service
|
|
|
|
*/
|
|
|
|
public function stopService(array $service, string $instanceName)
|
|
|
|
{
|
|
|
|
$args = [];
|
|
|
|
|
|
|
|
$serviceName = $service['name'];
|
|
|
|
$containerName = "sm_".$serviceName."_".$instanceName;
|
|
|
|
|
|
|
|
$args[] = 'stop';
|
|
|
|
$args[] = $containerName;
|
|
|
|
|
|
|
|
$cmdl = 'docker '.join(' ',array_map('escapeshellarg', $args));
|
|
|
|
|
2022-09-28 01:11:14 +02:00
|
|
|
//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);
|
2022-09-27 12:29:56 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
{
|
2022-09-28 01:11:14 +02:00
|
|
|
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;
|
2022-09-27 12:29:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function getServiceDataPath(array $service)
|
|
|
|
{
|
|
|
|
return $this->dataPath."/".$service['name'];
|
|
|
|
}
|
|
|
|
|
2022-09-28 01:11:14 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|