mirror of
https://github.com/vitodeploy/vito.git
synced 2025-07-01 05:56:16 +00:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
9d4c7972ed | |||
abe27cde01 | |||
49137a757b | |||
97e20206e8 | |||
176ff3bbc4 | |||
b2440586d6 | |||
4b8e798e66 | |||
6143eb94b4 | |||
e52903c649 | |||
1a5cf4c57a | |||
d8ece27964 | |||
f54c754971 | |||
a1cf09e35d |
29
app/Actions/Site/CreateCommand.php
Normal file
29
app/Actions/Site/CreateCommand.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Site;
|
||||||
|
|
||||||
|
use App\Models\Command;
|
||||||
|
use App\Models\Site;
|
||||||
|
|
||||||
|
class CreateCommand
|
||||||
|
{
|
||||||
|
public function create(Site $site, array $input): Command
|
||||||
|
{
|
||||||
|
$script = new Command([
|
||||||
|
'site_id' => $site->id,
|
||||||
|
'name' => $input['name'],
|
||||||
|
'command' => $input['command'],
|
||||||
|
]);
|
||||||
|
$script->save();
|
||||||
|
|
||||||
|
return $script;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'command' => ['required', 'string'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -68,6 +68,9 @@ public function create(Server $server, array $input): Site
|
|||||||
'content' => '',
|
'content' => '',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// create base commands if any
|
||||||
|
$site->commands()->createMany($site->type()->baseCommands());
|
||||||
|
|
||||||
// install site
|
// install site
|
||||||
dispatch(function () use ($site) {
|
dispatch(function () use ($site) {
|
||||||
$site->type()->install();
|
$site->type()->install();
|
||||||
|
25
app/Actions/Site/EditCommand.php
Normal file
25
app/Actions/Site/EditCommand.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Site;
|
||||||
|
|
||||||
|
use App\Models\Command;
|
||||||
|
|
||||||
|
class EditCommand
|
||||||
|
{
|
||||||
|
public function edit(Command $command, array $input): Command
|
||||||
|
{
|
||||||
|
$command->name = $input['name'];
|
||||||
|
$command->command = $input['command'];
|
||||||
|
$command->save();
|
||||||
|
|
||||||
|
return $command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'command' => ['required', 'string'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
58
app/Actions/Site/ExecuteCommand.php
Normal file
58
app/Actions/Site/ExecuteCommand.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Site;
|
||||||
|
|
||||||
|
use App\Enums\CommandExecutionStatus;
|
||||||
|
use App\Models\Command;
|
||||||
|
use App\Models\CommandExecution;
|
||||||
|
use App\Models\ServerLog;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class ExecuteCommand
|
||||||
|
{
|
||||||
|
public function execute(Command $command, User $user, array $input): CommandExecution
|
||||||
|
{
|
||||||
|
$execution = new CommandExecution([
|
||||||
|
'command_id' => $command->id,
|
||||||
|
'server_id' => $command->site->server_id,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'variables' => $input['variables'] ?? [],
|
||||||
|
'status' => CommandExecutionStatus::EXECUTING,
|
||||||
|
]);
|
||||||
|
$execution->save();
|
||||||
|
|
||||||
|
dispatch(function () use ($execution, $command) {
|
||||||
|
$content = $execution->getContent();
|
||||||
|
$log = ServerLog::make($execution->server, 'command-'.$command->id.'-'.strtotime('now'));
|
||||||
|
$log->save();
|
||||||
|
$execution->server_log_id = $log->id;
|
||||||
|
$execution->save();
|
||||||
|
$execution->server->os()->runScript(
|
||||||
|
path: $command->site->path,
|
||||||
|
script: $content,
|
||||||
|
user: $command->site->user,
|
||||||
|
serverLog: $log,
|
||||||
|
variables: $execution->variables
|
||||||
|
);
|
||||||
|
$execution->status = CommandExecutionStatus::COMPLETED;
|
||||||
|
$execution->save();
|
||||||
|
})->catch(function () use ($execution) {
|
||||||
|
$execution->status = CommandExecutionStatus::FAILED;
|
||||||
|
$execution->save();
|
||||||
|
})->onConnection('ssh');
|
||||||
|
|
||||||
|
return $execution;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function rules(array $input): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'variables' => 'array',
|
||||||
|
'variables.*' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'max:255',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ class UpdateBranch
|
|||||||
public function update(Site $site, array $input): void
|
public function update(Site $site, array $input): void
|
||||||
{
|
{
|
||||||
$site->branch = $input['branch'];
|
$site->branch = $input['branch'];
|
||||||
|
app(Git::class)->fetchOrigin($site);
|
||||||
app(Git::class)->checkout($site);
|
app(Git::class)->checkout($site);
|
||||||
$site->save();
|
$site->save();
|
||||||
}
|
}
|
||||||
|
32
app/Cli/Commands/AbstractCommand.php
Normal file
32
app/Cli/Commands/AbstractCommand.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Cli\Commands;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
use function Laravel\Prompts\error;
|
||||||
|
|
||||||
|
abstract class AbstractCommand extends Command
|
||||||
|
{
|
||||||
|
private User|null $user = null;
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
if ($this->user) {
|
||||||
|
return $this->user->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var User $user */
|
||||||
|
$user = User::query()->first();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
error('The application is not setup');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
18
app/Cli/Commands/InfoCommand.php
Normal file
18
app/Cli/Commands/InfoCommand.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Cli\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class InfoCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'info';
|
||||||
|
|
||||||
|
protected $description = 'Show the application information';
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$this->info('Version: '.config('app.version'));
|
||||||
|
$this->info('Timezone: '.config('app.timezone'));
|
||||||
|
}
|
||||||
|
}
|
29
app/Cli/Commands/Projects/ProjectsListCommand.php
Normal file
29
app/Cli/Commands/Projects/ProjectsListCommand.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Cli\Commands\Projects;
|
||||||
|
|
||||||
|
use App\Cli\Commands\AbstractCommand;
|
||||||
|
use App\Models\Project;
|
||||||
|
|
||||||
|
use function Laravel\Prompts\table;
|
||||||
|
|
||||||
|
class ProjectsListCommand extends AbstractCommand
|
||||||
|
{
|
||||||
|
protected $signature = 'projects:list';
|
||||||
|
|
||||||
|
protected $description = 'Show projects list';
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$projects = Project::all();
|
||||||
|
|
||||||
|
table(
|
||||||
|
headers: ['ID', 'Name', 'Selected'],
|
||||||
|
rows: $projects->map(fn (Project $project) => [
|
||||||
|
$project->id,
|
||||||
|
$project->name,
|
||||||
|
$project->id === $this->user()->current_project_id ? 'Yes' : 'No',
|
||||||
|
])->toArray(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
35
app/Cli/Commands/Projects/ProjectsSelectCommand.php
Normal file
35
app/Cli/Commands/Projects/ProjectsSelectCommand.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Cli\Commands\Projects;
|
||||||
|
|
||||||
|
use App\Cli\Commands\AbstractCommand;
|
||||||
|
use App\Models\Project;
|
||||||
|
|
||||||
|
use function Laravel\Prompts\error;
|
||||||
|
use function Laravel\Prompts\info;
|
||||||
|
|
||||||
|
class ProjectsSelectCommand extends AbstractCommand
|
||||||
|
{
|
||||||
|
protected $signature = 'projects:select {project}';
|
||||||
|
|
||||||
|
protected $description = 'Select a project';
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$project = Project::query()->find($this->argument('project'));
|
||||||
|
|
||||||
|
if (! $project) {
|
||||||
|
error('The project does not exist');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->user()->update([
|
||||||
|
'current_project_id' => $project->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
info(__('The project [:project] has been selected' , [
|
||||||
|
'project' => $project->name,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Cli\Commands\ServerProviders;
|
||||||
|
|
||||||
|
use App\Cli\Commands\AbstractCommand;
|
||||||
|
use function Laravel\Prompts\select;
|
||||||
|
use function Laravel\Prompts\text;
|
||||||
|
|
||||||
|
class ServerProvidersCreateCommand extends AbstractCommand
|
||||||
|
{
|
||||||
|
protected $signature = 'server-providers:create';
|
||||||
|
|
||||||
|
protected $description = 'Create a new server provider';
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$provider = select(
|
||||||
|
label: 'What is the server provider?',
|
||||||
|
options: collect(config('core.server_providers'))
|
||||||
|
->filter(fn($provider) => $provider != \App\Enums\ServerProvider::CUSTOM)
|
||||||
|
->mapWithKeys(fn($provider) => [$provider => $provider]),
|
||||||
|
);
|
||||||
|
$profile = text(
|
||||||
|
label: 'What should we call this provider profile?',
|
||||||
|
required: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Cli\Commands\ServerProviders;
|
||||||
|
|
||||||
|
use App\Cli\Commands\AbstractCommand;
|
||||||
|
use App\Models\Project;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\ServerProvider;
|
||||||
|
use function Laravel\Prompts\table;
|
||||||
|
|
||||||
|
class ServerProvidersListCommand extends AbstractCommand
|
||||||
|
{
|
||||||
|
protected $signature = 'server-providers:list';
|
||||||
|
|
||||||
|
protected $description = 'Show server providers list';
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$providers = $this->user()->serverProviders;
|
||||||
|
|
||||||
|
table(
|
||||||
|
headers: ['ID', 'Provider', 'Name', 'Created At'],
|
||||||
|
rows: $providers->map(fn (ServerProvider $provider) => [
|
||||||
|
$provider->id,
|
||||||
|
$provider->provider,
|
||||||
|
$provider->profile,
|
||||||
|
$provider->created_at_by_timezone,
|
||||||
|
])->toArray(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
27
app/Cli/Commands/Servers/ServersCreateCommand.php
Normal file
27
app/Cli/Commands/Servers/ServersCreateCommand.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Cli\Commands\Servers;
|
||||||
|
|
||||||
|
use App\Cli\Commands\AbstractCommand;
|
||||||
|
use function Laravel\Prompts\select;
|
||||||
|
use function Laravel\Prompts\text;
|
||||||
|
|
||||||
|
class ServersCreateCommand extends AbstractCommand
|
||||||
|
{
|
||||||
|
protected $signature = 'servers:create';
|
||||||
|
|
||||||
|
protected $description = 'Create a new server';
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$name = text(
|
||||||
|
label: 'What is the server name?',
|
||||||
|
required: true,
|
||||||
|
);
|
||||||
|
$os = select(
|
||||||
|
label: 'What is the server OS?',
|
||||||
|
options: collect(config('core.operating_systems'))
|
||||||
|
->mapWithKeys(fn($value) => [$value => $value]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
34
app/Cli/Commands/Servers/ServersListCommand.php
Normal file
34
app/Cli/Commands/Servers/ServersListCommand.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Cli\Commands\Servers;
|
||||||
|
|
||||||
|
use App\Cli\Commands\AbstractCommand;
|
||||||
|
use App\Models\Project;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use function Laravel\Prompts\table;
|
||||||
|
|
||||||
|
class ServersListCommand extends AbstractCommand
|
||||||
|
{
|
||||||
|
protected $signature = 'servers:list';
|
||||||
|
|
||||||
|
protected $description = 'Show servers list';
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$servers = $this->user()->currentProject->servers;
|
||||||
|
|
||||||
|
table(
|
||||||
|
headers: ['ID', 'Name', 'IP', 'Provider', 'OS', 'Status', 'Created At'],
|
||||||
|
rows: $servers->map(fn (Server $server) => [
|
||||||
|
$server->id,
|
||||||
|
$server->name,
|
||||||
|
$server->ip,
|
||||||
|
$server->provider,
|
||||||
|
$server->os,
|
||||||
|
$server->status,
|
||||||
|
$server->created_at_by_timezone,
|
||||||
|
])->toArray(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
81
app/Cli/Commands/SetupCommand.php
Normal file
81
app/Cli/Commands/SetupCommand.php
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Cli\Commands;
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use function Laravel\Prompts\text;
|
||||||
|
use function Laravel\Prompts\info;
|
||||||
|
|
||||||
|
class SetupCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'setup';
|
||||||
|
|
||||||
|
protected $description = 'Setup the application';
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$this->prepareStorage();
|
||||||
|
|
||||||
|
$this->migrate();
|
||||||
|
|
||||||
|
$this->makeUser();
|
||||||
|
|
||||||
|
$this->makeProject();
|
||||||
|
|
||||||
|
info('The application has been setup');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function prepareStorage(): void
|
||||||
|
{
|
||||||
|
File::ensureDirectoryExists(storage_path());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function migrate(): void
|
||||||
|
{
|
||||||
|
$this->call('migrate', ['--force' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeUser(): void
|
||||||
|
{
|
||||||
|
$user = User::query()->first();
|
||||||
|
if ($user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = text(
|
||||||
|
label: 'What is your name?',
|
||||||
|
required: true,
|
||||||
|
);
|
||||||
|
$email = text(
|
||||||
|
label: 'What is your email?',
|
||||||
|
required: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
User::query()->create([
|
||||||
|
'name' => $name,
|
||||||
|
'email' => $email,
|
||||||
|
'password' => bcrypt(str()->random(16)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeProject(): void
|
||||||
|
{
|
||||||
|
$project = Project::query()->first();
|
||||||
|
|
||||||
|
if ($project) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$project = Project::query()->create([
|
||||||
|
'name' => 'default',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::query()->first();
|
||||||
|
$user->update([
|
||||||
|
'current_project_id' => $project->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
43
app/Cli/Kernel.php
Normal file
43
app/Cli/Kernel.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Cli;
|
||||||
|
|
||||||
|
use Illuminate\Console\Application as Artisan;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Illuminate\Database\Console\Migrations\InstallCommand;
|
||||||
|
use Illuminate\Database\Console\Migrations\MigrateCommand;
|
||||||
|
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||||
|
|
||||||
|
class Kernel extends ConsoleKernel
|
||||||
|
{
|
||||||
|
protected $commands = [
|
||||||
|
'command.migrate',
|
||||||
|
'command.migrate.install'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the commands for the application.
|
||||||
|
*/
|
||||||
|
protected function commands(): void
|
||||||
|
{
|
||||||
|
$this->load(__DIR__ . '/Commands');
|
||||||
|
|
||||||
|
$this->app->singleton('command.migrate', function ($app) {
|
||||||
|
return new MigrateCommand($app['migrator'], $app[Dispatcher::class]);
|
||||||
|
});
|
||||||
|
$this->app->singleton('command.migrate.install', function ($app) {
|
||||||
|
return new InstallCommand($app['migration.repository']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function shouldDiscoverCommands(): false
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getArtisan(): ?Artisan
|
||||||
|
{
|
||||||
|
return $this->artisan = (new Artisan($this->app, $this->events, $this->app->version()))
|
||||||
|
->resolveCommands($this->commands);
|
||||||
|
}
|
||||||
|
}
|
26
app/Console/Commands/ClearLogsCommand.php
Normal file
26
app/Console/Commands/ClearLogsCommand.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\ServerLog;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class ClearLogsCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'logs:clear';
|
||||||
|
|
||||||
|
protected $description = 'Clear all server logs';
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$this->info('Clearing logs...');
|
||||||
|
|
||||||
|
ServerLog::query()->delete();
|
||||||
|
|
||||||
|
File::cleanDirectory(Storage::disk('server-logs')->path(''));
|
||||||
|
|
||||||
|
$this->info('Logs cleared!');
|
||||||
|
}
|
||||||
|
}
|
12
app/Enums/CommandExecutionStatus.php
Normal file
12
app/Enums/CommandExecutionStatus.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
final class CommandExecutionStatus
|
||||||
|
{
|
||||||
|
const EXECUTING = 'executing';
|
||||||
|
|
||||||
|
const COMPLETED = 'completed';
|
||||||
|
|
||||||
|
const FAILED = 'failed';
|
||||||
|
}
|
@ -11,4 +11,6 @@ final class SiteFeature
|
|||||||
const SSL = 'ssl';
|
const SSL = 'ssl';
|
||||||
|
|
||||||
const QUEUES = 'queues';
|
const QUEUES = 'queues';
|
||||||
|
|
||||||
|
const COMMANDS = 'commands';
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ public function run(Server $server, Request $request)
|
|||||||
|
|
||||||
return response()->stream(
|
return response()->stream(
|
||||||
function () use ($server, $request, $ssh, $log, $currentDir) {
|
function () use ($server, $request, $ssh, $log, $currentDir) {
|
||||||
$command = 'cd '.$currentDir.' && '.$request->command.' && echo "VITO_WORKING_DIR: $(pwd)"';
|
$command = 'cd '.$currentDir.' && '.$request->command.' && echo -n "VITO_WORKING_DIR: " && pwd';
|
||||||
$output = '';
|
$output = '';
|
||||||
$ssh->exec(command: $command, log: $log, stream: true, streamCallback: function ($out) use (&$output) {
|
$ssh->exec(command: $command, log: $log, stream: true, streamCallback: function ($out) use (&$output) {
|
||||||
echo preg_replace('/^VITO_WORKING_DIR:.*(\r?\n)?/m', '', $out);
|
echo preg_replace('/^VITO_WORKING_DIR:.*(\r?\n)?/m', '', $out);
|
||||||
@ -63,7 +63,7 @@ function () use ($server, $request, $ssh, $log, $currentDir) {
|
|||||||
public function workingDir(Server $server)
|
public function workingDir(Server $server)
|
||||||
{
|
{
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'dir' => Cache::get('console.'.$server->id.'.dir'),
|
'dir' => Cache::get('console.'.$server->id.'.dir', '~'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
71
app/Models/Command.php
Normal file
71
app/Models/Command.php
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $site_id
|
||||||
|
* @property string $name
|
||||||
|
* @property string $command
|
||||||
|
* @property Carbon $created_at
|
||||||
|
* @property Carbon $updated_at
|
||||||
|
* @property Collection<CommandExecution> $executions
|
||||||
|
* @property ?CommandExecution $lastExecution
|
||||||
|
* @property Site $site
|
||||||
|
*/
|
||||||
|
class Command extends AbstractModel
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'site_id',
|
||||||
|
'name',
|
||||||
|
'command',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'site_id' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function boot(): void
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::deleting(function (Command $command) {
|
||||||
|
$command->executions()->delete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function site(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Site::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVariables(): array
|
||||||
|
{
|
||||||
|
$variables = [];
|
||||||
|
preg_match_all('/\${(.*?)}/', $this->command, $matches);
|
||||||
|
foreach ($matches[1] as $match) {
|
||||||
|
$variables[] = $match;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_unique($variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function executions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(CommandExecution::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lastExecution(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(CommandExecution::class)->latest();
|
||||||
|
}
|
||||||
|
}
|
83
app/Models/CommandExecution.php
Normal file
83
app/Models/CommandExecution.php
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\CommandExecutionStatus;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $command_id
|
||||||
|
* @property int $server_id
|
||||||
|
* @property int $user_id
|
||||||
|
* @property ?int $server_log_id
|
||||||
|
* @property array $variables
|
||||||
|
* @property string $status
|
||||||
|
* @property Carbon $created_at
|
||||||
|
* @property Carbon $updated_at
|
||||||
|
* @property Command $command
|
||||||
|
* @property ?ServerLog $serverLog
|
||||||
|
* @property Server $server
|
||||||
|
* @property ?User $user
|
||||||
|
*/
|
||||||
|
class CommandExecution extends AbstractModel
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'command_id',
|
||||||
|
'server_id',
|
||||||
|
'user_id',
|
||||||
|
'server_log_id',
|
||||||
|
'variables',
|
||||||
|
'status',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'command_id' => 'integer',
|
||||||
|
'server_id' => 'integer',
|
||||||
|
'user_id' => 'integer',
|
||||||
|
'server_log_id' => 'integer',
|
||||||
|
'variables' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static array $statusColors = [
|
||||||
|
CommandExecutionStatus::EXECUTING => 'warning',
|
||||||
|
CommandExecutionStatus::COMPLETED => 'success',
|
||||||
|
CommandExecutionStatus::FAILED => 'danger',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function command(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Command::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContent(): string
|
||||||
|
{
|
||||||
|
$content = $this->command->command;
|
||||||
|
foreach ($this->variables as $variable => $value) {
|
||||||
|
if (is_string($value) && ! empty($value)) {
|
||||||
|
$content = str_replace('${'.$variable.'}', $value, $content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serverLog(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ServerLog::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function server(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Server::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
@ -103,7 +103,7 @@ public static function parse(User $user, Server $server, string $path, string $s
|
|||||||
array_shift($lines);
|
array_shift($lines);
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
if (preg_match('/^([drwx\-]+)\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+([\w\s:\-]+)\s+(.+)$/', $line, $matches)) {
|
if (preg_match('/^([drwx\-]+)\s+(\d+)\s+([\w\-]+)\s+([\w\-]+)\s+(\d+)\s+([\w\s:\-]+)\s+(.+)$/', $line, $matches)) {
|
||||||
$type = match ($matches[1][0]) {
|
$type = match ($matches[1][0]) {
|
||||||
'-' => 'file',
|
'-' => 'file',
|
||||||
'd' => 'directory',
|
'd' => 'directory',
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
* @property Server $server
|
* @property Server $server
|
||||||
* @property ServerLog[] $logs
|
* @property ServerLog[] $logs
|
||||||
* @property Deployment[] $deployments
|
* @property Deployment[] $deployments
|
||||||
|
* @property Command[] $commands
|
||||||
* @property ?GitHook $gitHook
|
* @property ?GitHook $gitHook
|
||||||
* @property DeploymentScript $deploymentScript
|
* @property DeploymentScript $deploymentScript
|
||||||
* @property Queue[] $queues
|
* @property Queue[] $queues
|
||||||
@ -144,6 +145,11 @@ public function deployments(): HasMany
|
|||||||
return $this->hasMany(Deployment::class);
|
return $this->hasMany(Deployment::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function commands(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Command::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function gitHook(): HasOne
|
public function gitHook(): HasOne
|
||||||
{
|
{
|
||||||
return $this->hasOne(GitHook::class);
|
return $this->hasOne(GitHook::class);
|
||||||
|
61
app/Policies/CommandPolicy.php
Normal file
61
app/Policies/CommandPolicy.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Enums\SiteFeature;
|
||||||
|
use App\Models\Command;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\Site;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
|
||||||
|
class CommandPolicy
|
||||||
|
{
|
||||||
|
use HandlesAuthorization;
|
||||||
|
|
||||||
|
public function viewAny(User $user, Site $site, Server $server): bool
|
||||||
|
{
|
||||||
|
return ($user->isAdmin() || $server->project->users->contains($user)) &&
|
||||||
|
$server->isReady() &&
|
||||||
|
$site->hasFeature(SiteFeature::COMMANDS) &&
|
||||||
|
$site->isReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(User $user, Command $command, Site $site, Server $server): bool
|
||||||
|
{
|
||||||
|
return ($user->isAdmin() || $server->project->users->contains($user)) &&
|
||||||
|
$site->server_id === $server->id &&
|
||||||
|
$server->isReady() &&
|
||||||
|
$site->isReady() &&
|
||||||
|
$site->hasFeature(SiteFeature::COMMANDS) &&
|
||||||
|
$command->site_id === $site->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(User $user, Site $site, Server $server): bool
|
||||||
|
{
|
||||||
|
return ($user->isAdmin() || $server->project->users->contains($user)) &&
|
||||||
|
$server->isReady() &&
|
||||||
|
$site->hasFeature(SiteFeature::COMMANDS) &&
|
||||||
|
$site->isReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(User $user, Command $command, Site $site, Server $server): bool
|
||||||
|
{
|
||||||
|
return ($user->isAdmin() || $server->project->users->contains($user)) &&
|
||||||
|
$site->server_id === $server->id &&
|
||||||
|
$server->isReady() &&
|
||||||
|
$site->isReady() &&
|
||||||
|
$site->hasFeature(SiteFeature::COMMANDS) &&
|
||||||
|
$command->site_id === $site->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(User $user, Command $command, Site $site, Server $server): bool
|
||||||
|
{
|
||||||
|
return ($user->isAdmin() || $server->project->users->contains($user)) &&
|
||||||
|
$site->server_id === $server->id &&
|
||||||
|
$server->isReady() &&
|
||||||
|
$site->isReady() &&
|
||||||
|
$site->hasFeature(SiteFeature::COMMANDS) &&
|
||||||
|
$command->site_id === $site->id;
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,9 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
|
if ($this->app->runningInConsole()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Fortify::ignoreRoutes();
|
Fortify::ignoreRoutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,6 +39,8 @@ public function boot(): void
|
|||||||
return new FTP;
|
return new FTP;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (! $this->app->runningInConsole()) {
|
||||||
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
|
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,9 @@ class RouteServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
|
if ($this->app->runningInConsole()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
$this->configureRateLimiting();
|
$this->configureRateLimiting();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,11 +38,18 @@ class WebServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
|
if ($this->app->runningInConsole()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Filament::registerPanel($this->panel(Panel::make()));
|
Filament::registerPanel($this->panel(Panel::make()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
|
if ($this->app->runningInConsole()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
FilamentView::registerRenderHook(
|
FilamentView::registerRenderHook(
|
||||||
PanelsRenderHook::SIDEBAR_NAV_START,
|
PanelsRenderHook::SIDEBAR_NAV_START,
|
||||||
fn () => Livewire::mount(SelectProject::class)
|
fn () => Livewire::mount(SelectProject::class)
|
||||||
|
@ -39,4 +39,18 @@ public function checkout(Site $site): void
|
|||||||
$site->id
|
$site->id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws SSHError
|
||||||
|
*/
|
||||||
|
public function fetchOrigin(Site $site): void
|
||||||
|
{
|
||||||
|
$site->server->ssh($site->user)->exec(
|
||||||
|
view('ssh.git.fetch-origin', [
|
||||||
|
'path' => $site->path,
|
||||||
|
]),
|
||||||
|
'fetch-origin',
|
||||||
|
$site->id
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,11 @@ public function editRules(array $input): array
|
|||||||
return $this->createRules($input);
|
return $this->createRules($input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function baseCommands(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
protected function progress(int $percentage): void
|
protected function progress(int $percentage): void
|
||||||
{
|
{
|
||||||
$this->site->progress = $percentage;
|
$this->site->progress = $percentage;
|
||||||
|
54
app/SiteTypes/Laravel.php
Executable file → Normal file
54
app/SiteTypes/Laravel.php
Executable file → Normal file
@ -2,4 +2,56 @@
|
|||||||
|
|
||||||
namespace App\SiteTypes;
|
namespace App\SiteTypes;
|
||||||
|
|
||||||
class Laravel extends PHPSite {}
|
class Laravel extends PHPSite
|
||||||
|
{
|
||||||
|
public function baseCommands(): array
|
||||||
|
{
|
||||||
|
return array_merge(parent::baseCommands(), [
|
||||||
|
// Initial Setup Commands
|
||||||
|
[
|
||||||
|
'name' => 'Generate Application Key',
|
||||||
|
'command' => 'php artisan key:generate',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Create Storage Symbolic Link',
|
||||||
|
'command' => 'php artisan storage:link',
|
||||||
|
],
|
||||||
|
// Database Commands
|
||||||
|
[
|
||||||
|
'name' => 'Run Database Migrations',
|
||||||
|
'command' => 'php artisan migrate --force',
|
||||||
|
],
|
||||||
|
// Cache & Optimization Commands
|
||||||
|
[
|
||||||
|
'name' => 'Optimize Application',
|
||||||
|
'command' => 'php artisan optimize',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Clear All Application Optimizations',
|
||||||
|
'command' => 'php artisan optimize:clear',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Clear Application Cache Only',
|
||||||
|
'command' => 'php artisan cache:clear',
|
||||||
|
],
|
||||||
|
// Queue Commands
|
||||||
|
[
|
||||||
|
'name' => 'Restart Queue Workers',
|
||||||
|
'command' => 'php artisan queue:restart',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Clear All Failed Queue Jobs',
|
||||||
|
'command' => 'php artisan queue:flush',
|
||||||
|
],
|
||||||
|
// Application State Commands
|
||||||
|
[
|
||||||
|
'name' => 'Put Application in Maintenance Mode',
|
||||||
|
'command' => 'php artisan down --retry=5 --refresh=6 --quiet',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Bring Application Online',
|
||||||
|
'command' => 'php artisan up',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -13,6 +13,7 @@ public function supportedFeatures(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
SiteFeature::DEPLOYMENT,
|
SiteFeature::DEPLOYMENT,
|
||||||
|
SiteFeature::COMMANDS,
|
||||||
SiteFeature::ENV,
|
SiteFeature::ENV,
|
||||||
SiteFeature::SSL,
|
SiteFeature::SSL,
|
||||||
SiteFeature::QUEUES,
|
SiteFeature::QUEUES,
|
||||||
@ -55,4 +56,9 @@ public function install(): void
|
|||||||
$this->progress(65);
|
$this->progress(65);
|
||||||
$this->site->php()?->restart();
|
$this->site->php()?->restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function baseCommands(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,12 +21,23 @@ public function supportedFeatures(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
SiteFeature::DEPLOYMENT,
|
SiteFeature::DEPLOYMENT,
|
||||||
|
SiteFeature::COMMANDS,
|
||||||
SiteFeature::ENV,
|
SiteFeature::ENV,
|
||||||
SiteFeature::SSL,
|
SiteFeature::SSL,
|
||||||
SiteFeature::QUEUES,
|
SiteFeature::QUEUES,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function baseCommands(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'name' => 'Install Composer Dependencies',
|
||||||
|
'command' => 'composer install --no-dev --no-interaction --no-progress',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function createRules(array $input): array
|
public function createRules(array $input): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
@ -19,4 +19,6 @@ public function install(): void;
|
|||||||
public function editRules(array $input): array;
|
public function editRules(array $input): array;
|
||||||
|
|
||||||
public function edit(): void;
|
public function edit(): void;
|
||||||
|
|
||||||
|
public function baseCommands(): array;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ public function supportedFeatures(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
SiteFeature::SSL,
|
SiteFeature::SSL,
|
||||||
|
SiteFeature::COMMANDS,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ protected function getHeaderActions(): array
|
|||||||
];
|
];
|
||||||
|
|
||||||
foreach ($this->script->getVariables() as $variable) {
|
foreach ($this->script->getVariables() as $variable) {
|
||||||
$form[] = TextInput::make($variable)
|
$form[] = TextInput::make('variables.'.$variable)
|
||||||
->label($variable)
|
->label($variable)
|
||||||
->rules(fn (Get $get) => ExecuteScript::rules($get())['variables.*']);
|
->rules(fn (Get $get) => ExecuteScript::rules($get())['variables.*']);
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,10 @@ public function getWidgets(): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->site->isReady()) {
|
if ($this->site->isReady()) {
|
||||||
|
if (in_array(SiteFeature::COMMANDS, $this->site->type()->supportedFeatures())) {
|
||||||
|
$widgets[] = [Widgets\Commands::class, ['site' => $this->site]];
|
||||||
|
}
|
||||||
|
|
||||||
if (in_array(SiteFeature::DEPLOYMENT, $this->site->type()->supportedFeatures())) {
|
if (in_array(SiteFeature::DEPLOYMENT, $this->site->type()->supportedFeatures())) {
|
||||||
$widgets[] = [Widgets\DeploymentsList::class, ['site' => $this->site]];
|
$widgets[] = [Widgets\DeploymentsList::class, ['site' => $this->site]];
|
||||||
}
|
}
|
||||||
|
176
app/Web/Pages/Servers/Sites/Widgets/Commands.php
Normal file
176
app/Web/Pages/Servers/Sites/Widgets/Commands.php
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Web\Pages\Servers\Sites\Widgets;
|
||||||
|
|
||||||
|
use App\Actions\Site\CreateCommand;
|
||||||
|
use App\Actions\Site\EditCommand;
|
||||||
|
use App\Actions\Site\ExecuteCommand;
|
||||||
|
use App\Models\Command;
|
||||||
|
use App\Models\CommandExecution;
|
||||||
|
use App\Models\Site;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Get;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Support\Enums\MaxWidth;
|
||||||
|
use Filament\Tables\Actions\Action;
|
||||||
|
use Filament\Tables\Actions\DeleteAction;
|
||||||
|
use Filament\Tables\Actions\EditAction;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Widgets\TableWidget as Widget;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\View\ComponentAttributeBag;
|
||||||
|
|
||||||
|
class Commands extends Widget
|
||||||
|
{
|
||||||
|
public Site $site;
|
||||||
|
|
||||||
|
protected $listeners = ['$refresh'];
|
||||||
|
|
||||||
|
protected function getTableQuery(): Builder
|
||||||
|
{
|
||||||
|
return Command::query()->where('site_id', $this->site->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function applySortingToTableQuery(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->latest('created_at');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getTableColumns(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
TextColumn::make('name'),
|
||||||
|
TextColumn::make('lastExecution.status')
|
||||||
|
->label('Status')
|
||||||
|
->badge()
|
||||||
|
->color(fn (Command $record) => CommandExecution::$statusColors[$record->lastExecution?->status])
|
||||||
|
->sortable(),
|
||||||
|
TextColumn::make('created_at')
|
||||||
|
->label('Last Execution At')
|
||||||
|
->formatStateUsing(fn (Command $record) => $record->lastExecution?->created_at_by_timezone)
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getTableHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Action::make('new-command')
|
||||||
|
->label('Create a Command')
|
||||||
|
->modalDescription('The command will be executed inside the site\'s directory')
|
||||||
|
->icon('heroicon-o-plus')
|
||||||
|
->authorize(fn () => auth()->user()->can('create', [Command::class, $this->site, $this->site->server]))
|
||||||
|
->action(function (array $data) {
|
||||||
|
run_action($this, function () use ($data) {
|
||||||
|
app(CreateCommand::class)->create($this->site, $data);
|
||||||
|
|
||||||
|
$this->dispatch('$refresh');
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->success()
|
||||||
|
->title('Command created!')
|
||||||
|
->send();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->form([
|
||||||
|
TextInput::make('name')
|
||||||
|
->rules(CreateCommand::rules()['name']),
|
||||||
|
TextInput::make('command')
|
||||||
|
->placeholder('php artisan my:command')
|
||||||
|
->rules(CreateCommand::rules()['command'])
|
||||||
|
->helperText('You can use variables like ${VARIABLE_NAME} in the command. The variables will be asked when executing the command'),
|
||||||
|
])
|
||||||
|
->modalSubmitActionLabel('Create')
|
||||||
|
->modalHeading('New Command')
|
||||||
|
->modalWidth('md'),
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->query($this->getTableQuery())
|
||||||
|
->headerActions($this->getTableHeaderActions())
|
||||||
|
->columns($this->getTableColumns())
|
||||||
|
->heading('Commands')
|
||||||
|
->defaultPaginationPageOption(5)
|
||||||
|
->searchable(false)
|
||||||
|
->actions([
|
||||||
|
Action::make('execute')
|
||||||
|
->hiddenLabel()
|
||||||
|
->tooltip('Execute')
|
||||||
|
->icon('heroicon-o-play')
|
||||||
|
->modalWidth(MaxWidth::Medium)
|
||||||
|
->modalSubmitActionLabel('Execute')
|
||||||
|
->form(function (Command $record) {
|
||||||
|
$form = [
|
||||||
|
TextInput::make('command')->default($record->command)->disabled(),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($record->getVariables() as $variable) {
|
||||||
|
$form[] = TextInput::make('variables.'.$variable)
|
||||||
|
->label($variable)
|
||||||
|
->rules(fn (Get $get) => ExecuteCommand::rules($get())['variables.*']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $form;
|
||||||
|
})
|
||||||
|
->authorize(fn (Command $record) => auth()->user()->can('update', [$record->site, $record->site->server]))
|
||||||
|
->action(function (array $data, Command $record) {
|
||||||
|
/** @var \App\Models\User $user */
|
||||||
|
$user = auth()->user();
|
||||||
|
app(ExecuteCommand::class)->execute($record, $user, $data);
|
||||||
|
$this->dispatch('$refresh');
|
||||||
|
}),
|
||||||
|
Action::make('logs')
|
||||||
|
->hiddenLabel()
|
||||||
|
->tooltip('Last Log')
|
||||||
|
->icon('heroicon-o-eye')
|
||||||
|
->modalHeading('View Last Execution Log')
|
||||||
|
->modalContent(function (Command $record) {
|
||||||
|
return view('components.console-view', [
|
||||||
|
'slot' => $record->lastExecution?->serverLog?->getContent() ?? 'Not executed yet',
|
||||||
|
'attributes' => new ComponentAttributeBag,
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->modalSubmitAction(false)
|
||||||
|
->modalCancelActionLabel('Close'),
|
||||||
|
EditAction::make('edit')
|
||||||
|
->hiddenLabel()
|
||||||
|
->tooltip('Edit')
|
||||||
|
->modalHeading('Edit Command')
|
||||||
|
->mutateRecordDataUsing(function (array $data, Command $record) {
|
||||||
|
return [
|
||||||
|
'name' => $record->name,
|
||||||
|
'command' => $record->command,
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->form([
|
||||||
|
TextInput::make('name')
|
||||||
|
->rules(EditCommand::rules()['name']),
|
||||||
|
TextInput::make('command')
|
||||||
|
->rules(EditCommand::rules()['command'])
|
||||||
|
->helperText('You can use variables like ${VARIABLE_NAME} in the command. The variables will be asked when executing the command'),
|
||||||
|
|
||||||
|
])
|
||||||
|
->authorize(fn (Command $record) => auth()->user()->can('update', [$record, $this->site, $this->site->server]))
|
||||||
|
->using(function (array $data, Command $record) {
|
||||||
|
app(EditCommand::class)->edit($record, $data);
|
||||||
|
$this->dispatch('$refresh');
|
||||||
|
})
|
||||||
|
->modalWidth(MaxWidth::Medium),
|
||||||
|
DeleteAction::make('delete')
|
||||||
|
->icon('heroicon-o-trash')
|
||||||
|
->hiddenLabel()
|
||||||
|
->tooltip('Delete')
|
||||||
|
->modalHeading('Delete Command')
|
||||||
|
->authorize(fn (Command $record) => auth()->user()->can('delete', [$record, $this->site, $this->site->server]))
|
||||||
|
->using(function (array $data, Command $record) {
|
||||||
|
$record->delete();
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -180,6 +180,16 @@ public function infolist(Infolist $infolist): Infolist
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
TextEntry::make('repository')
|
||||||
|
->label('Repository')
|
||||||
|
->visible(fn (Site $record) => $record->repository)
|
||||||
|
->formatStateUsing(fn (Site $record) => $record->repository)
|
||||||
|
->inlineLabel(),
|
||||||
|
TextEntry::make('branch')
|
||||||
|
->label('Branch')
|
||||||
|
->visible(fn (Site $record) => $record->branch)
|
||||||
|
->formatStateUsing(fn (Site $record) => $record->branch)
|
||||||
|
->inlineLabel(),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->record($this->site);
|
->record($this->site);
|
||||||
|
2
bootstrap/cli-cache/.gitignore
vendored
Normal file
2
bootstrap/cli-cache/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
69
bootstrap/cli.php
Normal file
69
bootstrap/cli.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
|
||||||
|
putenv('APP_SERVICES_CACHE='.__DIR__.'/cli-cache/services.php');
|
||||||
|
putenv('APP_PACKAGES_CACHE='.__DIR__.'/cli-cache/packages.php');
|
||||||
|
putenv('APP_CONFIG_CACHE='.__DIR__.'/cli-cache/config.php');
|
||||||
|
putenv('APP_ROUTES_CACHE='.__DIR__.'/cli-cache/routes.php');
|
||||||
|
putenv('APP_EVENTS_CACHE='.__DIR__.'/cli-cache/events.php');
|
||||||
|
putenv('LOG_CHANNEL=syslog');
|
||||||
|
putenv('QUEUE_CONNECTION=sync');
|
||||||
|
putenv('CACHE_DRIVER=null');
|
||||||
|
putenv('DB_DATABASE=database.sqlite');
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Create The Application
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The first thing we will do is create a new Laravel application instance
|
||||||
|
| which serves as the "glue" for all the components of Laravel, and is
|
||||||
|
| the IoC container for the system binding all of the various parts.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
$app = new Application(
|
||||||
|
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
|
||||||
|
);
|
||||||
|
|
||||||
|
$app->useStoragePath(getenv('HOME') . '/.vito/storage');
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Bind Important Interfaces
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next, we need to bind some important interfaces into the container so
|
||||||
|
| we will be able to resolve them when needed. The kernels serve the
|
||||||
|
| incoming requests to this application from both the web and CLI.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// $app->singleton(
|
||||||
|
// Illuminate\Contracts\Http\Kernel::class,
|
||||||
|
// App\Http\Kernel::class
|
||||||
|
// );
|
||||||
|
|
||||||
|
$app->singleton(
|
||||||
|
Illuminate\Contracts\Console\Kernel::class,
|
||||||
|
App\Cli\Kernel::class
|
||||||
|
);
|
||||||
|
|
||||||
|
$app->singleton(
|
||||||
|
Illuminate\Contracts\Debug\ExceptionHandler::class,
|
||||||
|
App\Exceptions\Handler::class
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Return The Application
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This script returns the application instance. The instance is given to
|
||||||
|
| the calling script so we can separate the building of the instances
|
||||||
|
| from the actual running of the application and sending responses.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
return $app;
|
20
box.json
Normal file
20
box.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"directories": [
|
||||||
|
"app",
|
||||||
|
"bootstrap",
|
||||||
|
"config",
|
||||||
|
"database",
|
||||||
|
"public",
|
||||||
|
"resources",
|
||||||
|
"routes",
|
||||||
|
"storage",
|
||||||
|
"vendor"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"artisan"
|
||||||
|
],
|
||||||
|
"main": "cli",
|
||||||
|
"output": "vito-cli.phar",
|
||||||
|
"chmod": "0755",
|
||||||
|
"stub": true
|
||||||
|
}
|
53
cli
Normal file
53
cli
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Register The Auto Loader
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Composer provides a convenient, automatically generated class loader
|
||||||
|
| for our application. We just need to utilize it! We'll require it
|
||||||
|
| into the script here so that we do not have to worry about the
|
||||||
|
| loading of any our classes "manually". Feels great to relax.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
$autoloader = require file_exists(__DIR__.'/vendor/autoload.php') ? __DIR__.'/vendor/autoload.php' : __DIR__.'/../../autoload.php';
|
||||||
|
|
||||||
|
$app = require_once __DIR__.'/bootstrap/cli.php';
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Run The Artisan Application
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When we run the console application, the current CLI command will be
|
||||||
|
| executed in this console and the response sent back to a terminal
|
||||||
|
| or another output device for the developers. Here goes nothing!
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||||
|
|
||||||
|
$status = $kernel->handle(
|
||||||
|
$input = new Symfony\Component\Console\Input\ArgvInput,
|
||||||
|
new Symfony\Component\Console\Output\ConsoleOutput
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Shutdown The Application
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Once Artisan has finished running, we will fire off the shutdown events
|
||||||
|
| so that any final work may be done by the application before we shut
|
||||||
|
| down the process. This is the last thing to happen to the request.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
$kernel->terminate($input, $status);
|
||||||
|
|
||||||
|
exit($status);
|
@ -15,6 +15,7 @@
|
|||||||
"filament/filament": "^3.2",
|
"filament/filament": "^3.2",
|
||||||
"laravel/fortify": "^1.17",
|
"laravel/fortify": "^1.17",
|
||||||
"laravel/framework": "^11.0",
|
"laravel/framework": "^11.0",
|
||||||
|
"laravel/prompts": "^0.3.5",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^2.8",
|
"laravel/tinker": "^2.8",
|
||||||
"mobiledetect/mobiledetectlib": "^4.8",
|
"mobiledetect/mobiledetectlib": "^4.8",
|
||||||
|
16
composer.lock
generated
16
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "be3e63b7efd71f649cbffb0d469ba7c1",
|
"content-hash": "e211da7974e07c3b74ad59ce245a9446",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "anourvalar/eloquent-serialize",
|
"name": "anourvalar/eloquent-serialize",
|
||||||
@ -2559,16 +2559,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/prompts",
|
"name": "laravel/prompts",
|
||||||
"version": "v0.3.3",
|
"version": "v0.3.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/prompts.git",
|
"url": "https://github.com/laravel/prompts.git",
|
||||||
"reference": "749395fcd5f8f7530fe1f00dfa84eb22c83d94ea"
|
"reference": "57b8f7efe40333cdb925700891c7d7465325d3b1"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/prompts/zipball/749395fcd5f8f7530fe1f00dfa84eb22c83d94ea",
|
"url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1",
|
||||||
"reference": "749395fcd5f8f7530fe1f00dfa84eb22c83d94ea",
|
"reference": "57b8f7efe40333cdb925700891c7d7465325d3b1",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -2582,7 +2582,7 @@
|
|||||||
"laravel/framework": ">=10.17.0 <10.25.0"
|
"laravel/framework": ">=10.17.0 <10.25.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"illuminate/collections": "^10.0|^11.0",
|
"illuminate/collections": "^10.0|^11.0|^12.0",
|
||||||
"mockery/mockery": "^1.5",
|
"mockery/mockery": "^1.5",
|
||||||
"pestphp/pest": "^2.3|^3.4",
|
"pestphp/pest": "^2.3|^3.4",
|
||||||
"phpstan/phpstan": "^1.11",
|
"phpstan/phpstan": "^1.11",
|
||||||
@ -2612,9 +2612,9 @@
|
|||||||
"description": "Add beautiful and user-friendly forms to your command-line applications.",
|
"description": "Add beautiful and user-friendly forms to your command-line applications.",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/laravel/prompts/issues",
|
"issues": "https://github.com/laravel/prompts/issues",
|
||||||
"source": "https://github.com/laravel/prompts/tree/v0.3.3"
|
"source": "https://github.com/laravel/prompts/tree/v0.3.5"
|
||||||
},
|
},
|
||||||
"time": "2024-12-30T15:53:31+00:00"
|
"time": "2025-02-11T13:34:40+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/sanctum",
|
"name": "laravel/sanctum",
|
||||||
|
22
config/cli.php
Normal file
22
config/cli.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'providers' => [
|
||||||
|
Illuminate\Bus\BusServiceProvider::class,
|
||||||
|
Illuminate\Cache\CacheServiceProvider::class,
|
||||||
|
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
|
||||||
|
Illuminate\Database\DatabaseServiceProvider::class,
|
||||||
|
Illuminate\Encryption\EncryptionServiceProvider::class,
|
||||||
|
Illuminate\Filesystem\FilesystemServiceProvider::class,
|
||||||
|
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
|
||||||
|
Illuminate\Hashing\HashServiceProvider::class,
|
||||||
|
Illuminate\Mail\MailServiceProvider::class,
|
||||||
|
Illuminate\Notifications\NotificationServiceProvider::class,
|
||||||
|
Illuminate\Pagination\PaginationServiceProvider::class,
|
||||||
|
Illuminate\Pipeline\PipelineServiceProvider::class,
|
||||||
|
Illuminate\Queue\QueueServiceProvider::class,
|
||||||
|
Illuminate\Redis\RedisServiceProvider::class,
|
||||||
|
Illuminate\Translation\TranslationServiceProvider::class,
|
||||||
|
App\Providers\AppServiceProvider::class,
|
||||||
|
],
|
||||||
|
];
|
26
database/factories/CommandExecutionFactory.php
Normal file
26
database/factories/CommandExecutionFactory.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Enums\CommandExecutionStatus;
|
||||||
|
use App\Models\Command;
|
||||||
|
use App\Models\CommandExecution;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class CommandExecutionFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = CommandExecution::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'command_id' => Command::factory(),
|
||||||
|
'status' => $this->faker->randomElement([
|
||||||
|
CommandExecutionStatus::COMPLETED,
|
||||||
|
CommandExecutionStatus::FAILED,
|
||||||
|
CommandExecutionStatus::EXECUTING,
|
||||||
|
]),
|
||||||
|
'variables' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
24
database/factories/CommandFactory.php
Normal file
24
database/factories/CommandFactory.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Command;
|
||||||
|
use App\Models\Site;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
class CommandFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Command::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $this->faker->words(3, true),
|
||||||
|
'command' => 'php artisan '.$this->faker->word,
|
||||||
|
'created_at' => Carbon::now(),
|
||||||
|
'updated_at' => Carbon::now(),
|
||||||
|
'site_id' => Site::factory(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('commands', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedInteger('site_id');
|
||||||
|
$table->string('name');
|
||||||
|
$table->text('command');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('commands');
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('command_executions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('command_id');
|
||||||
|
$table->unsignedInteger('server_id');
|
||||||
|
$table->unsignedBigInteger('user_id');
|
||||||
|
$table->unsignedBigInteger('server_log_id')->nullable();
|
||||||
|
$table->json('variables')->nullable();
|
||||||
|
$table->string('status');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('command_executions');
|
||||||
|
}
|
||||||
|
};
|
@ -1,21 +1,54 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
TAG=$1
|
BRANCH=""
|
||||||
|
TAGS=()
|
||||||
|
|
||||||
if [ -z "$TAG" ]; then
|
# Parse arguments
|
||||||
echo "No tag provided"
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--branch)
|
||||||
|
BRANCH="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--tags)
|
||||||
|
IFS=',' read -r -a TAGS <<< "$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
if [ -z "$BRANCH" ]; then
|
||||||
|
echo "No branch provided. Use --branch <git_branch>"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ ${#TAGS[@]} -eq 0 ]; then
|
||||||
|
echo "No tags provided. Use --tags tag1,tag2,tag3"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clone the specified branch of the repo
|
||||||
rm -rf /tmp/vito
|
rm -rf /tmp/vito
|
||||||
|
git clone --branch "$BRANCH" --depth 1 git@github.com:vitodeploy/vito.git /tmp/vito
|
||||||
git clone git@github.com:vitodeploy/vito.git /tmp/vito
|
|
||||||
|
|
||||||
cd /tmp/vito || exit
|
cd /tmp/vito || exit
|
||||||
|
|
||||||
|
# Prepare tag arguments for docker buildx
|
||||||
|
TAG_ARGS=()
|
||||||
|
for TAG in "${TAGS[@]}"; do
|
||||||
|
# Trim whitespace to avoid invalid tag formatting
|
||||||
|
TAG_CLEANED=$(echo -n "$TAG" | xargs)
|
||||||
|
TAG_ARGS+=("-t" "vitodeploy/vito:$TAG_CLEANED")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Build and push the image
|
||||||
docker buildx build . \
|
docker buildx build . \
|
||||||
-f docker/Dockerfile \
|
-f docker/Dockerfile \
|
||||||
-t vitodeploy/vito:"$TAG" \
|
"${TAG_ARGS[@]}" \
|
||||||
--platform linux/amd64,linux/arm64 \
|
--platform linux/amd64,linux/arm64 \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
--push
|
--push
|
||||||
|
7
resources/views/ssh/git/fetch-origin.blade.php
Normal file
7
resources/views/ssh/git/fetch-origin.blade.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
if ! cd {{ $path }}; then
|
||||||
|
echo 'VITO_SSH_ERROR' && exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git fetch origin; then
|
||||||
|
echo 'VITO_SSH_ERROR' && exit 1
|
||||||
|
fi
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update
|
sudo DEBIAN_FRONTEND=noninteractive apt-get update
|
||||||
|
|
||||||
if ! sudo DEBIAN_FRONTEND=noninteractive apt-get install -y php{{ $version }} php{{ $version }}-fpm php{{ $version }}-mbstring php{{ $version }}-mysql php{{ $version }}-gd php{{ $version }}-xml php{{ $version }}-curl php{{ $version }}-gettext php{{ $version }}-zip php{{ $version }}-bcmath php{{ $version }}-soap php{{ $version }}-redis php{{ $version }}-sqlite3 php{{ $version }}-tokenizer php{{ $version }}-pgsql php{{ $version }}-pdo; then
|
if ! sudo DEBIAN_FRONTEND=noninteractive apt-get install -y php{{ $version }} php{{ $version }}-fpm php{{ $version }}-mbstring php{{ $version }}-mysql php{{ $version }}-gd php{{ $version }}-xml php{{ $version }}-curl php{{ $version }}-gettext php{{ $version }}-zip php{{ $version }}-bcmath php{{ $version }}-soap php{{ $version }}-redis php{{ $version }}-sqlite3 php{{ $version }}-tokenizer php{{ $version }}-pgsql php{{ $version }}-pdo php{{ $version }}-intl; then
|
||||||
echo 'VITO_SSH_ERROR' && exit 1
|
echo 'VITO_SSH_ERROR' && exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
130
tests/Feature/CommandsTest.php
Normal file
130
tests/Feature/CommandsTest.php
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Facades\SSH;
|
||||||
|
use App\Web\Pages\Servers\Sites\View;
|
||||||
|
use App\Web\Pages\Servers\Sites\Widgets\Commands;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class CommandsTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_see_commands(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$this->get(
|
||||||
|
View::getUrl([
|
||||||
|
'server' => $this->server,
|
||||||
|
'site' => $this->site,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee($this->site->domain)
|
||||||
|
->assertSee('Commands');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_command(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
Livewire::test(Commands::class, ['site' => $this->site])
|
||||||
|
->assertTableHeaderActionsExistInOrder(['new-command'])
|
||||||
|
->callTableAction('new-command', null, [
|
||||||
|
'name' => 'Test Command',
|
||||||
|
'command' => 'echo "${MESSAGE}"',
|
||||||
|
])
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('commands', [
|
||||||
|
'site_id' => $this->site->id,
|
||||||
|
'name' => 'Test Command',
|
||||||
|
'command' => 'echo "${MESSAGE}"',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_edit_command(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$command = $this->site->commands()->create([
|
||||||
|
'name' => 'Test Command',
|
||||||
|
'command' => 'echo "${MESSAGE}"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Commands::class, ['site' => $this->site])
|
||||||
|
->callTableAction('edit', $command->id, [
|
||||||
|
'name' => 'Updated Command',
|
||||||
|
'command' => 'ls -la',
|
||||||
|
])
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('commands', [
|
||||||
|
'id' => $command->id,
|
||||||
|
'site_id' => $this->site->id,
|
||||||
|
'name' => 'Updated Command',
|
||||||
|
'command' => 'ls -la',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_delete_command(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$command = $this->site->commands()->create([
|
||||||
|
'name' => 'Test Command',
|
||||||
|
'command' => 'echo "${MESSAGE}"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Commands::class, ['site' => $this->site])
|
||||||
|
->callTableAction('delete', $command->id)
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('commands', [
|
||||||
|
'id' => $command->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_execute_command(): void
|
||||||
|
{
|
||||||
|
SSH::fake('echo "Hello, world!"');
|
||||||
|
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$command = $this->site->commands()->create([
|
||||||
|
'name' => 'Test Command',
|
||||||
|
'command' => 'echo "${MESSAGE}"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Commands::class, ['site' => $this->site])
|
||||||
|
->callTableAction('execute', $command->id, [
|
||||||
|
'variables' => [
|
||||||
|
'MESSAGE' => 'Hello, world!',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('command_executions', [
|
||||||
|
'command_id' => $command->id,
|
||||||
|
'variables' => json_encode(['MESSAGE' => 'Hello, world!']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_execute_command_validation_error(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$command = $this->site->commands()->create([
|
||||||
|
'name' => 'Test Command',
|
||||||
|
'command' => 'echo "${MESSAGE}"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Commands::class, ['site' => $this->site])
|
||||||
|
->callTableAction('execute', $command->id, [])
|
||||||
|
->assertHasActionErrors();
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user