configurationManager = $configurationManager; $this->containerDefinitionFetcher = $containerDefinitionFetcher; $this->dockerHubManager = $dockerHubManager; $this->guzzleClient = new \GuzzleHttp\Client( [ 'curl' => [ CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock', ], ] ); } private function BuildApiUrl(string $url) : string { return sprintf('http://localhost/%s/%s', self::API_VERSION, $url); } private function BuildImageName(Container $container) : string { $channel = $this->GetCurrentChannel(); if ($channel === 'develop') { return $container->GetContainerName() . ':develop'; } else { return $container->GetContainerName() . ':latest'; } } public function GetContainerRunningState(Container $container) : IContainerState { $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier()))); try { $response = $this->guzzleClient->get($url); } catch (ClientException $ex) { if($ex->getCode() === 404) { return new ImageDoesNotExistState(); } throw $ex; } $responseBody = json_decode((string)$response->getBody(), true); if ($responseBody['State']['Running'] === true) { return new RunningState(); } else { return new StoppedState(); } } public function GetContainerUpdateState(Container $container) : IContainerState { $channel = $this->GetCurrentChannel(); if ($channel === 'develop') { $tag = 'develop'; } else { $tag = 'latest'; } $runningDigest = $this->GetRepoDigestOfContainer($container->GetIdentifier()); $remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($container->GetContainerName(), $tag); if ($runningDigest === $remoteDigest) { return new VersionEqualState(); } else { return new VersionDifferentState(); } } public function GetContainerStartingState(Container $container) : IContainerState { $runningState = $this->GetContainerRunningState($container); if ($runningState instanceof StoppedState) { return new StoppedState(); } elseif ($runningState instanceof ImageDoesNotExistState) { return new ImageDoesNotExistState(); } $containerName = $container->GetIdentifier(); if ($container->GetInternalPorts() !== null) { foreach($container->GetInternalPorts()->GetInternalPorts() as $internalPort) { $connection = @fsockopen($containerName, $internalPort, $errno, $errstr, 0.1); if ($connection) { fclose($connection); return new RunningState(); } else { return new StartingState(); } } } else { return new RunningState(); } } public function DeleteContainer(Container $container) { $url = $this->BuildApiUrl(sprintf('containers/%s', urlencode($container->GetIdentifier()))); try { $this->guzzleClient->delete($url); } catch (\Exception $e) {} } public function GetLogs(Container $container) : string { $url = $this->BuildApiUrl( sprintf( 'containers/%s/logs?stdout=true&stderr=true', urlencode($container->GetIdentifier()) )); $responseBody = (string)$this->guzzleClient->get($url)->getBody(); $response = ""; $separator = "\r\n"; $line = strtok($responseBody, $separator); $response = substr($line, 8) . "\n"; while ($line !== false) { $line = strtok($separator); $response .= substr($line, 8) . "\n"; } return $response; } public function StartContainer(Container $container) { $url = $this->BuildApiUrl(sprintf('containers/%s/start', urlencode($container->GetIdentifier()))); $this->guzzleClient->post($url); } public function CreateVolumes(Container $container) { $url = $this->BuildApiUrl('volumes/create'); foreach($container->GetVolumes()->GetVolumes() as $volume) { $forbiddenChars = [ '/', ]; $firstChar = substr($volume->name, 0, 1); if(!in_array($firstChar, $forbiddenChars)) { $this->guzzleClient->request( 'POST', $url, [ 'json' => [ 'name' => $volume->name, ], ] ); } } } public function CreateContainer(Container $container) { $volumes = []; foreach($container->GetVolumes()->GetVolumes() as $volume) { $volumeEntry = $volume->name . ':' . $volume->mountPoint; if($volume->isWritable) { $volumeEntry = $volumeEntry . ':' . 'rw'; } else { $volumeEntry = $volumeEntry . ':' . 'ro'; } $volumes[] = $volumeEntry; } $exposedPorts = []; foreach($container->GetPorts()->GetPorts() as $port) { $exposedPorts[$port] = null; } $requestBody = [ 'Image' => $this->BuildImageName($container), ]; if(count($volumes) > 0) { $requestBody['HostConfig']['Binds'] = $volumes; } $envs = $container->GetEnvironmentVariables()->GetVariables(); foreach($envs as $key => $env) { $patterns = ['/%(.*)%/']; if(preg_match($patterns[0], $env, $out) === 1) { $replacements = array(); if($out[1] === 'NC_DOMAIN') { $replacements[1] = $this->configurationManager->GetDomain(); } elseif ($out[1] === 'AIO_TOKEN') { $replacements[1] = $this->configurationManager->GetToken(); } elseif ($out[1] === 'BORGBACKUP_MODE') { $replacements[1] = $this->configurationManager->GetBackupMode(); } elseif ($out[1] === 'AIO_URL') { $replacements[1] = $this->configurationManager->GetAIOURL(); } else { $replacements[1] = $this->configurationManager->GetSecret($out[1]); } $envs[$key] = preg_replace($patterns, $replacements, $env); } } if(count($envs) > 0) { $requestBody['Env'] = $envs; } $requestBody['HostConfig']['RestartPolicy']['Name'] = $container->GetRestartPolicy(); if(count($exposedPorts) > 0) { $requestBody['ExposedPorts'] = $exposedPorts; foreach($container->GetPorts()->GetPorts() as $port) { $portNumber = explode("/", $port); $requestBody['HostConfig']['PortBindings'][$port] = [ [ 'HostPort' => $portNumber[0], ] ]; } } // Special things for the backup container which should not be exposed in the containers.json if ($container->GetIdentifier() === 'nextcloud-aio-borgbackup') { $requestBody['HostConfig']['CapAdd'] = ["SYS_ADMIN"]; $requestBody['HostConfig']['Devices'] = [["PathOnHost" => "/dev/fuse", "PathInContainer" => "/dev/fuse", "CgroupPermissions" => "rwm"]]; $requestBody['HostConfig']['SecurityOpt'] = ["apparmor:unconfined"]; } $url = $this->BuildApiUrl('containers/create?name=' . $container->GetIdentifier()); $this->guzzleClient->request( 'POST', $url, [ 'json' => $requestBody ] ); } public function PullContainer(Container $container) { $url = $this->BuildApiUrl(sprintf('images/create?fromImage=%s', urlencode($this->BuildImageName($container)))); try { $this->guzzleClient->post($url); } catch (ClientException $ex) { throw $ex; } } private function isContainerUpdateAvailable(string $id) : string { $container = $this->containerDefinitionFetcher->GetContainerById($id); $updateAvailable = ""; if ($container->GetUpdateState() instanceof VersionDifferentState) { $updateAvailable = '1'; } foreach ($container->GetDependsOn() as $dependency) { $updateAvailable .= $this->isContainerUpdateAvailable($dependency); } return $updateAvailable; } public function isAnyUpdateAvailable() { $id = 'nextcloud-aio-apache'; if ($this->configurationManager->GetIsContainerUpateAvailable()) { return true; } if ($this->isContainerUpdateAvailable($id) !== "") { $this->configurationManager->SetIsContainerUpateAvailable(true); return true; } else { return false; } } private function GetRepoDigestOfContainer(string $containerName) : ?string { try { $containerUrl = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName)); $containerOutput = json_decode($this->guzzleClient->get($containerUrl)->getBody()->getContents(), true); $imageName = $containerOutput['Image']; $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName)); $imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true); if(isset($imageOutput['RepoDigests']) && count($imageOutput['RepoDigests']) === 1) { $fullDigest = $imageOutput['RepoDigests'][0]; return substr($fullDigest, strpos($fullDigest, "@") + 1); } return null; } catch (\Exception $e) { return null; } } public function GetCurrentChannel() : string { $cacheKey = 'aio-ChannelName'; $channelName = apcu_fetch($cacheKey); if($channelName !== false && is_string($channelName)) { return $channelName; } $containerName = 'nextcloud-aio-mastercontainer'; $url = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName)); try { $output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true); $containerChecksum = $output['Image']; $tagArray = explode(':', $output['Config']['Image']); $tag = $tagArray[1]; apcu_add($cacheKey, $tag); return $tag; } catch (\Exception $ex) { } return 'latest'; } public function IsMastercontainerUpdateAvailable() : bool { $imageName = 'nextcloud/all-in-one'; $containerName = 'nextcloud-aio-mastercontainer'; $channel = $this->GetCurrentChannel(); if ($channel === 'develop') { $tag = 'develop'; } else { $tag = 'latest'; } $runningDigest = $this->GetRepoDigestOfContainer($containerName); $remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($imageName, $tag); if ($remoteDigest === $runningDigest) { return false; } else { return true; } } public function sendNotification(Container $container, string $subject, string $message) { if ($this->GetContainerStartingState($container) instanceof RunningState) { $containerName = $container->GetIdentifier(); // schedule the exec $url = $this->BuildApiUrl(sprintf('containers/%s/exec', urlencode($containerName))); $response = json_decode( $this->guzzleClient->request( 'POST', $url, [ 'json' => [ 'AttachStdout' => true, 'Tty' => true, 'Cmd' => [ 'bash', '/notify.sh', $subject, $message ], ], ] )->getBody()->getContents(), true ); // get the id from the response $id = $response['Id']; // start the exec $url = $this->BuildApiUrl(sprintf('exec/%s/start', $id)); $this->guzzleClient->request( 'POST', $url, [ 'json' => [ 'Detach' => false, 'Tty' => true, ], ] ); } } public function DisconnectContainerFromNetwork(Container $container) { $url = $this->BuildApiUrl( sprintf('networks/%s/disconnect', 'nextcloud-aio') ); try { $this->guzzleClient->request( 'POST', $url, [ 'json' => [ 'container' => $container->GetIdentifier(), ], ] ); } catch (ServerException $e) {} } private function ConnectContainerIdToNetwork(string $id) { $url = $this->BuildApiUrl('networks/create'); try { $this->guzzleClient->request( 'POST', $url, [ 'json' => [ 'name' => 'nextcloud-aio', 'checkDuplicate' => true, 'internal' => true, ] ] ); } catch (ClientException $e) { if($e->getCode() !== 409) { throw $e; } } $url = $this->BuildApiUrl( sprintf('networks/%s/connect', 'nextcloud-aio') ); try { $this->guzzleClient->request( 'POST', $url, [ 'json' => [ 'container' => $id, ] ] ); } catch (ClientException $e) { if($e->getCode() !== 403) { throw $e; } } } public function ConnectMasterContainerToNetwork() { $this->ConnectContainerIdToNetwork('nextcloud-aio-mastercontainer'); } public function ConnectContainerToNetwork(Container $container) { $this->ConnectContainerIdToNetwork($container->GetIdentifier()); } public function StopContainer(Container $container) { $url = $this->BuildApiUrl(sprintf('containers/%s/stop?t=%s', urlencode($container->GetIdentifier()), $container->GetMaxShutdownTime())); $this->guzzleClient->post($url); } public function GetBackupcontainerExitCode() : int { $containerName = 'nextcloud-aio-borgbackup'; $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName))); try { $response = $this->guzzleClient->get($url); } catch (ClientException $ex) { if ($ex->getCode() === 404) { return -1; } throw $ex; } $responseBody = json_decode((string)$response->getBody(), true); $exitCode = $responseBody['State']['ExitCode']; if (is_int($exitCode)) { return $exitCode; } else { return -1; } } }