feat: execute command in container

This commit is contained in:
Andras Bacsai 2023-12-07 13:07:16 +01:00
parent f542bcf428
commit 1158b2f4db
10 changed files with 194 additions and 74 deletions

View File

@ -16,31 +16,28 @@ class Command extends Component
{ {
public string $command; public string $command;
public string $container; public string $container;
public string $dir; public $containers;
public $server; public $parameters;
public $resource;
public string $type;
public string $workDir = '';
public Server $server;
public $servers = []; public $servers = [];
protected $rules = [ protected $rules = [
'server' => 'required', 'server' => 'required',
'container' => 'required', 'container' => 'required',
'command' => 'required', 'command' => 'required',
]; 'workDir' => 'nullable',
protected $validationAttributes = [
'server' => 'server',
'container' => 'container',
'command' => 'command',
]; ];
public function mount() public function mount()
{ {
$this->containers = collect(); $this->containers = collect();
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->query = request()->query();
if (data_get($this->parameters, 'application_uuid')) { if (data_get($this->parameters, 'application_uuid')) {
$this->type = 'application'; $this->type = 'application';
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail(); $this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server; $this->server = $this->resource->destination->server;
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0); $containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
if ($containers->count() > 0) { if ($containers->count() > 0) {
@ -67,22 +64,27 @@ public function mount()
} }
} }
$this->resource = $resource; $this->resource = $resource;
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server; $this->server = $this->resource->destination->server;
$this->container = $this->resource->uuid; $this->container = $this->resource->uuid;
$this->containers->push($this->container); $this->containers->push($this->container);
} else if (data_get($this->parameters, 'service_uuid')) { } else if (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service'; $this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$service_name = data_get($this->parameters, 'service_name'); $this->resource->applications()->get()->each(function ($application) {
$this->serviceSubType = $this->resource->applications()->where('name', $service_name)->first(); if (str(data_get($application, 'status'))->contains('running')) {
if (!$this->serviceSubType) { $this->containers->push(data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'));
$this->serviceSubType = $this->resource->databases()->where('name', $service_name)->first(); }
} });
$this->status = $this->resource->status; $this->resource->databases()->get()->each(function ($database) {
if (str(data_get($database, 'status'))->contains('running')) {
$this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'));
}
});
$this->server = $this->resource->server; $this->server = $this->resource->server;
$this->container = data_get($this->parameters, 'service_name') . '-' . $this->resource->uuid; }
$this->containers->push($this->container); if ($this->containers->count() > 1) {
$this->container = $this->containers->first();
} }
} }
@ -90,10 +92,9 @@ public function runCommand()
{ {
$this->validate(); $this->validate();
try { try {
if (!empty($this->dir)) { if (!empty($this->workDir)) {
$exec = "docker exec -w {$this->dir} {$this->container} {$this->command}"; $exec = "docker exec -w {$this->workDir} {$this->container} {$this->command}";
} } else {
else {
$exec = "docker exec {$this->container} {$this->command}"; $exec = "docker exec {$this->container} {$this->command}";
} }
$activity = remote_process([$exec], $this->server, ignore_errors: true); $activity = remote_process([$exec], $this->server, ignore_errors: true);
@ -102,4 +103,8 @@ public function runCommand()
return handleError($e, $this); return handleError($e, $this);
} }
} }
} public function render()
{
return view('livewire.project.shared.execute-container-command');
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace App\Http\Livewire\Project\Shared;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Livewire\Component;
class ExecuteContainerCommand extends Component
{
public string $command;
public string $container;
public $containers;
public $parameters;
public $resource;
public string $type;
public string $workDir = '';
public Server $server;
public $servers = [];
protected $rules = [
'server' => 'required',
'container' => 'required',
'command' => 'required',
'workDir' => 'nullable',
];
public function mount()
{
$this->containers = collect();
$this->parameters = get_route_parameters();
if (data_get($this->parameters, 'application_uuid')) {
$this->type = 'application';
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
$this->server = $this->resource->destination->server;
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
if ($containers->count() > 0) {
$containers->each(function ($container) {
$this->containers->push(str_replace('/', '', $container['Names']));
});
}
} else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database';
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
}
}
}
$this->resource = $resource;
$this->server = $this->resource->destination->server;
$this->container = $this->resource->uuid;
$this->containers->push($this->container);
} else if (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$this->resource->applications()->get()->each(function ($application) {
if (str(data_get($application, 'status'))->contains('running')) {
$this->containers->push(data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'));
}
});
$this->resource->databases()->get()->each(function ($database) {
if (str(data_get($database, 'status'))->contains('running')) {
$this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'));
}
});
$this->server = $this->resource->server;
}
if ($this->containers->count() > 1) {
$this->container = $this->containers->first();
}
}
public function runCommand()
{
$this->validate();
try {
if (!empty($this->workDir)) {
$exec = "docker exec -w {$this->workDir} {$this->container} {$this->command}";
} else {
$exec = "docker exec {$this->container} {$this->command}";
}
$activity = remote_process([$exec], $this->server, ignore_errors: true);
$this->emit('newMonitorActivity', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.shared.execute-container-command');
}
}

View File

@ -3,17 +3,17 @@
href="{{ route('project.application.configuration', $parameters) }}"> href="{{ route('project.application.configuration', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
<a class="{{ request()->routeIs('project.application.deployments') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.application.command') ? 'text-white' : '' }}"
href="{{ route('project.application.deployments', $parameters) }}"> href="{{ route('project.application.command', $parameters) }}">
<button>Deployments</button> <button>Execute Command</button>
</a> </a>
<a class="{{ request()->routeIs('project.application.logs') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.application.logs') ? 'text-white' : '' }}"
href="{{ route('project.application.logs', $parameters) }}"> href="{{ route('project.application.logs', $parameters) }}">
<button>Logs</button> <button>Logs</button>
</a> </a>
<a class="{{ request()->routeIs('project.application.command') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.application.deployments') ? 'text-white' : '' }}"
href="{{ route('project.application.command', $parameters) }}"> href="{{ route('project.application.deployments', $parameters) }}">
<button>Run command</button> <button>Deployments</button>
</a> </a>
<x-applications.links :application="$application" /> <x-applications.links :application="$application" />
<div class="flex-1"></div> <div class="flex-1"></div>

View File

@ -3,14 +3,14 @@
href="{{ route('project.database.configuration', $parameters) }}"> href="{{ route('project.database.configuration', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
<a class="{{ request()->routeIs('project.database.command') ? 'text-white' : '' }}"
href="{{ route('project.database.command', $parameters) }}">
<button>Execute Command</button>
</a>
<a class="{{ request()->routeIs('project.database.logs') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.database.logs') ? 'text-white' : '' }}"
href="{{ route('project.database.logs', $parameters) }}"> href="{{ route('project.database.logs', $parameters) }}">
<button>Logs</button> <button>Logs</button>
</a> </a>
<a class="{{ request()->routeIs('project.database.command') ? 'text-white' : '' }}"
href="{{ route('project.database.command', $parameters) }}">
<button>Run command</button>
</a>
@if ( @if (
$database->getMorphClass() === 'App\Models\StandalonePostgresql' || $database->getMorphClass() === 'App\Models\StandalonePostgresql' ||
$database->getMorphClass() === 'App\Models\StandaloneMongodb' || $database->getMorphClass() === 'App\Models\StandaloneMongodb' ||

View File

@ -38,7 +38,8 @@ class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"
</button> </button>
@endif @endif
@if (serviceStatus($service) === 'exited') @if (serviceStatus($service) === 'exited')
<button wire:click='stop(true)' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> <button wire:click='stop(true)'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 " viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 " viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path fill="red" d="M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z" /> <path fill="red" d="M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z" />
<path fill="red" <path fill="red"

View File

@ -1,30 +0,0 @@
<div>
@if ($type === 'application')
<livewire:project.application.heading :application="$resource" />
@elseif ($type === 'database')
<livewire:project.database.heading :database="$resource" />
@elseif ($type === 'service')
<livewire:project.service.navbar :service="$resource" :parameters="$parameters" :query="$query" />
<div class="pt-5 pb-5">
<a class="{{ request()->routeIs('project.service.show') ? 'text-white' : '' }}"
href="{{ route('project.service.show', $parameters) }}">
<button><- Back</button>
</a>
</div>
@endif
<form class="flex flex-col justify-center gap-2 xl:items-end xl:flex-row" wire:submit.prevent='runCommand'>
<x-forms.input placeholder="ls -l" autofocus id="command" label="Command" required />
<x-forms.input id="dir" label="Working directory" />
<x-forms.select label="Container" id="container" required>
<option selected>Select container</option>
@foreach ($containers as $container)
<option value="{{ $container }}">{{ $container }}</option>
@endforeach
</x-forms.select>
<x-forms.button type="submit">Execute Command
</x-forms.button>
</form>
<div class="container w-full pt-10 mx-auto">
<livewire:activity-monitor header="Command output" />
</div>
</div>

View File

@ -1,12 +1,16 @@
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" x-init="$wire.checkStatus" wire:poll.10000ms="checkStatus"> <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" x-init="$wire.checkStatus" wire:poll.10000ms="checkStatus">
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" /> <livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex h-full pt-6" > <div class="flex h-full pt-6">
<div class="flex flex-col items-start gap-4 min-w-fit"> <div class="flex flex-col items-start gap-4 min-w-fit">
<a target="_blank" href="{{ $service->documentation() }}">Documentation <x-external-link /></a> <a target="_blank" href="{{ $service->documentation() }}">Documentation <x-external-link /></a>
<a :class="activeTab === 'service-stack' && 'text-white'" <a :class="activeTab === 'service-stack' && 'text-white'"
@click.prevent="activeTab = 'service-stack'; @click.prevent="activeTab = 'service-stack';
window.location.hash = 'service-stack'" window.location.hash = 'service-stack'"
href="#">Service Stack</a> href="#">Service Stack</a>
<a :class="activeTab === 'execute-command' && 'text-white'"
@click.prevent="activeTab = 'execute-command';
window.location.hash = 'execute-command'"
href="#">Execute Command</a>
<a :class="activeTab === 'storages' && 'text-white'" <a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; @click.prevent="activeTab = 'storages';
window.location.hash = 'storages'" window.location.hash = 'storages'"
@ -112,6 +116,9 @@ class="hover:text-warning">Logs</span></a>
<div x-cloak x-show="activeTab === 'webhooks'"> <div x-cloak x-show="activeTab === 'webhooks'">
<livewire:project.shared.webhooks :resource="$service" /> <livewire:project.shared.webhooks :resource="$service" />
</div> </div>
<div x-cloak x-show="activeTab === 'execute-command'">
<livewire:project.shared.execute-container-command :resource="$service" />
</div>
<div x-cloak x-show="activeTab === 'environment-variables'"> <div x-cloak x-show="activeTab === 'environment-variables'">
<div x-cloak x-show="activeTab === 'environment-variables'"> <div x-cloak x-show="activeTab === 'environment-variables'">
<livewire:project.shared.environment-variable.all :resource="$service" /> <livewire:project.shared.environment-variable.all :resource="$service" />

View File

@ -26,10 +26,6 @@
<button>Logs</button> <button>Logs</button>
</a> </a>
@endif @endif
<a class="{{ request()->routeIs('project.service.command') ? 'text-white' : '' }}"
href="{{ route('project.service.command', $parameters) }}">
<button>Run command</button>
</a>
</div> </div>
<div class="w-full pl-8"> <div class="w-full pl-8">
@isset($serviceApplication) @isset($serviceApplication)

View File

@ -0,0 +1,31 @@
<div>
@if ($type === 'application')
<h1>Execute Command</h1>
<livewire:project.application.heading :application="$resource" />
@elseif ($type === 'database')
<h1>Execute Command</h1>
<livewire:project.database.heading :database="$resource" />
@elseif ($type === 'service')
<h2>Execute Command</h2>
@endif
@if (count($containers) > 0)
<form class="flex flex-col gap-2 pt-4" wire:submit.prevent='runCommand'>
<div class="flex gap-2">
<x-forms.input placeholder="ls -l" autofocus id="command" label="Command" required />
<x-forms.input id="workDir" label="Working directory" />
</div>
<x-forms.select label="Container" id="container" required>
<option disabled selected>Select container</option>
@foreach ($containers as $container)
<option value="{{ $container }}">{{ $container }}</option>
@endforeach
</x-forms.select>
<x-forms.button type="submit">Run</x-forms.button>
</form>
@else
<div class="pt-4">No containers are not running.</div>
@endif
<div class="container w-full pt-10 mx-auto">
<livewire:activity-monitor header="Command output" />
</div>
</div>

View File

@ -6,13 +6,13 @@
use App\Http\Controllers\MagicController; use App\Http\Controllers\MagicController;
use App\Http\Controllers\ProjectController; use App\Http\Controllers\ProjectController;
use App\Http\Livewire\Project\Application\Configuration as ApplicationConfiguration; use App\Http\Livewire\Project\Application\Configuration as ApplicationConfiguration;
use App\Http\Livewire\Project\Application\Command as ApplicationCommand;
use App\Http\Livewire\Boarding\Index as BoardingIndex; use App\Http\Livewire\Boarding\Index as BoardingIndex;
use App\Http\Livewire\Project\Service\Index as ServiceIndex; use App\Http\Livewire\Project\Service\Index as ServiceIndex;
use App\Http\Livewire\Project\Service\Show as ServiceShow; use App\Http\Livewire\Project\Service\Show as ServiceShow;
use App\Http\Livewire\Dev\Compose as Compose; use App\Http\Livewire\Dev\Compose as Compose;
use App\Http\Livewire\Dashboard; use App\Http\Livewire\Dashboard;
use App\Http\Livewire\Project\CloneProject; use App\Http\Livewire\Project\CloneProject;
use App\Http\Livewire\Project\Shared\ExecuteContainerCommand;
use App\Http\Livewire\Project\Shared\Logs; use App\Http\Livewire\Project\Shared\Logs;
use App\Http\Livewire\Security\ApiTokens; use App\Http\Livewire\Security\ApiTokens;
use App\Http\Livewire\Server\All; use App\Http\Livewire\Server\All;
@ -122,21 +122,21 @@
)->name('project.application.deployment'); )->name('project.application.deployment');
Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}/logs', Logs::class)->name('project.application.logs'); Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}/logs', Logs::class)->name('project.application.logs');
Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}/command', ApplicationCommand::class)->name('project.application.command'); Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}/command', ExecuteContainerCommand::class)->name('project.application.command');
// Databases // Databases
Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}', [DatabaseController::class, 'configuration'])->name('project.database.configuration'); Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}', [DatabaseController::class, 'configuration'])->name('project.database.configuration');
Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}/backups', [DatabaseController::class, 'backups'])->name('project.database.backups.all'); Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}/backups', [DatabaseController::class, 'backups'])->name('project.database.backups.all');
Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}/backups/{backup_uuid}', [DatabaseController::class, 'executions'])->name('project.database.backups.executions'); Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}/backups/{backup_uuid}', [DatabaseController::class, 'executions'])->name('project.database.backups.executions');
Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}/logs', Logs::class)->name('project.database.logs'); Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}/logs', Logs::class)->name('project.database.logs');
Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}/command', ApplicationCommand::class)->name('project.database.command'); Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}/command', ExecuteContainerCommand::class)->name('project.database.command');
// Services // Services
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}', ServiceIndex::class)->name('project.service.configuration'); Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}', ServiceIndex::class)->name('project.service.configuration');
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}', ServiceShow::class)->name('project.service.show'); Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}', ServiceShow::class)->name('project.service.show');
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}/logs', Logs::class)->name('project.service.logs'); Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}/logs', Logs::class)->name('project.service.logs');
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}/command', ApplicationCommand::class)->name('project.service.command'); Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/command', ExecuteContainerCommand::class)->name('project.service.command');
}); });
Route::middleware(['auth'])->group(function () { Route::middleware(['auth'])->group(function () {