mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 02:24:11 -05:00
✨ Add support for apk package manager (on Alpine Linux) (#6189)
This commit is contained in:
@@ -106,6 +106,15 @@ class CheckUpdates
|
||||
$out['osId'] = $osId;
|
||||
$out['package_manager'] = $packageManager;
|
||||
|
||||
return $out;
|
||||
case 'apk':
|
||||
instant_remote_process(['apk update -q'], $server);
|
||||
$output = instant_remote_process(['LANG=C apk list --upgradable 2>/dev/null'], $server);
|
||||
|
||||
$out = $this->parseApkOutput($output);
|
||||
$out['osId'] = $osId;
|
||||
$out['package_manager'] = $packageManager;
|
||||
|
||||
return $out;
|
||||
default:
|
||||
return [
|
||||
@@ -273,4 +282,32 @@ class CheckUpdates
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function parseApkOutput(string $output): array
|
||||
{
|
||||
$updates = [];
|
||||
$lines = explode("\n", $output);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
// Skip empty lines
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Example line: docker-cli-compose-2.31.0-r5 x86_64 {docker-cli-compose} (Apache-2.0) [upgradable from: docker-cli-compose-2.31.0-r4]
|
||||
if (preg_match('/^(.+)-([0-9]\S*) (\S+) \{\S+\} \([^)]+\) \[upgradable from: .+?-([0-9][^\]]+)\]$/', $line, $matches)) {
|
||||
$updates[] = [
|
||||
'package' => $matches[1],
|
||||
'new_version' => $matches[2],
|
||||
'architecture' => $matches[3],
|
||||
'current_version' => $matches[4],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_updates' => count($updates),
|
||||
'updates' => $updates,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ class InstallDocker
|
||||
$command = $command->merge([$this->getSuseDockerInstallCommand()]);
|
||||
} elseif ($supported_os_type->contains('arch')) {
|
||||
$command = $command->merge([$this->getArchDockerInstallCommand()]);
|
||||
} elseif ($supported_os_type->contains('alpine')) {
|
||||
$command = $command->merge([$this->getAlpineDockerInstallCommand()]);
|
||||
} else {
|
||||
$command = $command->merge([$this->getGenericDockerInstallCommand()]);
|
||||
}
|
||||
@@ -93,9 +95,8 @@ class InstallDocker
|
||||
"jq -s '.[0] * .[1]' /etc/docker/daemon.json.coolify /etc/docker/daemon.json | tee /etc/docker/daemon.json.appended > /dev/null",
|
||||
'mv /etc/docker/daemon.json.appended /etc/docker/daemon.json',
|
||||
"echo 'Restarting Docker Engine...'",
|
||||
'systemctl enable docker >/dev/null 2>&1 || true',
|
||||
'systemctl restart docker',
|
||||
]);
|
||||
$command = $command->merge($this->getDockerServiceCommands($supported_os_type->contains('alpine')));
|
||||
if ($server->isSwarm()) {
|
||||
$command = $command->merge([
|
||||
'docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true',
|
||||
@@ -154,6 +155,28 @@ class InstallDocker
|
||||
'systemctl start docker.service';
|
||||
}
|
||||
|
||||
private function getAlpineDockerInstallCommand(): string
|
||||
{
|
||||
return 'apk update && '.
|
||||
'apk add docker docker-cli-buildx docker-cli-compose && '.
|
||||
'mkdir -p /etc/docker';
|
||||
}
|
||||
|
||||
private function getDockerServiceCommands(bool $usesOpenRc): array
|
||||
{
|
||||
if ($usesOpenRc) {
|
||||
return [
|
||||
'rc-update add docker default',
|
||||
'rc-service docker restart',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'systemctl enable docker >/dev/null 2>&1 || true',
|
||||
'systemctl restart docker',
|
||||
];
|
||||
}
|
||||
|
||||
private function getGenericDockerInstallCommand(): string
|
||||
{
|
||||
return 'curl -fsSL https://get.docker.com | sh';
|
||||
|
||||
@@ -53,6 +53,8 @@ class InstallPrerequisites
|
||||
"echo 'Installing Prerequisites for Arch Linux...'",
|
||||
'pacman -Syu --noconfirm --needed curl wget git jq',
|
||||
]);
|
||||
} elseif ($supported_os_type->contains('alpine')) {
|
||||
$command = $command->merge($this->getAlpinePrerequisiteCommands());
|
||||
} else {
|
||||
throw new \Exception('Unsupported OS type for prerequisites installation');
|
||||
}
|
||||
@@ -61,4 +63,18 @@ class InstallPrerequisites
|
||||
|
||||
return remote_process($command, $server);
|
||||
}
|
||||
|
||||
private function getAlpinePrerequisiteCommands(): array
|
||||
{
|
||||
return [
|
||||
"echo 'Installing Prerequisites for Alpine Linux...'",
|
||||
"sed -i '/^#.*\\/community/s/^#//' /etc/apk/repositories 2>/dev/null || true",
|
||||
'apk update',
|
||||
'command -v bash >/dev/null || apk add bash',
|
||||
'command -v curl >/dev/null || apk add curl',
|
||||
'command -v wget >/dev/null || apk add wget',
|
||||
'command -v git >/dev/null || apk add git',
|
||||
'command -v jq >/dev/null || apk add jq',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,10 @@ class UpdatePackage
|
||||
$commandAll = 'pacman -Syu --noconfirm';
|
||||
$commandInstall = 'pacman -S --noconfirm '.$sanitizedPackage;
|
||||
break;
|
||||
case 'apk':
|
||||
$commandAll = 'apk update && apk upgrade';
|
||||
$commandInstall = 'apk upgrade '.$sanitizedPackage;
|
||||
break;
|
||||
default:
|
||||
return [
|
||||
'error' => 'OS not supported',
|
||||
|
||||
@@ -210,12 +210,18 @@ class SshMultiplexingHelper
|
||||
|
||||
$delimiter = base64_encode(Hash::make($command));
|
||||
$command = str_replace($delimiter, '', $command);
|
||||
$remoteShellCommand = self::remoteShellCommand();
|
||||
|
||||
return $sshCommand.self::escapedUserAtHost($server)." 'bash -se' << \\$delimiter".PHP_EOL
|
||||
return $sshCommand.self::escapedUserAtHost($server)." '{$remoteShellCommand}' << \\$delimiter".PHP_EOL
|
||||
.$command.PHP_EOL
|
||||
.$delimiter;
|
||||
}
|
||||
|
||||
private static function remoteShellCommand(): string
|
||||
{
|
||||
return 'if command -v bash >/dev/null 2>&1; then exec bash -se; else exec sh -se; fi';
|
||||
}
|
||||
|
||||
public static function getConnectionTimeout(Server $server): int
|
||||
{
|
||||
$timeout = data_get($server, 'settings.connection_timeout');
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
</x-slot:actions>
|
||||
|
||||
<x-callout type="info" title="Supported package managers">
|
||||
Automated package discovery currently supports apt, dnf, and zypper. Weekly status notifications
|
||||
can be managed from
|
||||
Automated package discovery currently supports apk, apt, dnf, pacman, and zypper. Weekly status
|
||||
notifications can be managed from
|
||||
<a class="font-medium underline" href="{{ route('notifications.email') }}"
|
||||
{{ wireNavigate() }}>notification settings</a>.
|
||||
</x-callout>
|
||||
|
||||
@@ -156,7 +156,7 @@ it('adds mux options to ssh commands only after the explicit master is ready', f
|
||||
->toContain('-o ControlMaster=auto')
|
||||
->toContain("-o ControlPath=/var/www/html/storage/app/ssh/mux/mux_{$server->uuid}")
|
||||
->toContain('-o ControlPersist=3600')
|
||||
->toContain("'bash -se' << \\")
|
||||
->toContain("'if command -v bash >/dev/null 2>&1; then exec bash -se; else exec sh -se; fi' << \\")
|
||||
->not->toContain('<< $delimiter');
|
||||
|
||||
Process::assertRan(fn ($process) => str_contains($process->command, 'ssh -fN '));
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\Server\CheckUpdates;
|
||||
use App\Actions\Server\InstallDocker;
|
||||
use App\Actions\Server\InstallPrerequisites;
|
||||
|
||||
it('installs Bash while bootstrapping Alpine prerequisites', function () {
|
||||
$method = new ReflectionMethod(InstallPrerequisites::class, 'getAlpinePrerequisiteCommands');
|
||||
|
||||
$commands = $method->invoke(new InstallPrerequisites);
|
||||
|
||||
expect($commands)->toContain('command -v bash >/dev/null || apk add bash');
|
||||
});
|
||||
|
||||
it('installs every Docker CLI plugin required on Alpine', function () {
|
||||
$method = new ReflectionMethod(InstallDocker::class, 'getAlpineDockerInstallCommand');
|
||||
|
||||
$command = $method->invoke(new InstallDocker);
|
||||
|
||||
expect($command)->toContain('apk add docker docker-cli-buildx docker-cli-compose');
|
||||
});
|
||||
|
||||
it('uses OpenRC instead of systemd to restart Docker on Alpine', function () {
|
||||
$method = new ReflectionMethod(InstallDocker::class, 'getDockerServiceCommands');
|
||||
|
||||
$action = new InstallDocker;
|
||||
$commands = $method->invoke($action, true);
|
||||
|
||||
expect($commands)
|
||||
->toBe(['rc-update add docker default', 'rc-service docker restart'])
|
||||
->each->not->toContain('systemctl')
|
||||
->and($method->invoke($action, false))
|
||||
->toBe(['systemctl enable docker >/dev/null 2>&1 || true', 'systemctl restart docker']);
|
||||
});
|
||||
|
||||
it('parses Alpine package updates', function () {
|
||||
$method = new ReflectionMethod(CheckUpdates::class, 'parseApkOutput');
|
||||
$output = <<<'OUTPUT'
|
||||
docker-cli-compose-2.31.0-r5 x86_64 {docker-cli-compose} (Apache-2.0) [upgradable from: docker-cli-compose-2.31.0-r4]
|
||||
libcrypto3-3.3.4-r0 aarch64 {openssl} (Apache-2.0) [upgradable from: libcrypto3-3.3.3-r0]
|
||||
OUTPUT;
|
||||
|
||||
$result = $method->invoke(new CheckUpdates, $output);
|
||||
|
||||
expect($result)->toBe([
|
||||
'total_updates' => 2,
|
||||
'updates' => [
|
||||
[
|
||||
'package' => 'docker-cli-compose',
|
||||
'new_version' => '2.31.0-r5',
|
||||
'architecture' => 'x86_64',
|
||||
'current_version' => '2.31.0-r4',
|
||||
],
|
||||
[
|
||||
'package' => 'libcrypto3',
|
||||
'new_version' => '3.3.4-r0',
|
||||
'architecture' => 'aarch64',
|
||||
'current_version' => '3.3.3-r0',
|
||||
],
|
||||
],
|
||||
]);
|
||||
});
|
||||
@@ -23,6 +23,16 @@ class SshMultiplexingDisableTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function test_remote_shell_prefers_bash_and_falls_back_to_sh()
|
||||
{
|
||||
$reflection = new \ReflectionMethod(SshMultiplexingHelper::class, 'remoteShellCommand');
|
||||
|
||||
$this->assertSame(
|
||||
'if command -v bash >/dev/null 2>&1; then exec bash -se; else exec sh -se; fi',
|
||||
$reflection->invoke(null)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_generate_ssh_command_accepts_disable_multiplexing_parameter()
|
||||
{
|
||||
$reflection = new \ReflectionMethod(SshMultiplexingHelper::class, 'generateSshCommand');
|
||||
|
||||
Reference in New Issue
Block a user