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": {
"type": "string",
"minLength": 1,
"pattern": "^[a-z0-9/-]+$"
"pattern": "^(ghcr.io/)?[a-z0-9/-]+$"
},
"expose": {
"type": "array",

View file

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

View file

@ -3,12 +3,12 @@
namespace AIO\Docker;
use AIO\Container\Container;
use AIO\Container\VersionState;
use AIO\Container\ContainerState;
use AIO\Container\VersionState;
use AIO\ContainerDefinitionFetcher;
use AIO\Data\ConfigurationManager;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use AIO\ContainerDefinitionFetcher;
use http\Env\Response;
readonly class DockerActionManager {
@ -18,7 +18,8 @@ readonly class DockerActionManager {
public function __construct(
private ConfigurationManager $configurationManager,
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']]);
}
@ -35,8 +36,7 @@ readonly class DockerActionManager {
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())));
try {
$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())));
try {
$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();
if ($tag === '%AIO_CHANNEL%') {
$tag = $this->GetCurrentChannel();
@ -88,7 +86,7 @@ readonly class DockerActionManager {
if ($runningDigests === null) {
return VersionState::Different;
}
$remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($container->GetContainerName(), $tag);
$remoteDigest = $this->GetLatestDigestOfTag($container->GetContainerName(), $tag);
if ($remoteDigest === null) {
return VersionState::Equal;
}
@ -101,8 +99,7 @@ readonly class DockerActionManager {
return VersionState::Different;
}
public function GetContainerStartingState(Container $container) : ContainerState
{
public function GetContainerStartingState(Container $container): ContainerState {
$runningState = $this->GetContainerRunningState($container);
if ($runningState === ContainerState::Stopped || $runningState === ContainerState::ImageDoesNotExist) {
return $runningState;
@ -140,8 +137,7 @@ readonly class DockerActionManager {
}
}
public function GetLogs(string $id) : string
{
public function GetLogs(string $id): string {
$url = $this->BuildApiUrl(
sprintf(
'containers/%s/logs?stdout=true&stderr=true&timestamps=true',
@ -171,8 +167,7 @@ readonly class DockerActionManager {
}
}
public function CreateVolumes(Container $container): void
{
public function CreateVolumes(Container $container): void {
$url = $this->BuildApiUrl('volumes/create');
foreach ($container->GetVolumes()->GetVolumes() as $volume) {
$forbiddenChars = [
@ -610,7 +605,7 @@ readonly class DockerActionManager {
$tag = $this->GetCurrentChannel();
}
$remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($container->GetContainerName(), $tag);
$remoteDigest = $this->GetLatestDigestOfTag($container->GetContainerName(), $tag);
if ($remoteDigest === null) {
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);
$encodedImageName = urlencode($imageName);
$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);
$updateAvailable = "";
@ -671,8 +664,7 @@ readonly class DockerActionManager {
}
}
private function getBackupVolumes(string $id) : string
{
private function getBackupVolumes(string $id): string {
$container = $this->containerDefinitionFetcher->GetContainerById($id);
$backupVolumes = '';
@ -691,8 +683,7 @@ readonly class DockerActionManager {
return array_unique($backupVolumesArray);
}
private function GetNextcloudExecCommands(string $id) : string
{
private function GetNextcloudExecCommands(string $id): string {
$container = $this->containerDefinitionFetcher->GetContainerById($id);
$nextcloudExecCommands = '';
@ -705,8 +696,7 @@ readonly class DockerActionManager {
return $nextcloudExecCommands;
}
private function GetAllNextcloudExecCommands() : string
{
private function GetAllNextcloudExecCommands(): string {
$id = 'nextcloud-aio-apache';
return 'NEXTCLOUD_EXEC_COMMANDS=' . $this->GetNextcloudExecCommands($id);
}
@ -780,8 +770,7 @@ readonly class DockerActionManager {
return 'latest';
}
public function IsMastercontainerUpdateAvailable() : bool
{
public function IsMastercontainerUpdateAvailable(): bool {
$imageName = 'nextcloud/all-in-one';
$containerName = 'nextcloud-aio-mastercontainer';
@ -791,7 +780,7 @@ readonly class DockerActionManager {
if ($runningDigests === null) {
return true;
}
$remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($imageName, $tag);
$remoteDigest = $this->GetLatestDigestOfTag($imageName, $tag);
if ($remoteDigest === null) {
return false;
}
@ -804,8 +793,7 @@ readonly class DockerActionManager {
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) {
$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(
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') {
return;
}
@ -923,15 +909,13 @@ readonly class DockerActionManager {
}
}
public function ConnectMasterContainerToNetwork() : void
{
public function ConnectMasterContainerToNetwork(): void {
$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.
// $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.
// 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.
@ -958,8 +942,7 @@ readonly class DockerActionManager {
}
}
public function GetBackupcontainerExitCode() : int
{
public function GetBackupcontainerExitCode(): int {
$containerName = 'nextcloud-aio-borgbackup';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName)));
try {
@ -981,8 +964,7 @@ readonly class DockerActionManager {
}
}
public function GetDatabasecontainerExitCode() : int
{
public function GetDatabasecontainerExitCode(): int {
$containerName = 'nextcloud-aio-database';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName)));
try {
@ -1057,4 +1039,13 @@ readonly class DockerActionManager {
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;
}
}
}