Skip to content

Commit

Permalink
feat: fully functional terminal for command center
Browse files Browse the repository at this point in the history
  • Loading branch information
LEstradioto committed Aug 15, 2024
1 parent fcfbba4 commit c2ea899
Show file tree
Hide file tree
Showing 15 changed files with 624 additions and 57 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
46 changes: 46 additions & 0 deletions app/Livewire/Project/Shared/Terminal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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::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) {
$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');
}
}
69 changes: 47 additions & 22 deletions app/Livewire/RunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
}
29 changes: 29 additions & 0 deletions app/Models/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,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 @@ -315,6 +322,15 @@ public function setupDynamicProxyConfiguration()
],
],
],
'coolify-terminal' => [
'loadBalancer' => [
'servers' => [
0 => [
'url' => 'http://coolify-terminal:6002',
],
],
],
],
],
],
];
Expand Down Expand Up @@ -344,6 +360,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 @@ -377,6 +403,9 @@ public function setupDynamicProxyConfiguration()
handle /app/* {
reverse_proxy coolify-realtime:6001
}
handle /terminal/* {
reverse_proxy coolify-terminal:6002
}
reverse_proxy coolify:80
}";
$base64 = base64_encode($caddy_file);
Expand Down
14 changes: 12 additions & 2 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions docker-compose.windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit c2ea899

Please sign in to comment.