/dev/null || true'], $server), (string) instant_remote_process(["docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}} {{.Subnet}}{{end}}'"], $server), ); return remote_process(self::installCommands($architecture, $daemonConfig), $server); } /** * Sysbox must be registered in Docker, its services must run, and FUSE 3 must be available. */ public static function isInstalled(Server $server): bool { $runtimes = instant_remote_process([self::installedCheckCommand()], $server, false); return in_array(self::RUNTIME, explode(' ', trim((string) $runtimes)), true); } /** * Prints the Docker runtimes when the Sysbox services run and FUSE 3 is available. */ public static function installedCheckCommand(): string { return "systemctl is-active --quiet sysbox-mgr sysbox-fs && test -x /usr/bin/fusermount3 && docker info --format '{{range \$name, \$runtime := .Runtimes}}{{\$name}} {{end}}'"; } public static function isKernelSupported(string $kernel): bool { if (! preg_match('/\A(\d+)\.(\d+)/', $kernel, $matches)) { return false; } return version_compare("{$matches[1]}.{$matches[2]}", self::MINIMUM_KERNEL, '>='); } /** * Returns the Docker daemon configuration with "bip" and "default-address-pools" set to the values that * Docker already uses, or null when both are already set. * * @param string $bridge " " of the default bridge network, for example "172.17.0.1 172.17.0.0/16". */ public static function daemonConfigWithNetworkSettings(?string $currentConfig, string $bridge): ?string { $config = filled(trim((string) $currentConfig)) ? json_decode($currentConfig) : new \stdClass; if (! $config instanceof \stdClass) { throw new RuntimeException(self::DAEMON_CONFIG.' is not a valid JSON object. Fix it, then install Sysbox again.'); } $hasBip = filled($config->bip ?? null); $hasAddressPools = filled($config->{'default-address-pools'} ?? null); if ($hasBip && $hasAddressPools) { return null; } if (! $hasBip) { if (! preg_match('/\A(\d{1,3}(?:\.\d{1,3}){3}) \d{1,3}(?:\.\d{1,3}){3}\/(\d{1,2})\z/', trim($bridge), $matches)) { throw new RuntimeException('Could not read the address of the default Docker bridge network.'); } $config->bip = "{$matches[1]}/{$matches[2]}"; } if (! $hasAddressPools) { $config->{'default-address-pools'} = self::dockerDefaultAddressPools(); } // Sysbox checks both keys with a regex that expects one key per indented line. return json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n"; } /** * @return array */ public static function installCommands(string $architecture, ?string $daemonConfig): array { $version = config('constants.github_runner.sysbox.version'); $checksum = config("constants.github_runner.sysbox.checksums.{$architecture}"); $package = "/tmp/sysbox-ce_{$version}_{$architecture}.deb"; $url = "https://github.com/nestybox/sysbox/releases/download/v{$version}/sysbox-ce_{$version}.linux_{$architecture}.deb"; $apt = 'DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=300'; $commands = ["echo 'Installing Sysbox {$version}.'"]; if ($daemonConfig !== null) { $commands[] = "echo 'Saving the current Docker network settings in ".self::DAEMON_CONFIG." so that Docker does not restart.'"; $commands[] = 'mkdir -p /etc/docker'; $commands[] = 'cp '.self::DAEMON_CONFIG.' '.self::DAEMON_CONFIG.'.before-sysbox 2>/dev/null || true'; $commands[] = 'echo '.base64_encode($daemonConfig).' | base64 -d | tee '.self::DAEMON_CONFIG.' > /dev/null'; } return [ ...$commands, "echo 'Downloading the Sysbox package.'", "curl -fsSL -o {$package} {$url}", "echo '{$checksum} {$package}' | sha256sum -c -", "echo 'Installing the Sysbox package. Docker reloads its configuration, running containers are not restarted.'", "{$apt} update", // Sysbox needs fusermount3, but its "fuse" dependency is satisfied by FUSE 2 on Debian 12. "{$apt} install -y fuse3 {$package}", "rm -f {$package}", 'systemctl is-active --quiet sysbox-mgr sysbox-fs', 'test -x /usr/bin/fusermount3', "docker info --format '{{range \$name, \$runtime := .Runtimes}}{{\$name}} {{end}}' | grep -qw ".self::RUNTIME, "echo 'Sysbox is installed.'", ]; } /** * The address pools that Docker uses when "default-address-pools" is not set. * * @return array */ private static function dockerDefaultAddressPools(): array { $pools = []; foreach (range(17, 31) as $octet) { $pools[] = ['base' => "172.{$octet}.0.0/16", 'size' => 16]; } $pools[] = ['base' => '192.168.0.0/16', 'size' => 20]; return $pools; } }