341 lines
12 KiB
PHP
341 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Project\Shared;
|
|
|
|
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 $selected_container = 'default';
|
|
|
|
public $container;
|
|
|
|
public Collection $containers;
|
|
|
|
public $parameters;
|
|
|
|
public $resource;
|
|
|
|
public string $type;
|
|
|
|
public Server $server;
|
|
|
|
public Collection $servers;
|
|
|
|
public bool $hasShell = true;
|
|
|
|
public bool $containersLoaded = false;
|
|
|
|
public bool $autoConnectAttempted = false;
|
|
|
|
public bool $isConnecting = false;
|
|
|
|
public bool $isConnected = false;
|
|
|
|
public string $connectionStatus = 'Loading containers...';
|
|
|
|
protected $rules = [
|
|
'server' => 'required',
|
|
'container' => 'required',
|
|
'command' => 'required',
|
|
];
|
|
|
|
public function getListeners()
|
|
{
|
|
$teamId = auth()->user()->currentTeam()->id;
|
|
|
|
return [
|
|
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
|
|
];
|
|
}
|
|
|
|
public function mount()
|
|
{
|
|
if (! auth()->user()->isAdmin()) {
|
|
abort(403);
|
|
}
|
|
|
|
$this->parameters = get_route_parameters();
|
|
$this->containers = collect();
|
|
$this->servers = collect();
|
|
if (data_get($this->parameters, 'application_uuid')) {
|
|
$this->type = 'application';
|
|
$this->resource = Application::whereUuid($this->parameters['application_uuid'])->firstOrFail();
|
|
if ($this->resource->destination->server->isFunctional() && $this->resource->destination->server->isTerminalEnabled()) {
|
|
$this->servers = $this->servers->push($this->resource->destination->server);
|
|
}
|
|
foreach ($this->resource->additional_servers as $server) {
|
|
if ($server->isFunctional() && $server->isTerminalEnabled()) {
|
|
$this->servers = $this->servers->push($server);
|
|
}
|
|
}
|
|
$this->loadContainers();
|
|
} elseif (data_get($this->parameters, 'database_uuid')) {
|
|
$this->type = 'database';
|
|
$resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
|
|
if (is_null($resource)) {
|
|
abort(404);
|
|
}
|
|
$this->resource = $resource;
|
|
if ($this->resource->destination->server->isFunctional() && $this->resource->destination->server->isTerminalEnabled()) {
|
|
$this->servers = $this->servers->push($this->resource->destination->server);
|
|
}
|
|
$this->loadContainers();
|
|
} elseif (data_get($this->parameters, 'service_uuid')) {
|
|
$this->type = 'service';
|
|
$this->resource = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
|
|
if ($this->resource->server->isFunctional() && $this->resource->server->isTerminalEnabled()) {
|
|
$this->servers = $this->servers->push($this->resource->server);
|
|
}
|
|
$this->loadContainers();
|
|
} elseif (data_get($this->parameters, 'server_uuid')) {
|
|
$this->type = 'server';
|
|
$this->resource = Server::ownedByCurrentTeam()->whereUuid($this->parameters['server_uuid'])->firstOrFail();
|
|
$this->server = $this->resource;
|
|
$this->containersLoaded = true; // Server doesn't need container loading
|
|
$this->connectionStatus = 'Waiting for terminal to be ready...';
|
|
}
|
|
}
|
|
|
|
public function loadContainers()
|
|
{
|
|
foreach ($this->servers as $server) {
|
|
if (data_get($this->parameters, 'application_uuid')) {
|
|
if ($server->isSwarm()) {
|
|
$containers = collect([
|
|
[
|
|
'Names' => $this->resource->uuid.'_'.$this->resource->uuid,
|
|
],
|
|
]);
|
|
} else {
|
|
$containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true);
|
|
}
|
|
foreach ($containers as $container) {
|
|
// if container state is running
|
|
if (data_get($container, 'State') === 'running') {
|
|
$payload = [
|
|
'server' => $server,
|
|
'container' => $container,
|
|
];
|
|
$this->containers = $this->containers->push($payload);
|
|
}
|
|
}
|
|
} elseif (data_get($this->parameters, 'database_uuid')) {
|
|
if ($this->resource->isRunning()) {
|
|
$this->containers = $this->containers->push([
|
|
'server' => $server,
|
|
'container' => [
|
|
'Names' => $this->resource->uuid,
|
|
],
|
|
]);
|
|
}
|
|
} elseif (data_get($this->parameters, 'service_uuid')) {
|
|
$this->resource->applications()->get()->each(function ($application) {
|
|
if ($application->isRunning()) {
|
|
$this->containers->push([
|
|
'server' => $this->resource->server,
|
|
'container' => [
|
|
'Names' => data_get($application, 'name').'-'.data_get($this->resource, 'uuid'),
|
|
],
|
|
]);
|
|
}
|
|
});
|
|
$this->resource->databases()->get()->each(function ($database) {
|
|
if ($database->isRunning()) {
|
|
$this->containers->push([
|
|
'server' => $this->resource->server,
|
|
'container' => [
|
|
'Names' => data_get($database, 'name').'-'.data_get($this->resource, 'uuid'),
|
|
],
|
|
]);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if ($this->containers->count() > 0) {
|
|
$this->container = $this->containers->first();
|
|
}
|
|
if ($this->containers->count() === 1) {
|
|
$this->selected_container = data_get($this->containers->first(), 'container.Names');
|
|
}
|
|
|
|
$this->containersLoaded = true;
|
|
$this->connectionStatus = 'Waiting for terminal to be ready...';
|
|
}
|
|
|
|
public function initializeTerminalConnection()
|
|
{
|
|
try {
|
|
// Only auto-connect if containers are loaded and we haven't attempted before
|
|
if (! $this->containersLoaded || $this->autoConnectAttempted || $this->isConnecting) {
|
|
return;
|
|
}
|
|
|
|
$this->autoConnectAttempted = true;
|
|
|
|
// Ensure component is in a stable state before proceeding
|
|
$this->skipRender();
|
|
|
|
$this->isConnecting = true;
|
|
|
|
if ($this->type === 'server') {
|
|
$this->connectionStatus = 'Establishing connection to server terminal...';
|
|
$this->connectToServer();
|
|
} elseif ($this->containers->count() === 1) {
|
|
$this->connectionStatus = 'Establishing connection to container terminal...';
|
|
$this->connectToContainer();
|
|
} else {
|
|
$this->isConnecting = false;
|
|
$this->connectionStatus = '';
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// Log the error but don't let it bubble up to cause snapshot issues
|
|
logger()->error('Terminal auto-connection failed', [
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString(),
|
|
'component_id' => $this->getId(),
|
|
]);
|
|
|
|
// Reset state to allow manual connection
|
|
$this->autoConnectAttempted = false;
|
|
$this->isConnecting = false;
|
|
$this->connectionStatus = 'Auto-connection failed. Please use the reconnect button.';
|
|
}
|
|
}
|
|
|
|
#[On('terminalConnected')]
|
|
public function terminalConnected()
|
|
{
|
|
$this->isConnected = true;
|
|
$this->isConnecting = false;
|
|
$this->connectionStatus = '';
|
|
}
|
|
|
|
#[On('terminalDisconnected')]
|
|
public function terminalDisconnected()
|
|
{
|
|
$this->isConnected = false;
|
|
$this->isConnecting = false;
|
|
$this->autoConnectAttempted = false;
|
|
$this->connectionStatus = 'Connection lost. Click Reconnect to try again.';
|
|
}
|
|
|
|
private function checkShellAvailability(Server $server, string $container): bool
|
|
{
|
|
$escapedContainer = escapeshellarg($container);
|
|
try {
|
|
instant_remote_process([
|
|
"docker exec {$escapedContainer} bash -c 'exit 0' 2>/dev/null || ".
|
|
"docker exec {$escapedContainer} sh -c 'exit 0' 2>/dev/null",
|
|
], $server);
|
|
|
|
return true;
|
|
} catch (\Throwable) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
#[On('connectToServer')]
|
|
public function connectToServer()
|
|
{
|
|
try {
|
|
if ($this->server->isForceDisabled()) {
|
|
throw new \RuntimeException('Server is disabled.');
|
|
}
|
|
if (! $this->server->isTerminalEnabled()) {
|
|
throw new \RuntimeException('Terminal access is disabled on this server.');
|
|
}
|
|
$this->hasShell = true;
|
|
$this->isConnecting = true;
|
|
$this->connectionStatus = 'Establishing connection to server terminal...';
|
|
$this->dispatch(
|
|
'send-terminal-command',
|
|
false,
|
|
data_get($this->server, 'name'),
|
|
data_get($this->server, 'uuid')
|
|
);
|
|
} catch (\Throwable $e) {
|
|
$this->isConnecting = false;
|
|
$this->connectionStatus = 'Connection failed.';
|
|
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
#[On('connectToContainer')]
|
|
public function connectToContainer()
|
|
{
|
|
if ($this->selected_container === 'default') {
|
|
$this->dispatch('error', 'Please select a container.');
|
|
|
|
return;
|
|
}
|
|
try {
|
|
// Validate container name format
|
|
if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $this->selected_container)) {
|
|
throw new \InvalidArgumentException('Invalid container name format');
|
|
}
|
|
|
|
// Verify container exists in our allowed list
|
|
$container = collect($this->containers)->firstWhere('container.Names', $this->selected_container);
|
|
if (is_null($container)) {
|
|
throw new \RuntimeException('Container not found.');
|
|
}
|
|
|
|
// Verify server ownership and status
|
|
$server = data_get($container, 'server');
|
|
if (! $server || ! $server instanceof Server) {
|
|
throw new \RuntimeException('Invalid server configuration.');
|
|
}
|
|
|
|
if ($server->isForceDisabled()) {
|
|
throw new \RuntimeException('Server is disabled.');
|
|
}
|
|
|
|
// Additional ownership verification based on resource type
|
|
$resourceServer = match ($this->type) {
|
|
'application' => $this->resource->destination->server,
|
|
'database' => $this->resource->destination->server,
|
|
'service' => $this->resource->server,
|
|
default => throw new \RuntimeException('Invalid resource type.')
|
|
};
|
|
|
|
if ($server->id !== $resourceServer->id && ! $this->resource->additional_servers->contains('id', $server->id)) {
|
|
throw new \RuntimeException('Server ownership verification failed.');
|
|
}
|
|
|
|
$this->hasShell = $this->checkShellAvailability($server, data_get($container, 'container.Names'));
|
|
if (! $this->hasShell) {
|
|
$this->isConnecting = false;
|
|
$this->connectionStatus = 'Shell not available in container.';
|
|
|
|
return;
|
|
}
|
|
|
|
$this->isConnecting = true;
|
|
$this->connectionStatus = 'Establishing connection to container terminal...';
|
|
$this->dispatch(
|
|
'send-terminal-command',
|
|
true,
|
|
data_get($container, 'container.Names'),
|
|
data_get($container, 'server.uuid')
|
|
);
|
|
} catch (\Throwable $e) {
|
|
$this->isConnecting = false;
|
|
$this->connectionStatus = 'Connection failed.';
|
|
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.project.shared.execute-container-command');
|
|
}
|
|
}
|