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