update files
This commit is contained in:
@@ -2,17 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Livewire\Project\Shared;
|
namespace App\Livewire\Project\Shared;
|
||||||
|
|
||||||
use App\Actions\Server\RunCommand;
|
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class ExecuteContainerCommand extends Component
|
class ExecuteContainerCommand extends Component
|
||||||
{
|
{
|
||||||
public string $command;
|
|
||||||
|
|
||||||
public string $container;
|
public string $container;
|
||||||
|
|
||||||
public Collection $containers;
|
public Collection $containers;
|
||||||
@@ -23,8 +21,6 @@ class ExecuteContainerCommand extends Component
|
|||||||
|
|
||||||
public string $type;
|
public string $type;
|
||||||
|
|
||||||
public string $workDir = '';
|
|
||||||
|
|
||||||
public Server $server;
|
public Server $server;
|
||||||
|
|
||||||
public Collection $servers;
|
public Collection $servers;
|
||||||
@@ -33,7 +29,6 @@ class ExecuteContainerCommand extends Component
|
|||||||
'server' => 'required',
|
'server' => 'required',
|
||||||
'container' => 'required',
|
'container' => 'required',
|
||||||
'command' => 'required',
|
'command' => 'required',
|
||||||
'workDir' => 'nullable',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
@@ -115,7 +110,8 @@ class ExecuteContainerCommand extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function runCommand()
|
#[On('connectToContainer')]
|
||||||
|
public function connectToContainer()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (data_get($this->parameters, 'application_uuid')) {
|
if (data_get($this->parameters, 'application_uuid')) {
|
||||||
@@ -132,14 +128,13 @@ class ExecuteContainerCommand extends Component
|
|||||||
if ($server->isForceDisabled()) {
|
if ($server->isForceDisabled()) {
|
||||||
throw new \RuntimeException('Server is disabled.');
|
throw new \RuntimeException('Server is disabled.');
|
||||||
}
|
}
|
||||||
$cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'";
|
|
||||||
if (! empty($this->workDir)) {
|
$this->dispatch('send-terminal-command',
|
||||||
$exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}";
|
true,
|
||||||
} else {
|
$container_name,
|
||||||
$exec = "docker exec {$container_name} {$cmd}";
|
$server->uuid,
|
||||||
}
|
);
|
||||||
$activity = RunCommand::run(server: $server, command: $exec);
|
|
||||||
$this->dispatch('activityMonitor', $activity->id);
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
51
app/Livewire/Project/Shared/Terminal.php
Normal file
51
app/Livewire/Project/Shared/Terminal.php
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
@@ -2,42 +2,96 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Actions\Server\RunCommand as ServerRunCommand;
|
use Livewire\Attributes\On;
|
||||||
use App\Models\Server;
|
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class RunCommand extends Component
|
class RunCommand extends Component
|
||||||
{
|
{
|
||||||
public string $command;
|
public $selected_uuid;
|
||||||
|
|
||||||
public $server;
|
|
||||||
|
|
||||||
public $servers = [];
|
public $servers = [];
|
||||||
|
|
||||||
protected $rules = [
|
public $containers = [];
|
||||||
'server' => 'required',
|
|
||||||
'command' => 'required',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $validationAttributes = [
|
|
||||||
'server' => 'server',
|
|
||||||
'command' => 'command',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function mount($servers)
|
public function mount($servers)
|
||||||
{
|
{
|
||||||
$this->servers = $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();
|
return collect($this->servers)->flatMap(function ($server) {
|
||||||
try {
|
if (! $server->isFunctional()) {
|
||||||
$activity = ServerRunCommand::run(server: Server::where('uuid', $this->server)->first(), command: $this->command);
|
return [];
|
||||||
$this->dispatch('activityMonitor', $activity->id);
|
}
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return handleError($e, $this);
|
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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -305,6 +305,13 @@ respond 404
|
|||||||
'service' => 'coolify-realtime',
|
'service' => 'coolify-realtime',
|
||||||
'rule' => "Host(`{$host}`) && PathPrefix(`/app`)",
|
'rule' => "Host(`{$host}`) && PathPrefix(`/app`)",
|
||||||
],
|
],
|
||||||
|
'coolify-terminal-ws' => [
|
||||||
|
'entryPoints' => [
|
||||||
|
0 => 'http',
|
||||||
|
],
|
||||||
|
'service' => 'coolify-terminal',
|
||||||
|
'rule' => "Host(`{$host}`) && PathPrefix(`/terminal`)",
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'services' => [
|
'services' => [
|
||||||
'coolify' => [
|
'coolify' => [
|
||||||
@@ -325,6 +332,15 @@ respond 404
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'coolify-terminal' => [
|
||||||
|
'loadBalancer' => [
|
||||||
|
'servers' => [
|
||||||
|
0 => [
|
||||||
|
'url' => 'http://coolify-realtime:6002',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
@@ -354,6 +370,16 @@ respond 404
|
|||||||
'certresolver' => 'letsencrypt',
|
'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 = Yaml::dump($traefik_dynamic_conf, 12, 2);
|
||||||
$yaml =
|
$yaml =
|
||||||
@@ -387,6 +413,9 @@ $schema://$host {
|
|||||||
handle /app/* {
|
handle /app/* {
|
||||||
reverse_proxy coolify-realtime:6001
|
reverse_proxy coolify-realtime:6001
|
||||||
}
|
}
|
||||||
|
handle /terminal/* {
|
||||||
|
reverse_proxy coolify-realtime:6002
|
||||||
|
}
|
||||||
reverse_proxy coolify:80
|
reverse_proxy coolify:80
|
||||||
}";
|
}";
|
||||||
$base64 = base64_encode($caddy_file);
|
$base64 = base64_encode($caddy_file);
|
||||||
|
@@ -40,6 +40,20 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul
|
|||||||
return $containers;
|
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
|
function format_docker_command_output_to_json($rawOutput): Collection
|
||||||
{
|
{
|
||||||
$outputLines = explode(PHP_EOL, $rawOutput);
|
$outputLines = explode(PHP_EOL, $rawOutput);
|
||||||
|
@@ -44,10 +44,17 @@ services:
|
|||||||
- /data/coolify/_volumes/redis/:/data
|
- /data/coolify/_volumes/redis/:/data
|
||||||
# - coolify-redis-data-dev:/data
|
# - coolify-redis-data-dev:/data
|
||||||
soketi:
|
soketi:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./docker/coolify-realtime/Dockerfile
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- "${FORWARD_SOKETI_PORT:-6001}:6001"
|
- "${FORWARD_SOKETI_PORT:-6001}:6001"
|
||||||
|
- "6002:6002"
|
||||||
|
volumes:
|
||||||
|
- ./storage:/var/www/html/storage
|
||||||
|
- ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js
|
||||||
environment:
|
environment:
|
||||||
SOKETI_DEBUG: "false"
|
SOKETI_DEBUG: "false"
|
||||||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
|
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
|
||||||
|
@@ -110,18 +110,28 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
soketi:
|
soketi:
|
||||||
|
image: 'ghcr.io/coollabsio/coolify-realtime:latest'
|
||||||
ports:
|
ports:
|
||||||
- "${SOKETI_PORT:-6001}:6001"
|
- "${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:
|
environment:
|
||||||
SOKETI_DEBUG: "${SOKETI_DEBUG:-false}"
|
SOKETI_DEBUG: "${SOKETI_DEBUG:-false}"
|
||||||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
||||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||||
healthcheck:
|
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
|
interval: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
coolify-db:
|
coolify-db:
|
||||||
name: coolify-db
|
name: coolify-db
|
||||||
|
@@ -103,7 +103,7 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
soketi:
|
soketi:
|
||||||
image: 'quay.io/soketi/soketi:1.6-16-alpine'
|
image: 'ghcr.io/coollabsio/coolify-realtime:latest'
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
container_name: coolify-realtime
|
container_name: coolify-realtime
|
||||||
restart: always
|
restart: always
|
||||||
@@ -111,16 +111,25 @@ services:
|
|||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- "${SOKETI_PORT:-6001}:6001"
|
- "${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:
|
environment:
|
||||||
SOKETI_DEBUG: "${SOKETI_DEBUG:-false}"
|
SOKETI_DEBUG: "${SOKETI_DEBUG:-false}"
|
||||||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
||||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: wget -qO- http://localhost: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
|
interval: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
coolify-db:
|
coolify-db:
|
||||||
name: coolify-db
|
name: coolify-db
|
||||||
|
@@ -24,7 +24,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- coolify
|
- coolify
|
||||||
soketi:
|
soketi:
|
||||||
image: 'quay.io/soketi/soketi:1.6-16-alpine'
|
|
||||||
container_name: coolify-realtime
|
container_name: coolify-realtime
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
|
Reference in New Issue
Block a user