Skip to content

Commit

Permalink
Merge pull request #2586 from LEstradioto/feat--terminal-pty
Browse files Browse the repository at this point in the history
[FEAT] fully functioning terminal
  • Loading branch information
andrasbacsai committed Sep 13, 2024
2 parents 1738286 + b6080c2 commit 121afaa
Show file tree
Hide file tree
Showing 20 changed files with 808 additions and 60 deletions.
25 changes: 10 additions & 15 deletions app/Livewire/Project/Shared/ExecuteContainerCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,8 +21,6 @@ class ExecuteContainerCommand extends Component

public string $type;

public string $workDir = '';

public Server $server;

public Collection $servers;
Expand All @@ -33,7 +29,6 @@ class ExecuteContainerCommand extends Component
'server' => 'required',
'container' => 'required',
'command' => 'required',
'workDir' => 'nullable',
];

public function mount()
Expand Down Expand Up @@ -115,7 +110,8 @@ public function loadContainers()
}
}

public function runCommand()
#[On('connectToContainer')]
public function connectToContainer()
{
try {
if (data_get($this->parameters, 'application_uuid')) {
Expand All @@ -132,14 +128,13 @@ public function runCommand()
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);
}
Expand Down
51 changes: 51 additions & 0 deletions app/Livewire/Project/Shared/Terminal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace App\Livewire\Project\Shared;

use App\Models\Server;
use Livewire\Attributes\On;
use Livewire\Component;

class Terminal extends Component
{
#[On('send-terminal-command')]
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
{
$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 ($isContainer) {
ray($identifier);
$status = getContainerStatus($server, $identifier);
ray($status);
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
// 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);
}

public function render()
{
return view('livewire.project.shared.terminal');
}
}
100 changes: 77 additions & 23 deletions app/Livewire/RunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,96 @@

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 collect($this->servers)->flatMap(function ($server) {
if (! $server->isFunctional()) {
return [];
}

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) {
if (isDev()) {
if (data_get($resource, 'name') === 'coolify-db') {
$container_name = 'coolify-db';

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' => $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
);
}
}
29 changes: 29 additions & 0 deletions app/Models/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,13 @@ public function setupDynamicProxyConfiguration()
'service' => 'coolify-realtime',
'rule' => "Host(`{$host}`) && PathPrefix(`/app`)",
],
'coolify-terminal-ws' => [
'entryPoints' => [
0 => 'http',
],
'service' => 'coolify-terminal',
'rule' => "Host(`{$host}`) && PathPrefix(`/terminal`)",
],
],
'services' => [
'coolify' => [
Expand All @@ -325,6 +332,15 @@ public function setupDynamicProxyConfiguration()
],
],
],
'coolify-terminal' => [
'loadBalancer' => [
'servers' => [
0 => [
'url' => 'http://coolify-realtime:6002',
],
],
],
],
],
],
];
Expand Down Expand Up @@ -354,6 +370,16 @@ public function setupDynamicProxyConfiguration()
'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 =
Expand Down Expand Up @@ -387,6 +413,9 @@ public function setupDynamicProxyConfiguration()
handle /app/* {
reverse_proxy coolify-realtime:6001
}
handle /terminal/* {
reverse_proxy coolify-realtime:6002
}
reverse_proxy coolify:80
}";
$base64 = base64_encode($caddy_file);
Expand Down
14 changes: 14 additions & 0 deletions bootstrap/helpers/docker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,17 @@ 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:
- ./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}"
Expand Down
12 changes: 11 additions & 1 deletion docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,18 +110,28 @@ services:
retries: 10
timeout: 2s
soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:latest'
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
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
Expand Down
Loading

0 comments on commit 121afaa

Please sign in to comment.