feat(server): support Alpine package management and Docker setup
Add Alpine prerequisites, Docker CLI plugins, and OpenRC service handling while falling back to sh for SSH commands on hosts without Bash.
This commit is contained in:
parent
47a14765e1
commit
96790e6531
8 changed files with 115 additions and 20 deletions
|
|
@ -95,9 +95,8 @@ public function handle(Server $server)
|
|||
"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',
|
||||
|
|
@ -159,10 +158,23 @@ private function getArchDockerInstallCommand(): string
|
|||
private function getAlpineDockerInstallCommand(): string
|
||||
{
|
||||
return 'apk update && '.
|
||||
'apk add docker docker-cli-compose && '.
|
||||
'mkdir -p /etc/docker && '.
|
||||
'rc-update add docker default && '.
|
||||
'service docker start';
|
||||
'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
|
||||
|
|
|
|||
|
|
@ -54,16 +54,7 @@ public function handle(Server $server)
|
|||
'pacman -Syu --noconfirm --needed curl wget git jq',
|
||||
]);
|
||||
} elseif ($supported_os_type->contains('alpine')) {
|
||||
// The community repository holds docker and is commented out on some Alpine images
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Prerequisites for Alpine Linux...'",
|
||||
"sed -i '/^#.*\\/community/s/^#//' /etc/apk/repositories 2>/dev/null || true",
|
||||
'apk update',
|
||||
'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',
|
||||
]);
|
||||
$command = $command->merge($this->getAlpinePrerequisiteCommands());
|
||||
} else {
|
||||
throw new \Exception('Unsupported OS type for prerequisites installation');
|
||||
}
|
||||
|
|
@ -72,4 +63,18 @@ public function handle(Server $server)
|
|||
|
||||
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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ public function handle(Server $server, string $osId, ?string $package = null, ?s
|
|||
break;
|
||||
case 'apk':
|
||||
$commandAll = 'apk update && apk upgrade';
|
||||
$commandInstall = 'apk upgrade '.$package;
|
||||
$commandInstall = 'apk upgrade '.$sanitizedPackage;
|
||||
break;
|
||||
default:
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -210,12 +210,18 @@ public static function generateSshCommand(Server $server, string $command, bool
|
|||
|
||||
$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,7 +35,7 @@ class="server-settings-workspace application-settings-workspace mt-4 grid w-full
|
|||
</x-slot:actions>
|
||||
|
||||
<x-callout type="info" title="Supported package managers">
|
||||
Automated package discovery currently supports apk, apt, dnf, and zypper. Weekly status
|
||||
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>.
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ function makeMuxServer(): Server
|
|||
->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 '));
|
||||
|
|
|
|||
62
tests/Unit/Actions/Server/AlpinePackageManagerTest.php
Normal file
62
tests/Unit/Actions/Server/AlpinePackageManagerTest.php
Normal file
|
|
@ -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 @@ public function test_generate_ssh_command_method_exists()
|
|||
);
|
||||
}
|
||||
|
||||
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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue