From c2ea8996ee2689f54f55703ba7e8c3ea144784fe Mon Sep 17 00:00:00 2001 From: Luan Estradioto Date: Tue, 25 Jun 2024 15:29:33 -0300 Subject: [PATCH 01/91] feat: fully functional terminal for command center --- .../Shared/ExecuteContainerCommand.php | 25 +-- app/Livewire/Project/Shared/Terminal.php | 46 +++++ app/Livewire/RunCommand.php | 69 +++++-- app/Models/Server.php | 29 +++ docker-compose.dev.yml | 14 +- docker-compose.prod.yml | 13 ++ docker-compose.windows.yml | 18 ++ docker-compose.yml | 6 + package-lock.json | 53 ++++- package.json | 6 +- resources/js/app.js | 15 ++ .../execute-container-command.blade.php | 13 +- .../project/shared/terminal.blade.php | 191 ++++++++++++++++++ .../views/livewire/run-command.blade.php | 20 +- terminal-server.js | 163 +++++++++++++++ 15 files changed, 624 insertions(+), 57 deletions(-) create mode 100644 app/Livewire/Project/Shared/Terminal.php create mode 100644 resources/views/livewire/project/shared/terminal.blade.php create mode 100755 terminal-server.js diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 343915d9c..b560595b3 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -2,17 +2,15 @@ namespace App\Livewire\Project\Shared; -use App\Actions\Server\RunCommand; use App\Models\Application; use App\Models\Server; use App\Models\Service; use Illuminate\Support\Collection; +use Livewire\Attributes\On; use Livewire\Component; class ExecuteContainerCommand extends Component { - public string $command; - public string $container; public Collection $containers; @@ -23,8 +21,6 @@ class ExecuteContainerCommand extends Component public string $type; - public string $workDir = ''; - public Server $server; public Collection $servers; @@ -33,7 +29,6 @@ class ExecuteContainerCommand extends Component 'server' => 'required', 'container' => 'required', 'command' => 'required', - 'workDir' => 'nullable', ]; public function mount() @@ -115,7 +110,8 @@ class ExecuteContainerCommand extends Component } } - public function runCommand() + #[On('connectToContainer')] + public function connectToContainer() { try { if (data_get($this->parameters, 'application_uuid')) { @@ -132,14 +128,13 @@ class ExecuteContainerCommand extends Component if ($server->isForceDisabled()) { throw new \RuntimeException('Server is disabled.'); } - $cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'"; - if (! empty($this->workDir)) { - $exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}"; - } else { - $exec = "docker exec {$container_name} {$cmd}"; - } - $activity = RunCommand::run(server: $server, command: $exec); - $this->dispatch('activityMonitor', $activity->id); + + $this->dispatch('send-terminal-command', + true, + $container_name, + $server->uuid, + ); + } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php new file mode 100644 index 000000000..331392118 --- /dev/null +++ b/app/Livewire/Project/Shared/Terminal.php @@ -0,0 +1,46 @@ +firstOrFail(); + + if (auth()->user()) { + $teams = auth()->user()->teams->pluck('id'); + if (! $teams->contains($server->team_id) && ! $teams->contains(0)) { + throw new \Exception('User is not part of the team that owns this server'); + } + } + + if ($isContainer) { + $status = getContainerStatus($server, $identifier); + if ($status !== 'running') { + return handleError(new \Exception('Container is not running'), $this); + } + $command = generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + } else { + $command = generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + } + + // ssh command is sent back to frontend then to websocket + // this is done because the websocket connection is not available here + // a better solution would be to remove websocket on NodeJS and work with something like + // 1. Laravel Pusher/Echo connection (not possible without a sdk) + // 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies) + // 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used + $this->dispatch('send-back-command', $command); + } + + public function render() + { + return view('livewire.project.shared.terminal'); + } +} diff --git a/app/Livewire/RunCommand.php b/app/Livewire/RunCommand.php index c2d3adeea..aae02d4e1 100644 --- a/app/Livewire/RunCommand.php +++ b/app/Livewire/RunCommand.php @@ -2,42 +2,67 @@ namespace App\Livewire; -use App\Actions\Server\RunCommand as ServerRunCommand; use App\Models\Server; +use Livewire\Attributes\On; use Livewire\Component; class RunCommand extends Component { - public string $command; - - public $server; + public $selected_uuid; public $servers = []; - protected $rules = [ - 'server' => 'required', - 'command' => 'required', - ]; - - protected $validationAttributes = [ - 'server' => 'server', - 'command' => 'command', - ]; + public $containers = []; public function mount($servers) { $this->servers = $servers; - $this->server = $servers[0]->uuid; + $this->selected_uuid = $servers[0]->uuid; + $this->containers = $this->getAllActiveContainers(); } - public function runCommand() + private function getAllActiveContainers() { - $this->validate(); - try { - $activity = ServerRunCommand::run(server: Server::where('uuid', $this->server)->first(), command: $this->command); - $this->dispatch('activityMonitor', $activity->id); - } catch (\Throwable $e) { - return handleError($e, $this); - } + return Server::all()->flatMap(function ($server) { + if (! $server->isFunctional()) { + return []; + } + + return $server->definedResources() + ->filter(fn ($resource) => str_starts_with($resource->status, 'running:')) + ->map(function ($resource) use ($server) { + $container_name = $resource->uuid; + + if (class_basename($resource) === 'Application' || class_basename($resource) === 'Service') { + if ($server->isSwarm()) { + $container_name = $resource->uuid.'_'.$resource->uuid; + } else { + $current_containers = getCurrentApplicationContainerStatus($server, $resource->id, includePullrequests: true); + $container_name = data_get($current_containers->first(), 'Names'); + } + } + + return [ + 'name' => $resource->name, + 'connection_name' => $container_name, + 'uuid' => $resource->uuid, + 'status' => $resource->status, + 'server' => $server, + 'server_uuid' => $server->uuid, + ]; + }); + }); + } + + #[On('connectToContainer')] + public function connectToContainer() + { + $container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid); + + $this->dispatch('send-terminal-command', + isset($container), + $container['connection_name'] ?? $this->selected_uuid, + $container['server_uuid'] ?? $this->selected_uuid + ); } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 8a7325beb..337d7d7fa 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -295,6 +295,13 @@ respond 404 'service' => 'coolify-realtime', 'rule' => "Host(`{$host}`) && PathPrefix(`/app`)", ], + 'coolify-terminal-ws' => [ + 'entryPoints' => [ + 0 => 'http', + ], + 'service' => 'coolify-terminal', + 'rule' => "Host(`{$host}`) && PathPrefix(`/terminal`)", + ], ], 'services' => [ 'coolify' => [ @@ -315,6 +322,15 @@ respond 404 ], ], ], + 'coolify-terminal' => [ + 'loadBalancer' => [ + 'servers' => [ + 0 => [ + 'url' => 'http://coolify-terminal:6002', + ], + ], + ], + ], ], ], ]; @@ -344,6 +360,16 @@ respond 404 'certresolver' => 'letsencrypt', ], ]; + $traefik_dynamic_conf['http']['routers']['coolify-terminal-wss'] = [ + 'entryPoints' => [ + 0 => 'https', + ], + 'service' => 'coolify-terminal', + 'rule' => "Host(`{$host}`) && PathPrefix(`/terminal`)", + 'tls' => [ + 'certresolver' => 'letsencrypt', + ], + ]; } $yaml = Yaml::dump($traefik_dynamic_conf, 12, 2); $yaml = @@ -377,6 +403,9 @@ $schema://$host { handle /app/* { reverse_proxy coolify-realtime:6001 } + handle /terminal/* { + reverse_proxy coolify-terminal:6002 + } reverse_proxy coolify:80 }"; $base64 = base64_encode($caddy_file); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 7eda14d41..9710e0fae 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -52,8 +52,18 @@ services: SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}" + terminal: + env_file: + - .env + pull_policy: always + working_dir: /var/www/html + ports: + - "${FORWARD_TERMINAL_PORT:-6002}:6002" + volumes: + - .:/var/www/html:cached + command: sh -c "apk add --no-cache openssh-client && node --watch /var/www/html/terminal-server.js" vite: - image: node:20 + image: node:alpine pull_policy: always working_dir: /var/www/html # environment: @@ -62,7 +72,7 @@ services: - "${VITE_PORT:-5173}:${VITE_PORT:-5173}" volumes: - .:/var/www/html:cached - command: sh -c "npm install && npm run dev" + command: sh -c "apk add --no-cache make g++ python3 && npm install && npm run dev" networks: - coolify testing-host: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b8156cab5..5f7b5e935 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -125,6 +125,19 @@ services: interval: 5s retries: 10 timeout: 2s + terminal: + working_dir: /var/www/html + ports: + - "${TERMINAL_PORT:-6002}:6002" + volumes: + - .:/var/www/html:cached + command: sh -c "apk add --no-cache openssh-client && node /var/www/html/terminal-server.js" + healthcheck: + test: wget -qO- http://localhost:6002/ready || exit 1 + interval: 5s + retries: 10 + timeout: 2s + volumes: coolify-db: name: coolify-db diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml index af5ecc0f7..ab3c7197a 100644 --- a/docker-compose.windows.yml +++ b/docker-compose.windows.yml @@ -121,6 +121,24 @@ services: interval: 5s retries: 10 timeout: 2s + terminal: + image: node:alpine + pull_policy: always + container_name: coolify-terminal + restart: always + env_file: + - .env + working_dir: /var/www/html + ports: + - "${TERMINAL_PORT:-6002}:6002" + volumes: + - .:/var/www/html:cached + command: sh -c "apk add --no-cache openssh-client && node /var/www/html/terminal-server.js" + healthcheck: + test: wget -qO- http://localhost:6002/ready || exit 1 + interval: 5s + retries: 10 + timeout: 2s volumes: coolify-db: name: coolify-db diff --git a/docker-compose.yml b/docker-compose.yml index 8eed44f8c..4f1c03127 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,12 @@ services: restart: always networks: - coolify + terminal: + image: node:alpine + container_name: coolify-terminal + restart: always + networks: + - coolify networks: coolify: name: coolify diff --git a/package-lock.json b/package-lock.json index bec5a7f66..ed15acfe0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,13 @@ "dependencies": { "@tailwindcss/forms": "0.5.7", "@tailwindcss/typography": "0.5.13", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", "ioredis": "5.4.1", - "tailwindcss-scrollbar": "0.1.0" + "node-pty": "^1.0.0", + "tailwindcss-scrollbar": "0.1.0", + "ws": "^8.17.0" }, "devDependencies": { "@vitejs/plugin-vue": "4.5.1", @@ -692,6 +696,19 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" + }, "node_modules/alpinejs": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.0.tgz", @@ -1474,6 +1491,11 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -1491,6 +1513,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "dependencies": { + "nan": "^2.17.0" + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -2123,6 +2154,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yaml": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", diff --git a/package.json b/package.json index b4609a025..9c2541ecc 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,12 @@ "dependencies": { "@tailwindcss/forms": "0.5.7", "@tailwindcss/typography": "0.5.13", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", "ioredis": "5.4.1", - "tailwindcss-scrollbar": "0.1.0" + "node-pty": "^1.0.0", + "tailwindcss-scrollbar": "0.1.0", + "ws": "^8.17.0" } } diff --git a/resources/js/app.js b/resources/js/app.js index befec919e..f450cbe29 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -4,3 +4,18 @@ // const app = createApp({}); // app.component("magic-bar", MagicBar); // app.mount("#vue"); + +import { Terminal } from '@xterm/xterm'; +import '@xterm/xterm/css/xterm.css'; +import { FitAddon } from '@xterm/addon-fit'; + +if (!window.term) { + window.term = new Terminal({ + cols: 80, + rows: 30, + fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', + cursorBlink: true + }); + window.fitAddon = new FitAddon(); + window.term.loadAddon(window.fitAddon); +} diff --git a/resources/views/livewire/project/shared/execute-container-command.blade.php b/resources/views/livewire/project/shared/execute-container-command.blade.php index 680d6e0e1..167f4178b 100644 --- a/resources/views/livewire/project/shared/execute-container-command.blade.php +++ b/resources/views/livewire/project/shared/execute-container-command.blade.php @@ -22,11 +22,8 @@
@if (count($containers) > 0) -
-
- - -
+ @if (data_get($this->parameters, 'application_uuid')) @@ -47,14 +44,14 @@ @endif - Run + Start Connection
@else
No containers are not running.
@endif
-
- +
+
diff --git a/resources/views/livewire/project/shared/terminal.blade.php b/resources/views/livewire/project/shared/terminal.blade.php new file mode 100644 index 000000000..ecb1a0e50 --- /dev/null +++ b/resources/views/livewire/project/shared/terminal.blade.php @@ -0,0 +1,191 @@ +
+
+
+ (connection closed) +
+
+
+
+ + +
+ + @script + + @endscript +
diff --git a/resources/views/livewire/run-command.blade.php b/resources/views/livewire/run-command.blade.php index 7911f0470..1f0162940 100644 --- a/resources/views/livewire/run-command.blade.php +++ b/resources/views/livewire/run-command.blade.php @@ -1,19 +1,23 @@
-
- - + + @foreach ($servers as $server) @if ($loop->first) @else @endif + @foreach ($containers as $container) + @if ($container['server_uuid'] == $server->uuid) + + @endif + @endforeach @endforeach - Execute Command - + Start Connection -
- -
+
diff --git a/terminal-server.js b/terminal-server.js new file mode 100755 index 000000000..3923c3b5c --- /dev/null +++ b/terminal-server.js @@ -0,0 +1,163 @@ +import { WebSocketServer } from 'ws'; +import http from 'http'; +import pty from 'node-pty'; + +const server = http.createServer((req, res) => { + if (req.url === '/ready') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('OK'); + } else { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } +}); + +const wss = new WebSocketServer({ server, path: '/terminal' }); +const userSessions = new Map(); + +wss.on('connection', (ws) => { + const userId = generateUserId(); + const userSession = { ws, userId, ptyProcess: null, isActive: false }; + userSessions.set(userId, userSession); + + ws.on('message', (message) => handleMessage(userSession, message)); + ws.on('error', (err) => handleError(err, userId)); + ws.on('close', () => handleClose(userId)); +}); + +const messageHandlers = { + message: (session, data) => session.ptyProcess.write(data), + resize: (session, { cols, rows }) => session.ptyProcess.resize(cols, rows), + pause: (session) => session.ptyProcess.pause(), + resume: (session) => session.ptyProcess.resume(), + checkActive: (session, data) => { + if (data === 'force' && session.isActive) { + killPtyProcess(session.userId); + } else { + session.ws.send(session.isActive); + } + }, + command: (session, data) => handleCommand(session.ws, data, session.userId) +}; + +function handleMessage(userSession, message) { + const parsed = parseMessage(message); + if (!parsed) return; + + Object.entries(parsed).forEach(([key, value]) => { + const handler = messageHandlers[key]; + if (handler && (userSession.isActive || key === 'checkActive' || key === 'command')) { + handler(userSession, value); + } + }); +} + +function parseMessage(message) { + try { + return JSON.parse(message); + } catch (e) { + console.error('Failed to parse message:', e); + return null; + } +} + +async function handleCommand(ws, command, userId) { + const userSession = userSessions.get(userId); + + if (userSession && userSession.isActive) { + await killPtyProcess(userId); + } + + const commandString = command[0].split('\n').join(' '); + const timeout = extractTimeout(commandString); + const sshArgs = extractSshArgs(commandString); + const hereDocContent = extractHereDocContent(commandString); + const options = { + name: 'xterm-color', + cols: 80, + rows: 30, + cwd: process.env.HOME, + env: process.env + }; + + // NOTE: - Initiates a process within the Terminal container + // Establishes an SSH connection to root@coolify with RequestTTY enabled + // Executes the 'docker exec' command to connect to a specific container + // If the user types 'exit', it terminates the container connection and reverts to the server. + const ptyProcess = pty.spawn('ssh', sshArgs.concat(['bash']), options); + userSession.ptyProcess = ptyProcess; + userSession.isActive = true; + ptyProcess.write(hereDocContent + '\n'); + ptyProcess.write('clear\n'); + + ws.send('pty-ready'); + + ptyProcess.onData((data) => ws.send(data)); + + ptyProcess.onExit(({ exitCode, signal }) => { + console.error(`Process exited with code ${exitCode} and signal ${signal}`); + userSession.isActive = false; + }); + + if (timeout) { + setTimeout(async () => { + await killPtyProcess(userId); + }, timeout * 1000); + } +} + +async function handleError(err, userId) { + console.error('WebSocket error:', err); + await killPtyProcess(userId); +} + +async function handleClose(userId) { + await killPtyProcess(userId); + userSessions.delete(userId); +} + +async function killPtyProcess(userId) { + const session = userSessions.get(userId); + if (!session?.ptyProcess) return false; + + return new Promise((resolve) => { + session.ptyProcess.on('exit', () => { + session.isActive = false; + resolve(true); + }); + + session.ptyProcess.kill(); + }); +} + +function generateUserId() { + return Math.random().toString(36).substring(2, 11); +} + +function extractTimeout(commandString) { + const timeoutMatch = commandString.match(/timeout (\d+)/); + return timeoutMatch ? parseInt(timeoutMatch[1], 10) : null; +} + +function extractSshArgs(commandString) { + const sshCommandMatch = commandString.match(/ssh (.+?) 'bash -se'/); + let sshArgs = sshCommandMatch ? sshCommandMatch[1].split(' ') : []; + sshArgs = sshArgs.map(arg => arg === 'RequestTTY=no' ? 'RequestTTY=yes' : arg); + if (!sshArgs.includes('RequestTTY=yes')) { + sshArgs.push('-o', 'RequestTTY=yes'); + } + return sshArgs; +} + +function extractHereDocContent(commandString) { + const delimiterMatch = commandString.match(/<< (\S+)/); + const delimiter = delimiterMatch ? delimiterMatch[1] : null; + const escapedDelimiter = delimiter.slice(1).trim().replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); + const hereDocRegex = new RegExp(`<< \\\\${escapedDelimiter}([\\s\\S\\.]*?)${escapedDelimiter}`); + const hereDocMatch = commandString.match(hereDocRegex); + return hereDocMatch ? hereDocMatch[1] : ''; +} + +server.listen(6002, () => { + console.log('Server listening on port 6002'); +}); From 548fc21e4008b2723d81f273535b3b0c82a6acc6 Mon Sep 17 00:00:00 2001 From: Luan Estradioto Date: Thu, 15 Aug 2024 02:38:06 -0300 Subject: [PATCH 02/91] added ws authentication --- app/Livewire/RunCommand.php | 5 +- package-lock.json | 32 +- package.json | 4 +- .../project/shared/terminal.blade.php | 309 +++++++++--------- routes/web.php | 6 + terminal-server.js | 43 ++- 6 files changed, 238 insertions(+), 161 deletions(-) diff --git a/app/Livewire/RunCommand.php b/app/Livewire/RunCommand.php index aae02d4e1..2d01cbca0 100644 --- a/app/Livewire/RunCommand.php +++ b/app/Livewire/RunCommand.php @@ -29,7 +29,10 @@ class RunCommand extends Component } return $server->definedResources() - ->filter(fn ($resource) => str_starts_with($resource->status, 'running:')) + ->filter(function ($resource) { + $status = method_exists($resource, 'realStatus') ? $resource->realStatus() : (method_exists($resource, 'status') ? $resource->status() : 'exited'); + return str_starts_with($status, 'running:'); + }) ->map(function ($resource) use ($server) { $container_name = $resource->uuid; diff --git a/package-lock.json b/package-lock.json index ed15acfe0..a9e742135 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", + "cookie": "^0.6.0", + "dotenv": "^16.4.5", "ioredis": "5.4.1", "node-pty": "^1.0.0", "tailwindcss-scrollbar": "0.1.0", @@ -18,7 +20,7 @@ "devDependencies": { "@vitejs/plugin-vue": "4.5.1", "autoprefixer": "10.4.19", - "axios": "1.7.2", + "axios": "^1.7.4", "laravel-echo": "1.16.1", "laravel-vite-plugin": "0.8.1", "postcss": "8.4.38", @@ -783,10 +785,11 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "dev": true, + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -956,6 +959,15 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1016,6 +1028,18 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.692", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.692.tgz", diff --git a/package.json b/package.json index 9c2541ecc..882ad2e01 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "devDependencies": { "@vitejs/plugin-vue": "4.5.1", "autoprefixer": "10.4.19", - "axios": "1.7.2", + "axios": "^1.7.4", "laravel-echo": "1.16.1", "laravel-vite-plugin": "0.8.1", "postcss": "8.4.38", @@ -23,6 +23,8 @@ "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", + "cookie": "^0.6.0", + "dotenv": "^16.4.5", "ioredis": "5.4.1", "node-pty": "^1.0.0", "tailwindcss-scrollbar": "0.1.0", diff --git a/resources/views/livewire/project/shared/terminal.blade.php b/resources/views/livewire/project/shared/terminal.blade.php index ecb1a0e50..4e1a086e2 100644 --- a/resources/views/livewire/project/shared/terminal.blade.php +++ b/resources/views/livewire/project/shared/terminal.blade.php @@ -24,168 +24,169 @@ @script - + // Type CTRL + D or exit in the terminal + if (data === '\x04' || (data === '\r' && stripAnsiCommands(commandBuffer).trim() === 'exit')) { + checkIfProcessIsRunningAndKillIt(); + setTimeout(() => { + term.reset(); + term.write('(connection closed)'); + $data.terminalActive = false; + }, 500); + commandBuffer = ''; + } else if (data === '\r') { + commandBuffer = ''; + } else { + commandBuffer += data; + } + }); + + function stripAnsiCommands(input) { + return input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, ''); + } + + // Copy and paste + // Enables ctrl + c and ctrl + v + // defaults otherwise to ctrl + insert, shift + insert + term.attachCustomKeyEventHandler((arg) => { + if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") { + navigator.clipboard.readText() + .then(text => { + socket.send(JSON.stringify({ + message: text + })); + }) + }; + + if (arg.ctrlKey && arg.code === "KeyC" && arg.type === "keydown") { + const selection = term.getSelection(); + if (selection) { + navigator.clipboard.writeText(selection); + return false; + } + } + return true; + }); + + $wire.on('send-back-command', function(command) { + socket.send(JSON.stringify({ + command: command + })); + }); + + window.addEventListener('beforeunload', function(e) { + checkIfProcessIsRunningAndKillIt(); + }); + + function checkIfProcessIsRunningAndKillIt() { + socket.send(JSON.stringify({ + checkActive: 'force' + })); + } + + window.onresize = function() { + $data.resizeTerminal() + }; + + Alpine.data('data', () => ({ + fullscreen: false, + terminalActive: false, + init() { + this.$watch('terminalActive', (value) => { + this.$nextTick(() => { + if (value) { + $refs.terminalWrapper.style.display = 'block'; + this.resizeTerminal(); + } else { + $refs.terminalWrapper.style.display = 'none'; + } + }); + }); + }, + makeFullscreen() { + this.fullscreen = !this.fullscreen; + $nextTick(() => { + this.resizeTerminal() + }) + }, + + resizeTerminal() { + if (!this.terminalActive) return; + + fitAddon.fit(); + const height = $refs.terminalWrapper.clientHeight; + const rows = height / term._core._renderService._charSizeService.height - 1; + var termWidth = term.cols; + var termHeight = parseInt(rows.toString(), 10); + term.resize(termWidth, termHeight); + socket.send(JSON.stringify({ + resize: { + cols: termWidth, + rows: termHeight + } + })); + } + })); + + initializeWebSocket(); + @endscript - + \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index e2ccfc704..01cf2762b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -154,6 +154,12 @@ Route::middleware(['auth', 'verified'])->group(function () { }); Route::get('/command-center', CommandCenterIndex::class)->name('command-center'); + Route::post('/terminal/auth', function () { + if (auth()->check()) { + return response()->json(['authenticated' => true], 200); + } + return response()->json(['authenticated' => false], 401); + })->name('terminal.auth'); Route::prefix('invitations')->group(function () { Route::get('/{uuid}', [Controller::class, 'accept_invitation'])->name('team.invitation.accept'); diff --git a/terminal-server.js b/terminal-server.js index 3923c3b5c..a1555bf30 100755 --- a/terminal-server.js +++ b/terminal-server.js @@ -1,6 +1,9 @@ import { WebSocketServer } from 'ws'; import http from 'http'; import pty from 'node-pty'; +import axios from 'axios'; +import cookie from 'cookie'; +import 'dotenv/config' const server = http.createServer((req, res) => { if (req.url === '/ready') { @@ -12,7 +15,45 @@ const server = http.createServer((req, res) => { } }); -const wss = new WebSocketServer({ server, path: '/terminal' }); +const verifyClient = async (info, callback) => { + const cookies = cookie.parse(info.req.headers.cookie || ''); + const origin = new URL(info.origin); + const protocol = origin.protocol; + const xsrfToken = cookies['XSRF-TOKEN']; + + // Generate session cookie name based on APP_NAME + const appName = process.env.APP_NAME || 'laravel'; + const sessionCookieName = `${appName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}_session`; + const laravelSession = cookies[sessionCookieName]; + + // Verify presence of required tokens + if (!laravelSession || !xsrfToken) { + return callback(false, 401, 'Unauthorized: Missing required tokens'); + } + + try { + // Authenticate with Laravel backend + const response = await axios.post(`${protocol}//coolify/terminal/auth`, null, { + headers: { + 'Cookie': `${sessionCookieName}=${laravelSession}`, + 'X-XSRF-TOKEN': xsrfToken + }, + }); + + if (response.status === 200) { + // Authentication successful + callback(true); + } else { + callback(false, 401, 'Unauthorized: Invalid credentials'); + } + } catch (error) { + console.error('Authentication error:', error.message); + callback(false, 500, 'Internal Server Error'); + } +}; + + +const wss = new WebSocketServer({ server, path: '/terminal', verifyClient: verifyClient }); const userSessions = new Map(); wss.on('connection', (ws) => { From 2b8c9920d85c82756c2e4a1ac9d0ac1443e0295d Mon Sep 17 00:00:00 2001 From: Luan Estradioto Date: Thu, 15 Aug 2024 20:52:50 -0300 Subject: [PATCH 03/91] removed extra container and added new process to soketi container --- app/Models/Server.php | 4 +- docker-compose.dev.yml | 20 +++++----- docker-compose.prod.yml | 22 +++++------ docker-compose.windows.yml | 31 ++++++--------- docker-compose.yml | 6 --- docker/soketi-entrypoint/soketi-entrypoint.sh | 39 +++++++++++++++++++ 6 files changed, 70 insertions(+), 52 deletions(-) create mode 100644 docker/soketi-entrypoint/soketi-entrypoint.sh diff --git a/app/Models/Server.php b/app/Models/Server.php index 337d7d7fa..44746324b 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -326,7 +326,7 @@ respond 404 'loadBalancer' => [ 'servers' => [ 0 => [ - 'url' => 'http://coolify-terminal:6002', + 'url' => 'http://coolify-realtime:6002', ], ], ], @@ -404,7 +404,7 @@ $schema://$host { reverse_proxy coolify-realtime:6001 } handle /terminal/* { - reverse_proxy coolify-terminal:6002 + reverse_proxy coolify-realtime:6002 } reverse_proxy coolify:80 }"; diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9710e0fae..64eb1d2a4 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -47,21 +47,19 @@ services: - .env ports: - "${FORWARD_SOKETI_PORT:-6001}:6001" + - "6002:6002" + volumes: + - ./docker/soketi-entrypoint/soketi-entrypoint.sh:/soketi-entrypoint.sh + - ./package.json:/terminal/package.json + - ./package-lock.json:/terminal/package-lock.json + - ./terminal-server.js:/terminal/terminal-server.js + - ./storage:/var/www/html/storage + entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"] environment: SOKETI_DEBUG: "false" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}" - terminal: - env_file: - - .env - pull_policy: always - working_dir: /var/www/html - ports: - - "${FORWARD_TERMINAL_PORT:-6002}:6002" - volumes: - - .:/var/www/html:cached - command: sh -c "apk add --no-cache openssh-client && node --watch /var/www/html/terminal-server.js" vite: image: node:alpine pull_policy: always @@ -72,7 +70,7 @@ services: - "${VITE_PORT:-5173}:${VITE_PORT:-5173}" volumes: - .:/var/www/html:cached - command: sh -c "apk add --no-cache make g++ python3 && npm install && npm run dev" + command: sh -c "npm install && npm run dev" networks: - coolify testing-host: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 5f7b5e935..5fc6a9919 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -115,25 +115,21 @@ services: soketi: ports: - "${SOKETI_PORT:-6001}:6001" + - "6002:6002" + volumes: + - ./docker/soketi-entrypoint/soketi-entrypoint.sh:/soketi-entrypoint.sh + - ./package.json:/terminal/package.json + - ./package-lock.json:/terminal/package-lock.json + - ./terminal-server.js:/terminal/terminal-server.js + - ./storage:/var/www/html/storage + entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"] environment: SOKETI_DEBUG: "${SOKETI_DEBUG:-false}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}" healthcheck: - test: wget -qO- http://127.0.0.1:6001/ready || exit 1 - interval: 5s - retries: 10 - timeout: 2s - terminal: - working_dir: /var/www/html - ports: - - "${TERMINAL_PORT:-6002}:6002" - volumes: - - .:/var/www/html:cached - command: sh -c "apk add --no-cache openssh-client && node /var/www/html/terminal-server.js" - healthcheck: - test: wget -qO- http://localhost:6002/ready || exit 1 + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"] interval: 5s retries: 10 timeout: 2s diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml index ab3c7197a..1b800a5d6 100644 --- a/docker-compose.windows.yml +++ b/docker-compose.windows.yml @@ -102,7 +102,7 @@ services: interval: 5s retries: 10 timeout: 2s - soketi: +soketi: image: 'quay.io/soketi/soketi:1.6-16-alpine' pull_policy: always container_name: coolify-realtime @@ -111,34 +111,25 @@ services: - .env ports: - "${SOKETI_PORT:-6001}:6001" + - "6002:6002" + volumes: + - ./docker/soketi-entrypoint/soketi-entrypoint.sh:/soketi-entrypoint.sh + - ./package.json:/terminal/package.json + - ./package-lock.json:/terminal/package-lock.json + - ./terminal-server.js:/terminal/terminal-server.js + - ./storage:/var/www/html/storage + entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"] environment: SOKETI_DEBUG: "${SOKETI_DEBUG:-false}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}" healthcheck: - test: wget -qO- http://localhost:6001/ready || exit 1 - interval: 5s - retries: 10 - timeout: 2s - terminal: - image: node:alpine - pull_policy: always - container_name: coolify-terminal - restart: always - env_file: - - .env - working_dir: /var/www/html - ports: - - "${TERMINAL_PORT:-6002}:6002" - volumes: - - .:/var/www/html:cached - command: sh -c "apk add --no-cache openssh-client && node /var/www/html/terminal-server.js" - healthcheck: - test: wget -qO- http://localhost:6002/ready || exit 1 + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"] interval: 5s retries: 10 timeout: 2s + volumes: coolify-db: name: coolify-db diff --git a/docker-compose.yml b/docker-compose.yml index 4f1c03127..8eed44f8c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,12 +28,6 @@ services: restart: always networks: - coolify - terminal: - image: node:alpine - container_name: coolify-terminal - restart: always - networks: - - coolify networks: coolify: name: coolify diff --git a/docker/soketi-entrypoint/soketi-entrypoint.sh b/docker/soketi-entrypoint/soketi-entrypoint.sh new file mode 100644 index 000000000..808e306e7 --- /dev/null +++ b/docker/soketi-entrypoint/soketi-entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# Install openssh-client +apk add --no-cache openssh-client make g++ python3 + +cd /terminal + +# Install npm dependencies +npm ci + +# Rebuild node-pty +npm rebuild node-pty --update-binary + +# Function to timestamp logs +timestamp() { + date "+%Y-%m-%d %H:%M:%S" +} + +# Start the terminal server in the background with logging +node --watch /terminal/terminal-server.js > >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 & +TERMINAL_PID=$! + +# Start the Soketi process in the background with logging +node /app/bin/server.js start > >(while read line; do echo "$(timestamp) [SOKETI] $line"; done) 2>&1 & +SOKETI_PID=$! + +# Function to forward signals to child processes +forward_signal() { + kill -$1 $TERMINAL_PID $SOKETI_PID +} + +# Forward SIGTERM to child processes +trap 'forward_signal TERM' TERM + +# Wait for any process to exit +wait -n + +# Exit with status of process that exited first +exit $? \ No newline at end of file From 117fbeb07c1a5405e38b0f39c1655f7bddeb19cc Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Wed, 11 Sep 2024 12:19:27 +0200 Subject: [PATCH 04/91] fixes for terminal --- app/Livewire/Project/Shared/Terminal.php | 16 +- app/Livewire/RunCommand.php | 46 ++- bootstrap/helpers/docker.php | 14 + docker-compose.dev.yml | 10 +- docker-compose.prod.yml | 1 + docker-compose.windows.yml | 4 +- docker-compose.yml | 1 - docker/coolify-realtime/Dockerfile | 9 + docker/coolify-realtime/package.json | 13 + .../soketi-entrypoint.sh | 14 +- .../coolify-realtime/terminal-server.js | 62 ++-- .../project/shared/terminal.blade.php | 315 +++++++++--------- 12 files changed, 279 insertions(+), 226 deletions(-) create mode 100644 docker/coolify-realtime/Dockerfile create mode 100644 docker/coolify-realtime/package.json rename docker/{soketi-entrypoint => coolify-realtime}/soketi-entrypoint.sh (79%) rename terminal-server.js => docker/coolify-realtime/terminal-server.js (81%) diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php index 331392118..e070ee367 100644 --- a/app/Livewire/Project/Shared/Terminal.php +++ b/app/Livewire/Project/Shared/Terminal.php @@ -11,17 +11,19 @@ class Terminal extends Component #[On('send-terminal-command')] public function sendTerminalCommand($isContainer, $identifier, $serverUuid) { - $server = Server::whereUuid($serverUuid)->firstOrFail(); + $server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail(); - if (auth()->user()) { - $teams = auth()->user()->teams->pluck('id'); - if (! $teams->contains($server->team_id) && ! $teams->contains(0)) { - throw new \Exception('User is not part of the team that owns this server'); - } - } + // if (auth()->user()) { + // $teams = auth()->user()->teams->pluck('id'); + // if (! $teams->contains($server->team_id) && ! $teams->contains(0)) { + // throw new \Exception('User is not part of the team that owns this server'); + // } + // } if ($isContainer) { + ray($identifier); $status = getContainerStatus($server, $identifier); + ray($status); if ($status !== 'running') { return handleError(new \Exception('Container is not running'), $this); } diff --git a/app/Livewire/RunCommand.php b/app/Livewire/RunCommand.php index 2d01cbca0..449ab1ea9 100644 --- a/app/Livewire/RunCommand.php +++ b/app/Livewire/RunCommand.php @@ -2,7 +2,6 @@ namespace App\Livewire; -use App\Models\Server; use Livewire\Attributes\On; use Livewire\Component; @@ -23,7 +22,7 @@ class RunCommand extends Component private function getAllActiveContainers() { - return Server::all()->flatMap(function ($server) { + return collect($this->servers)->flatMap(function ($server) { if (! $server->isFunctional()) { return []; } @@ -31,25 +30,52 @@ class RunCommand extends Component return $server->definedResources() ->filter(function ($resource) { $status = method_exists($resource, 'realStatus') ? $resource->realStatus() : (method_exists($resource, 'status') ? $resource->status() : 'exited'); + return str_starts_with($status, 'running:'); }) ->map(function ($resource) use ($server) { - $container_name = $resource->uuid; + if (isDev()) { + if (data_get($resource, 'name') === 'coolify-db') { + $container_name = 'coolify-db'; - if (class_basename($resource) === 'Application' || class_basename($resource) === 'Service') { - if ($server->isSwarm()) { - $container_name = $resource->uuid.'_'.$resource->uuid; - } else { - $current_containers = getCurrentApplicationContainerStatus($server, $resource->id, includePullrequests: true); - $container_name = data_get($current_containers->first(), 'Names'); + return [ + 'name' => $resource->name, + 'connection_name' => $container_name, + 'uuid' => $resource->uuid, + 'status' => 'running', + 'server' => $server, + 'server_uuid' => $server->uuid, + ]; } } + if (class_basename($resource) === 'Application') { + if (! $server->isSwarm()) { + $current_containers = getCurrentApplicationContainerStatus($server, $resource->id, includePullrequests: true); + } + $status = $resource->status; + } elseif (class_basename($resource) === 'Service') { + $current_containers = getCurrentServiceContainerStatus($server, $resource->id); + $status = $resource->status(); + } else { + $status = getContainerStatus($server, $resource->uuid); + if ($status === 'running') { + $current_containers = collect([ + 'Names' => $resource->name, + ]); + } + } + if ($server->isSwarm()) { + $container_name = $resource->uuid.'_'.$resource->uuid; + } else { + $container_name = data_get($current_containers->first(), 'Names'); + } + return [ 'name' => $resource->name, 'connection_name' => $container_name, 'uuid' => $resource->uuid, - 'status' => $resource->status, + 'status' => $status, 'server' => $server, 'server_uuid' => $server->uuid, ]; diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 90093deb8..825668743 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -40,6 +40,20 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul return $containers; } +function getCurrentServiceContainerStatus(Server $server, int $id): Collection +{ + $containers = collect([]); + if (! $server->isSwarm()) { + $containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --format '{{json .}}' "], $server); + $containers = format_docker_command_output_to_json($containers); + $containers = $containers->filter(); + + return $containers; + } + + return $containers; +} + function format_docker_command_output_to_json($rawOutput): Collection { $outputLines = explode(PHP_EOL, $rawOutput); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 64eb1d2a4..16bb354a5 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -43,25 +43,23 @@ services: - /data/coolify/_volumes/redis/:/data # - coolify-redis-data-dev:/data soketi: + build: + context: . + dockerfile: ./docker/coolify-realtime/Dockerfile env_file: - .env ports: - "${FORWARD_SOKETI_PORT:-6001}:6001" - "6002:6002" volumes: - - ./docker/soketi-entrypoint/soketi-entrypoint.sh:/soketi-entrypoint.sh - - ./package.json:/terminal/package.json - - ./package-lock.json:/terminal/package-lock.json - - ./terminal-server.js:/terminal/terminal-server.js - ./storage:/var/www/html/storage - entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"] environment: SOKETI_DEBUG: "false" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}" vite: - image: node:alpine + image: node:20 pull_policy: always working_dir: /var/www/html # environment: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index ea882dd2d..536a7183b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -109,6 +109,7 @@ services: retries: 10 timeout: 2s soketi: + image: 'ghcr.io/coollabsio/coolify-realtime:latest' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml index 0db35eb96..2246e1867 100644 --- a/docker-compose.windows.yml +++ b/docker-compose.windows.yml @@ -102,8 +102,8 @@ services: interval: 5s retries: 10 timeout: 2s -soketi: - image: 'quay.io/soketi/soketi:1.6-16-alpine' + soketi: + image: 'ghcr.io/coollabsio/coolify-realtime:latest' pull_policy: always container_name: coolify-realtime restart: always diff --git a/docker-compose.yml b/docker-compose.yml index 930c0a6b9..7d71ba8e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,7 +24,6 @@ services: networks: - coolify soketi: - image: 'quay.io/soketi/soketi:1.6-16-alpine' container_name: coolify-realtime restart: always networks: diff --git a/docker/coolify-realtime/Dockerfile b/docker/coolify-realtime/Dockerfile new file mode 100644 index 000000000..9a7a68376 --- /dev/null +++ b/docker/coolify-realtime/Dockerfile @@ -0,0 +1,9 @@ +FROM quay.io/soketi/soketi:1.6-16-alpine +WORKDIR /terminal +RUN apk add --no-cache openssh-client make g++ python3 +COPY docker/coolify-realtime/package.json ./ +RUN npm i +RUN npm rebuild node-pty --update-binary +COPY docker/coolify-realtime/soketi-entrypoint.sh /soketi-entrypoint.sh +COPY docker/coolify-realtime/terminal-server.js /terminal/terminal-server.js +ENTRYPOINT ["/bin/sh", "/soketi-entrypoint.sh"] diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json new file mode 100644 index 000000000..90d4f77db --- /dev/null +++ b/docker/coolify-realtime/package.json @@ -0,0 +1,13 @@ +{ + "private": true, + "type": "module", + "dependencies": { + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", + "cookie": "^0.6.0", + "axios": "1.7.5", + "dotenv": "^16.4.5", + "node-pty": "^1.0.0", + "ws": "^8.17.0" + } +} diff --git a/docker/soketi-entrypoint/soketi-entrypoint.sh b/docker/coolify-realtime/soketi-entrypoint.sh similarity index 79% rename from docker/soketi-entrypoint/soketi-entrypoint.sh rename to docker/coolify-realtime/soketi-entrypoint.sh index 808e306e7..04e43ac81 100644 --- a/docker/soketi-entrypoint/soketi-entrypoint.sh +++ b/docker/coolify-realtime/soketi-entrypoint.sh @@ -1,16 +1,4 @@ #!/bin/sh - -# Install openssh-client -apk add --no-cache openssh-client make g++ python3 - -cd /terminal - -# Install npm dependencies -npm ci - -# Rebuild node-pty -npm rebuild node-pty --update-binary - # Function to timestamp logs timestamp() { date "+%Y-%m-%d %H:%M:%S" @@ -36,4 +24,4 @@ trap 'forward_signal TERM' TERM wait -n # Exit with status of process that exited first -exit $? \ No newline at end of file +exit $? diff --git a/terminal-server.js b/docker/coolify-realtime/terminal-server.js similarity index 81% rename from terminal-server.js rename to docker/coolify-realtime/terminal-server.js index a1555bf30..6afbd3445 100755 --- a/terminal-server.js +++ b/docker/coolify-realtime/terminal-server.js @@ -16,40 +16,40 @@ const server = http.createServer((req, res) => { }); const verifyClient = async (info, callback) => { - const cookies = cookie.parse(info.req.headers.cookie || ''); - const origin = new URL(info.origin); - const protocol = origin.protocol; - const xsrfToken = cookies['XSRF-TOKEN']; + const cookies = cookie.parse(info.req.headers.cookie || ''); + const origin = new URL(info.origin); + const protocol = origin.protocol; + const xsrfToken = cookies['XSRF-TOKEN']; - // Generate session cookie name based on APP_NAME - const appName = process.env.APP_NAME || 'laravel'; - const sessionCookieName = `${appName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}_session`; - const laravelSession = cookies[sessionCookieName]; + // Generate session cookie name based on APP_NAME + const appName = process.env.APP_NAME || 'laravel'; + const sessionCookieName = `${appName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}_session`; + const laravelSession = cookies[sessionCookieName]; - // Verify presence of required tokens - if (!laravelSession || !xsrfToken) { - return callback(false, 401, 'Unauthorized: Missing required tokens'); - } - - try { - // Authenticate with Laravel backend - const response = await axios.post(`${protocol}//coolify/terminal/auth`, null, { - headers: { - 'Cookie': `${sessionCookieName}=${laravelSession}`, - 'X-XSRF-TOKEN': xsrfToken - }, - }); - - if (response.status === 200) { - // Authentication successful - callback(true); - } else { - callback(false, 401, 'Unauthorized: Invalid credentials'); + // Verify presence of required tokens + if (!laravelSession || !xsrfToken) { + return callback(false, 401, 'Unauthorized: Missing required tokens'); + } + + try { + // Authenticate with Laravel backend + const response = await axios.post(`${protocol}//coolify/terminal/auth`, null, { + headers: { + 'Cookie': `${sessionCookieName}=${laravelSession}`, + 'X-XSRF-TOKEN': xsrfToken + }, + }); + + if (response.status === 200) { + // Authentication successful + callback(true); + } else { + callback(false, 401, 'Unauthorized: Invalid credentials'); + } + } catch (error) { + console.error('Authentication error:', error.message); + callback(false, 500, 'Internal Server Error'); } - } catch (error) { - console.error('Authentication error:', error.message); - callback(false, 500, 'Internal Server Error'); - } }; diff --git a/resources/views/livewire/project/shared/terminal.blade.php b/resources/views/livewire/project/shared/terminal.blade.php index 4e1a086e2..f351bd42f 100644 --- a/resources/views/livewire/project/shared/terminal.blade.php +++ b/resources/views/livewire/project/shared/terminal.blade.php @@ -1,11 +1,12 @@
-
- (connection closed) +
+ (connection closed)
+ :class="fullscreen ? 'fullscreen' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'">
@script - + window.onresize = function() { + $data.resizeTerminal() + }; + + Alpine.data('data', () => ({ + fullscreen: false, + terminalActive: false, + init() { + this.$watch('terminalActive', (value) => { + this.$nextTick(() => { + if (value) { + $refs.terminalWrapper.style.display = 'block'; + this.resizeTerminal(); + } else { + $refs.terminalWrapper.style.display = 'none'; + } + }); + }); + }, + makeFullscreen() { + this.fullscreen = !this.fullscreen; + $nextTick(() => { + this.resizeTerminal() + }) + }, + + resizeTerminal() { + if (!this.terminalActive) return; + + fitAddon.fit(); + const height = $refs.terminalWrapper.clientHeight; + const rows = height / term._core._renderService._charSizeService.height - 1; + var termWidth = term.cols; + var termHeight = parseInt(rows.toString(), 10); + term.resize(termWidth, termHeight); + socket.send(JSON.stringify({ + resize: { + cols: termWidth, + rows: termHeight + } + })); + } + })); + + initializeWebSocket(); + @endscript -
\ No newline at end of file +
From 483b4f8eb7e1fcfcd5d2cdfe6d8a2d8748e34742 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:31:35 +0200 Subject: [PATCH 05/91] Update CONTRIBUTING.md --- CONTRIBUTING.md | 130 ++++++++++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 53 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9618bfae5..e12d8662f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,29 @@ -# Contributing +# Contributing to Coolify > "First, thanks for considering contributing to my project. It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai) You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel. +## Table of Contents -## Code Contribution +1. [Setup Development Environment](#1-setup-development-environment) +2. [Verify Installation](#2-verify-installation-optional) +3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository) +4. [Set up Environment Variables](#4-set-up-environment-variables) +5. [Start Coolify](#5-start-coolify) +6. [Start Development](#6-start-development) +7. [Development Notes](#7-development-notes) +8. [Create a Pull Request](#8-create-a-pull-request) +9. [Additional Contribution Guidelines](#additional-contribution-guidelines) -## 1. Setup your development environment +--- + +## 1. Setup Development Environment Follow the steps below for your operating system: -### Windows +
+Windows 1. Install `docker-ce`, Docker Desktop (or similar): - Docker CE (recommended): @@ -25,7 +37,10 @@ Follow the steps below for your operating system: 2. Install Spin: - Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2) -### MacOS +
+ +
+MacOS 1. Install Orbstack, Docker Desktop (or similar): - Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop): @@ -36,7 +51,10 @@ Follow the steps below for your operating system: 2. Install Spin: - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin) -### Linux +
+ +
+Linux 1. Install Docker Engine, Docker Desktop (or similar): - Docker Engine (recommended, as there is no VM overhead): @@ -47,8 +65,9 @@ Follow the steps below for your operating system: 2. Install Spin: - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions) +
-## 2. Verify installation (optional) +## 2. Verify Installation (Optional) After installing Docker (or Orbstack) and Spin, verify the installation: @@ -60,25 +79,20 @@ After installing Docker (or Orbstack) and Spin, verify the installation: ``` You should see version information for both Docker and Spin. - -## 3. Fork the Coolify repository and setup your local repository +## 3. Fork and Setup Local Repository 1. Fork the [Coolify](https://github.com/coollabsio/coolify) repository to your GitHub account. -2. Install a code editor on your machine (below are some popular choices, choose one): +2. Install a code editor on your machine (choose one): - - Visual Studio Code (recommended free): - - Windows/macOS/Linux: Download and install from [https://code.visualstudio.com/download](https://code.visualstudio.com/download) - - - Cursor (recommended but paid for getting the full benefits): - - Windows/macOS/Linux: Download and install from [https://www.cursor.com/](https://www.cursor.com/) - - - Zed (very fast code editor): - - macOS/Linux: Download and install from [https://zed.dev/download](https://zed.dev/download) - - Windows: Not available yet + | Editor | Platform | Download Link | + |--------|----------|---------------| + | Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download) | + | Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/) | + | Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download) | 3. Clone the Coolify Repository from your fork to your local machine - - Use `git clone` in the command line + - Use `git clone` in the command line, or - Use GitHub Desktop (recommended): - Download and install from [https://desktop.github.com/](https://desktop.github.com/) - Open GitHub Desktop and login with your GitHub account @@ -86,37 +100,32 @@ After installing Docker (or Orbstack) and Spin, verify the installation: 4. Open the cloned Coolify Repository in your chosen code editor. - ## 4. Set up Environment Variables 1. In the Code Editor, locate the `.env.development.example` file in the root directory of your local Coolify repository. - 2. Duplicate the `.env.development.example` file and rename the copy to `.env`. - 3. Open the new `.env` file and review its contents. Adjust any environment variables as needed for your development setup. - 4. If you encounter errors during database migrations, update the database connection settings in your `.env` file. Use the IP address or hostname of your PostgreSQL database container. You can find this information by running `docker ps` after executing `spin up`. - 5. Save the changes to your `.env` file. - ## 5. Start Coolify 1. Open a terminal in the local Coolify directory. - 2. Run the following command in the terminal (leave that terminal open): - ``` + ```bash spin up ``` - Note: You may see some errors, but don't worry; this is expected. + +> [!NOTE] +> You may see some errors, but don't worry; this is expected. 3. If you encounter permission errors, especially on macOS, use: - ``` + ```bash sudo spin up ``` -Note: If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again. - +> [!NOTE] +> If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again. ## 6. Start Development @@ -126,15 +135,17 @@ Note: If you change environment variables afterwards or anything seems broken, p - Password: `password` 2. Additional development tools: - - Laravel Horizon (scheduler): `http://localhost:8000/horizon` - Note: Only accessible when logged in as root user - - Mailpit (email catcher): `http://localhost:8025` - - Telescope (debugging tool): `http://localhost:8000/telescope` - Note: Disabled by default (so the database is not overloaded), enable by adding the following environment variable to your `.env` file: - ```env - TELESCOPE_ENABLED=true - ``` + | Tool | URL | Note | + |------|-----|------| + | Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user | + | Mailpit (email catcher) | `http://localhost:8025` | | + | Telescope (debugging tool) | `http://localhost:8000/telescope` | Disabled by default | +> [!NOTE] +> To enable Telescope, add the following to your `.env` file: +> ```env +> TELESCOPE_ENABLED=true +> ``` ## 7. Development Notes @@ -150,18 +161,12 @@ When working on Coolify, keep the following in mind: docker exec -it coolify php artisan migrate:fresh --seed ``` -3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any envrionement specific issues. +3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any environment-specific issues. -Remember, forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches. +> [!IMPORTANT] +> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches. - -## 8. Contributing a New Service - -To add a new service to Coolify, please refer to our documentation: -[Adding a New Service](https://coolify.io/docs/knowledge-base/add-a-service) - - -## 9. Create a Pull Request +## 8. Create a Pull Request 1. After making changes or adding a new service: - Commit your changes to your forked repository. @@ -179,11 +184,30 @@ To add a new service to Coolify, please refer to our documentation: - In the description, explain the changes you've made. - Reference any related issues by using keywords like "Fixes #123" or "Closes #456". -4. Important note: - Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch. +> [!IMPORTANT] +> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch. -5. Submit your PR: +4. Submit your PR: - Review your changes one last time. - Click "Create pull request" to submit. +> [!NOTE] +> Make sure your PR is out of draft mode as soon as it's ready for review. PRs that are in draft mode for a long time may be closed by maintainers. + After submission, maintainers will review your PR and may request changes or provide feedback. + +## Additional Contribution Guidelines + +### Contributing a New Service + +To add a new service to Coolify, please refer to our documentation: +[Adding a New Service](https://coolify.io/docs/knowledge-base/contribute/service) + +### Contributing to Documentation + +To contribute to the Coolify documentation, please refer to this guide: +[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/CONTRIBUTING.md) + +--- + +Thank you for contributing to Coolify! Your efforts help make this project better for everyone. From 0881d001d655a3b4d8fd3d9bc9d04530f75e393a Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:34:37 +0200 Subject: [PATCH 06/91] Update CONTRIBUTING.md --- CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e12d8662f..86903e9f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -208,6 +208,5 @@ To add a new service to Coolify, please refer to our documentation: To contribute to the Coolify documentation, please refer to this guide: [Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/CONTRIBUTING.md) ---- -Thank you for contributing to Coolify! Your efforts help make this project better for everyone. +**Thank you for contributing to Coolify! Your efforts help make this project better for everyone.** From 410b55cf039acb16abd4860309a997ba8273eb20 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:39:24 +0200 Subject: [PATCH 07/91] Update CONTRIBUTING.md --- CONTRIBUTING.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86903e9f5..590360ddb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,8 +16,6 @@ You can ask for guidance anytime on our [Discord server](https://coollabs.io/dis 8. [Create a Pull Request](#8-create-a-pull-request) 9. [Additional Contribution Guidelines](#additional-contribution-guidelines) ---- - ## 1. Setup Development Environment Follow the steps below for your operating system: @@ -207,6 +205,3 @@ To add a new service to Coolify, please refer to our documentation: To contribute to the Coolify documentation, please refer to this guide: [Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/CONTRIBUTING.md) - - -**Thank you for contributing to Coolify! Your efforts help make this project better for everyone.** From 35dfb1b0f82377418f95c19b9187bad85933e931 Mon Sep 17 00:00:00 2001 From: Luan Estradioto Date: Thu, 12 Sep 2024 01:58:56 -0300 Subject: [PATCH 08/91] fix: grouped process and docker execs are killed with ssh process fix: run clear command only if exists fix: link terminal js on dev compose better dx fix: add error on terminal ux --- app/Livewire/Project/Shared/Terminal.php | 3 ++ docker-compose.dev.yml | 1 + docker/coolify-realtime/terminal-server.js | 40 +++++++++++++++---- .../project/shared/terminal.blade.php | 14 ++++--- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php index e070ee367..70b8fd18f 100644 --- a/app/Livewire/Project/Shared/Terminal.php +++ b/app/Livewire/Project/Shared/Terminal.php @@ -38,6 +38,9 @@ class Terminal extends Component // 1. Laravel Pusher/Echo connection (not possible without a sdk) // 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies) // 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used + // 4. Follow-up discussions here: + // - https://github.com/coollabsio/coolify/issues/2298 + // - https://github.com/coollabsio/coolify/discussions/3362 $this->dispatch('send-back-command', $command); } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 16bb354a5..dfd8684cc 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -53,6 +53,7 @@ services: - "6002:6002" volumes: - ./storage:/var/www/html/storage + - ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js environment: SOKETI_DEBUG: "false" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" diff --git a/docker/coolify-realtime/terminal-server.js b/docker/coolify-realtime/terminal-server.js index 6afbd3445..c03f9b0df 100755 --- a/docker/coolify-realtime/terminal-server.js +++ b/docker/coolify-realtime/terminal-server.js @@ -106,7 +106,12 @@ async function handleCommand(ws, command, userId) { const userSession = userSessions.get(userId); if (userSession && userSession.isActive) { - await killPtyProcess(userId); + const result = await killPtyProcess(userId); + if (!result) { + // if terminal is still active, even after we tried to kill it, dont continue and show error + ws.send('unprocessable'); + return; + } } const commandString = command[0].split('\n').join(' '); @@ -129,7 +134,8 @@ async function handleCommand(ws, command, userId) { userSession.ptyProcess = ptyProcess; userSession.isActive = true; ptyProcess.write(hereDocContent + '\n'); - ptyProcess.write('clear\n'); + // clear the terminal if the user has clear command + ptyProcess.write('command -v clear >/dev/null 2>&1 && clear\n'); ws.send('pty-ready'); @@ -162,12 +168,32 @@ async function killPtyProcess(userId) { if (!session?.ptyProcess) return false; return new Promise((resolve) => { - session.ptyProcess.on('exit', () => { - session.isActive = false; - resolve(true); - }); + // Loop to ensure terminal is killed before continuing + let killAttempts = 0; + const maxAttempts = 5; - session.ptyProcess.kill(); + const attemptKill = () => { + killAttempts++; + + // session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098 + // patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947 + session.ptyProcess.write('kill -TERM -$$ && exit\n'); + + setTimeout(() => { + if (!session.isActive || !session.ptyProcess) { + resolve(true); + return; + } + + if (killAttempts < maxAttempts) { + attemptKill(); + } else { + resolve(false); + } + }, 500); + }; + + attemptKill(); }); } diff --git a/resources/views/livewire/project/shared/terminal.blade.php b/resources/views/livewire/project/shared/terminal.blade.php index f351bd42f..11c59810e 100644 --- a/resources/views/livewire/project/shared/terminal.blade.php +++ b/resources/views/livewire/project/shared/terminal.blade.php @@ -1,12 +1,11 @@
-
- (connection closed) +
+
+ :class="fullscreen ? 'fullscreen' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'">
- If you have any problem, please contact us.
@endif From dd8a2dd3c144c76d6bcfa4380b98fe0ce57ebcd8 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 13 Sep 2024 08:23:05 +0200 Subject: [PATCH 13/91] chore: Update coolify environment variable assignment with double quotes --- app/Jobs/ApplicationDeploymentJob.php | 8 ++++---- bootstrap/helpers/shared.php | 28 +++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 718cea639..c6dd2e5f4 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -919,10 +919,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $envs->push("COOLIFY_BRANCH={$local_branch}"); + $envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); } if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + $envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\""); } } @@ -978,10 +978,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $envs->push("COOLIFY_BRANCH={$local_branch}"); + $envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); } if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + $envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\""); } } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 028d20f33..591517457 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2100,16 +2100,16 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // TODO: move this in a shared function if (! $parsedServiceVariables->has('COOLIFY_APP_NAME')) { - $parsedServiceVariables->put('COOLIFY_APP_NAME', $resource->name); + $parsedServiceVariables->put('COOLIFY_APP_NAME', "\"{$resource->name}\""); } if (! $parsedServiceVariables->has('COOLIFY_SERVER_IP')) { - $parsedServiceVariables->put('COOLIFY_SERVER_IP', $resource->destination->server->ip); + $parsedServiceVariables->put('COOLIFY_SERVER_IP', "\"{$resource->destination->server->ip}\""); } if (! $parsedServiceVariables->has('COOLIFY_ENVIRONMENT_NAME')) { - $parsedServiceVariables->put('COOLIFY_ENVIRONMENT_NAME', $resource->environment->name); + $parsedServiceVariables->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\""); } if (! $parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) { - $parsedServiceVariables->put('COOLIFY_PROJECT_NAME', $resource->project()->name); + $parsedServiceVariables->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\""); } $parsedServiceVariables = $parsedServiceVariables->map(function ($value, $key) use ($envs_from_coolify) { @@ -3469,13 +3469,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $branch = "pull/{$pullRequestId}/head"; } if ($originalResource->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $coolifyEnvironments->put('COOLIFY_BRANCH', $branch); + $coolifyEnvironments->put('COOLIFY_BRANCH', "\"{$branch}\""); } } // Add COOLIFY_CONTAINER_NAME to environment if ($resource->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', $containerName); + $coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', "\"{$containerName}\""); } if ($isApplication) { @@ -3723,30 +3723,30 @@ function add_coolify_default_environment_variables(StandaloneRedis|StandalonePos } if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_APP_NAME')->isEmpty()) { if ($isAssociativeArray) { - $where_to_add->put('COOLIFY_APP_NAME', $resource->name); + $where_to_add->put('COOLIFY_APP_NAME', "\"{$resource->name}\""); } else { - $where_to_add->push("COOLIFY_APP_NAME={$resource->name}"); + $where_to_add->push("COOLIFY_APP_NAME=\"{$resource->name}\""); } } if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_SERVER_IP')->isEmpty()) { if ($isAssociativeArray) { - $where_to_add->put('COOLIFY_SERVER_IP', $ip); + $where_to_add->put('COOLIFY_SERVER_IP', "\"{$ip}\""); } else { - $where_to_add->push("COOLIFY_SERVER_IP={$ip}"); + $where_to_add->push("COOLIFY_SERVER_IP=\"{$ip}\""); } } if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_ENVIRONMENT_NAME')->isEmpty()) { if ($isAssociativeArray) { - $where_to_add->put('COOLIFY_ENVIRONMENT_NAME', $resource->environment->name); + $where_to_add->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\""); } else { - $where_to_add->push("COOLIFY_ENVIRONMENT_NAME={$resource->environment->name}"); + $where_to_add->push("COOLIFY_ENVIRONMENT_NAME=\"{$resource->environment->name}\""); } } if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_PROJECT_NAME')->isEmpty()) { if ($isAssociativeArray) { - $where_to_add->put('COOLIFY_PROJECT_NAME', $resource->project()->name); + $where_to_add->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\""); } else { - $where_to_add->push("COOLIFY_PROJECT_NAME={$resource->project()->name}"); + $where_to_add->push("COOLIFY_PROJECT_NAME=\"{$resource->project()->name}\""); } } } From aa4980289da029a7d6b1948a06ec7f14bf9f03b5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 13 Sep 2024 10:51:51 +0200 Subject: [PATCH 14/91] feat: make coolify full width by default --- resources/views/components/navbar.blade.php | 8 +++++--- resources/views/layouts/app.blade.php | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index 7da619377..a7a3c2109 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -1,7 +1,7 @@
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 2481d9bd2..e0f044353 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -9,6 +9,10 @@ open: false, init() { this.pageWidth = localStorage.getItem('pageWidth'); + if (!this.pageWidth) { + this.pageWidth = 'full'; + localStorage.setItem('pageWidth', 'full'); + } } }" x-cloak class="mx-auto" :class="pageWidth === 'full' ? '' : 'max-w-7xl'">