Add support for ghcr.io (#6134)

Signed-off-by: Jean-Yves <7360784+docjyJ@users.noreply.github.com>
Signed-off-by: Simon L. <szaimen@e.mail.de>
Co-authored-by: Simon L. <szaimen@e.mail.de>
This commit is contained in:
Jean-Yves 2025-03-13 12:55:18 +01:00 committed by GitHub
parent a6246f9544
commit e97d4b0a3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 183 additions and 103 deletions

View file

@ -0,0 +1,12 @@
{
"aio_services_v1": [
{
"container_name": "nextcloud-aio-helloworld",
"display_name": "Hello world",
"documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/helloworld",
"image": "ghcr.io/docjyj/aio-helloworld",
"image_tag": "%AIO_CHANNEL%",
"restart": "unless-stopped"
}
]
}

View file

@ -0,0 +1,8 @@
## Hello World
This container is a template for creating a community container.
### Repository
https://github.com/docjyj/aio-helloworld
### Maintainer
https://github.com/docjyj

View file

@ -15,7 +15,7 @@
"image": { "image": {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
"pattern": "^[a-z0-9/-]+$" "pattern": "^(ghcr.io/)?[a-z0-9/-]+$"
}, },
"expose": { "expose": {
"type": "array", "type": "array",

View file

@ -4,6 +4,7 @@ namespace AIO;
use AIO\Docker\DockerHubManager; use AIO\Docker\DockerHubManager;
use DI\Container; use DI\Container;
use AIO\Docker\GitHubContainerRegistryManager;
class DependencyInjection class DependencyInjection
{ {
@ -15,6 +16,11 @@ class DependencyInjection
new DockerHubManager() new DockerHubManager()
); );
$container->set(
GitHubContainerRegistryManager::class,
new GitHubContainerRegistryManager()
);
$container->set( $container->set(
\AIO\Data\ConfigurationManager::class, \AIO\Data\ConfigurationManager::class,
new \AIO\Data\ConfigurationManager() new \AIO\Data\ConfigurationManager()
@ -24,7 +30,8 @@ class DependencyInjection
new \AIO\Docker\DockerActionManager( new \AIO\Docker\DockerActionManager(
$container->get(\AIO\Data\ConfigurationManager::class), $container->get(\AIO\Data\ConfigurationManager::class),
$container->get(\AIO\ContainerDefinitionFetcher::class), $container->get(\AIO\ContainerDefinitionFetcher::class),
$container->get(DockerHubManager::class) $container->get(DockerHubManager::class),
$container->get(GitHubContainerRegistryManager::class)
) )
); );
$container->set( $container->set(

View file

@ -3,12 +3,12 @@
namespace AIO\Docker; namespace AIO\Docker;
use AIO\Container\Container; use AIO\Container\Container;
use AIO\Container\VersionState;
use AIO\Container\ContainerState; use AIO\Container\ContainerState;
use AIO\Container\VersionState;
use AIO\ContainerDefinitionFetcher;
use AIO\Data\ConfigurationManager; use AIO\Data\ConfigurationManager;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use AIO\ContainerDefinitionFetcher;
use http\Env\Response; use http\Env\Response;
readonly class DockerActionManager { readonly class DockerActionManager {
@ -16,18 +16,19 @@ readonly class DockerActionManager {
private Client $guzzleClient; private Client $guzzleClient;
public function __construct( public function __construct(
private ConfigurationManager $configurationManager, private ConfigurationManager $configurationManager,
private ContainerDefinitionFetcher $containerDefinitionFetcher, private ContainerDefinitionFetcher $containerDefinitionFetcher,
private DockerHubManager $dockerHubManager private DockerHubManager $dockerHubManager,
private GitHubContainerRegistryManager $gitHubContainerRegistryManager
) { ) {
$this->guzzleClient = new Client(['curl' => [CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock']]); $this->guzzleClient = new Client(['curl' => [CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock']]);
} }
private function BuildApiUrl(string $url) : string { private function BuildApiUrl(string $url): string {
return sprintf('http://127.0.0.1/%s/%s', self::API_VERSION, $url); return sprintf('http://127.0.0.1/%s/%s', self::API_VERSION, $url);
} }
private function BuildImageName(Container $container) : string { private function BuildImageName(Container $container): string {
$tag = $container->GetImageTag(); $tag = $container->GetImageTag();
if ($tag === '%AIO_CHANNEL%') { if ($tag === '%AIO_CHANNEL%') {
$tag = $this->GetCurrentChannel(); $tag = $this->GetCurrentChannel();
@ -35,8 +36,7 @@ readonly class DockerActionManager {
return $container->GetContainerName() . ':' . $tag; return $container->GetContainerName() . ':' . $tag;
} }
public function GetContainerRunningState(Container $container) : ContainerState public function GetContainerRunningState(Container $container): ContainerState {
{
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier()))); $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier())));
try { try {
$response = $this->guzzleClient->get($url); $response = $this->guzzleClient->get($url);
@ -56,8 +56,7 @@ readonly class DockerActionManager {
} }
} }
public function GetContainerRestartingState(Container $container) : ContainerState public function GetContainerRestartingState(Container $container): ContainerState {
{
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier()))); $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier())));
try { try {
$response = $this->guzzleClient->get($url); $response = $this->guzzleClient->get($url);
@ -77,8 +76,7 @@ readonly class DockerActionManager {
} }
} }
public function GetContainerUpdateState(Container $container) : VersionState public function GetContainerUpdateState(Container $container): VersionState {
{
$tag = $container->GetImageTag(); $tag = $container->GetImageTag();
if ($tag === '%AIO_CHANNEL%') { if ($tag === '%AIO_CHANNEL%') {
$tag = $this->GetCurrentChannel(); $tag = $this->GetCurrentChannel();
@ -88,12 +86,12 @@ readonly class DockerActionManager {
if ($runningDigests === null) { if ($runningDigests === null) {
return VersionState::Different; return VersionState::Different;
} }
$remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($container->GetContainerName(), $tag); $remoteDigest = $this->GetLatestDigestOfTag($container->GetContainerName(), $tag);
if ($remoteDigest === null) { if ($remoteDigest === null) {
return VersionState::Equal; return VersionState::Equal;
} }
foreach($runningDigests as $runningDigest) { foreach ($runningDigests as $runningDigest) {
if ($runningDigest === $remoteDigest) { if ($runningDigest === $remoteDigest) {
return VersionState::Equal; return VersionState::Equal;
} }
@ -101,8 +99,7 @@ readonly class DockerActionManager {
return VersionState::Different; return VersionState::Different;
} }
public function GetContainerStartingState(Container $container) : ContainerState public function GetContainerStartingState(Container $container): ContainerState {
{
$runningState = $this->GetContainerRunningState($container); $runningState = $this->GetContainerRunningState($container);
if ($runningState === ContainerState::Stopped || $runningState === ContainerState::ImageDoesNotExist) { if ($runningState === ContainerState::Stopped || $runningState === ContainerState::ImageDoesNotExist) {
return $runningState; return $runningState;
@ -110,9 +107,9 @@ readonly class DockerActionManager {
$containerName = $container->GetIdentifier(); $containerName = $container->GetIdentifier();
$internalPort = $container->GetInternalPort(); $internalPort = $container->GetInternalPort();
if($internalPort === '%APACHE_PORT%') { if ($internalPort === '%APACHE_PORT%') {
$internalPort = $this->configurationManager->GetApachePort(); $internalPort = $this->configurationManager->GetApachePort();
} elseif($internalPort === '%TALK_PORT%') { } elseif ($internalPort === '%TALK_PORT%') {
$internalPort = $this->configurationManager->GetTalkPort(); $internalPort = $this->configurationManager->GetTalkPort();
} }
@ -129,7 +126,7 @@ readonly class DockerActionManager {
} }
} }
public function DeleteContainer(Container $container) : void { public function DeleteContainer(Container $container): void {
$url = $this->BuildApiUrl(sprintf('containers/%s?v=true', urlencode($container->GetIdentifier()))); $url = $this->BuildApiUrl(sprintf('containers/%s?v=true', urlencode($container->GetIdentifier())));
try { try {
$this->guzzleClient->delete($url); $this->guzzleClient->delete($url);
@ -140,8 +137,7 @@ readonly class DockerActionManager {
} }
} }
public function GetLogs(string $id) : string public function GetLogs(string $id): string {
{
$url = $this->BuildApiUrl( $url = $this->BuildApiUrl(
sprintf( sprintf(
'containers/%s/logs?stdout=true&stderr=true&timestamps=true', 'containers/%s/logs?stdout=true&stderr=true&timestamps=true',
@ -162,7 +158,7 @@ readonly class DockerActionManager {
return $response; return $response;
} }
public function StartContainer(Container $container) : void { public function StartContainer(Container $container): void {
$url = $this->BuildApiUrl(sprintf('containers/%s/start', urlencode($container->GetIdentifier()))); $url = $this->BuildApiUrl(sprintf('containers/%s/start', urlencode($container->GetIdentifier())));
try { try {
$this->guzzleClient->post($url); $this->guzzleClient->post($url);
@ -171,10 +167,9 @@ readonly class DockerActionManager {
} }
} }
public function CreateVolumes(Container $container): void public function CreateVolumes(Container $container): void {
{
$url = $this->BuildApiUrl('volumes/create'); $url = $this->BuildApiUrl('volumes/create');
foreach($container->GetVolumes()->GetVolumes() as $volume) { foreach ($container->GetVolumes()->GetVolumes() as $volume) {
$forbiddenChars = [ $forbiddenChars = [
'/', '/',
]; ];
@ -184,7 +179,7 @@ readonly class DockerActionManager {
} }
$firstChar = substr($volume->name, 0, 1); $firstChar = substr($volume->name, 0, 1);
if(!in_array($firstChar, $forbiddenChars)) { if (!in_array($firstChar, $forbiddenChars)) {
$this->guzzleClient->request( $this->guzzleClient->request(
'POST', 'POST',
$url, $url,
@ -198,7 +193,7 @@ readonly class DockerActionManager {
} }
} }
public function CreateContainer(Container $container) : void { public function CreateContainer(Container $container): void {
$volumes = []; $volumes = [];
foreach ($container->GetVolumes()->GetVolumes() as $volume) { foreach ($container->GetVolumes()->GetVolumes() as $volume) {
// // NEXTCLOUD_MOUNT gets added via bind-mount later on // // NEXTCLOUD_MOUNT gets added via bind-mount later on
@ -226,12 +221,12 @@ readonly class DockerActionManager {
$requestBody['HostConfig']['Binds'] = $volumes; $requestBody['HostConfig']['Binds'] = $volumes;
} }
foreach($container->GetSecrets() as $secret) { foreach ($container->GetSecrets() as $secret) {
$this->configurationManager->GetAndGenerateSecret($secret); $this->configurationManager->GetAndGenerateSecret($secret);
} }
$aioVariables = $container->GetAioVariables()->GetVariables(); $aioVariables = $container->GetAioVariables()->GetVariables();
foreach($aioVariables as $variable) { foreach ($aioVariables as $variable) {
$config = $this->configurationManager->GetConfig(); $config = $this->configurationManager->GetConfig();
$variableArray = explode('=', $variable); $variableArray = explode('=', $variable);
$config[$variableArray[0]] = $variableArray[1]; $config[$variableArray[0]] = $variableArray[1];
@ -244,7 +239,7 @@ readonly class DockerActionManager {
if ($container->GetIdentifier() === 'nextcloud-aio-nextcloud') { if ($container->GetIdentifier() === 'nextcloud-aio-nextcloud') {
$envs[] = $this->GetAllNextcloudExecCommands(); $envs[] = $this->GetAllNextcloudExecCommands();
} }
foreach($envs as $key => $env) { foreach ($envs as $key => $env) {
// TODO: This whole block below is a hack and needs to get reworked in order to support multiple substitutions per line by default for all envs // TODO: This whole block below is a hack and needs to get reworked in order to support multiple substitutions per line by default for all envs
if (str_starts_with($env, 'extra_params=')) { if (str_starts_with($env, 'extra_params=')) {
$env = str_replace('%COLLABORA_SECCOMP_POLICY%', $this->configurationManager->GetCollaboraSeccompPolicy(), $env); $env = str_replace('%COLLABORA_SECCOMP_POLICY%', $this->configurationManager->GetCollaboraSeccompPolicy(), $env);
@ -256,12 +251,12 @@ readonly class DockerActionManager {
// Original implementation // Original implementation
$patterns = ['/%(.*)%/']; $patterns = ['/%(.*)%/'];
if(preg_match($patterns[0], $env, $out) === 1) { if (preg_match($patterns[0], $env, $out) === 1) {
$replacements = array(); $replacements = array();
if($out[1] === 'NC_DOMAIN') { if ($out[1] === 'NC_DOMAIN') {
$replacements[1] = $this->configurationManager->GetDomain(); $replacements[1] = $this->configurationManager->GetDomain();
} elseif($out[1] === 'NC_BASE_DN') { } elseif ($out[1] === 'NC_BASE_DN') {
$replacements[1] = $this->configurationManager->GetBaseDN(); $replacements[1] = $this->configurationManager->GetBaseDN();
} elseif ($out[1] === 'AIO_TOKEN') { } elseif ($out[1] === 'AIO_TOKEN') {
$replacements[1] = $this->configurationManager->GetToken(); $replacements[1] = $this->configurationManager->GetToken();
@ -391,10 +386,10 @@ readonly class DockerActionManager {
} else { } else {
$replacements[1] = ''; $replacements[1] = '';
} }
// Allow to get local ip-address of database container which allows to talk to it even in host mode (the container that requires this needs to be started first then) // Allow to get local ip-address of database container which allows to talk to it even in host mode (the container that requires this needs to be started first then)
} elseif ($out[1] === 'AIO_DATABASE_HOST') { } elseif ($out[1] === 'AIO_DATABASE_HOST') {
$replacements[1] = gethostbyname('nextcloud-aio-database'); $replacements[1] = gethostbyname('nextcloud-aio-database');
// Allow to get local ip-address of caddy container and add it to trusted proxies automatically // Allow to get local ip-address of caddy container and add it to trusted proxies automatically
} elseif ($out[1] === 'CADDY_IP_ADDRESS') { } elseif ($out[1] === 'CADDY_IP_ADDRESS') {
$replacements[1] = ''; $replacements[1] = '';
$communityContainers = $this->configurationManager->GetEnabledCommunityContainers(); $communityContainers = $this->configurationManager->GetEnabledCommunityContainers();
@ -419,7 +414,7 @@ readonly class DockerActionManager {
} }
} }
if(count($envs) > 0) { if (count($envs) > 0) {
$requestBody['Env'] = $envs; $requestBody['Env'] = $envs;
} }
@ -429,7 +424,7 @@ readonly class DockerActionManager {
$exposedPorts = []; $exposedPorts = [];
if ($container->GetInternalPort() !== 'host') { if ($container->GetInternalPort() !== 'host') {
foreach($container->GetPorts()->GetPorts() as $value) { foreach ($container->GetPorts()->GetPorts() as $value) {
$port = $value->port; $port = $value->port;
$protocol = $value->protocol; $protocol = $value->protocol;
if ($port === '%APACHE_PORT%') { if ($port === '%APACHE_PORT%') {
@ -449,7 +444,7 @@ readonly class DockerActionManager {
$requestBody['HostConfig']['NetworkMode'] = 'host'; $requestBody['HostConfig']['NetworkMode'] = 'host';
} }
if(count($exposedPorts) > 0) { if (count($exposedPorts) > 0) {
$requestBody['ExposedPorts'] = $exposedPorts; $requestBody['ExposedPorts'] = $exposedPorts;
foreach ($container->GetPorts()->GetPorts() as $value) { foreach ($container->GetPorts()->GetPorts() as $value) {
$port = $value->port; $port = $value->port;
@ -474,16 +469,16 @@ readonly class DockerActionManager {
$portWithProtocol = $port . '/' . $protocol; $portWithProtocol = $port . '/' . $protocol;
$requestBody['HostConfig']['PortBindings'][$portWithProtocol] = [ $requestBody['HostConfig']['PortBindings'][$portWithProtocol] = [
[ [
'HostPort' => $port, 'HostPort' => $port,
'HostIp' => $ipBinding, 'HostIp' => $ipBinding,
] ]
]; ];
} }
} }
$devices = []; $devices = [];
foreach($container->GetDevices() as $device) { foreach ($container->GetDevices() as $device) {
if ($device === '/dev/dri' && ! $this->configurationManager->isDriDeviceEnabled()) { if ($device === '/dev/dri' && !$this->configurationManager->isDriDeviceEnabled()) {
continue; continue;
} }
$devices[] = ["PathOnHost" => $device, "PathInContainer" => $device, "CgroupPermissions" => "rwm"]; $devices[] = ["PathOnHost" => $device, "PathInContainer" => $device, "CgroupPermissions" => "rwm"];
@ -510,7 +505,7 @@ readonly class DockerActionManager {
} }
$tmpfs = []; $tmpfs = [];
foreach($container->GetTmpfs() as $tmp) { foreach ($container->GetTmpfs() as $tmp) {
$mode = ""; $mode = "";
if (str_contains($tmp, ':')) { if (str_contains($tmp, ':')) {
$mode = explode(':', $tmp)[1]; $mode = explode(':', $tmp)[1];
@ -519,7 +514,7 @@ readonly class DockerActionManager {
$tmpfs[$tmp] = $mode; $tmpfs[$tmp] = $mode;
} }
if (count($tmpfs) > 0) { if (count($tmpfs) > 0) {
$requestBody['HostConfig']['Tmpfs'] = $tmpfs; $requestBody['HostConfig']['Tmpfs'] = $tmpfs;
} }
$requestBody['HostConfig']['Init'] = $container->GetInit(); $requestBody['HostConfig']['Init'] = $container->GetInit();
@ -563,22 +558,22 @@ readonly class DockerActionManager {
} }
} }
} }
// Special things for the talk container which should not be exposed in the containers.json // Special things for the talk container which should not be exposed in the containers.json
} elseif ($container->GetIdentifier() === 'nextcloud-aio-talk') { } elseif ($container->GetIdentifier() === 'nextcloud-aio-talk') {
// This is needed due to a bug in libwebsockets which cannot handle unlimited ulimits // This is needed due to a bug in libwebsockets which cannot handle unlimited ulimits
$requestBody['HostConfig']['Ulimits'] = [["Name" => "nofile", "Hard" => 200000, "Soft" => 200000]]; $requestBody['HostConfig']['Ulimits'] = [["Name" => "nofile", "Hard" => 200000, "Soft" => 200000]];
// // Special things for the nextcloud container which should not be exposed in the containers.json // // Special things for the nextcloud container which should not be exposed in the containers.json
// } elseif ($container->GetIdentifier() === 'nextcloud-aio-nextcloud') { // } elseif ($container->GetIdentifier() === 'nextcloud-aio-nextcloud') {
// foreach ($container->GetVolumes()->GetVolumes() as $volume) { // foreach ($container->GetVolumes()->GetVolumes() as $volume) {
// if ($volume->name !== $this->configurationManager->GetNextcloudMount()) { // if ($volume->name !== $this->configurationManager->GetNextcloudMount()) {
// continue; // continue;
// } // }
// $mounts[] = ["Type" => "bind", "Source" => $volume->name, "Target" => $volume->mountPoint, "ReadOnly" => !$volume->isWritable, "BindOptions" => [ "Propagation" => "rshared"]]; // $mounts[] = ["Type" => "bind", "Source" => $volume->name, "Target" => $volume->mountPoint, "ReadOnly" => !$volume->isWritable, "BindOptions" => [ "Propagation" => "rshared"]];
// } // }
// Special things for the caddy community container // Special things for the caddy community container
} elseif ($container->GetIdentifier() === 'nextcloud-aio-caddy') { } elseif ($container->GetIdentifier() === 'nextcloud-aio-caddy') {
$requestBody['HostConfig']['ExtraHosts'] = ['host.docker.internal:host-gateway']; $requestBody['HostConfig']['ExtraHosts'] = ['host.docker.internal:host-gateway'];
// Special things for the collabora container which should not be exposed in the containers.json // Special things for the collabora container which should not be exposed in the containers.json
} elseif ($container->GetIdentifier() === 'nextcloud-aio-collabora') { } elseif ($container->GetIdentifier() === 'nextcloud-aio-collabora') {
if ($this->configurationManager->GetAdditionalCollaboraOptions() !== '') { if ($this->configurationManager->GetAdditionalCollaboraOptions() !== '') {
$requestBody['Cmd'] = [$this->configurationManager->GetAdditionalCollaboraOptions()]; $requestBody['Cmd'] = [$this->configurationManager->GetAdditionalCollaboraOptions()];
@ -604,13 +599,13 @@ readonly class DockerActionManager {
} }
public function isDockerHubReachable(Container $container) : bool { public function isDockerHubReachable(Container $container): bool {
$tag = $container->GetImageTag(); $tag = $container->GetImageTag();
if ($tag === '%AIO_CHANNEL%') { if ($tag === '%AIO_CHANNEL%') {
$tag = $this->GetCurrentChannel(); $tag = $this->GetCurrentChannel();
} }
$remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($container->GetContainerName(), $tag); $remoteDigest = $this->GetLatestDigestOfTag($container->GetContainerName(), $tag);
if ($remoteDigest === null) { if ($remoteDigest === null) {
return false; return false;
@ -619,8 +614,7 @@ readonly class DockerActionManager {
} }
} }
public function PullImage(Container $container) : void public function PullImage(Container $container): void {
{
$imageName = $this->BuildImageName($container); $imageName = $this->BuildImageName($container);
$encodedImageName = urlencode($imageName); $encodedImageName = urlencode($imageName);
$url = $this->BuildApiUrl(sprintf('images/create?fromImage=%s', $encodedImageName)); $url = $this->BuildApiUrl(sprintf('images/create?fromImage=%s', $encodedImageName));
@ -643,8 +637,7 @@ readonly class DockerActionManager {
} }
} }
private function isContainerUpdateAvailable(string $id) : string private function isContainerUpdateAvailable(string $id): string {
{
$container = $this->containerDefinitionFetcher->GetContainerById($id); $container = $this->containerDefinitionFetcher->GetContainerById($id);
$updateAvailable = ""; $updateAvailable = "";
@ -657,7 +650,7 @@ readonly class DockerActionManager {
return $updateAvailable; return $updateAvailable;
} }
public function isAnyUpdateAvailable() : bool { public function isAnyUpdateAvailable(): bool {
// return early if instance is not installed // return early if instance is not installed
if (!$this->configurationManager->wasStartButtonClicked()) { if (!$this->configurationManager->wasStartButtonClicked()) {
return false; return false;
@ -671,8 +664,7 @@ readonly class DockerActionManager {
} }
} }
private function getBackupVolumes(string $id) : string private function getBackupVolumes(string $id): string {
{
$container = $this->containerDefinitionFetcher->GetContainerById($id); $container = $this->containerDefinitionFetcher->GetContainerById($id);
$backupVolumes = ''; $backupVolumes = '';
@ -685,14 +677,13 @@ readonly class DockerActionManager {
return $backupVolumes; return $backupVolumes;
} }
private function getAllBackupVolumes() : array { private function getAllBackupVolumes(): array {
$id = 'nextcloud-aio-apache'; $id = 'nextcloud-aio-apache';
$backupVolumesArray = explode(' ', $this->getBackupVolumes($id)); $backupVolumesArray = explode(' ', $this->getBackupVolumes($id));
return array_unique($backupVolumesArray); return array_unique($backupVolumesArray);
} }
private function GetNextcloudExecCommands(string $id) : string private function GetNextcloudExecCommands(string $id): string {
{
$container = $this->containerDefinitionFetcher->GetContainerById($id); $container = $this->containerDefinitionFetcher->GetContainerById($id);
$nextcloudExecCommands = ''; $nextcloudExecCommands = '';
@ -705,13 +696,12 @@ readonly class DockerActionManager {
return $nextcloudExecCommands; return $nextcloudExecCommands;
} }
private function GetAllNextcloudExecCommands() : string private function GetAllNextcloudExecCommands(): string {
{
$id = 'nextcloud-aio-apache'; $id = 'nextcloud-aio-apache';
return 'NEXTCLOUD_EXEC_COMMANDS=' . $this->GetNextcloudExecCommands($id); return 'NEXTCLOUD_EXEC_COMMANDS=' . $this->GetNextcloudExecCommands($id);
} }
private function GetRepoDigestsOfContainer(string $containerName) : ?array { private function GetRepoDigestsOfContainer(string $containerName): ?array {
try { try {
$containerUrl = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName)); $containerUrl = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName));
$containerOutput = json_decode($this->guzzleClient->get($containerUrl)->getBody()->getContents(), true); $containerOutput = json_decode($this->guzzleClient->get($containerUrl)->getBody()->getContents(), true);
@ -732,7 +722,7 @@ readonly class DockerActionManager {
$repoDigestArray = []; $repoDigestArray = [];
$oneDigestGiven = false; $oneDigestGiven = false;
foreach($imageOutput['RepoDigests'] as $repoDigest) { foreach ($imageOutput['RepoDigests'] as $repoDigest) {
$digestPosition = strpos($repoDigest, '@'); $digestPosition = strpos($repoDigest, '@');
if ($digestPosition === false) { if ($digestPosition === false) {
error_log('Somehow the RepoDigest of ' . $containerName . ' does not contain a @.'); error_log('Somehow the RepoDigest of ' . $containerName . ' does not contain a @.');
@ -752,10 +742,10 @@ readonly class DockerActionManager {
} }
} }
public function GetCurrentChannel() : string { public function GetCurrentChannel(): string {
$cacheKey = 'aio-ChannelName'; $cacheKey = 'aio-ChannelName';
$channelName = apcu_fetch($cacheKey); $channelName = apcu_fetch($cacheKey);
if($channelName !== false && is_string($channelName)) { if ($channelName !== false && is_string($channelName)) {
return $channelName; return $channelName;
} }
@ -765,7 +755,7 @@ readonly class DockerActionManager {
$output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true); $output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true);
$containerChecksum = $output['Image']; $containerChecksum = $output['Image'];
$tagArray = explode(':', $output['Config']['Image']); $tagArray = explode(':', $output['Config']['Image']);
if (count($tagArray) === 2) { if (count($tagArray) === 2) {
$tag = $tagArray[1]; $tag = $tagArray[1];
} else { } else {
error_log("No tag was found when getting the current channel. You probably did not follow the documentation correctly. Changing the channel to the default 'latest'."); error_log("No tag was found when getting the current channel. You probably did not follow the documentation correctly. Changing the channel to the default 'latest'.");
@ -780,8 +770,7 @@ readonly class DockerActionManager {
return 'latest'; return 'latest';
} }
public function IsMastercontainerUpdateAvailable() : bool public function IsMastercontainerUpdateAvailable(): bool {
{
$imageName = 'nextcloud/all-in-one'; $imageName = 'nextcloud/all-in-one';
$containerName = 'nextcloud-aio-mastercontainer'; $containerName = 'nextcloud-aio-mastercontainer';
@ -791,7 +780,7 @@ readonly class DockerActionManager {
if ($runningDigests === null) { if ($runningDigests === null) {
return true; return true;
} }
$remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($imageName, $tag); $remoteDigest = $this->GetLatestDigestOfTag($imageName, $tag);
if ($remoteDigest === null) { if ($remoteDigest === null) {
return false; return false;
} }
@ -804,8 +793,7 @@ readonly class DockerActionManager {
return true; return true;
} }
public function sendNotification(Container $container, string $subject, string $message, string $file = '/notify.sh') : void public function sendNotification(Container $container, string $subject, string $message, string $file = '/notify.sh'): void {
{
if ($this->GetContainerStartingState($container) === ContainerState::Running) { if ($this->GetContainerStartingState($container) === ContainerState::Running) {
$containerName = $container->GetIdentifier(); $containerName = $container->GetIdentifier();
@ -849,8 +837,7 @@ readonly class DockerActionManager {
} }
} }
private function DisconnectContainerFromBridgeNetwork(string $id) : void private function DisconnectContainerFromBridgeNetwork(string $id): void {
{
$url = $this->BuildApiUrl( $url = $this->BuildApiUrl(
sprintf('networks/%s/disconnect', 'bridge') sprintf('networks/%s/disconnect', 'bridge')
@ -870,8 +857,7 @@ readonly class DockerActionManager {
} }
} }
private function ConnectContainerIdToNetwork(string $id, string $internalPort, string $network = 'nextcloud-aio', bool $createNetwork = true, string $alias = '') : void private function ConnectContainerIdToNetwork(string $id, string $internalPort, string $network = 'nextcloud-aio', bool $createNetwork = true, string $alias = ''): void {
{
if ($internalPort === 'host') { if ($internalPort === 'host') {
return; return;
} }
@ -902,9 +888,9 @@ readonly class DockerActionManager {
$url = $this->BuildApiUrl( $url = $this->BuildApiUrl(
sprintf('networks/%s/connect', $network) sprintf('networks/%s/connect', $network)
); );
$jsonPayload = [ 'Container' => $id ]; $jsonPayload = ['Container' => $id];
if ($alias !== '' ) { if ($alias !== '') {
$jsonPayload['EndpointConfig'] = ['Aliases' => [ $alias ]]; $jsonPayload['EndpointConfig'] = ['Aliases' => [$alias]];
} }
try { try {
@ -923,15 +909,13 @@ readonly class DockerActionManager {
} }
} }
public function ConnectMasterContainerToNetwork() : void public function ConnectMasterContainerToNetwork(): void {
{
$this->ConnectContainerIdToNetwork('nextcloud-aio-mastercontainer', ''); $this->ConnectContainerIdToNetwork('nextcloud-aio-mastercontainer', '');
// Don't disconnect here since it slows down the initial login by a lot. Is getting done during cron.sh instead. // Don't disconnect here since it slows down the initial login by a lot. Is getting done during cron.sh instead.
// $this->DisconnectContainerFromBridgeNetwork('nextcloud-aio-mastercontainer'); // $this->DisconnectContainerFromBridgeNetwork('nextcloud-aio-mastercontainer');
} }
public function ConnectContainerToNetwork(Container $container) : void public function ConnectContainerToNetwork(Container $container): void {
{
// Add a secondary alias for domaincheck container, to keep it as similar to actual apache controller as possible. // Add a secondary alias for domaincheck container, to keep it as similar to actual apache controller as possible.
// If a reverse-proxy is relying on container name as hostname this allows it to operate as usual and still validate the domain // If a reverse-proxy is relying on container name as hostname this allows it to operate as usual and still validate the domain
// The domaincheck container and apache container are never supposed to be active at the same time because they use the same APACHE_PORT anyway, so this doesn't add any new constraints. // The domaincheck container and apache container are never supposed to be active at the same time because they use the same APACHE_PORT anyway, so this doesn't add any new constraints.
@ -947,7 +931,7 @@ readonly class DockerActionManager {
} }
} }
public function StopContainer(Container $container) : void { public function StopContainer(Container $container): void {
$url = $this->BuildApiUrl(sprintf('containers/%s/stop?t=%s', urlencode($container->GetIdentifier()), $container->GetMaxShutdownTime())); $url = $this->BuildApiUrl(sprintf('containers/%s/stop?t=%s', urlencode($container->GetIdentifier()), $container->GetMaxShutdownTime()));
try { try {
$this->guzzleClient->post($url); $this->guzzleClient->post($url);
@ -958,8 +942,7 @@ readonly class DockerActionManager {
} }
} }
public function GetBackupcontainerExitCode() : int public function GetBackupcontainerExitCode(): int {
{
$containerName = 'nextcloud-aio-borgbackup'; $containerName = 'nextcloud-aio-borgbackup';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName))); $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName)));
try { try {
@ -981,8 +964,7 @@ readonly class DockerActionManager {
} }
} }
public function GetDatabasecontainerExitCode() : int public function GetDatabasecontainerExitCode(): int {
{
$containerName = 'nextcloud-aio-database'; $containerName = 'nextcloud-aio-database';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName))); $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName)));
try { try {
@ -1004,7 +986,7 @@ readonly class DockerActionManager {
} }
} }
public function isLoginAllowed() : bool { public function isLoginAllowed(): bool {
$id = 'nextcloud-aio-apache'; $id = 'nextcloud-aio-apache';
$apacheContainer = $this->containerDefinitionFetcher->GetContainerById($id); $apacheContainer = $this->containerDefinitionFetcher->GetContainerById($id);
if ($this->GetContainerStartingState($apacheContainer) === ContainerState::Running) { if ($this->GetContainerStartingState($apacheContainer) === ContainerState::Running) {
@ -1013,7 +995,7 @@ readonly class DockerActionManager {
return true; return true;
} }
public function isBackupContainerRunning() : bool { public function isBackupContainerRunning(): bool {
$id = 'nextcloud-aio-borgbackup'; $id = 'nextcloud-aio-borgbackup';
$backupContainer = $this->containerDefinitionFetcher->GetContainerById($id); $backupContainer = $this->containerDefinitionFetcher->GetContainerById($id);
if ($this->GetContainerRunningState($backupContainer) === ContainerState::Running) { if ($this->GetContainerRunningState($backupContainer) === ContainerState::Running) {
@ -1022,7 +1004,7 @@ readonly class DockerActionManager {
return false; return false;
} }
private function GetCreatedTimeOfNextcloudImage() : ?string { private function GetCreatedTimeOfNextcloudImage(): ?string {
$imageName = 'nextcloud/aio-nextcloud' . ':' . $this->GetCurrentChannel(); $imageName = 'nextcloud/aio-nextcloud' . ':' . $this->GetCurrentChannel();
try { try {
$imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName)); $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName));
@ -1039,11 +1021,11 @@ readonly class DockerActionManager {
} }
} }
public function GetAndGenerateSecretWrapper(string $secretId) : string { public function GetAndGenerateSecretWrapper(string $secretId): string {
return $this->configurationManager->GetAndGenerateSecret($secretId); return $this->configurationManager->GetAndGenerateSecret($secretId);
} }
public function isNextcloudImageOutdated() : bool { public function isNextcloudImageOutdated(): bool {
$createdTime = $this->GetCreatedTimeOfNextcloudImage(); $createdTime = $this->GetCreatedTimeOfNextcloudImage();
if ($createdTime === null) { if ($createdTime === null) {
@ -1057,4 +1039,13 @@ readonly class DockerActionManager {
return false; return false;
} }
public function GetLatestDigestOfTag(string $imageName, string $tag): ?string {
$prefix = 'ghcr.io/';
if (str_starts_with($imageName, $prefix)) {
return $this->gitHubContainerRegistryManager->GetLatestDigestOfTag(str_replace($prefix, '', $imageName), $tag);
} else {
return $this->dockerHubManager->GetLatestDigestOfTag($imageName, $tag);
}
}
} }

View file

@ -0,0 +1,62 @@
<?php
namespace AIO\Docker;
use GuzzleHttp\Client;
readonly class GitHubContainerRegistryManager
{
private Client $guzzleClient;
public function __construct()
{
$this->guzzleClient = new Client();
}
public function GetLatestDigestOfTag(string $name, string $tag): ?string
{
$cacheKey = 'ghcr-manifest-' . $name . $tag;
$cachedVersion = apcu_fetch($cacheKey);
if ($cachedVersion !== false && is_string($cachedVersion)) {
return $cachedVersion;
}
// If one of the links below should ever become outdated, we can still upgrade the mastercontainer via the webinterface manually by opening '/api/docker/getwatchtower'
try {
$authTokenRequest = $this->guzzleClient->request(
'GET',
'https://ghcr.io/token?scope=repository:' . $name . ':pull'
);
$body = $authTokenRequest->getBody()->getContents();
$decodedBody = json_decode($body, true);
if (isset($decodedBody['token'])) {
$authToken = $decodedBody['token'];
$manifestRequest = $this->guzzleClient->request(
'HEAD',
'https://ghcr.io/v2/' . $name . '/manifests/' . $tag,
[
'headers' => [
'Accept' => 'application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json',
'Authorization' => 'Bearer ' . $authToken,
],
]
);
$responseHeaders = $manifestRequest->getHeader('docker-content-digest');
if (count($responseHeaders) === 1) {
$latestVersion = $responseHeaders[0];
apcu_add($cacheKey, $latestVersion, 600);
return $latestVersion;
}
}
error_log('Could not get digest of container ' . $name . ':' . $tag);
return null;
} catch (\Exception $e) {
error_log('Could not get digest of container ' . $name . ':' . $tag . ' ' . $e->getMessage());
return null;
}
}
}